diff --git a/e2e/commands/addNote.ts b/e2e/commands/addNote.ts index a5882c4dc..a4bedd5c8 100644 --- a/e2e/commands/addNote.ts +++ b/e2e/commands/addNote.ts @@ -1,4 +1,4 @@ -import {NightwatchBrowser} from 'nightwatch'; +import { NightwatchBrowser } from 'nightwatch'; const ADD_NOTE_BTN = '#add-note-btn'; const TEXTAREA = 'dialog-add-note textarea'; @@ -7,8 +7,7 @@ const NOTES_WRAPPER = 'notes'; module.exports = { async command(this: NightwatchBrowser, noteName: string) { - return this - .moveToElement(NOTES_WRAPPER, 10, 50) + return this.moveToElement(NOTES_WRAPPER, 10, 50) .waitForElementPresent(ADD_NOTE_BTN) .click(ADD_NOTE_BTN) @@ -16,7 +15,6 @@ module.exports = { .setValue(TEXTAREA, noteName) .click(ADD_NOTE_SUBMIT_BTN) - .moveToElement(NOTES_WRAPPER, 10, 50) - ; - } + .moveToElement(NOTES_WRAPPER, 10, 50); + }, }; diff --git a/e2e/commands/addTask.ts b/e2e/commands/addTask.ts index 657bc6849..914b5f22a 100644 --- a/e2e/commands/addTask.ts +++ b/e2e/commands/addTask.ts @@ -1,12 +1,11 @@ -import {NightwatchBrowser} from 'nightwatch'; +import { NightwatchBrowser } from 'nightwatch'; const ADD_TASK_GLOBAL_SEL = 'add-task-bar.global input'; const ROUTER_WRAPPER = '.route-wrapper'; module.exports = { async command(this: NightwatchBrowser, taskName: string) { - return this - .waitForElementVisible(ROUTER_WRAPPER) + return this.waitForElementVisible(ROUTER_WRAPPER) .setValue('body', 'A') .waitForElementVisible(ADD_TASK_GLOBAL_SEL) .setValue(ADD_TASK_GLOBAL_SEL, taskName) @@ -14,7 +13,6 @@ module.exports = { .pause(30) .setValue(ADD_TASK_GLOBAL_SEL, this.Keys.ESCAPE) .pause(30) - .waitForElementNotPresent(ADD_TASK_GLOBAL_SEL) - ; - } + .waitForElementNotPresent(ADD_TASK_GLOBAL_SEL); + }, }; diff --git a/e2e/commands/addTaskWithReminder.ts b/e2e/commands/addTaskWithReminder.ts index 313ed0e74..faeabc2c8 100644 --- a/e2e/commands/addTaskWithReminder.ts +++ b/e2e/commands/addTaskWithReminder.ts @@ -1,4 +1,4 @@ -import { AddTaskWithReminderParams, NBrowser, } from '../n-browser-interface'; +import { AddTaskWithReminderParams, NBrowser } from '../n-browser-interface'; const TASK = 'task'; const SCHEDULE_TASK_ITEM = 'task-additional-info-item:nth-child(2)'; @@ -12,22 +12,24 @@ const M = 60 * 1000; // being slightly longer than a minute prevents the edge case // of the wrong minute if the rest before takes to long -const DEFAULT_DELTA = (1.2 * M); +const DEFAULT_DELTA = 1.2 * M; // NOTE: needs to // be executed from work view module.exports = { - async command(this: NBrowser, { - title, - taskSel = TASK, - scheduleTime = Date.now() + DEFAULT_DELTA - }: AddTaskWithReminderParams) { + async command( + this: NBrowser, + { + title, + taskSel = TASK, + scheduleTime = Date.now() + DEFAULT_DELTA, + }: AddTaskWithReminderParams, + ) { const d = new Date(scheduleTime); const h = d.getHours(); const m = d.getMinutes(); - return this - .addTask(title) + return this.addTask(title) .openPanelForTask(taskSel) .waitForElementVisible(SCHEDULE_TASK_ITEM) .click(SCHEDULE_TASK_ITEM) @@ -43,6 +45,5 @@ module.exports = { .waitForElementVisible(DIALOG_SUBMIT) .click(DIALOG_SUBMIT) .waitForElementNotPresent(DIALOG); - } + }, }; - diff --git a/e2e/commands/goToDefaultProject.ts b/e2e/commands/goToDefaultProject.ts index 6ecea8347..9becd28c3 100644 --- a/e2e/commands/goToDefaultProject.ts +++ b/e2e/commands/goToDefaultProject.ts @@ -1,5 +1,5 @@ -import {NightwatchBrowser} from 'nightwatch'; -import {BASE} from '../e2e.const'; +import { NightwatchBrowser } from 'nightwatch'; +import { BASE } from '../e2e.const'; const BASE_URL = `${BASE}`; @@ -15,14 +15,12 @@ const SPLIT = `split`; module.exports = { async command(this: NightwatchBrowser) { - return this - .url(BASE_URL) + return this.url(BASE_URL) .waitForElementVisible(EXPAND_PROJECT_BTN) .click(EXPAND_PROJECT_BTN) .waitForElementVisible(DEFAULT_PROJECT_BTN) .click(DEFAULT_PROJECT_BTN) .waitForElementVisible(BACKLOG) - .waitForElementVisible(SPLIT) - ; - } + .waitForElementVisible(SPLIT); + }, }; diff --git a/e2e/commands/openPanelForTask.ts b/e2e/commands/openPanelForTask.ts index 413092aa0..4ade65e67 100644 --- a/e2e/commands/openPanelForTask.ts +++ b/e2e/commands/openPanelForTask.ts @@ -1,18 +1,16 @@ -import {NBrowser} from '../n-browser-interface'; +import { NBrowser } from '../n-browser-interface'; const SIDE_INNER = '.additional-info-panel'; // NOTE: needs to be executed from work view module.exports = { async command(this: NBrowser, taskSel: string) { - return this - .waitForElementPresent(taskSel) + return this.waitForElementPresent(taskSel) .pause(50) .moveToElement(taskSel, 100, 15) .click(taskSel) .sendKeys(taskSel, this.Keys.ARROW_RIGHT) .waitForElementVisible(SIDE_INNER) - .pause(50) - ; - } + .pause(50); + }, }; diff --git a/e2e/n-browser-interface.ts b/e2e/n-browser-interface.ts index ff8dd903d..b62be5788 100644 --- a/e2e/n-browser-interface.ts +++ b/e2e/n-browser-interface.ts @@ -1,4 +1,4 @@ -import {NightwatchBrowser} from 'nightwatch'; +import { NightwatchBrowser } from 'nightwatch'; export interface AddTaskWithReminderParams { title: string; diff --git a/e2e/nightwatch.conf.js b/e2e/nightwatch.conf.js index 34baed337..dacac4946 100644 --- a/e2e/nightwatch.conf.js +++ b/e2e/nightwatch.conf.js @@ -1,20 +1,18 @@ module.exports = { // An array of folders (excluding subfolders) where your tests are located; // if this is not specified, the test source must be passed as the second argument to the test runner. - src_folders: [ - '../out-tsc/e2e/src' - ], - output_folder: "./e2e-test-results", - custom_commands_path: "out-tsc/e2e/commands", + src_folders: ['../out-tsc/e2e/src'], + output_folder: './e2e-test-results', + custom_commands_path: 'out-tsc/e2e/commands', test_workers: { enabled: true, - workers: "auto" + workers: 'auto', }, webdriver: { start_process: true, port: 9515, server_path: require('chromedriver').path, - cli_args: [] + cli_args: [], }, test_settings: { @@ -34,8 +32,8 @@ module.exports = { ], w3c: false, prefs: { - "profile.default_content_setting_values.geolocation": 1, - "profile.default_content_setting_values.notifications": 2, + 'profile.default_content_setting_values.geolocation': 1, + 'profile.default_content_setting_values.notifications': 2, }, }, }, @@ -43,13 +41,13 @@ module.exports = { enabled: true, // if you want to keep screenshots on_failure: true, on_error: true, - path: './e2e/screenshots' // save screenshots here + path: './e2e/screenshots', // save screenshots here }, globals: { waitForConditionPollInterval: 500, waitForConditionTimeout: 10000, retryAssertionTimeout: 1000, - } - } - } + }, + }, + }, }; diff --git a/e2e/src/daily-summary.e2e.ts b/e2e/src/daily-summary.e2e.ts index b7613a301..b5a12957e 100644 --- a/e2e/src/daily-summary.e2e.ts +++ b/e2e/src/daily-summary.e2e.ts @@ -1,5 +1,5 @@ -import {BASE} from '../e2e.const'; -import {NBrowser} from '../n-browser-interface'; +import { BASE } from '../e2e.const'; +import { NBrowser } from '../n-browser-interface'; const URL = `${BASE}/#/tag/TODAY/daily-summary`; const ADD_TASK_BTN_SEL = '.action-nav > button:first-child'; @@ -8,19 +8,21 @@ const ADD_TASK_GLOBAL_SEL = 'add-task-bar.global input'; module.exports = { '@tags': ['daily-summary'], - 'Daily summary message': (browser: NBrowser) => browser - .url(URL) - .waitForElementVisible('.done-headline') - .assert.containsText('.done-headline', 'Take a moment to celebrate') - .end(), + 'Daily summary message': (browser: NBrowser) => + browser + .url(URL) + .waitForElementVisible('.done-headline') + .assert.containsText('.done-headline', 'Take a moment to celebrate') + .end(), - 'show any added task in table': (browser: NBrowser) => browser - .url(URL) - .waitForElementVisible(ADD_TASK_BTN_SEL) - .click(ADD_TASK_BTN_SEL) - .waitForElementVisible(ADD_TASK_GLOBAL_SEL) + 'show any added task in table': (browser: NBrowser) => + browser + .url(URL) + .waitForElementVisible(ADD_TASK_BTN_SEL) + .click(ADD_TASK_BTN_SEL) + .waitForElementVisible(ADD_TASK_GLOBAL_SEL) - .setValue(ADD_TASK_GLOBAL_SEL, 'test task hohoho') - .setValue(ADD_TASK_GLOBAL_SEL, browser.Keys.ENTER) - .end(), + .setValue(ADD_TASK_GLOBAL_SEL, 'test task hohoho') + .setValue(ADD_TASK_GLOBAL_SEL, browser.Keys.ENTER) + .end(), }; diff --git a/e2e/src/project-bookmark.e2e.ts b/e2e/src/project-bookmark.e2e.ts index 4dc5ccd42..052f97067 100644 --- a/e2e/src/project-bookmark.e2e.ts +++ b/e2e/src/project-bookmark.e2e.ts @@ -1,4 +1,4 @@ -import {NBrowser} from '../n-browser-interface'; +import { NBrowser } from '../n-browser-interface'; const TOGGLE_BOOKMARK_BAR_BTN = '.action-nav button:nth-child(2)'; const BOOKMARK_BAR_OPTS_BTN = 'bookmark-bar .list-controls button:first-of-type'; @@ -15,25 +15,26 @@ const FIRST_BOOKMARK = `${BOOKMARK}:first-of-type`; module.exports = { '@tags': ['project', 'bookmark'], - 'create a bookmark': (browser: NBrowser) => browser - .goToDefaultProject() + 'create a bookmark': (browser: NBrowser) => + browser + .goToDefaultProject() - .waitForElementVisible(TOGGLE_BOOKMARK_BAR_BTN) - .click(TOGGLE_BOOKMARK_BAR_BTN) + .waitForElementVisible(TOGGLE_BOOKMARK_BAR_BTN) + .click(TOGGLE_BOOKMARK_BAR_BTN) - .waitForElementVisible(BOOKMARK_BAR_OPTS_BTN) - .click(BOOKMARK_BAR_OPTS_BTN) + .waitForElementVisible(BOOKMARK_BAR_OPTS_BTN) + .click(BOOKMARK_BAR_OPTS_BTN) - .waitForElementVisible(ADD_BOOKMARK_BTN) - .click(ADD_BOOKMARK_BTN) + .waitForElementVisible(ADD_BOOKMARK_BTN) + .click(ADD_BOOKMARK_BTN) - .waitForElementVisible(BOOKMARK_TITLE_INP) - .setValue(BOOKMARK_TITLE_INP, 'Some bookmark title') - .setValue(BOOKMARK_URL_INP, 'bookmark-url') - .click(BOOKMARK_SUBMIT_BTN) + .waitForElementVisible(BOOKMARK_TITLE_INP) + .setValue(BOOKMARK_TITLE_INP, 'Some bookmark title') + .setValue(BOOKMARK_URL_INP, 'bookmark-url') + .click(BOOKMARK_SUBMIT_BTN) - .waitForElementVisible(FIRST_BOOKMARK) - .assert.elementPresent(FIRST_BOOKMARK) - .assert.containsText(FIRST_BOOKMARK, 'Some bookmark title') - .end(), + .waitForElementVisible(FIRST_BOOKMARK) + .assert.elementPresent(FIRST_BOOKMARK) + .assert.containsText(FIRST_BOOKMARK, 'Some bookmark title') + .end(), }; diff --git a/e2e/src/project-note.e2e.ts b/e2e/src/project-note.e2e.ts index aa53f4511..08cde3eb3 100644 --- a/e2e/src/project-note.e2e.ts +++ b/e2e/src/project-note.e2e.ts @@ -1,31 +1,32 @@ -import {NBrowser} from '../n-browser-interface'; +import { NBrowser } from '../n-browser-interface'; const NOTES_WRAPPER = 'notes'; const NOTE = 'notes note'; const FIRST_NOTE = `${NOTE}:first-of-type`; - module.exports = { '@tags': ['project', 'note'], - 'create a note': (browser: NBrowser) => browser - .goToDefaultProject() - .addNote('Some new Note') + 'create a note': (browser: NBrowser) => + browser + .goToDefaultProject() + .addNote('Some new Note') - .moveToElement(NOTES_WRAPPER, 10, 50) - .waitForElementVisible(FIRST_NOTE) - .assert.elementPresent(FIRST_NOTE) - .assert.containsText(FIRST_NOTE, 'Some new Note') - .end(), + .moveToElement(NOTES_WRAPPER, 10, 50) + .waitForElementVisible(FIRST_NOTE) + .assert.elementPresent(FIRST_NOTE) + .assert.containsText(FIRST_NOTE, 'Some new Note') + .end(), - 'new note should be still available after reload': (browser: NBrowser) => browser - .goToDefaultProject() - .addNote('Some new Note') - .execute('window.location.reload()') - .waitForElementPresent(NOTES_WRAPPER) - .moveToElement(NOTES_WRAPPER, 10, 50) - .waitForElementVisible(FIRST_NOTE) - .assert.elementPresent(FIRST_NOTE) - .assert.containsText(FIRST_NOTE, 'Some new Note') - .end(), + 'new note should be still available after reload': (browser: NBrowser) => + browser + .goToDefaultProject() + .addNote('Some new Note') + .execute('window.location.reload()') + .waitForElementPresent(NOTES_WRAPPER) + .moveToElement(NOTES_WRAPPER, 10, 50) + .waitForElementVisible(FIRST_NOTE) + .assert.elementPresent(FIRST_NOTE) + .assert.containsText(FIRST_NOTE, 'Some new Note') + .end(), }; diff --git a/e2e/src/project.e2e.ts b/e2e/src/project.e2e.ts index 651def8be..64fe5ce9b 100644 --- a/e2e/src/project.e2e.ts +++ b/e2e/src/project.e2e.ts @@ -1,5 +1,5 @@ -import {BASE} from '../e2e.const'; -import {NBrowser} from '../n-browser-interface'; +import { BASE } from '../e2e.const'; +import { NBrowser } from '../n-browser-interface'; const BASE_URL = `${BASE}`; @@ -32,70 +32,70 @@ const GLOBAL_ERROR_ALERT = '.global-error-alert'; module.exports = { '@tags': ['project'], + 'navigate to project settings': (browser: NBrowser) => + browser + .url(BASE_URL) + .waitForElementVisible(EXPAND_PROJECT_BTN) + .click(EXPAND_PROJECT_BTN) + .waitForElementVisible(DEFAULT_PROJECT_BTN) + .moveToElement(DEFAULT_PROJECT_BTN, 20, 20) + .waitForElementVisible(DEFAULT_PROJECT_ADV_BTN) + .click(DEFAULT_PROJECT_ADV_BTN) + .waitForElementVisible(WORK_CTX_MENU) + .waitForElementVisible(PROJECT_SETTINGS_BTN) - 'navigate to project settings': (browser: NBrowser) => browser - .url(BASE_URL) - .waitForElementVisible(EXPAND_PROJECT_BTN) - .click(EXPAND_PROJECT_BTN) - .waitForElementVisible(DEFAULT_PROJECT_BTN) - .moveToElement(DEFAULT_PROJECT_BTN, 20, 20) - .waitForElementVisible(DEFAULT_PROJECT_ADV_BTN) - .click(DEFAULT_PROJECT_ADV_BTN) - .waitForElementVisible(WORK_CTX_MENU) - .waitForElementVisible(PROJECT_SETTINGS_BTN) + // navigate to + .click(PROJECT_SETTINGS_BTN) - // navigate to - .click(PROJECT_SETTINGS_BTN) + .waitForElementVisible('.component-wrapper .mat-h1') + .assert.containsText('.component-wrapper .mat-h1', 'Project Specific Settings') + .end(), - .waitForElementVisible('.component-wrapper .mat-h1') - .assert.containsText('.component-wrapper .mat-h1', 'Project Specific Settings') - .end(), + 'create project': (browser: NBrowser) => + browser + .url(BASE_URL) + .waitForElementVisible(EXPAND_PROJECT_BTN) + .click(EXPAND_PROJECT_BTN) + .waitForElementVisible(CREATE_PROJECT_BTN) + .click(CREATE_PROJECT_BTN) + .waitForElementVisible(PROJECT_NAME_INPUT) + .setValue(PROJECT_NAME_INPUT, 'Cool Test Project') + .click(SUBMIT_BTN) + .waitForElementVisible(SECOND_PROJECT) + .assert.elementPresent(SECOND_PROJECT) + .assert.containsText(SECOND_PROJECT, 'Cool Test Project') - 'create project': (browser: NBrowser) => browser - .url(BASE_URL) - .waitForElementVisible(EXPAND_PROJECT_BTN) - .click(EXPAND_PROJECT_BTN) - .waitForElementVisible(CREATE_PROJECT_BTN) - .click(CREATE_PROJECT_BTN) - .waitForElementVisible(PROJECT_NAME_INPUT) - .setValue(PROJECT_NAME_INPUT, 'Cool Test Project') - .click(SUBMIT_BTN) + // navigate to + .waitForElementVisible(SECOND_PROJECT_BTN) + .click(SECOND_PROJECT_BTN) - .waitForElementVisible(SECOND_PROJECT) - .assert.elementPresent(SECOND_PROJECT) - .assert.containsText(SECOND_PROJECT, 'Cool Test Project') + .waitForElementVisible(BACKLOG) + .waitForElementVisible(SPLIT) + .assert.containsText(WORK_CTX_TITLE, 'Cool Test Project') + .end(), - // navigate to - .waitForElementVisible(SECOND_PROJECT_BTN) - .click(SECOND_PROJECT_BTN) + 'navigate to default': (browser: NBrowser) => + browser + .goToDefaultProject() - .waitForElementVisible(BACKLOG) - .waitForElementVisible(SPLIT) - .assert.containsText(WORK_CTX_TITLE, 'Cool Test Project') - .end(), + .assert.urlEquals(`${BASE}/#/project/DEFAULT/tasks`) + .assert.containsText(WORK_CTX_TITLE, 'Super Productivity') + .end(), + 'navigate to daily summary from project without error': (browser: NBrowser) => + browser + // Go to project page + .goToDefaultProject() - 'navigate to default': (browser: NBrowser) => browser - .goToDefaultProject() + .click(READY_TO_WORK_BTN) - .assert.urlEquals(`${BASE}/#/project/DEFAULT/tasks`) - .assert.containsText(WORK_CTX_TITLE, 'Super Productivity') - .end(), + // navigate to + .waitForElementVisible(FINISH_DAY_BTN) + .click(FINISH_DAY_BTN) - - 'navigate to daily summary from project without error': (browser: NBrowser) => browser - // Go to project page - .goToDefaultProject() - - .click(READY_TO_WORK_BTN) - - // navigate to - .waitForElementVisible(FINISH_DAY_BTN) - .click(FINISH_DAY_BTN) - - .waitForElementPresent(DAILY_SUMMARY) - .assert.urlEquals(`${BASE}/#/project/DEFAULT/daily-summary`) - .assert.elementNotPresent(GLOBAL_ERROR_ALERT) - .end(), + .waitForElementPresent(DAILY_SUMMARY) + .assert.urlEquals(`${BASE}/#/project/DEFAULT/daily-summary`) + .assert.elementNotPresent(GLOBAL_ERROR_ALERT) + .end(), }; diff --git a/e2e/src/reminders-schedule-page.e2e.ts b/e2e/src/reminders-schedule-page.e2e.ts index 3318735e6..1b7e5740e 100644 --- a/e2e/src/reminders-schedule-page.e2e.ts +++ b/e2e/src/reminders-schedule-page.e2e.ts @@ -1,5 +1,5 @@ -import {BASE} from '../e2e.const'; -import {NBrowser} from '../n-browser-interface'; +import { BASE } from '../e2e.const'; +import { NBrowser } from '../n-browser-interface'; const WORK_VIEW_URL = `${BASE}/`; @@ -19,37 +19,38 @@ const SCHEDULE_PAGE_TASK_2 = `${SCHEDULE_PAGE_TASKS}:nth-of-type(2)`; module.exports = { '@tags': ['task', 'reminder'], - 'should add a scheduled tasks': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .waitForElementPresent(READY_TO_WORK_BTN) - .addTaskWithReminder({title: '0 test task koko', scheduleTime: Date.now()}) - .waitForElementVisible(TASK) - .waitForElementVisible(TASK_SCHEDULE_BTN) - .assert.elementPresent(TASK_SCHEDULE_BTN) + 'should add a scheduled tasks': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .waitForElementPresent(READY_TO_WORK_BTN) + .addTaskWithReminder({ title: '0 test task koko', scheduleTime: Date.now() }) + .waitForElementVisible(TASK) + .waitForElementVisible(TASK_SCHEDULE_BTN) + .assert.elementPresent(TASK_SCHEDULE_BTN) - // Navigate to scheduled page and check if entry is there - .click(SCHEDULE_ROUTE_BTN) - .waitForElementVisible(SCHEDULE_PAGE_CMP) - .waitForElementVisible(SCHEDULE_PAGE_TASK_1) - .assert.containsText(SCHEDULE_PAGE_TASK_1, '0 test task koko') - .end(), + // Navigate to scheduled page and check if entry is there + .click(SCHEDULE_ROUTE_BTN) + .waitForElementVisible(SCHEDULE_PAGE_CMP) + .waitForElementVisible(SCHEDULE_PAGE_TASK_1) + .assert.containsText(SCHEDULE_PAGE_TASK_1, '0 test task koko') + .end(), + 'should add multiple scheduled tasks': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .waitForElementPresent(READY_TO_WORK_BTN) + .addTaskWithReminder({ title: '0 test task koko', taskSel: TASK }) + .addTaskWithReminder({ title: '2 hihihi', taskSel: TASK_2 }) + .waitForElementVisible(TASK) + .waitForElementVisible(TASK_SCHEDULE_BTN) + .assert.elementPresent(TASK_SCHEDULE_BTN) + .assert.elementPresent(TASK_SCHEDULE_BTN_2) - 'should add multiple scheduled tasks': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .waitForElementPresent(READY_TO_WORK_BTN) - .addTaskWithReminder({title: '0 test task koko', taskSel: TASK}) - .addTaskWithReminder({title: '2 hihihi', taskSel: TASK_2}) - .waitForElementVisible(TASK) - .waitForElementVisible(TASK_SCHEDULE_BTN) - .assert.elementPresent(TASK_SCHEDULE_BTN) - .assert.elementPresent(TASK_SCHEDULE_BTN_2) - - // Navigate to scheduled page and check if entry is there - .click(SCHEDULE_ROUTE_BTN) - .waitForElementVisible(SCHEDULE_PAGE_CMP) - .waitForElementVisible(SCHEDULE_PAGE_TASK_1) - .assert.containsText(SCHEDULE_PAGE_TASK_1, '0 test task koko') - .assert.containsText(SCHEDULE_PAGE_TASK_2, '2 hihihi') - .end(), + // Navigate to scheduled page and check if entry is there + .click(SCHEDULE_ROUTE_BTN) + .waitForElementVisible(SCHEDULE_PAGE_CMP) + .waitForElementVisible(SCHEDULE_PAGE_TASK_1) + .assert.containsText(SCHEDULE_PAGE_TASK_1, '0 test task koko') + .assert.containsText(SCHEDULE_PAGE_TASK_2, '2 hihihi') + .end(), }; diff --git a/e2e/src/reminders-view-task.e2e.ts b/e2e/src/reminders-view-task.e2e.ts index 6add97c75..e8c56f1e2 100644 --- a/e2e/src/reminders-view-task.e2e.ts +++ b/e2e/src/reminders-view-task.e2e.ts @@ -23,62 +23,68 @@ const SCHEDULE_MAX_WAIT_TIME = 180000; module.exports = { '@tags': ['task', 'reminder', 'schedule'], - 'should display a modal with a scheduled task if due': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .addTaskWithReminder({title: '0 A task', scheduleTime: Date.now()}) - .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME) - .assert.elementPresent(DIALOG) - .waitForElementVisible(DIALOG_TASK1) - .assert.elementPresent(DIALOG_TASK1) - .assert.containsText(DIALOG_TASK1, '0 A task') - .end(), - - 'should display a modal with 2 scheduled task if due': (browser: NBrowser) => { - return browser + 'should display a modal with a scheduled task if due': (browser: NBrowser) => + browser .url(WORK_VIEW_URL) - // NOTE: tasks are sorted by due time - .addTaskWithReminder({title: '0 B task'}) - .addTaskWithReminder({title: '1 B task', scheduleTime: Date.now()}) + .addTaskWithReminder({ title: '0 A task', scheduleTime: Date.now() }) .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME) .assert.elementPresent(DIALOG) - .waitForElementVisible(DIALOG_TASK1, SCHEDULE_MAX_WAIT_TIME) - .waitForElementVisible(DIALOG_TASK2, SCHEDULE_MAX_WAIT_TIME) - .assert.containsText(DIALOG_TASKS_WRAPPER, '0 B task') - .assert.containsText(DIALOG_TASKS_WRAPPER, '1 B task') - .end(); + .waitForElementVisible(DIALOG_TASK1) + .assert.elementPresent(DIALOG_TASK1) + .assert.containsText(DIALOG_TASK1, '0 A task') + .end(), + + 'should display a modal with 2 scheduled task if due': (browser: NBrowser) => { + return ( + browser + .url(WORK_VIEW_URL) + // NOTE: tasks are sorted by due time + .addTaskWithReminder({ title: '0 B task' }) + .addTaskWithReminder({ title: '1 B task', scheduleTime: Date.now() }) + .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME) + .assert.elementPresent(DIALOG) + .waitForElementVisible(DIALOG_TASK1, SCHEDULE_MAX_WAIT_TIME) + .waitForElementVisible(DIALOG_TASK2, SCHEDULE_MAX_WAIT_TIME) + .assert.containsText(DIALOG_TASKS_WRAPPER, '0 B task') + .assert.containsText(DIALOG_TASKS_WRAPPER, '1 B task') + .end() + ); }, - 'should start single task': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .addTaskWithReminder({title: '0 C task', scheduleTime: Date.now()}) - .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME) - .waitForElementVisible(DIALOG_TASK1) - .click(D_PLAY) - .pause(100) - .assert.cssClassPresent(TODAY_TASK_1, 'isCurrent') - .end(), + 'should start single task': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .addTaskWithReminder({ title: '0 C task', scheduleTime: Date.now() }) + .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME) + .waitForElementVisible(DIALOG_TASK1) + .click(D_PLAY) + .pause(100) + .assert.cssClassPresent(TODAY_TASK_1, 'isCurrent') + .end(), 'should manually empty list via add to today': (browser: NBrowser) => { const start = Date.now() + 100000; - return browser - .url(WORK_VIEW_URL) - // NOTE: tasks are sorted by due time - .addTaskWithReminder({title: '0 D task xyz', scheduleTime: start}) - .addTaskWithReminder({title: '1 D task xyz', scheduleTime: start}) - .addTaskWithReminder({title: '2 D task xyz', scheduleTime: Date.now()}) - .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME + 120000) - // wait for all tasks to be present - .waitForElementVisible(DIALOG_TASK1, SCHEDULE_MAX_WAIT_TIME + 120000) - .waitForElementVisible(DIALOG_TASK2, SCHEDULE_MAX_WAIT_TIME + 120000) - .waitForElementVisible(DIALOG_TASK3, SCHEDULE_MAX_WAIT_TIME + 120000) - .pause(100) - .assert.containsText(DIALOG_TASKS_WRAPPER, '0 D task xyz') - .assert.containsText(DIALOG_TASKS_WRAPPER, '1 D task xyz') - .assert.containsText(DIALOG_TASKS_WRAPPER, '2 D task xyz') - .click(DIALOG_TASK1 + TO_TODAY_SUF) - .click(DIALOG_TASK2 + TO_TODAY_SUF) - .pause(50) - .assert.containsText(DIALOG_TASK1, 'D task xyz') - .end(); - } + return ( + browser + .url(WORK_VIEW_URL) + // NOTE: tasks are sorted by due time + .addTaskWithReminder({ title: '0 D task xyz', scheduleTime: start }) + .addTaskWithReminder({ title: '1 D task xyz', scheduleTime: start }) + .addTaskWithReminder({ title: '2 D task xyz', scheduleTime: Date.now() }) + .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME + 120000) + // wait for all tasks to be present + .waitForElementVisible(DIALOG_TASK1, SCHEDULE_MAX_WAIT_TIME + 120000) + .waitForElementVisible(DIALOG_TASK2, SCHEDULE_MAX_WAIT_TIME + 120000) + .waitForElementVisible(DIALOG_TASK3, SCHEDULE_MAX_WAIT_TIME + 120000) + .pause(100) + .assert.containsText(DIALOG_TASKS_WRAPPER, '0 D task xyz') + .assert.containsText(DIALOG_TASKS_WRAPPER, '1 D task xyz') + .assert.containsText(DIALOG_TASKS_WRAPPER, '2 D task xyz') + .click(DIALOG_TASK1 + TO_TODAY_SUF) + .click(DIALOG_TASK2 + TO_TODAY_SUF) + .pause(50) + .assert.containsText(DIALOG_TASK1, 'D task xyz') + .end() + ); + }, }; diff --git a/e2e/src/short-syntax.e2e.ts b/e2e/src/short-syntax.e2e.ts index 9d87e6be6..12c43bb9e 100644 --- a/e2e/src/short-syntax.e2e.ts +++ b/e2e/src/short-syntax.e2e.ts @@ -9,12 +9,13 @@ const READY_TO_WORK_BTN = '.ready-to-work-btn'; module.exports = { '@tags': ['work-view', 'task', 'short-syntax'], - 'should add task with project via short syntax': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .waitForElementVisible(READY_TO_WORK_BTN) - .addTask('0 test task koko +s') - .waitForElementVisible(TASK) - .assert.visible(TASK) - .assert.containsText(TASK_TAGS, 'Super Productivity') - .end(), + 'should add task with project via short syntax': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .waitForElementVisible(READY_TO_WORK_BTN) + .addTask('0 test task koko +s') + .waitForElementVisible(TASK) + .assert.visible(TASK) + .assert.containsText(TASK_TAGS, 'Super Productivity') + .end(), }; diff --git a/e2e/src/work-view.e2e.ts b/e2e/src/work-view.e2e.ts index 4aa56627d..cdf1eb586 100644 --- a/e2e/src/work-view.e2e.ts +++ b/e2e/src/work-view.e2e.ts @@ -1,6 +1,5 @@ -import {BASE} from '../e2e.const'; -import {NBrowser} from '../n-browser-interface'; - +import { BASE } from '../e2e.const'; +import { NBrowser } from '../n-browser-interface'; const ADD_TASK_INITIAL = 'add-task-bar:not(.global) input'; const ADD_TASK_GLOBAL = 'add-task-bar.global input'; @@ -12,71 +11,74 @@ const READY_TO_WORK_BTN = '.ready-to-work-btn'; module.exports = { '@tags': ['work-view', 'task'], - 'should add task via key combo': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .waitForElementVisible(READY_TO_WORK_BTN) - .addTask('0 test task koko') - .waitForElementVisible(TASK) - .assert.visible(TASK) - .assert.containsText(TASK, '0 test task koko') - .end(), + 'should add task via key combo': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .waitForElementVisible(READY_TO_WORK_BTN) + .addTask('0 test task koko') + .waitForElementVisible(TASK) + .assert.visible(TASK) + .assert.containsText(TASK, '0 test task koko') + .end(), - 'should add a task from initial bar': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .waitForElementVisible(ADD_TASK_INITIAL) + 'should add a task from initial bar': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .waitForElementVisible(ADD_TASK_INITIAL) - .setValue(ADD_TASK_INITIAL, '1 test task hihi') - .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) + .setValue(ADD_TASK_INITIAL, '1 test task hihi') + .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) - .waitForElementVisible(TASK) - .assert.visible(TASK) - .assert.containsText(TASK, '1 test task hihi') - .end(), + .waitForElementVisible(TASK) + .assert.visible(TASK) + .assert.containsText(TASK, '1 test task hihi') + .end(), - 'should add 2 tasks from initial bar': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .waitForElementVisible(ADD_TASK_INITIAL) + 'should add 2 tasks from initial bar': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .waitForElementVisible(ADD_TASK_INITIAL) - .setValue(ADD_TASK_INITIAL, '2 test task hihi') - .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) - .setValue(ADD_TASK_INITIAL, '3 some other task') - .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) + .setValue(ADD_TASK_INITIAL, '2 test task hihi') + .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) + .setValue(ADD_TASK_INITIAL, '3 some other task') + .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) - .waitForElementVisible(TASK) - .assert.visible(TASK) - .assert.containsText(TASK + ':nth-child(1)', '2 test task hihi') - .assert.containsText(TASK + ':nth-child(2)', '3 some other task') - .end(), + .waitForElementVisible(TASK) + .assert.visible(TASK) + .assert.containsText(TASK + ':nth-child(1)', '2 test task hihi') + .assert.containsText(TASK + ':nth-child(2)', '3 some other task') + .end(), + 'should add multiple tasks from header button': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .waitForElementVisible(ADD_TASK_BTN) + .click(ADD_TASK_BTN) + .waitForElementVisible(ADD_TASK_GLOBAL) - 'should add multiple tasks from header button': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .waitForElementVisible(ADD_TASK_BTN) - .click(ADD_TASK_BTN) - .waitForElementVisible(ADD_TASK_GLOBAL) + .setValue(ADD_TASK_GLOBAL, '4 test task hohoho') + .setValue(ADD_TASK_GLOBAL, browser.Keys.ENTER) + .setValue(ADD_TASK_GLOBAL, '5 some other task xoxo') + .setValue(ADD_TASK_GLOBAL, browser.Keys.ENTER) - .setValue(ADD_TASK_GLOBAL, '4 test task hohoho') - .setValue(ADD_TASK_GLOBAL, browser.Keys.ENTER) - .setValue(ADD_TASK_GLOBAL, '5 some other task xoxo') - .setValue(ADD_TASK_GLOBAL, browser.Keys.ENTER) + .waitForElementVisible(TASK) + .assert.visible(TASK) + // NOTE: global adds to top rather than bottom + .assert.containsText(TASK + ':nth-child(1)', '5 some other task xoxo') + .assert.containsText(TASK + ':nth-child(2)', '4 test task hohoho') + .end(), - .waitForElementVisible(TASK) - .assert.visible(TASK) - // NOTE: global adds to top rather than bottom - .assert.containsText(TASK + ':nth-child(1)', '5 some other task xoxo') - .assert.containsText(TASK + ':nth-child(2)', '4 test task hohoho') - .end(), + 'should still show created task after reload': (browser: NBrowser) => + browser + .url(WORK_VIEW_URL) + .waitForElementVisible(READY_TO_WORK_BTN) + .addTask('0 test task lolo') + .waitForElementVisible(TASK) + .execute('window.location.reload()') - - 'should still show created task after reload': (browser: NBrowser) => browser - .url(WORK_VIEW_URL) - .waitForElementVisible(READY_TO_WORK_BTN) - .addTask('0 test task lolo') - .waitForElementVisible(TASK) - .execute('window.location.reload()') - - .waitForElementVisible(TASK) - .assert.visible(TASK) - .assert.containsText(TASK, '0 test task lolo') - .end(), + .waitForElementVisible(TASK) + .assert.visible(TASK) + .assert.containsText(TASK, '0 test task lolo') + .end(), }; diff --git a/electron/backup.ts b/electron/backup.ts index 6c451be97..9af9ea18e 100644 --- a/electron/backup.ts +++ b/electron/backup.ts @@ -1,5 +1,12 @@ import { app, ipcMain } from 'electron'; -import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from 'fs'; +import { + existsSync, + mkdirSync, + readdirSync, + readFileSync, + statSync, + writeFileSync, +} from 'fs'; import { IPC } from './ipc-events.const'; import { answerRenderer } from './better-ipc'; import { LocalBackupMeta } from '../src/app/imex/local-backup/local-backup.model'; @@ -24,22 +31,27 @@ export function initBackupAdapter(backupDir: string) { if (!files.length) { return false; } - const filesWithMeta: LocalBackupMeta[] = files.map((fileName: string): LocalBackupMeta => ({ - name: fileName, - path: path.join(BACKUP_DIR, fileName), - folder: BACKUP_DIR, - created: statSync(path.join(BACKUP_DIR, fileName)).mtime.getTime() - })); + const filesWithMeta: LocalBackupMeta[] = files.map( + (fileName: string): LocalBackupMeta => ({ + name: fileName, + path: path.join(BACKUP_DIR, fileName), + folder: BACKUP_DIR, + created: statSync(path.join(BACKUP_DIR, fileName)).mtime.getTime(), + }), + ); filesWithMeta.sort((a: LocalBackupMeta, b: LocalBackupMeta) => a.created - b.created); - console.log('Avilable Backup Files: ', (filesWithMeta?.map && filesWithMeta.map(f => f.path))); + console.log( + 'Avilable Backup Files: ', + filesWithMeta?.map && filesWithMeta.map((f) => f.path), + ); return filesWithMeta.reverse()[0]; }); // RESTORE_BACKUP answerRenderer(IPC.BACKUP_LOAD_DATA, (backupPath): string => { console.log('Reading backup file: ', backupPath); - return readFileSync(backupPath, {encoding: 'utf8'}); + return readFileSync(backupPath, { encoding: 'utf8' }); }); } @@ -64,11 +76,7 @@ function getDateStr(): string { const mm = today.getMonth() + 1; // January is 0! const yyyy = today.getFullYear(); - const dds = (dd < 10) - ? '0' + dd - : dd.toString(); - const mms = (mm < 10) - ? '0' + mm - : mm.toString(); + const dds = dd < 10 ? '0' + dd : dd.toString(); + const mms = mm < 10 ? '0' + mm : mm.toString(); return `${yyyy}-${mms}-${dds}`; } diff --git a/electron/better-ipc.ts b/electron/better-ipc.ts index 921252e71..5d18c7714 100644 --- a/electron/better-ipc.ts +++ b/electron/better-ipc.ts @@ -1,10 +1,14 @@ import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; // TODO make available for both -const getSendChannel = channel => `%better-ipc-send-channel-${channel}`; +const getSendChannel = (channel) => `%better-ipc-send-channel-${channel}`; // TODO add all typing -export const answerRenderer = (browserWindowOrChannel, channelOrCallback, callbackOrNothing?) => { +export const answerRenderer = ( + browserWindowOrChannel, + channelOrCallback, + callbackOrNothing?, +) => { let window; let channel; let callback; @@ -25,7 +29,9 @@ export const answerRenderer = (browserWindowOrChannel, channelOrCallback, callba const sendChannel = getSendChannel(channel); const listener = async (event: IpcMainEvent, data) => { - const browserWindow: BrowserWindow | null = BrowserWindow.fromWebContents(event.sender); + const browserWindow: BrowserWindow | null = BrowserWindow.fromWebContents( + event.sender, + ); if (window && window.id !== browserWindow?.id) { return; @@ -37,7 +43,7 @@ export const answerRenderer = (browserWindowOrChannel, channelOrCallback, callba } }; - const {dataChannel, errorChannel, userData} = data; + const { dataChannel, errorChannel, userData } = data; try { send(dataChannel, await callback(userData, browserWindow)); diff --git a/electron/dbus.ts b/electron/dbus.ts index dfaf18316..c0f5bc4be 100644 --- a/electron/dbus.ts +++ b/electron/dbus.ts @@ -29,7 +29,7 @@ let iface; function init(params) { sessionBus = dbus.sessionBus(); -// Check the connection was successful + // Check the connection was successful if (!sessionBus) { isDBusError = true; errorHandler(`DBus: Could not connect to the DBus session bus.`); @@ -39,7 +39,9 @@ function init(params) { // If there was an error, warn user and fail if (e) { isDBusError = true; - errorHandler(`DBus: Could not request service name ${serviceName}, the error was: ${e}.`); + errorHandler( + `DBus: Could not request service name ${serviceName}, the error was: ${e}.`, + ); } // Return code 0x1 means we successfully had the name @@ -52,11 +54,13 @@ function init(params) { */ } else { isDBusError = true; - errorHandler(`DBus: Failed to request service name '${serviceName}'.Check what return code '${retCode}' means.`); + errorHandler( + `DBus: Failed to request service name '${serviceName}'.Check what return code '${retCode}' means.`, + ); } }); -// Function called when we have successfully got the service name we wanted + // Function called when we have successfully got the service name we wanted function proceed() { // First, we need to create our interface description (here we will only expose method calls) ifaceDesc = { @@ -108,7 +112,7 @@ function init(params) { }, emit: () => { // no nothing, as usual - } + }, }; // Now we need to actually export our interface on our object @@ -144,20 +148,22 @@ if (!isDBusError) { } if (iface) { - iface.emit('pomodoroUpdate', (isOnBreak ? 1 : 0), currentSessionTime, currentSessionInitialTime); + iface.emit( + 'pomodoroUpdate', + isOnBreak ? 1 : 0, + currentSessionTime, + currentSessionInitialTime, + ); } else { errorHandler('DBus: interface not ready yet'); isErrorShownOnce = true; } - } + }, }; } else { module.exports = { - init: () => { - }, - setTask: () => { - }, - updatePomodoro: () => { - } + init: () => {}, + setTask: () => {}, + updatePomodoro: () => {}, }; } diff --git a/electron/debug.ts b/electron/debug.ts index 135642076..3c4df0e62 100644 --- a/electron/debug.ts +++ b/electron/debug.ts @@ -4,16 +4,16 @@ import OpenDevToolsOptions = Electron.OpenDevToolsOptions; const electron = require('electron'); const localShortcut = require('electron-localshortcut'); -const {app, BrowserWindow} = electron; +const { app, BrowserWindow } = electron; const isMacOS = process.platform === 'darwin'; const devToolsOptions: OpenDevToolsOptions = { - mode: 'bottom' + mode: 'bottom', }; function toggleDevTools(win = BrowserWindow.getFocusedWindow()) { if (win) { - const {webContents} = win; + const { webContents } = win; if (webContents.isDevToolsOpened()) { webContents.closeDevTools(); } else { @@ -72,11 +72,14 @@ function inspectElements() { // }; export const initDebug = (opts, isAddReload) => { - opts = Object.assign({ - enabled: null, - showDevTools: true, - ...devToolsOptions, - }, opts); + opts = Object.assign( + { + enabled: null, + showDevTools: true, + ...devToolsOptions, + }, + opts, + ); console.log(opts); if (opts.enabled === false) { @@ -114,5 +117,3 @@ export const initDebug = (opts, isAddReload) => { } }); }; - - diff --git a/electron/error-handler.ts b/electron/error-handler.ts index ca393ef20..4eb4dc5dc 100644 --- a/electron/error-handler.ts +++ b/electron/error-handler.ts @@ -42,7 +42,10 @@ function _handleError(e, additionalLogInfo, errObj) { stack, }); } else { - console.error('ERR', 'Electron Error: Frontend not loaded. Could not send error to renderer.'); + console.error( + 'ERR', + 'Electron Error: Frontend not loaded. Could not send error to renderer.', + ); error('Electron Error: Frontend not loaded. Could not send error to renderer.'); throw errObj; } diff --git a/electron/git-log.ts b/electron/git-log.ts index 1901f5349..18472c22e 100644 --- a/electron/git-log.ts +++ b/electron/git-log.ts @@ -3,16 +3,20 @@ import { IPC } from './ipc-events.const'; export const getGitLog = (data) => { const exec = require('child_process').exec; - const cmd = 'git --no-pager log --graph --pretty=format:\'%s (%cr) <%an>\' --abbrev-commit --since=4am'; - - exec(cmd, { - cwd: data.cwd - }, (error, stdout) => { - const mainWin = getWin(); - mainWin.webContents.send(IPC.GIT_LOG_RESPONSE, { - stdout, - requestId: data.requestId - }); - }); + const cmd = + "git --no-pager log --graph --pretty=format:'%s (%cr) <%an>' --abbrev-commit --since=4am"; + exec( + cmd, + { + cwd: data.cwd, + }, + (error, stdout) => { + const mainWin = getWin(); + mainWin.webContents.send(IPC.GIT_LOG_RESPONSE, { + stdout, + requestId: data.requestId, + }); + }, + ); }; diff --git a/electron/indicator.ts b/electron/indicator.ts index bd0c1386f..fd2eae841 100644 --- a/electron/indicator.ts +++ b/electron/indicator.ts @@ -7,7 +7,7 @@ import { getWin } from './main-window'; let tray; let isIndicatorRunning = false; let DIR: string; -let shouldUseDarkColors : boolean; +let shouldUseDarkColors: boolean; const isGnomeShellExtensionRunning = false; @@ -16,7 +16,7 @@ export const initIndicator = ({ quitApp, app, ICONS_FOLDER, - forceDarkTray + forceDarkTray, }: { showApp: () => void; quitApp: () => void; @@ -30,9 +30,7 @@ export const initIndicator = ({ initAppListeners(app); initListeners(); - const suf = shouldUseDarkColors - ? '-d.png' - : '-l.png'; + const suf = shouldUseDarkColors ? '-d.png' : '-l.png'; tray = new Tray(DIR + `stopped${suf}`); tray.setContextMenu(createContextMenu(showApp, quitApp)); @@ -55,10 +53,8 @@ function initAppListeners(app) { } function initListeners() { - ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, {progress}) => { - const suf = shouldUseDarkColors - ? '-d' - : '-l'; + ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress }) => { + const suf = shouldUseDarkColors ? '-d' : '-l'; if (typeof progress === 'number' && progress > 0 && isFinite(progress)) { const f = Math.min(Math.round(progress * 15), 15); const t = DIR + `running-anim${suf}/${f || 0}.png`; @@ -86,9 +82,7 @@ function initListeners() { tray.setTitle(msg); } else { tray.setTitle(''); - const suf = shouldUseDarkColors - ? '-d.png' - : '-l.png'; + const suf = shouldUseDarkColors ? '-d.png' : '-l.png'; setTrayIcon(tray, DIR + `stopped${suf}`); } } @@ -133,11 +127,13 @@ function createIndicatorStr(task): string { function createContextMenu(showApp, quitApp) { return Menu.buildFromTemplate([ { - label: 'Show App', click: showApp + label: 'Show App', + click: showApp, }, { - label: 'Quit', click: quitApp - } + label: 'Quit', + click: quitApp, + }, ]); } @@ -146,6 +142,7 @@ export const isRunning = () => { }; let curIco: string; + function setTrayIcon(tr: Tray, icoPath: string) { if (icoPath !== curIco) { curIco = icoPath; diff --git a/electron/ipc-events.const.ts b/electron/ipc-events.const.ts index f0d116877..fb259508a 100644 --- a/electron/ipc-events.const.ts +++ b/electron/ipc-events.const.ts @@ -53,4 +53,3 @@ export enum IPC { maybe_PROJECT_CHANGED = 'PROJECT_CHANGED', maybe_COMPLETE_DATA_RELOAD = 'COMPLETE_DATA_RELOAD', } - diff --git a/electron/jira.ts b/electron/jira.ts index eb0c734b0..548dc3491 100644 --- a/electron/jira.ts +++ b/electron/jira.ts @@ -7,8 +7,17 @@ import { JiraCfg } from '../src/app/features/issue/providers/jira/jira.model'; import fetch from 'node-fetch'; import { Agent } from 'https'; -export const sendJiraRequest = ({requestId, requestInit, url, jiraCfg}: - { requestId: string; requestInit: RequestInit; url: string, jiraCfg: JiraCfg }) => { +export const sendJiraRequest = ({ + requestId, + requestInit, + url, + jiraCfg, +}: { + requestId: string; + requestInit: RequestInit; + url: string; + jiraCfg: JiraCfg; +}) => { const mainWin = getWin(); // console.log('--------------------------------------------------------------------'); // console.log(url); @@ -19,11 +28,11 @@ export const sendJiraRequest = ({requestId, requestInit, url, jiraCfg}: // allow self signed certificates ...(jiraCfg && jiraCfg.isAllowSelfSignedCertificate ? { - agent: new Agent({ - rejectUnauthorized: false, - }) - } - : {}) + agent: new Agent({ + rejectUnauthorized: false, + }), + } + : {}), }) .then((response) => { // console.log('JIRA_RAW_RESPONSE', response); @@ -31,14 +40,13 @@ export const sendJiraRequest = ({requestId, requestInit, url, jiraCfg}: console.log('Jira Error Error Response ELECTRON: ', response); try { console.log(JSON.stringify(response)); - } catch (e) { - } + } catch (e) {} throw Error(response.statusText); } return response; }) - .then(res => res.text()) - .then(text => text ? JSON.parse(text) : {}) + .then((res) => res.text()) + .then((text) => (text ? JSON.parse(text) : {})) .then((response) => { mainWin.webContents.send(IPC.JIRA_CB_EVENT, { response, @@ -56,7 +64,7 @@ export const sendJiraRequest = ({requestId, requestInit, url, jiraCfg}: // TODO simplify and do encoding in frontend service export const setupRequestHeadersForImages = (jiraCfg: JiraCfg, wonkyCookie?: string) => { - const {host, protocol} = parseHostAndPort(jiraCfg); + const { host, protocol } = parseHostAndPort(jiraCfg); // TODO export to util fn const _b64EncodeUnicode = (str) => { @@ -64,12 +72,12 @@ export const setupRequestHeadersForImages = (jiraCfg: JiraCfg, wonkyCookie?: str }; const encoded = _b64EncodeUnicode(`${jiraCfg.userName}:${jiraCfg.password}`); const filter = { - urls: [`${protocol}://${host}/*`] + urls: [`${protocol}://${host}/*`], }; if (jiraCfg.isWonkyCookieMode && !wonkyCookie) { session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => { - callback({cancel: true}); + callback({ cancel: true }); }); } @@ -81,14 +89,16 @@ export const setupRequestHeadersForImages = (jiraCfg: JiraCfg, wonkyCookie?: str } else { details.requestHeaders.authorization = `Basic ${encoded}`; } - callback({requestHeaders: details.requestHeaders}); + callback({ requestHeaders: details.requestHeaders }); }); }; const MATCH_PROTOCOL_REG_EX = /(^[^:]+):\/\//; const MATCH_PORT_REG_EX = /:\d{2,4}/; -const parseHostAndPort = (config: JiraCfg): { host: string, protocol: string, port: number } => { +const parseHostAndPort = ( + config: JiraCfg, +): { host: string; protocol: string; port: number } => { let host: string = config.host as string; let protocol; let port; @@ -118,5 +128,5 @@ const parseHostAndPort = (config: JiraCfg): { host: string, protocol: string, po } // console.log({host, protocol, port}); - return {host, protocol, port}; + return { host, protocol, port }; }; diff --git a/electron/lazy-set-interval.ts b/electron/lazy-set-interval.ts index b2291243b..b061613b5 100644 --- a/electron/lazy-set-interval.ts +++ b/electron/lazy-set-interval.ts @@ -1,4 +1,7 @@ -export const lazySetInterval = (func: () => void, intervalDuration: number): () => void => { +export const lazySetInterval = ( + func: () => void, + intervalDuration: number, +): (() => void) => { let lastTimeoutId: any; const interval = () => { diff --git a/electron/lockscreen.ts b/electron/lockscreen.ts index b482c8897..0373d420b 100644 --- a/electron/lockscreen.ts +++ b/electron/lockscreen.ts @@ -2,14 +2,16 @@ import { exec } from 'child_process'; export default (cb?, customCommands?) => { const lockCommands = customCommands || { - darwin: '/System/Library/CoreServices/"Menu Extras"/User.menu/Contents/Resources/CGSession -suspend', + darwin: + '/System/Library/CoreServices/"Menu Extras"/User.menu/Contents/Resources/CGSession -suspend', win32: 'rundll32.exe user32.dll, LockWorkStation', - linux: '(hash gnome-screensaver-command 2>/dev/null && gnome-screensaver-command -l) || (hash dm-tool 2>/dev/null && dm-tool lock) || (qdbus org.freedesktop.ScreenSaver /ScreenSaver Lock)' + linux: + '(hash gnome-screensaver-command 2>/dev/null && gnome-screensaver-command -l) || (hash dm-tool 2>/dev/null && dm-tool lock) || (qdbus org.freedesktop.ScreenSaver /ScreenSaver Lock)', }; if (Object.keys(lockCommands).indexOf(process.platform) === -1) { throw new Error(`lockscreen doesn't support your platform (${process.platform})`); } else { - exec(lockCommands[process.platform], (err, stdout) => cb ? cb(err, stdout) : null); + exec(lockCommands[process.platform], (err, stdout) => (cb ? cb(err, stdout) : null)); } }; diff --git a/electron/main-window.ts b/electron/main-window.ts index c4b7f7431..ecfe3796c 100644 --- a/electron/main-window.ts +++ b/electron/main-window.ts @@ -7,7 +7,7 @@ import { Menu, MenuItemConstructorOptions, MessageBoxReturnValue, - shell + shell, } from 'electron'; import { errorHandler } from './error-handler'; import { join, normalize } from 'path'; @@ -23,7 +23,7 @@ const mainWinModule: { isAppReady: boolean; } = { win: undefined, - isAppReady: false + isAppReady: false, }; export const getWin = (): BrowserWindow => { @@ -65,7 +65,7 @@ export const createWindow = ({ const mainWindowState = windowStateKeeper({ defaultWidth: 800, - defaultHeight: 800 + defaultHeight: 800, }); mainWin = new BrowserWindow({ @@ -81,18 +81,18 @@ export const createWindow = ({ nodeIntegration: true, // make remote module work with those two settings enableRemoteModule: true, - contextIsolation: false + contextIsolation: false, }, - icon: ICONS_FOLDER + '/icon_256x256.png' + icon: ICONS_FOLDER + '/icon_256x256.png', }); mainWindowState.manage(mainWin); const url = customUrl ? customUrl - : (IS_DEV) - ? 'http://localhost:4200' - : format({ + : IS_DEV + ? 'http://localhost:4200' + : format({ pathname: normalize(join(__dirname, '../dist/index.html')), protocol: 'file:', slashes: true, @@ -106,7 +106,7 @@ export const createWindow = ({ console.log('No custom styles detected at ' + CSS_FILE_PATH); } else { console.log('Loading custom styles from ' + CSS_FILE_PATH); - const styles = readFileSync(CSS_FILE_PATH, {encoding: 'utf8'}); + const styles = readFileSync(CSS_FILE_PATH, { encoding: 'utf8' }); mainWin.webContents.insertCSS(styles).then(console.log).catch(console.error); } }); @@ -142,8 +142,7 @@ function initWinEventListeners(app: any) { event.preventDefault(); // needed for mac; especially for jira urls we might have a host like this www.host.de// const urlObj = new URL(url); - urlObj.pathname = urlObj.pathname - .replace('//', '/'); + urlObj.pathname = urlObj.pathname.replace('//', '/'); const wellFormedUrl = urlObj.toString(); const wasOpened = shell.openExternal(wellFormedUrl); if (!wasOpened) { @@ -162,27 +161,30 @@ function initWinEventListeners(app: any) { function createMenu(quitApp) { // Create application menu to enable copy & pasting on MacOS - const menuTpl = [{ - label: 'Application', - submenu: [ - {label: 'About Super Productivity', selector: 'orderFrontStandardAboutPanel:'}, - {type: 'separator'}, - { - label: 'Quit', click: quitApp - } - ] - }, { - label: 'Edit', - submenu: [ - {label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:'}, - {label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:'}, - {type: 'separator'}, - {label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:'}, - {label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:'}, - {label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:'}, - {label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:'} - ] - } + const menuTpl = [ + { + label: 'Application', + submenu: [ + { label: 'About Super Productivity', selector: 'orderFrontStandardAboutPanel:' }, + { type: 'separator' }, + { + label: 'Quit', + click: quitApp, + }, + ], + }, + { + label: 'Edit', + submenu: [ + { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' }, + { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' }, + { type: 'separator' }, + { label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:' }, + { label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:' }, + { label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:' }, + { label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:' }, + ], + }, ]; const menuTplOUT = menuTpl as MenuItemConstructorOptions[]; @@ -191,9 +193,7 @@ function createMenu(quitApp) { } // TODO this is ugly as f+ck -const appCloseHandler = ( - app: App, -) => { +const appCloseHandler = (app: App) => { let ids: string[] = []; const _quitApp = () => { @@ -201,14 +201,14 @@ const appCloseHandler = ( mainWin.close(); }; - ipcMain.on(IPC.REGISTER_BEFORE_CLOSE, (ev, {id}) => { + ipcMain.on(IPC.REGISTER_BEFORE_CLOSE, (ev, { id }) => { ids.push(id); }); - ipcMain.on(IPC.UNREGISTER_BEFORE_CLOSE, (ev, {id}) => { - ids = ids.filter(idIn => idIn !== id); + ipcMain.on(IPC.UNREGISTER_BEFORE_CLOSE, (ev, { id }) => { + ids = ids.filter((idIn) => idIn !== id); }); - ipcMain.on(IPC.BEFORE_CLOSE_DONE, (ev, {id}) => { - ids = ids.filter(idIn => idIn !== id); + ipcMain.on(IPC.BEFORE_CLOSE_DONE, (ev, { id }) => { + ids = ids.filter((idIn) => idIn !== id); console.log(IPC.BEFORE_CLOSE_DONE, id, ids); if (ids.length === 0) { mainWin.close(); @@ -230,20 +230,21 @@ const appCloseHandler = ( return; } if (appCfg && appCfg.misc.isConfirmBeforeExit && !(app as any).isQuiting) { - dialog.showMessageBox(mainWin, - { + dialog + .showMessageBox(mainWin, { type: 'question', buttons: ['Yes', 'No'], title: 'Confirm', - message: 'Are you sure you want to quit?' - }).then((choice: MessageBoxReturnValue) => { - if (choice.response === 1) { - return; - } else if (choice.response === 0) { - _quitApp(); - return; - } - }); + message: 'Are you sure you want to quit?', + }) + .then((choice: MessageBoxReturnValue) => { + if (choice.response === 1) { + return; + } else if (choice.response === 0) { + _quitApp(); + return; + } + }); } else { _quitApp(); } @@ -253,9 +254,7 @@ const appCloseHandler = ( }); }; -const appMinimizeHandler = ( - app: App, -) => { +const appMinimizeHandler = (app: App) => { if (!(app as any).isQuiting) { mainWin.on('minimize', (event) => { getSettings(mainWin, (appCfg) => { diff --git a/electron/main.ts b/electron/main.ts index 04b26c0e3..35505e827 100644 --- a/electron/main.ts +++ b/electron/main.ts @@ -1,5 +1,13 @@ 'use strict'; -import { App, app, BrowserWindow, globalShortcut, ipcMain, powerMonitor, protocol } from 'electron'; +import { + App, + app, + BrowserWindow, + globalShortcut, + ipcMain, + powerMonitor, + protocol, +} from 'electron'; import * as electronDl from 'electron-dl'; import { info } from 'electron-log'; @@ -23,7 +31,7 @@ const ICONS_FOLDER = __dirname + '/assets/icons/'; const IS_MAC = process.platform === 'darwin'; const IS_LINUX = process.platform === 'linux'; const DESKTOP_ENV = process.env.DESKTOP_SESSION; -const IS_GNOME = (DESKTOP_ENV === 'gnome' || DESKTOP_ENV === 'gnome-xorg'); +const IS_GNOME = DESKTOP_ENV === 'gnome' || DESKTOP_ENV === 'gnome-xorg'; const IS_DEV = process.env.NODE_ENV === 'DEV'; let isShowDevTools: boolean = IS_DEV; @@ -72,11 +80,11 @@ const appIN: MyApp = app; // NOTE: to get rid of the warning => https://github.com/electron/electron/issues/18397 appIN.allowRendererProcessReuse = true; -initDebug({showDevTools: isShowDevTools}, IS_DEV); +initDebug({ showDevTools: isShowDevTools }, IS_DEV); // NOTE: opening the folder crashes the mas build if (!IS_MAC) { - electronDl({openFolderWhenDone: true}); + electronDl({ openFolderWhenDone: true }); } let mainWin: BrowserWindow; // keep app active to keep time tracking running @@ -133,7 +141,9 @@ appIN.on('ready', () => { const sendIdleMsgIfOverMin = (idleTime) => { // sometimes when starting a second instance we get here although we don't want to if (!mainWin) { - info('special case occurred when trackTimeFn is called even though, this is a second instance of the app'); + info( + 'special case occurred when trackTimeFn is called even though, this is a second instance of the app', + ); return; } @@ -229,9 +239,9 @@ ipcMain.on(IPC.LOCK_SCREEN, () => { } }); -ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, {progress, mode}) => { +ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress, mode }) => { if (mainWin) { - mainWin.setProgressBar(Math.min(Math.max(progress, 0), 1), {mode}); + mainWin.setProgressBar(Math.min(Math.max(progress, 0), 1), { mode }); } }); @@ -239,9 +249,12 @@ ipcMain.on(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, (ev, cfg) => { registerShowAppShortCuts(cfg); }); -ipcMain.on(IPC.JIRA_SETUP_IMG_HEADERS, (ev, {jiraCfg, wonkyCookie}: { jiraCfg: JiraCfg; wonkyCookie?: string }) => { - setupRequestHeadersForImages(jiraCfg, wonkyCookie); -}); +ipcMain.on( + IPC.JIRA_SETUP_IMG_HEADERS, + (ev, { jiraCfg, wonkyCookie }: { jiraCfg: JiraCfg; wonkyCookie?: string }) => { + setupRequestHeadersForImages(jiraCfg, wonkyCookie); + }, +); ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => { sendJiraRequest(request); @@ -263,7 +276,7 @@ function createIndicator() { showApp, quitApp, ICONS_FOLDER, - forceDarkTray + forceDarkTray, }); } @@ -290,7 +303,7 @@ function registerShowAppShortCuts(cfg: KeyboardConfig) { if (cfg) { Object.keys(cfg) - .filter((key: (keyof KeyboardConfig)) => GLOBAL_KEY_CFG_KEYS.includes(key)) + .filter((key: keyof KeyboardConfig) => GLOBAL_KEY_CFG_KEYS.includes(key)) .forEach((key) => { let actionFn: () => void; const shortcut = cfg[key]; @@ -359,7 +372,9 @@ function showOrFocus(passedWin) { // sometimes when starting a second instance we get here although we don't want to if (!win) { - info('special case occurred when showOrFocus is called even though, this is a second instance of the app'); + info( + 'special case occurred when showOrFocus is called even though, this is a second instance of the app', + ); return; } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 95a768f46..4031f4db0 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,7 +4,7 @@ import { HostListener, OnDestroy, ViewChild, - ViewContainerRef + ViewContainerRef, } from '@angular/core'; import { ChromeExtensionInterfaceService } from './core/chrome-extension-interface/chrome-extension-interface.service'; import { ShortcutService } from './core-ui/shortcut/shortcut.service'; @@ -49,20 +49,15 @@ const productivityTip: string[] = w.productivityTips && w.productivityTips[w.ran selector: 'app-root', templateUrl: './app.component.html', styleUrls: ['./app.component.scss'], - animations: [ - blendInOutAnimation, - expandAnimation, - warpRouteAnimation, - fadeAnimation - ], + animations: [blendInOutAnimation, expandAnimation, warpRouteAnimation, fadeAnimation], changeDetection: ChangeDetectionStrategy.OnPush, }) export class AppComponent implements OnDestroy { productivityTipTitle: string = productivityTip && productivityTip[0]; productivityTipText: string = productivityTip && productivityTip[1]; - @ViewChild('notesElRef', {read: ViewContainerRef}) notesElRef?: ViewContainerRef; - @ViewChild('sideNavElRef', {read: ViewContainerRef}) sideNavElRef?: ViewContainerRef; + @ViewChild('notesElRef', { read: ViewContainerRef }) notesElRef?: ViewContainerRef; + @ViewChild('sideNavElRef', { read: ViewContainerRef }) sideNavElRef?: ViewContainerRef; isRTL: boolean = false; @@ -119,9 +114,15 @@ export class AppComponent implements OnDestroy { this._initElectronErrorHandler(); this._uiHelperService.initElectron(); - (this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.TRANSFER_SETTINGS_REQUESTED, () => { - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.TRANSFER_SETTINGS_TO_ELECTRON, this._globalConfigService.cfg); - }); + (this._electronService.ipcRenderer as typeof ipcRenderer).on( + IPC.TRANSFER_SETTINGS_REQUESTED, + () => { + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.TRANSFER_SETTINGS_TO_ELECTRON, + this._globalConfigService.cfg, + ); + }, + ); } else { // WEB VERSION if (this._swUpdate.isEnabled) { @@ -164,7 +165,9 @@ export class AppComponent implements OnDestroy { @HostListener('document:paste', ['$event']) async onPaste(ev: ClipboardEvent) { - if (await this.workContextService.isActiveWorkContextProject$.pipe(first()).toPromise()) { + if ( + await this.workContextService.isActiveWorkContextProject$.pipe(first()).toPromise() + ) { this._bookmarkService.createFromPaste(ev); } } @@ -184,14 +187,14 @@ export class AppComponent implements OnDestroy { label: T.APP.B_INSTALL.INSTALL, fn: () => { e.prompt(); - } + }, }, action2: { label: T.APP.B_INSTALL.IGNORE, fn: () => { sessionStorage.setItem(SS_WEB_APP_INSTALL, 'true'); - } - } + }, + }, }); } @@ -200,11 +203,15 @@ export class AppComponent implements OnDestroy { } scrollToNotes() { - (this.notesElRef as ViewContainerRef).element.nativeElement.scrollIntoView({behavior: 'smooth'}); + (this.notesElRef as ViewContainerRef).element.nativeElement.scrollIntoView({ + behavior: 'smooth', + }); } scrollToSidenav() { - (this.sideNavElRef as ViewContainerRef).element.nativeElement.scrollIntoView({behavior: 'smooth'}); + (this.sideNavElRef as ViewContainerRef).element.nativeElement.scrollIntoView({ + behavior: 'smooth', + }); } ngOnDestroy() { @@ -212,21 +219,26 @@ export class AppComponent implements OnDestroy { } private _initElectronErrorHandler() { - (this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.ERROR, (ev, data: { - error: any; - stack: any; - errorStr: string | unknown; - }) => { - const errMsg = (typeof data.errorStr === 'string') - ? data.errorStr - : ' INVALID ERROR MSG :( '; + (this._electronService.ipcRenderer as typeof ipcRenderer).on( + IPC.ERROR, + ( + ev, + data: { + error: any; + stack: any; + errorStr: string | unknown; + }, + ) => { + const errMsg = + typeof data.errorStr === 'string' ? data.errorStr : ' INVALID ERROR MSG :( '; - this._snackService.open({ - msg: errMsg, - type: 'ERROR' - }); - console.error(data); - }); + this._snackService.open({ + msg: errMsg, + type: 'ERROR', + }); + console.error(data); + }, + ); } private _initOfflineBanner() { @@ -246,46 +258,48 @@ export class AppComponent implements OnDestroy { private _requestPersistence() { if (navigator.storage) { // try to avoid data-loss - Promise.all([ - navigator.storage.persisted(), - ]).then(([persisted]): any => { - if (!persisted) { - return navigator.storage.persist() - .then(granted => { + Promise.all([navigator.storage.persisted()]) + .then(([persisted]): any => { + if (!persisted) { + return navigator.storage.persist().then((granted) => { if (granted) { console.log('Persistent store granted'); } else { const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED; console.warn('Persistence not allowed'); - this._snackService.open({msg}); + this._snackService.open({ msg }); } }); - - } else { - console.log('Persistence already allowed'); - } - }).catch((e) => { - console.log(e); - const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED; - this._snackService.open({msg}); - }); + } else { + console.log('Persistence already allowed'); + } + }) + .catch((e) => { + console.log(e); + const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED; + this._snackService.open({ msg }); + }); } } private _checkAvailableStorage() { if (environment.production) { if ('storage' in navigator && 'estimate' in navigator.storage) { - navigator.storage.estimate().then(({usage, quota}) => { + navigator.storage.estimate().then(({ usage, quota }) => { const u = usage || 0; const q = quota || 0; - const percentUsed = Math.round(u / q * 100); + const percentUsed = Math.round((u / q) * 100); const usageInMib = Math.round(u / (1024 * 1024)); const quotaInMib = Math.round(q / (1024 * 1024)); const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`; console.log(details); - if ((quotaInMib - usageInMib) <= 333) { - alert(`There is only very little disk space available (${quotaInMib - usageInMib}mb). This might affect how the app is running.`); + if (quotaInMib - usageInMib <= 333) { + alert( + `There is only very little disk space available (${ + quotaInMib - usageInMib + }mb). This might affect how the app is running.`, + ); } }); } diff --git a/src/app/app.constants.ts b/src/app/app.constants.ts index f8a3feb10..0bd269461 100644 --- a/src/app/app.constants.ts +++ b/src/app/app.constants.ts @@ -1,5 +1,5 @@ export const WORKLOG_DATE_STR_FORMAT = 'YYYY-MM-DD'; -export const IS_ELECTRON = (navigator.userAgent.toLowerCase().indexOf(' electron/') > -1); +export const IS_ELECTRON = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1; export const TRACKING_INTERVAL = 1000; export const MODEL_VERSION_KEY = '__modelVersion'; @@ -118,10 +118,7 @@ export const AUTO_SWITCH_LNGS: LanguageCode[] = [ LanguageCode.tr, ]; -export const RTL_LANGUAGES: LanguageCode[] = [ - LanguageCode.ar, - LanguageCode.fa -]; +export const RTL_LANGUAGES: LanguageCode[] = [LanguageCode.ar, LanguageCode.fa]; export enum THEME_COLOR_MAP { 'light-blue' = '#03a9f4', diff --git a/src/app/app.guard.ts b/src/app/app.guard.ts index 524592e59..183bea93f 100644 --- a/src/app/app.guard.ts +++ b/src/app/app.guard.ts @@ -1,5 +1,11 @@ import { Injectable } from '@angular/core'; -import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router'; +import { + ActivatedRouteSnapshot, + CanActivate, + Router, + RouterStateSnapshot, + UrlTree, +} from '@angular/router'; import { WorkContextService } from './features/work-context/work-context.service'; import { Observable, of } from 'rxjs'; import { concatMap, map, switchMap, take } from 'rxjs/operators'; @@ -8,60 +14,61 @@ import { TagService } from './features/tag/tag.service'; import { ProjectService } from './features/project/project.service'; import { DataInitService } from './core/data-init/data-init.service'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class ActiveWorkContextGuard implements CanActivate { - constructor( - private _workContextService: WorkContextService, - private _router: Router, - ) { - } + constructor(private _workContextService: WorkContextService, private _router: Router) {} - canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable { return this._workContextService.activeWorkContextTypeAndId$.pipe( take(1), - switchMap(({activeType, activeId}) => { - const {subPageType, param} = next.params; - const base = activeType === WorkContextType.TAG - ? 'tag' - : 'project'; + switchMap(({ activeType, activeId }) => { + const { subPageType, param } = next.params; + const base = activeType === WorkContextType.TAG ? 'tag' : 'project'; const url = `/${base}/${activeId}/${subPageType}${param ? '/' + param : ''}`; return of(this._router.parseUrl(url)); - }) + }), ); } } -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class ValidTagIdGuard implements CanActivate { constructor( private _tagService: TagService, private _dataInitService: DataInitService, - ) { - } + ) {} - canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const {id} = next.params; + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable { + const { id } = next.params; return this._dataInitService.isAllDataLoadedInitially$.pipe( concatMap(() => this._tagService.getTagById$(id)), take(1), - map(tag => !!tag), + map((tag) => !!tag), ); } } -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class ValidProjectIdGuard implements CanActivate { constructor( private _projectService: ProjectService, private _dataInitService: DataInitService, - ) { - } + ) {} - canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable { - const {id} = next.params; + canActivate( + next: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + ): Observable { + const { id } = next.params; return this._dataInitService.isAllDataLoadedInitially$.pipe( concatMap(() => this._projectService.getByIdOnce$(id)), - map(project => !!project), + map((project) => !!project), ); } } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index dc09d2d1d..915e6b698 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -1,4 +1,4 @@ -import { BrowserModule, HAMMER_GESTURE_CONFIG, HammerModule } from '@angular/platform-browser'; +import { BrowserModule, HAMMER_GESTURE_CONFIG, HammerModule, } from '@angular/platform-browser'; import { ErrorHandler, NgModule } from '@angular/core'; import { AppComponent } from './app.component'; import { ServiceWorkerModule } from '@angular/service-worker'; @@ -47,9 +47,7 @@ export function createTranslateLoader(http: HttpClient) { } @NgModule({ - declarations: [ - AppComponent, - ], + declarations: [AppComponent], imports: [ // Those features need to be included first for store not to mess up, probably because we use it initially at many places ConfigModule, @@ -79,13 +77,12 @@ export function createTranslateLoader(http: HttpClient) { BrowserAnimationsModule, HttpClientModule, HammerModule, - RouterModule.forRoot(APP_ROUTES, {useHash: true, relativeLinkResolution: 'legacy'}), + RouterModule.forRoot(APP_ROUTES, { useHash: true, relativeLinkResolution: 'legacy' }), // NOTE: both need to be present to use forFeature stores - StoreModule.forRoot(reducers, - { - metaReducers: [undoTaskDeleteMetaReducer, actionLoggerReducer], - ...(environment.production - ? { + StoreModule.forRoot(reducers, { + metaReducers: [undoTaskDeleteMetaReducer, actionLoggerReducer], + ...(environment.production + ? { runtimeChecks: { strictStateImmutability: false, strictActionImmutability: false, @@ -93,47 +90,44 @@ export function createTranslateLoader(http: HttpClient) { strictActionSerializability: false, }, } - : { + : { runtimeChecks: { strictStateImmutability: true, strictActionImmutability: true, strictStateSerializability: true, strictActionSerializability: true, }, - }) - } - ), + }), + }), EffectsModule.forRoot([]), - (!environment.production && !environment.stage) ? StoreDevtoolsModule.instrument() : [], + !environment.production && !environment.stage ? StoreDevtoolsModule.instrument() : [], ReactiveFormsModule, FormlyModule.forRoot({ extras: { - immutable: true + immutable: true, }, - validationMessages: [ - {name: 'pattern', message: 'Invalid input'}, - ], + validationMessages: [{ name: 'pattern', message: 'Invalid input' }], + }), + ServiceWorkerModule.register('ngsw-worker.js', { + enabled: !IS_ELECTRON && (environment.production || environment.stage), }), - ServiceWorkerModule.register('ngsw-worker.js', {enabled: !(IS_ELECTRON) && (environment.production || environment.stage)}), TranslateModule.forRoot({ loader: { provide: TranslateLoader, useFactory: createTranslateLoader, - deps: [HttpClient] - } + deps: [HttpClient], + }, }), - EntityDataModule + EntityDataModule, ], bootstrap: [AppComponent], providers: [ - {provide: ErrorHandler, useClass: GlobalErrorHandler}, - {provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig}, + { provide: ErrorHandler, useClass: GlobalErrorHandler }, + { provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig }, ], }) export class AppModule { - constructor( - private _languageService: LanguageService, - ) { + constructor(private _languageService: LanguageService) { this._languageService.setDefault(LanguageCode.en); this._languageService.setFromBrowserLngIfAutoSwitchLng(); } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 6804d74a9..fe7323b91 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -9,88 +9,112 @@ import { ProcrastinationComponent } from './features/procrastination/procrastina import { SchedulePageComponent } from './pages/schedule-page/schedule-page.component'; import { ProjectSettingsPageComponent } from './pages/project-settings-page/project-settings-page.component'; import { TagTaskPageComponent } from './pages/tag-task-page/tag-task-page.component'; -import { ActiveWorkContextGuard, ValidProjectIdGuard, ValidTagIdGuard } from './app.guard'; +import { + ActiveWorkContextGuard, + ValidProjectIdGuard, + ValidTagIdGuard, +} from './app.guard'; import { TagSettingsPageComponent } from './pages/tag-settings-page/tag-settings-page.component'; import { TODAY_TAG } from './features/tag/tag.const'; export const APP_ROUTES: Routes = [ - {path: 'config', component: ConfigPageComponent, data: {page: 'config'}}, - {path: 'schedule', component: SchedulePageComponent, data: {page: 'schedule'}}, - {path: 'procrastination', component: ProcrastinationComponent, data: {page: 'procrastination'}}, + { path: 'config', component: ConfigPageComponent, data: { page: 'config' } }, + { path: 'schedule', component: SchedulePageComponent, data: { page: 'schedule' } }, + { + path: 'procrastination', + component: ProcrastinationComponent, + data: { page: 'procrastination' }, + }, { path: 'tag/:id/tasks', component: TagTaskPageComponent, - data: {page: 'tag-tasks'}, - canActivate: [ValidTagIdGuard] + data: { page: 'tag-tasks' }, + canActivate: [ValidTagIdGuard], }, { path: 'tag/:id/settings', component: TagSettingsPageComponent, - data: {page: 'tag-settings'}, - canActivate: [ValidTagIdGuard] + data: { page: 'tag-settings' }, + canActivate: [ValidTagIdGuard], }, { path: 'tag/:id/worklog', component: WorklogComponent, - data: {page: 'worklog'}, - canActivate: [ValidTagIdGuard] + data: { page: 'worklog' }, + canActivate: [ValidTagIdGuard], }, // {path: 'tag/:id/metrics', component: MetricPageComponent, data: {page: 'metrics'}, canActivate: [ValidContextIdGuard]}, { path: 'tag/:id/daily-summary', component: DailySummaryComponent, - data: {page: 'daily-summary'}, - canActivate: [ValidTagIdGuard] + data: { page: 'daily-summary' }, + canActivate: [ValidTagIdGuard], }, { path: 'tag/:id/daily-summary/:dayStr', component: DailySummaryComponent, - data: {page: 'daily-summary'}, - canActivate: [ValidTagIdGuard] + data: { page: 'daily-summary' }, + canActivate: [ValidTagIdGuard], }, { path: 'project/:id/tasks', component: ProjectTaskPageComponent, - data: {page: 'project-tasks'}, - canActivate: [ValidProjectIdGuard] + data: { page: 'project-tasks' }, + canActivate: [ValidProjectIdGuard], }, { path: 'project/:id/settings', component: ProjectSettingsPageComponent, - data: {page: 'project-settings'}, - canActivate: [ValidProjectIdGuard] + data: { page: 'project-settings' }, + canActivate: [ValidProjectIdGuard], }, { path: 'project/:id/worklog', component: WorklogComponent, - data: {page: 'worklog'}, - canActivate: [ValidProjectIdGuard] + data: { page: 'worklog' }, + canActivate: [ValidProjectIdGuard], }, { path: 'project/:id/metrics', component: MetricPageComponent, - data: {page: 'metrics'}, - canActivate: [ValidProjectIdGuard] + data: { page: 'metrics' }, + canActivate: [ValidProjectIdGuard], }, { path: 'project/:id/daily-summary', component: DailySummaryComponent, - data: {page: 'daily-summary'}, - canActivate: [ValidProjectIdGuard] + data: { page: 'daily-summary' }, + canActivate: [ValidProjectIdGuard], }, { path: 'project/:id/daily-summary/:dayStr', component: DailySummaryComponent, - data: {page: 'daily-summary'}, - canActivate: [ValidProjectIdGuard] + data: { page: 'daily-summary' }, + canActivate: [ValidProjectIdGuard], + }, + { + path: 'project-overview', + component: ProjectOverviewPageComponent, + data: { page: 'project-overview' }, }, - {path: 'project-overview', component: ProjectOverviewPageComponent, data: {page: 'project-overview'}}, - {path: 'active/:subPageType', canActivate: [ActiveWorkContextGuard], component: ConfigPageComponent}, - {path: 'active/:subPageType/:param', canActivate: [ActiveWorkContextGuard], component: ConfigPageComponent}, - {path: 'active', canActivate: [ActiveWorkContextGuard], component: ConfigPageComponent}, + { + path: 'active/:subPageType', + canActivate: [ActiveWorkContextGuard], + component: ConfigPageComponent, + }, + { + path: 'active/:subPageType/:param', + canActivate: [ActiveWorkContextGuard], + component: ConfigPageComponent, + }, + { + path: 'active', + canActivate: [ActiveWorkContextGuard], + component: ConfigPageComponent, + }, - {path: '**', redirectTo: `tag/${TODAY_TAG.id}/tasks`}, + { path: '**', redirectTo: `tag/${TODAY_TAG.id}/tasks` }, ]; diff --git a/src/app/core-ui/core-ui.module.ts b/src/app/core-ui/core-ui.module.ts index 0436e8290..d6925006c 100644 --- a/src/app/core-ui/core-ui.module.ts +++ b/src/app/core-ui/core-ui.module.ts @@ -23,5 +23,4 @@ import { GlobalProgressBarModule } from './global-progress-bar/global-progress-b GlobalProgressBarModule, ], }) -export class CoreUiModule { -} +export class CoreUiModule {} diff --git a/src/app/core-ui/global-progress-bar/global-progress-bar-interceptor.service.ts b/src/app/core-ui/global-progress-bar/global-progress-bar-interceptor.service.ts index 260e11b67..7d48806a3 100644 --- a/src/app/core-ui/global-progress-bar/global-progress-bar-interceptor.service.ts +++ b/src/app/core-ui/global-progress-bar/global-progress-bar-interceptor.service.ts @@ -1,42 +1,50 @@ import { Injectable } from '@angular/core'; -import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { + HttpEvent, + HttpHandler, + HttpInterceptor, + HttpRequest, +} from '@angular/common/http'; import { Observable } from 'rxjs'; import { finalize } from 'rxjs/operators'; import { GlobalProgressBarService } from './global-progress-bar.service'; import axios from 'axios'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class GlobalProgressBarInterceptorService implements HttpInterceptor { + constructor(private globalProgressBarService: GlobalProgressBarService) { + axios.interceptors.request.use( + (config) => { + this.globalProgressBarService.countUp(config.url as string); + return config; + }, + (error) => { + this.globalProgressBarService.countDown(); + return Promise.reject(error); + }, + ); - constructor( - private globalProgressBarService: GlobalProgressBarService, - ) { - - axios.interceptors.request.use((config) => { - this.globalProgressBarService.countUp(config.url as string); - return config; - }, (error) => { - this.globalProgressBarService.countDown(); - return Promise.reject(error); - }); - - axios.interceptors.response.use((response) => { - this.globalProgressBarService.countDown(); - return response; - }, (error) => { - this.globalProgressBarService.countDown(); - return Promise.reject(error); - }); + axios.interceptors.response.use( + (response) => { + this.globalProgressBarService.countDown(); + return response; + }, + (error) => { + this.globalProgressBarService.countDown(); + return Promise.reject(error); + }, + ); } - intercept(req: HttpRequest, next: HttpHandler): Observable> { + intercept( + req: HttpRequest, + next: HttpHandler, + ): Observable> { this.globalProgressBarService.countUp(req.url); return next.handle(req).pipe( finalize(() => { this.globalProgressBarService.countDown(); - }) + }), ); } } - - diff --git a/src/app/core-ui/global-progress-bar/global-progress-bar.component.ts b/src/app/core-ui/global-progress-bar/global-progress-bar.component.ts index d17c21e6a..e2b3d79ea 100644 --- a/src/app/core-ui/global-progress-bar/global-progress-bar.component.ts +++ b/src/app/core-ui/global-progress-bar/global-progress-bar.component.ts @@ -1,17 +1,18 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { GlobalProgressBarService } from './global-progress-bar.service'; -import { fadeAnimation, fadeInOutBottomAnimation, fadeOutAnimation } from '../../ui/animations/fade.ani'; +import { + fadeAnimation, + fadeInOutBottomAnimation, + fadeOutAnimation, +} from '../../ui/animations/fade.ani'; @Component({ selector: 'global-progress-bar', templateUrl: './global-progress-bar.component.html', styleUrls: ['./global-progress-bar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [fadeAnimation, fadeInOutBottomAnimation, fadeOutAnimation] + animations: [fadeAnimation, fadeInOutBottomAnimation, fadeOutAnimation], }) export class GlobalProgressBarComponent { - constructor( - public globalProgressBarService: GlobalProgressBarService, - ) { - } + constructor(public globalProgressBarService: GlobalProgressBarService) {} } diff --git a/src/app/core-ui/global-progress-bar/global-progress-bar.module.ts b/src/app/core-ui/global-progress-bar/global-progress-bar.module.ts index 6ff5f7a8d..59ca21a84 100644 --- a/src/app/core-ui/global-progress-bar/global-progress-bar.module.ts +++ b/src/app/core-ui/global-progress-bar/global-progress-bar.module.ts @@ -7,18 +7,14 @@ import { GlobalProgressBarInterceptorService } from './global-progress-bar-inter @NgModule({ providers: [ - {provide: HTTP_INTERCEPTORS, useClass: GlobalProgressBarInterceptorService, multi: true} + { + provide: HTTP_INTERCEPTORS, + useClass: GlobalProgressBarInterceptorService, + multi: true, + }, ], - declarations: [ - GlobalProgressBarComponent, - ], - imports: [ - CommonModule, - UiModule, - ], - exports: [ - GlobalProgressBarComponent, - ] + declarations: [GlobalProgressBarComponent], + imports: [CommonModule, UiModule], + exports: [GlobalProgressBarComponent], }) -export class GlobalProgressBarModule { -} +export class GlobalProgressBarModule {} diff --git a/src/app/core-ui/global-progress-bar/global-progress-bar.service.ts b/src/app/core-ui/global-progress-bar/global-progress-bar.service.ts index 0f20ffef9..6eedfe2d9 100644 --- a/src/app/core-ui/global-progress-bar/global-progress-bar.service.ts +++ b/src/app/core-ui/global-progress-bar/global-progress-bar.service.ts @@ -6,34 +6,31 @@ import { T } from '../../t.const'; const DELAY = 100; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class GlobalProgressBarService { nrOfRequests$: BehaviorSubject = new BehaviorSubject(0); isShowGlobalProgressBar$: Observable = this.nrOfRequests$.pipe( - map(nr => nr > 0), + map((nr) => nr > 0), distinctUntilChanged(), - switchMap((isShow) => isShow - ? of(true) - : of(false).pipe(delay(DELAY)) - ), + switchMap((isShow) => (isShow ? of(true) : of(false).pipe(delay(DELAY)))), startWith(false), // @see https://blog.angular-university.io/angular-debugging/ delay(0), ); - private _label$: BehaviorSubject = new BehaviorSubject(null); + private _label$: BehaviorSubject = new BehaviorSubject( + null, + ); label$: Observable = this._label$.pipe( distinctUntilChanged(), - switchMap((label: string | null) => !!label - ? of(label) - : of(null).pipe(delay(DELAY)) + switchMap((label: string | null) => + !!label ? of(label) : of(null).pipe(delay(DELAY)), ), // @see https://blog.angular-university.io/angular-debugging/ delay(0), ); - constructor() { - } + constructor() {} countUp(url: string) { this.nrOfRequests$.next(this.nrOfRequests$.getValue() + 1); @@ -53,11 +50,10 @@ export class GlobalProgressBarService { if (PROGRESS_BAR_LABEL_MAP[url]) { return PROGRESS_BAR_LABEL_MAP[url]; } else { - const key = Object.keys(PROGRESS_BAR_LABEL_MAP).find((keyIn) => urlWithoutParams.includes(keyIn)); - return key - ? PROGRESS_BAR_LABEL_MAP[key] - : T.GPB.UNKNOWN; + const key = Object.keys(PROGRESS_BAR_LABEL_MAP).find((keyIn) => + urlWithoutParams.includes(keyIn), + ); + return key ? PROGRESS_BAR_LABEL_MAP[key] : T.GPB.UNKNOWN; } } } - diff --git a/src/app/core-ui/layout/layout.module.ts b/src/app/core-ui/layout/layout.module.ts index 0b1266fa7..ac78543fa 100644 --- a/src/app/core-ui/layout/layout.module.ts +++ b/src/app/core-ui/layout/layout.module.ts @@ -11,5 +11,4 @@ import { LAYOUT_FEATURE_NAME } from './store/layout.reducer'; ], declarations: [], }) -export class LayoutModule { -} +export class LayoutModule {} diff --git a/src/app/core-ui/layout/layout.service.ts b/src/app/core-ui/layout/layout.service.ts index 7874e5fb4..34b63203e 100644 --- a/src/app/core-ui/layout/layout.service.ts +++ b/src/app/core-ui/layout/layout.service.ts @@ -6,11 +6,15 @@ import { showAddTaskBar, toggleAddTaskBar, toggleShowNotes, - toggleSideNav + toggleSideNav, } from './store/layout.actions'; import { BehaviorSubject, EMPTY, merge, Observable, of } from 'rxjs'; import { select, Store } from '@ngrx/store'; -import { LayoutState, selectIsShowAddTaskBar, selectIsShowSideNav } from './store/layout.reducer'; +import { + LayoutState, + selectIsShowAddTaskBar, + selectIsShowSideNav, +} from './store/layout.reducer'; import { filter, map, switchMap, withLatestFrom } from 'rxjs/operators'; import { BreakpointObserver } from '@angular/cdk/layout'; import { NavigationStart, Router } from '@angular/router'; @@ -25,28 +29,30 @@ const XS_MAX = 599; providedIn: 'root', }) export class LayoutService { - isScreenXs$: Observable = this._breakPointObserver.observe([ - `(max-width: ${XS_MAX}px)`, - ]).pipe(map(result => result.matches)); + isScreenXs$: Observable = this._breakPointObserver + .observe([`(max-width: ${XS_MAX}px)`]) + .pipe(map((result) => result.matches)); - isShowAddTaskBar$: Observable = this._store$.pipe(select(selectIsShowAddTaskBar)); - isNavAlwaysVisible$: Observable = this._breakPointObserver.observe([ - `(min-width: ${NAV_ALWAYS_VISIBLE}px)`, - ]).pipe(map(result => result.matches)); - isNotesNextNavOver$: Observable = this._breakPointObserver.observe([ - `(min-width: ${NAV_OVER_NOTES_NEXT}px)`, - ]).pipe(map(result => result.matches)); - isNotesOver$: Observable = this._breakPointObserver.observe([ - `(min-width: ${BOTH_OVER}px)`, - ]).pipe(map(result => !result.matches)); - isNavOver$: Observable = this.isNotesNextNavOver$.pipe(map(v => !v)); + isShowAddTaskBar$: Observable = this._store$.pipe( + select(selectIsShowAddTaskBar), + ); + isNavAlwaysVisible$: Observable = this._breakPointObserver + .observe([`(min-width: ${NAV_ALWAYS_VISIBLE}px)`]) + .pipe(map((result) => result.matches)); + isNotesNextNavOver$: Observable = this._breakPointObserver + .observe([`(min-width: ${NAV_OVER_NOTES_NEXT}px)`]) + .pipe(map((result) => result.matches)); + isNotesOver$: Observable = this._breakPointObserver + .observe([`(min-width: ${BOTH_OVER}px)`]) + .pipe(map((result) => !result.matches)); + isNavOver$: Observable = this.isNotesNextNavOver$.pipe(map((v) => !v)); isScrolled$: BehaviorSubject = new BehaviorSubject(false); - private _isShowSideNav$: Observable = this._store$.pipe(select(selectIsShowSideNav)); + private _isShowSideNav$: Observable = this._store$.pipe( + select(selectIsShowSideNav), + ); isShowSideNav$: Observable = this._isShowSideNav$.pipe( switchMap((isShow) => { - return isShow - ? of(isShow) - : this.isNavAlwaysVisible$; + return isShow ? of(isShow) : this.isNavAlwaysVisible$; }), ); @@ -65,22 +71,23 @@ export class LayoutService { private _workContextService: WorkContextService, private _breakPointObserver: BreakpointObserver, ) { - this.isNavOver$.pipe( - switchMap((isNavOver) => isNavOver - ? merge( - this._router.events.pipe( - filter((ev) => ev instanceof NavigationStart) - ), - this._workContextService.onWorkContextChange$ - ).pipe( - withLatestFrom(this._isShowSideNav$), - filter(([, isShowSideNav]) => isShowSideNav), - ) - : EMPTY + this.isNavOver$ + .pipe( + switchMap((isNavOver) => + isNavOver + ? merge( + this._router.events.pipe(filter((ev) => ev instanceof NavigationStart)), + this._workContextService.onWorkContextChange$, + ).pipe( + withLatestFrom(this._isShowSideNav$), + filter(([, isShowSideNav]) => isShowSideNav), + ) + : EMPTY, + ), ) - ).subscribe(() => { - this.hideSideNav(); - }); + .subscribe(() => { + this.hideSideNav(); + }); } showAddTaskBar() { @@ -110,5 +117,4 @@ export class LayoutService { public hideNotes() { this._store$.dispatch(hideNotes()); } - } diff --git a/src/app/core-ui/layout/store/layout.actions.ts b/src/app/core-ui/layout/store/layout.actions.ts index 8216aaee7..35edaeecd 100644 --- a/src/app/core-ui/layout/store/layout.actions.ts +++ b/src/app/core-ui/layout/store/layout.actions.ts @@ -1,30 +1,15 @@ import { createAction } from '@ngrx/store'; -export const showAddTaskBar = createAction( - '[Layout] Show AddTaskBar', -); +export const showAddTaskBar = createAction('[Layout] Show AddTaskBar'); -export const hideAddTaskBar = createAction( - '[Layout] Hide AddTaskBar', -); +export const hideAddTaskBar = createAction('[Layout] Hide AddTaskBar'); -export const toggleAddTaskBar = createAction( - '[Layout] Toggle AddTaskBar', -); +export const toggleAddTaskBar = createAction('[Layout] Toggle AddTaskBar'); -export const hideSideNav = createAction( - '[Layout] Hide SideBar', -); +export const hideSideNav = createAction('[Layout] Hide SideBar'); -export const toggleSideNav = createAction( - '[Layout] Toggle SideBar', -); +export const toggleSideNav = createAction('[Layout] Toggle SideBar'); -export const toggleShowNotes = createAction( - '[Layout] ToggleShow Notes', -); - -export const hideNotes = createAction( - '[Layout] Hide Notes', -); +export const toggleShowNotes = createAction('[Layout] ToggleShow Notes'); +export const hideNotes = createAction('[Layout] Hide Notes'); diff --git a/src/app/core-ui/layout/store/layout.reducer.ts b/src/app/core-ui/layout/store/layout.reducer.ts index 1a4ce6904..86713be83 100644 --- a/src/app/core-ui/layout/store/layout.reducer.ts +++ b/src/app/core-ui/layout/store/layout.reducer.ts @@ -5,9 +5,15 @@ import { showAddTaskBar, toggleAddTaskBar, toggleShowNotes, - toggleSideNav + toggleSideNav, } from './layout.actions'; -import { Action, createFeatureSelector, createReducer, createSelector, on } from '@ngrx/store'; +import { + Action, + createFeatureSelector, + createReducer, + createSelector, + on, +} from '@ngrx/store'; export const LAYOUT_FEATURE_NAME = 'layout'; @@ -25,35 +31,49 @@ const _initialLayoutState: LayoutState = { isShowNotes: false, }; -export const selectLayoutFeatureState = createFeatureSelector(LAYOUT_FEATURE_NAME); +export const selectLayoutFeatureState = createFeatureSelector( + LAYOUT_FEATURE_NAME, +); -export const selectIsShowAddTaskBar = createSelector(selectLayoutFeatureState, state => state.isShowAddTaskBar); +export const selectIsShowAddTaskBar = createSelector( + selectLayoutFeatureState, + (state) => state.isShowAddTaskBar, +); -export const selectIsShowSideNav = createSelector(selectLayoutFeatureState, state => state.isShowSideNav); +export const selectIsShowSideNav = createSelector( + selectLayoutFeatureState, + (state) => state.isShowSideNav, +); -export const selectIsShowNotes = createSelector(selectLayoutFeatureState, (state) => state.isShowNotes); +export const selectIsShowNotes = createSelector( + selectLayoutFeatureState, + (state) => state.isShowNotes, +); const _reducer = createReducer( _initialLayoutState, - on(showAddTaskBar, (state) => ({...state, isShowAddTaskBar: true})), + on(showAddTaskBar, (state) => ({ ...state, isShowAddTaskBar: true })), - on(hideAddTaskBar, (state) => ({...state, isShowAddTaskBar: false})), + on(hideAddTaskBar, (state) => ({ ...state, isShowAddTaskBar: false })), - on(toggleAddTaskBar, (state) => ({...state, isShowAddTaskBar: !state.isShowAddTaskBar})), + on(toggleAddTaskBar, (state) => ({ + ...state, + isShowAddTaskBar: !state.isShowAddTaskBar, + })), - on(hideSideNav, (state) => ({...state, isShowSideNav: false})), + on(hideSideNav, (state) => ({ ...state, isShowSideNav: false })), - on(toggleSideNav, (state) => ({...state, isShowSideNav: !state.isShowSideNav})), + on(toggleSideNav, (state) => ({ ...state, isShowSideNav: !state.isShowSideNav })), - on(toggleShowNotes, (state) => ({...state, isShowNotes: !state.isShowNotes})), + on(toggleShowNotes, (state) => ({ ...state, isShowNotes: !state.isShowNotes })), - on(hideNotes, (state) => ({...state, isShowNotes: false})), + on(hideNotes, (state) => ({ ...state, isShowNotes: false })), ); export function reducer( state: LayoutState = _initialLayoutState, - action: Action + action: Action, ): LayoutState { return _reducer(state, action); } diff --git a/src/app/core-ui/main-header/main-header.component.html b/src/app/core-ui/main-header/main-header.component.html index 4680a8803..0be4512f4 100644 --- a/src/app/core-ui/main-header/main-header.component.html +++ b/src/app/core-ui/main-header/main-header.component.html @@ -23,7 +23,7 @@ diff --git a/src/app/core-ui/main-header/main-header.component.ts b/src/app/core-ui/main-header/main-header.component.ts index 3dd120f2b..387bb32b0 100644 --- a/src/app/core-ui/main-header/main-header.component.ts +++ b/src/app/core-ui/main-header/main-header.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, Renderer2, ViewChild } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + OnDestroy, + OnInit, + Renderer2, + ViewChild, +} from '@angular/core'; import { ProjectService } from '../../features/project/project.service'; import { LayoutService } from '../layout/layout.service'; import { BookmarkService } from '../../features/bookmark/bookmark.service'; @@ -21,28 +29,35 @@ import { SimpleCounter } from '../../features/simple-counter/simple-counter.mode templateUrl: './main-header.component.html', styleUrls: ['./main-header.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [fadeAnimation, expandFadeHorizontalAnimation] + animations: [fadeAnimation, expandFadeHorizontalAnimation], }) export class MainHeaderComponent implements OnInit, OnDestroy { T: typeof T = T; progressCircleRadius: number = 10; circumference: number = this.progressCircleRadius * Math.PI * 2; - @ViewChild('circleSvg', {static: true}) circleSvg?: ElementRef; + @ViewChild('circleSvg', { static: true }) circleSvg?: ElementRef; - currentTaskContext$: Observable = this.taskService.currentTaskParentOrCurrent$.pipe( - filter(ct => !!ct), - switchMap((currentTask) => this.workContextService.activeWorkContextId$.pipe( - filter((activeWorkContextId) => !!activeWorkContextId), - switchMap((activeWorkContextId) => { - if (currentTask.projectId === activeWorkContextId || currentTask.tagIds.includes(activeWorkContextId as string)) { - return of(null); - } - return currentTask.projectId - ? this.projectService.getByIdOnce$(currentTask.projectId) - : this._tagService.getTagById$(currentTask.tagIds[0]).pipe(first()); - }), - )), + currentTaskContext$: Observable< + Project | Tag | null + > = this.taskService.currentTaskParentOrCurrent$.pipe( + filter((ct) => !!ct), + switchMap((currentTask) => + this.workContextService.activeWorkContextId$.pipe( + filter((activeWorkContextId) => !!activeWorkContextId), + switchMap((activeWorkContextId) => { + if ( + currentTask.projectId === activeWorkContextId || + currentTask.tagIds.includes(activeWorkContextId as string) + ) { + return of(null); + } + return currentTask.projectId + ? this.projectService.getByIdOnce$(currentTask.projectId) + : this._tagService.getTagById$(currentTask.tagIds[0]).pipe(first()); + }), + ), + ), ); private _subs: Subscription = new Subscription(); @@ -57,8 +72,7 @@ export class MainHeaderComponent implements OnInit, OnDestroy { public readonly simpleCounterService: SimpleCounterService, private readonly _tagService: TagService, private readonly _renderer: Renderer2, - ) { - } + ) {} ngOnDestroy(): void { this._subs.unsubscribe(); @@ -72,7 +86,11 @@ export class MainHeaderComponent implements OnInit, OnDestroy { progress = 1; } const dashOffset = this.circumference * -1 * progress; - this._renderer.setStyle(this.circleSvg.nativeElement, 'stroke-dashoffset', dashOffset); + this._renderer.setStyle( + this.circleSvg.nativeElement, + 'stroke-dashoffset', + dashOffset, + ); } }); } diff --git a/src/app/core-ui/main-header/main-header.module.ts b/src/app/core-ui/main-header/main-header.module.ts index 81d43a8e0..d32be8f85 100644 --- a/src/app/core-ui/main-header/main-header.module.ts +++ b/src/app/core-ui/main-header/main-header.module.ts @@ -25,5 +25,4 @@ import { SimpleCounterModule } from '../../features/simple-counter/simple-counte declarations: [MainHeaderComponent], exports: [MainHeaderComponent], }) -export class MainHeaderModule { -} +export class MainHeaderModule {} diff --git a/src/app/core-ui/shortcut/shortcut.module.ts b/src/app/core-ui/shortcut/shortcut.module.ts index e74fce8c3..1cd8df01e 100644 --- a/src/app/core-ui/shortcut/shortcut.module.ts +++ b/src/app/core-ui/shortcut/shortcut.module.ts @@ -3,11 +3,7 @@ import { CommonModule } from '@angular/common'; import { RouterModule } from '@angular/router'; @NgModule({ - imports: [ - CommonModule, - RouterModule, - ], + imports: [CommonModule, RouterModule], declarations: [], }) -export class ShortcutModule { -} +export class ShortcutModule {} diff --git a/src/app/core-ui/shortcut/shortcut.service.ts b/src/app/core-ui/shortcut/shortcut.service.ts index 39c025865..74bfddd63 100644 --- a/src/app/core-ui/shortcut/shortcut.service.ts +++ b/src/app/core-ui/shortcut/shortcut.service.ts @@ -39,20 +39,22 @@ export class ShortcutService { private _translateService: TranslateService, private _ngZone: NgZone, ) { - this._activatedRoute.queryParams - .subscribe((params) => { - if (params && params.backlogPos) { - this.backlogPos = +params.backlogPos; - } - }); + this._activatedRoute.queryParams.subscribe((params) => { + if (params && params.backlogPos) { + this.backlogPos = +params.backlogPos; + } + }); // GLOBAL SHORTCUTS if (IS_ELECTRON) { - (this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.TASK_TOGGLE_START, () => { - this._ngZone.run(() => { - this._taskService.toggleStartTask(); - }); - }); + (this._electronService.ipcRenderer as typeof ipcRenderer).on( + IPC.TASK_TOGGLE_START, + () => { + this._ngZone.run(() => { + this._taskService.toggleStartTask(); + }); + }, + ); (this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.ADD_TASK, () => { this._ngZone.run(() => { this._layoutService.showAddTaskBar(); @@ -78,8 +80,13 @@ export class ShortcutService { const el = ev.target as HTMLElement; // don't run when inside input or text area and if no special keys are used - if ((el && el.tagName === 'INPUT' || el.tagName === 'TEXTAREA' || el.getAttribute('contenteditable')) - && !ev.ctrlKey && !ev.metaKey) { + if ( + ((el && el.tagName === 'INPUT') || + el.tagName === 'TEXTAREA' || + el.getAttribute('contenteditable')) && + !ev.ctrlKey && + !ev.metaKey + ) { return; } @@ -96,15 +103,12 @@ export class ShortcutService { backlogPos = 50; } this._router.navigate(['/active/tasks'], { - queryParams: {backlogPos} + queryParams: { backlogPos }, }); - } else if (checkKeyCombo(ev, keys.goToWorkView)) { this._router.navigate(['/active/tasks']); - } else if (checkKeyCombo(ev, keys.goToSettings)) { this._router.navigate(['/config']); - } else if (checkKeyCombo(ev, keys.goToScheduledView)) { this._router.navigate(['/active/calendar']); @@ -113,28 +117,29 @@ export class ShortcutService { // // } else if (checkKeyCombo(ev, keys.goToFocusMode)) { // this._router.navigate(['/focus-view']); - } else if (checkKeyCombo(ev, keys.toggleSideNav)) { this._layoutService.toggleSideNav(); ev.preventDefault(); - } else if (checkKeyCombo(ev, keys.addNewTask)) { this._layoutService.toggleAddTaskBar(); ev.preventDefault(); - } else if (checkKeyCombo(ev, keys.addNewNote)) { if (this._matDialog.openDialogs.length === 0) { this._matDialog.open(DialogAddNoteComponent); ev.preventDefault(); } - } else if (checkKeyCombo(ev, keys.openProjectNotes)) { ev.preventDefault(); if (this._workContextService.activeWorkContextType === WorkContextType.PROJECT) { this._layoutService.toggleNotes(); } else { this._snackService.open({ - msg: this._translateService.instant(T.GLOBAL_SNACK.SHORTCUT_WARN_OPEN_NOTES_FROM_TAG, {keyCombo: keys.openProjectNotes}), + msg: this._translateService.instant( + T.GLOBAL_SNACK.SHORTCUT_WARN_OPEN_NOTES_FROM_TAG, + { + keyCombo: keys.openProjectNotes, + }, + ), }); } } else if (checkKeyCombo(ev, keys.toggleBookmarks)) { @@ -143,19 +148,26 @@ export class ShortcutService { this._bookmarkService.toggleBookmarks(); } else { this._snackService.open({ - msg: this._translateService.instant(T.GLOBAL_SNACK.SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG, {keyCombo: keys.openProjectNotes}), + msg: this._translateService.instant( + T.GLOBAL_SNACK.SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG, + { keyCombo: keys.openProjectNotes }, + ), }); } - } else if (checkKeyCombo(ev, 'Ctrl+Shift+*') - && document.activeElement - && document.activeElement.getAttribute('routerlink') === '/procrastination') { + } else if ( + checkKeyCombo(ev, 'Ctrl+Shift+*') && + document.activeElement && + document.activeElement.getAttribute('routerlink') === '/procrastination' + ) { throw new Error('Intentional Error Fun (dont worry)'); } // special hidden dev tools combo to use them for production if (IS_ELECTRON) { if (checkKeyCombo(ev, 'Ctrl+Shift+J')) { - (this._electronService.ipcRenderer as typeof ipcRenderer).send('TOGGLE_DEV_TOOLS'); + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + 'TOGGLE_DEV_TOOLS', + ); } else if (checkKeyCombo(ev, keys.zoomIn)) { this._uiHelperService.zoomBy(0.05); } else if (checkKeyCombo(ev, keys.zoomOut)) { diff --git a/src/app/core-ui/side-nav/side-nav.component.ts b/src/app/core-ui/side-nav/side-nav.component.ts index b5b44d7ad..19dc557a1 100644 --- a/src/app/core-ui/side-nav/side-nav.component.ts +++ b/src/app/core-ui/side-nav/side-nav.component.ts @@ -8,7 +8,7 @@ import { Output, QueryList, ViewChild, - ViewChildren + ViewChildren, } from '@angular/core'; import { ProjectService } from '../../features/project/project.service'; import { T } from '../../t.const'; @@ -29,8 +29,10 @@ import { FocusKeyManager } from '@angular/cdk/a11y'; import { MatMenuItem } from '@angular/material/menu'; import { LayoutService } from '../layout/layout.service'; import { TaskService } from '../../features/tasks/task.service'; -import { LS_IS_PROJECT_LIST_EXPANDED, LS_IS_TAG_LIST_EXPANDED } from '../../core/persistence/ls-keys.const'; - +import { + LS_IS_PROJECT_LIST_EXPANDED, + LS_IS_TAG_LIST_EXPANDED, +} from '../../core/persistence/ls-keys.const'; @Component({ selector: 'side-nav', @@ -44,33 +46,35 @@ export class SideNavComponent implements OnDestroy { @ViewChildren('menuEntry') navEntries?: QueryList; keyboardFocusTimeout?: number; - @ViewChild('projectExpandBtn', {read: ElementRef}) projectExpandBtn?: ElementRef; + @ViewChild('projectExpandBtn', { read: ElementRef }) projectExpandBtn?: ElementRef; isProjectsExpanded: boolean = this.fetchProjectListState(); - isProjectsExpanded$: BehaviorSubject = new BehaviorSubject(this.isProjectsExpanded); - projectList$: Observable = this.isProjectsExpanded$.pipe( - switchMap(isExpanded => isExpanded - ? this.projectService.list$ - : combineLatest([ - this.projectService.list$, - this.workContextService.activeWorkContextId$ - ]).pipe( - map(([projects, id]) => projects.filter(p => p.id === id)) - ) - ) + isProjectsExpanded$: BehaviorSubject = new BehaviorSubject( + this.isProjectsExpanded, ); - @ViewChild('tagExpandBtn', {read: ElementRef}) tagExpandBtn?: ElementRef; + projectList$: Observable = this.isProjectsExpanded$.pipe( + switchMap((isExpanded) => + isExpanded + ? this.projectService.list$ + : combineLatest([ + this.projectService.list$, + this.workContextService.activeWorkContextId$, + ]).pipe(map(([projects, id]) => projects.filter((p) => p.id === id))), + ), + ); + @ViewChild('tagExpandBtn', { read: ElementRef }) tagExpandBtn?: ElementRef; isTagsExpanded: boolean = this.fetchTagListState(); - isTagsExpanded$: BehaviorSubject = new BehaviorSubject(this.isTagsExpanded); + isTagsExpanded$: BehaviorSubject = new BehaviorSubject( + this.isTagsExpanded, + ); tagList$: Observable = this.isTagsExpanded$.pipe( - switchMap(isExpanded => isExpanded - ? this.tagService.tagsNoMyDay$ - : combineLatest([ - this.tagService.tagsNoMyDay$, - this.workContextService.activeWorkContextId$ - ]).pipe( - map(([tags, id]) => tags.filter(t => t.id === id)) - ) - ) + switchMap((isExpanded) => + isExpanded + ? this.tagService.tagsNoMyDay$ + : combineLatest([ + this.tagService.tagsNoMyDay$, + this.workContextService.activeWorkContextId$, + ]).pipe(map(([tags, id]) => tags.filter((t) => t.id === id))), + ), ); T: typeof T = T; readonly PROJECTS_SIDE_NAV: string = 'PROJECTS_SIDE_NAV'; @@ -92,32 +96,46 @@ export class SideNavComponent implements OnDestroy { this._dragulaService.createGroup(this.PROJECTS_SIDE_NAV, { direction: 'vertical', moves: (el, container, handle) => { - return this.isProjectsExpanded && !!handle && handle.className.indexOf && handle.className.indexOf('drag-handle') > -1; - } + return ( + this.isProjectsExpanded && + !!handle && + handle.className.indexOf && + handle.className.indexOf('drag-handle') > -1 + ); + }, }); - this._subs.add(this.workContextService.activeWorkContextId$.subscribe(id => this.activeWorkContextId = id)); - - this._subs.add(this._dragulaService.dropModel(this.PROJECTS_SIDE_NAV) - .subscribe(({targetModel}) => { - // const {target, source, targetModel, item} = params; - const targetNewIds = targetModel.map((project: Project) => project.id); - this.projectService.updateOrder(targetNewIds); - }) + this._subs.add( + this.workContextService.activeWorkContextId$.subscribe( + (id) => (this.activeWorkContextId = id), + ), ); - this._subs.add(this._layoutService.isShowSideNav$.subscribe((isShow) => { - if (this.navEntries && isShow) { - this.keyManager = new FocusKeyManager(this.navEntries) - .withVerticalOrientation(true); - window.clearTimeout(this.keyboardFocusTimeout); - this.keyboardFocusTimeout = window.setTimeout(() => { - this.keyManager?.setFirstItemActive(); - }, 100); - // this.keyManager.change.subscribe((v) => console.log('this.keyManager.change', v)); - } else if (!isShow) { - this._taskService.focusFirstTaskIfVisible(); - } - })); + this._subs.add( + this._dragulaService + .dropModel(this.PROJECTS_SIDE_NAV) + .subscribe(({ targetModel }) => { + // const {target, source, targetModel, item} = params; + const targetNewIds = targetModel.map((project: Project) => project.id); + this.projectService.updateOrder(targetNewIds); + }), + ); + + this._subs.add( + this._layoutService.isShowSideNav$.subscribe((isShow) => { + if (this.navEntries && isShow) { + this.keyManager = new FocusKeyManager( + this.navEntries, + ).withVerticalOrientation(true); + window.clearTimeout(this.keyboardFocusTimeout); + this.keyboardFocusTimeout = window.setTimeout(() => { + this.keyManager?.setFirstItemActive(); + }, 100); + // this.keyManager.change.subscribe((v) => console.log('this.keyManager.change', v)); + } else if (!isShow) { + this._taskService.focusFirstTaskIfVisible(); + } + }), + ); } @HostListener('keydown', ['$event']) @@ -144,10 +162,8 @@ export class SideNavComponent implements OnDestroy { getThemeColor(color: THEME_COLOR_MAP | string): { [key: string]: string } { const standardColor = (THEME_COLOR_MAP as any)[color]; - const colorToUse = (standardColor) - ? standardColor - : color; - return {background: colorToUse}; + const colorToUse = standardColor ? standardColor : color; + return { background: colorToUse }; } onScrollToNotesClick() { @@ -155,7 +171,7 @@ export class SideNavComponent implements OnDestroy { } fetchProjectListState() { - return (localStorage.getItem(LS_IS_PROJECT_LIST_EXPANDED) === 'true'); + return localStorage.getItem(LS_IS_PROJECT_LIST_EXPANDED) === 'true'; } storeProjectListState(isExpanded: boolean) { @@ -164,7 +180,7 @@ export class SideNavComponent implements OnDestroy { } fetchTagListState() { - return (localStorage.getItem(LS_IS_TAG_LIST_EXPANDED) === 'true'); + return localStorage.getItem(LS_IS_TAG_LIST_EXPANDED) === 'true'; } storeTagListState(isExpanded: boolean) { @@ -179,21 +195,20 @@ export class SideNavComponent implements OnDestroy { } toggleExpandProjectsLeftRight(ev: KeyboardEvent) { - if ((ev.key === 'ArrowLeft' && this.isProjectsExpanded)) { + if (ev.key === 'ArrowLeft' && this.isProjectsExpanded) { this.storeProjectListState(false); this.isProjectsExpanded$.next(this.isProjectsExpanded); - } else if ((ev.key === 'ArrowRight') && !(this.isProjectsExpanded)) { + } else if (ev.key === 'ArrowRight' && !this.isProjectsExpanded) { this.storeProjectListState(true); this.isProjectsExpanded$.next(this.isProjectsExpanded); } } checkFocusProject(ev: KeyboardEvent) { - if ((ev.key === 'ArrowLeft') && this.projectExpandBtn?.nativeElement) { - const targetIndex = this.navEntries?.toArray() - .findIndex((value) => { - return value._getHostElement() === this.projectExpandBtn?.nativeElement; - }); + if (ev.key === 'ArrowLeft' && this.projectExpandBtn?.nativeElement) { + const targetIndex = this.navEntries?.toArray().findIndex((value) => { + return value._getHostElement() === this.projectExpandBtn?.nativeElement; + }); if (targetIndex) { this.keyManager?.setActiveItem(targetIndex); } @@ -207,21 +222,20 @@ export class SideNavComponent implements OnDestroy { } toggleExpandTagsLeftRight(ev: KeyboardEvent) { - if ((ev.key === 'ArrowLeft' && this.isTagsExpanded)) { + if (ev.key === 'ArrowLeft' && this.isTagsExpanded) { this.storeTagListState(false); this.isTagsExpanded$.next(this.isTagsExpanded); - } else if ((ev.key === 'ArrowRight') && !(this.isTagsExpanded)) { + } else if (ev.key === 'ArrowRight' && !this.isTagsExpanded) { this.storeTagListState(true); this.isTagsExpanded$.next(this.isTagsExpanded); } } checkFocusTag(ev: KeyboardEvent) { - if ((ev.key === 'ArrowLeft') && this.tagExpandBtn?.nativeElement) { - const targetIndex = this.navEntries?.toArray() - .findIndex((value) => { - return value._getHostElement() === this.tagExpandBtn?.nativeElement; - }); + if (ev.key === 'ArrowLeft' && this.tagExpandBtn?.nativeElement) { + const targetIndex = this.navEntries?.toArray().findIndex((value) => { + return value._getHostElement() === this.tagExpandBtn?.nativeElement; + }); if (targetIndex) { this.keyManager?.setActiveItem(targetIndex); } diff --git a/src/app/core-ui/side-nav/side-nav.module.ts b/src/app/core-ui/side-nav/side-nav.module.ts index 26df6126f..6d8f30ccb 100644 --- a/src/app/core-ui/side-nav/side-nav.module.ts +++ b/src/app/core-ui/side-nav/side-nav.module.ts @@ -7,20 +7,8 @@ import { DragulaModule } from 'ng2-dragula'; import { WorkContextMenuModule } from '../work-context-menu/work-context-menu.module'; @NgModule({ - - imports: [ - UiModule, - CommonModule, - RouterModule, - DragulaModule, - WorkContextMenuModule, - ], - declarations: [ - SideNavComponent, - ], - exports: [ - SideNavComponent, - ] + imports: [UiModule, CommonModule, RouterModule, DragulaModule, WorkContextMenuModule], + declarations: [SideNavComponent], + exports: [SideNavComponent], }) -export class SideNavModule { -} +export class SideNavModule {} diff --git a/src/app/core-ui/work-context-menu/work-context-menu.component.ts b/src/app/core-ui/work-context-menu/work-context-menu.component.ts index 7d1f84063..4fe2263d0 100644 --- a/src/app/core-ui/work-context-menu/work-context-menu.component.ts +++ b/src/app/core-ui/work-context-menu/work-context-menu.component.ts @@ -17,7 +17,7 @@ import { Project } from '../../features/project/project.model'; selector: 'work-context-menu', templateUrl: './work-context-menu.component.html', styleUrls: ['./work-context-menu.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class WorkContextMenuComponent implements OnDestroy { @Input() project!: Project; @@ -33,14 +33,11 @@ export class WorkContextMenuComponent implements OnDestroy { private _tagService: TagService, private _workContextService: WorkContextService, private _router: Router, - ) { - } + ) {} @Input('contextType') set contextTypeSet(v: WorkContextType) { - this.isForProject = (v === WorkContextType.PROJECT); - this.base = (this.isForProject) - ? 'project' - : 'tag'; + this.isForProject = v === WorkContextType.PROJECT; + this.base = this.isForProject ? 'project' : 'tag'; } ngOnDestroy(): void { @@ -48,19 +45,26 @@ export class WorkContextMenuComponent implements OnDestroy { } deleteTag() { - this._subs.add(this._confirmTagDelete().pipe( - filter(isDelete => isDelete && !!this.contextId), - switchMap(() => this._workContextService.activeWorkContextTypeAndId$.pipe(take(1))), - tap(({activeId}) => console.log(activeId, this.contextId)), - switchMap(({activeId}) => (activeId === this.contextId) - ? from(this._router.navigateByUrl('/')) - : of(true) - ), - ).subscribe(() => { - if (this.contextId) { - this._tagService.removeTag(this.contextId); - } - })); + this._subs.add( + this._confirmTagDelete() + .pipe( + filter((isDelete) => isDelete && !!this.contextId), + switchMap(() => + this._workContextService.activeWorkContextTypeAndId$.pipe(take(1)), + ), + tap(({ activeId }) => console.log(activeId, this.contextId)), + switchMap(({ activeId }) => + activeId === this.contextId + ? from(this._router.navigateByUrl('/')) + : of(true), + ), + ) + .subscribe(() => { + if (this.contextId) { + this._tagService.removeTag(this.contextId); + } + }), + ); } edit(project: Project) { @@ -77,14 +81,17 @@ export class WorkContextMenuComponent implements OnDestroy { return this._tagService.getTagById$(this.contextId).pipe( first(), - concatMap((tag: Tag) => this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - message: T.F.TAG.D_DELETE.CONFIRM_MSG, - translateParams: {tagName: tag.title}, - } - }).afterClosed()), + concatMap((tag: Tag) => + this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + message: T.F.TAG.D_DELETE.CONFIRM_MSG, + translateParams: { tagName: tag.title }, + }, + }) + .afterClosed(), + ), ); } - } diff --git a/src/app/core-ui/work-context-menu/work-context-menu.module.ts b/src/app/core-ui/work-context-menu/work-context-menu.module.ts index 39a591f5c..6119f773f 100644 --- a/src/app/core-ui/work-context-menu/work-context-menu.module.ts +++ b/src/app/core-ui/work-context-menu/work-context-menu.module.ts @@ -5,17 +5,8 @@ import { UiModule } from '../../ui/ui.module'; import { RouterModule } from '@angular/router'; @NgModule({ - declarations: [ - WorkContextMenuComponent, - ], - imports: [ - UiModule, - CommonModule, - RouterModule, - ], - exports: [ - WorkContextMenuComponent, - ] + declarations: [WorkContextMenuComponent], + imports: [UiModule, CommonModule, RouterModule], + exports: [WorkContextMenuComponent], }) -export class WorkContextMenuModule { -} +export class WorkContextMenuModule {} diff --git a/src/app/core/android/android.service.ts b/src/app/core/android/android.service.ts index 99713d94a..b97f1e609 100644 --- a/src/app/core/android/android.service.ts +++ b/src/app/core/android/android.service.ts @@ -16,45 +16,55 @@ interface TaskWithCategoryText extends Task { categoryHtml: string; } -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class AndroidService { - private _todayTagTasksFlat$: Observable = this._dataInitService.isAllDataLoadedInitially$.pipe( + private _todayTagTasksFlat$: Observable< + TaskWithCategoryText[] + > = this._dataInitService.isAllDataLoadedInitially$.pipe( switchMap(() => this._tagService.getTagById$(TODAY_TAG.id)), - switchMap(tag => this._taskService.getByIdsLive$(tag.taskIds)), - map(tasks => tasks && tasks.sort((a, b) => (a.isDone === b.isDone) - ? 0 - : (a.isDone ? 1 : -1) - )), - switchMap((tasks) => combineLatest([ - this._tagService.tags$, - this._projectService.list$, - ]).pipe(map(([tags, projects]) => { - return tasks - .filter(task => !!task) - .map(task => ({ - ...task, - category: [ - ...(task.projectId - ? [(projects.find((p) => p.id === task.projectId) as Project).title] - : []), - ...(task.tagIds.length - ? tags - .filter(tag => tag.id !== TODAY_TAG.id && task.tagIds.includes(tag.id)) - .map(tag => tag.title) - : []) - ].join(', '), - categoryHtml: [ - ...(task.projectId - ? [this._getCategoryHtml(projects.find(p => p.id === task.projectId) as Project)] - : []), - ...(task.tagIds.length - ? tags - .filter(tag => tag.id !== TODAY_TAG.id && task.tagIds.includes(tag.id)) - .map(tag => this._getCategoryHtml(tag)) - : []) - ].join(' ') - })); - })) + switchMap((tag) => this._taskService.getByIdsLive$(tag.taskIds)), + map( + (tasks) => + tasks && tasks.sort((a, b) => (a.isDone === b.isDone ? 0 : a.isDone ? 1 : -1)), + ), + switchMap((tasks) => + combineLatest([this._tagService.tags$, this._projectService.list$]).pipe( + map(([tags, projects]) => { + return tasks + .filter((task) => !!task) + .map((task) => ({ + ...task, + category: [ + ...(task.projectId + ? [(projects.find((p) => p.id === task.projectId) as Project).title] + : []), + ...(task.tagIds.length + ? tags + .filter( + (tag) => tag.id !== TODAY_TAG.id && task.tagIds.includes(tag.id), + ) + .map((tag) => tag.title) + : []), + ].join(', '), + categoryHtml: [ + ...(task.projectId + ? [ + this._getCategoryHtml( + projects.find((p) => p.id === task.projectId) as Project, + ), + ] + : []), + ...(task.tagIds.length + ? tags + .filter( + (tag) => tag.id !== TODAY_TAG.id && task.tagIds.includes(tag.id), + ) + .map((tag) => this._getCategoryHtml(tag)) + : []), + ].join(' '), + })); + }), + ), ), ); @@ -69,7 +79,7 @@ export class AndroidService { } init() { - this._todayTagTasksFlat$.subscribe(tasks => { + this._todayTagTasksFlat$.subscribe((tasks) => { androidInterface.updateTaskData(JSON.stringify(tasks)); }); } diff --git a/src/app/core/banner/banner.module.ts b/src/app/core/banner/banner.module.ts index 0cc6301eb..7443bda6b 100644 --- a/src/app/core/banner/banner.module.ts +++ b/src/app/core/banner/banner.module.ts @@ -4,16 +4,8 @@ import { BannerComponent } from './banner/banner.component'; import { UiModule } from '../../ui/ui.module'; @NgModule({ - declarations: [ - BannerComponent - ], - imports: [ - CommonModule, - UiModule, - ], - exports: [ - BannerComponent - ] + declarations: [BannerComponent], + imports: [CommonModule, UiModule], + exports: [BannerComponent], }) -export class BannerModule { -} +export class BannerModule {} diff --git a/src/app/core/banner/banner.service.ts b/src/app/core/banner/banner.service.ts index 6a5161a18..f63f79aa3 100644 --- a/src/app/core/banner/banner.service.ts +++ b/src/app/core/banner/banner.service.ts @@ -3,12 +3,12 @@ import { Banner, BannerId } from './banner.model'; import { Observable, ReplaySubject } from 'rxjs'; import { map } from 'rxjs/operators'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class BannerService { private _banners: Banner[] = []; private _banners$: ReplaySubject = new ReplaySubject(1); activeBanner$: Observable = this._banners$.pipe( - map((banners) => (banners && banners.length && banners[0]) || null) + map((banners) => (banners && banners.length && banners[0]) || null), ); constructor() { @@ -73,7 +73,7 @@ export class BannerService { } open(banner: Banner) { - const bannerToUpdate = this._banners.find(bannerIN => bannerIN.id === banner.id); + const bannerToUpdate = this._banners.find((bannerIN) => bannerIN.id === banner.id); if (bannerToUpdate) { Object.assign(bannerToUpdate, banner); } else { @@ -83,7 +83,7 @@ export class BannerService { } dismiss(bannerId: BannerId) { - const bannerIndex = this._banners.findIndex(bannerIN => bannerIN.id === bannerId); + const bannerIndex = this._banners.findIndex((bannerIN) => bannerIN.id === bannerId); if (bannerIndex > -1) { // NOTE splice mutates this._banners.splice(bannerIndex, 1); @@ -93,8 +93,8 @@ export class BannerService { // usually not required, but when we want to be sure dismissAll(bannerId: BannerId) { - if (this._banners.find(bannerIN => bannerIN.id === bannerId)) { - this._banners = this._banners.filter(banner => banner.id !== bannerId); + if (this._banners.find((bannerIN) => bannerIN.id === bannerId)) { + this._banners = this._banners.filter((banner) => banner.id !== bannerId); this._banners$.next(this._banners); } } diff --git a/src/app/core/banner/banner/banner.component.ts b/src/app/core/banner/banner/banner.component.ts index cea9e1f58..81ae1ffb8 100644 --- a/src/app/core/banner/banner/banner.component.ts +++ b/src/app/core/banner/banner/banner.component.ts @@ -11,7 +11,7 @@ import { T } from '../../../t.const'; templateUrl: './banner.component.html', styleUrls: ['./banner.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [slideAnimation] + animations: [slideAnimation], }) export class BannerComponent { T: typeof T = T; @@ -27,22 +27,15 @@ export class BannerComponent { } this._dirtyReference = activeBanner.id; - return merge( - of(null), - timer(500).pipe(mapTo(activeBanner)) - ); + return merge(of(null), timer(500).pipe(mapTo(activeBanner))); } else { this._dirtyReference = null; return of(null); } - }) + }), ); - constructor( - public bannerService: BannerService, - private _elementRef: ElementRef, - ) { - } + constructor(public bannerService: BannerService, private _elementRef: ElementRef) {} @ViewChild('wrapperEl') set wrapperEl(content: ElementRef) { if (content && content.nativeElement) { diff --git a/src/app/core/chrome-extension-interface/chrome-extension-interface.d.ts b/src/app/core/chrome-extension-interface/chrome-extension-interface.d.ts index 8e09aa6ed..851420142 100644 --- a/src/app/core/chrome-extension-interface/chrome-extension-interface.d.ts +++ b/src/app/core/chrome-extension-interface/chrome-extension-interface.d.ts @@ -1,6 +1,5 @@ -export type ExtensionInterfaceEventName - = 'SP_EXTENSION_READY' +export type ExtensionInterfaceEventName = + | 'SP_EXTENSION_READY' | 'SP_JIRA_RESPONSE' | 'SP_JIRA_REQUEST' - | 'IDLE_TIME' - ; + | 'IDLE_TIME'; diff --git a/src/app/core/chrome-extension-interface/chrome-extension-interface.module.ts b/src/app/core/chrome-extension-interface/chrome-extension-interface.module.ts index 2dfed39a7..a2bb79b8b 100644 --- a/src/app/core/chrome-extension-interface/chrome-extension-interface.module.ts +++ b/src/app/core/chrome-extension-interface/chrome-extension-interface.module.ts @@ -2,10 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; @NgModule({ - imports: [ - CommonModule - ], + imports: [CommonModule], declarations: [], }) -export class ChromeExtensionInterfaceModule { -} +export class ChromeExtensionInterfaceModule {} diff --git a/src/app/core/chrome-extension-interface/chrome-extension-interface.service.ts b/src/app/core/chrome-extension-interface/chrome-extension-interface.service.ts index 2c2217a46..f62d84174 100644 --- a/src/app/core/chrome-extension-interface/chrome-extension-interface.service.ts +++ b/src/app/core/chrome-extension-interface/chrome-extension-interface.service.ts @@ -27,7 +27,10 @@ export class ChromeExtensionInterfaceService { }); } - addEventListener(evName: ExtensionInterfaceEventName, cb: (ev: Event, data?: unknown) => void) { + addEventListener( + evName: ExtensionInterfaceEventName, + cb: (ev: Event, data?: unknown) => void, + ) { interfaceEl.addEventListener(evName, (ev: Event) => { const event = ev as CustomEvent; cb(event, event.detail); diff --git a/src/app/core/compression/compression.module.ts b/src/app/core/compression/compression.module.ts index 2d7349ec8..e979afc66 100644 --- a/src/app/core/compression/compression.module.ts +++ b/src/app/core/compression/compression.module.ts @@ -3,9 +3,6 @@ import { CommonModule } from '@angular/common'; @NgModule({ declarations: [], - imports: [ - CommonModule - ] + imports: [CommonModule], }) -export class CompressionModule { -} +export class CompressionModule {} diff --git a/src/app/core/compression/compression.service.ts b/src/app/core/compression/compression.service.ts index c02996eb4..6942cedb6 100644 --- a/src/app/core/compression/compression.service.ts +++ b/src/app/core/compression/compression.service.ts @@ -3,21 +3,19 @@ import { SnackService } from '../snack/snack.service'; import * as shortid from 'shortid'; import { T } from '../../t.const'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class CompressionService { private _w: Worker; private _activeInstances: any = {}; - constructor( - private readonly _snackService: SnackService, - ) { + constructor(private readonly _snackService: SnackService) { if (typeof (Worker as any) === 'undefined') { throw new Error('No web worker support'); } // Create a new this._w = new Worker('./lz.worker', { name: 'lz', - type: 'module' + type: 'module', }); this._w.addEventListener('message', this._onData.bind(this)); this._w.addEventListener('error', this._handleError.bind(this)); @@ -26,40 +24,43 @@ export class CompressionService { async compress(strToHandle: string): Promise { return this._promisifyWorker({ type: 'COMPRESS', - strToHandle + strToHandle, }); } async decompress(strToHandle: string): Promise { return this._promisifyWorker({ type: 'DECOMPRESS', - strToHandle + strToHandle, }); } async compressUTF16(strToHandle: string): Promise { return this._promisifyWorker({ type: 'COMPRESS_UTF16', - strToHandle + strToHandle, }); } async decompressUTF16(strToHandle: string): Promise { return this._promisifyWorker({ type: 'DECOMPRESS_UTF16', - strToHandle + strToHandle, }); } - private _promisifyWorker(params: { type: string; strToHandle: string }): Promise { + private _promisifyWorker(params: { + type: string; + strToHandle: string; + }): Promise { const id = shortid(); - const promise = new Promise(((resolve, reject) => { + const promise = new Promise((resolve, reject) => { this._activeInstances[id] = { resolve, reject, }; - })) as Promise; + }) as Promise; this._w.postMessage({ ...params, @@ -69,7 +70,7 @@ export class CompressionService { } private async _onData(msg: MessageEvent) { - const {id, strToHandle, err} = msg.data; + const { id, strToHandle, err } = msg.data; if (err) { this._activeInstances[id].reject(err); this._handleError(err); @@ -81,6 +82,6 @@ export class CompressionService { private _handleError(err: any) { console.error(err); - this._snackService.open({type: 'ERROR', msg: T.GLOBAL_SNACK.ERR_COMPRESSION}); + this._snackService.open({ type: 'ERROR', msg: T.GLOBAL_SNACK.ERR_COMPRESSION }); } } diff --git a/src/app/core/compression/lz.worker.ts b/src/app/core/compression/lz.worker.ts index 1b825a5ed..ed72a900f 100644 --- a/src/app/core/compression/lz.worker.ts +++ b/src/app/core/compression/lz.worker.ts @@ -18,7 +18,7 @@ function handleData(msgData: any) { } } -addEventListener('message', ({data}) => { +addEventListener('message', ({ data }) => { try { const strToHandle = handleData(data); postMessage({ diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 13559bbdf..a65f9ce8b 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -19,12 +19,6 @@ import { CompressionModule } from './compression/compression.module'; LocalBackupModule, CompressionModule, ], - exports: [ - PersistenceModule, - ChromeExtensionInterfaceModule, - SnackModule, - BannerModule, - ], + exports: [PersistenceModule, ChromeExtensionInterfaceModule, SnackModule, BannerModule], }) -export class CoreModule { -} +export class CoreModule {} diff --git a/src/app/core/data-init/data-init.service.ts b/src/app/core/data-init/data-init.service.ts index 0a5c63f9b..baf3150bf 100644 --- a/src/app/core/data-init/data-init.service.ts +++ b/src/app/core/data-init/data-init.service.ts @@ -12,18 +12,23 @@ import { loadAllData } from '../../root-store/meta/load-all-data.action'; import { isValidAppData } from '../../imex/sync/is-valid-app-data.util'; import { DataRepairService } from '../data-repair/data-repair.service'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class DataInitService { - isAllDataLoadedInitially$: Observable = from(this._persistenceService.project.loadState(true)).pipe( - concatMap((projectState: ProjectState) => this._migrationService.migrateIfNecessaryToProjectState$(projectState)), + isAllDataLoadedInitially$: Observable = from( + this._persistenceService.project.loadState(true), + ).pipe( + concatMap((projectState: ProjectState) => + this._migrationService.migrateIfNecessaryToProjectState$(projectState), + ), concatMap(() => from(this.reInit())), switchMap(() => this._workContextService.isActiveWorkContextProject$), - switchMap(isProject => isProject - // NOTE: this probably won't work some of the time - ? this._projectService.isRelatedDataLoadedForCurrentProject$ - : of(true) + switchMap((isProject) => + isProject + ? // NOTE: this probably won't work some of the time + this._projectService.isRelatedDataLoadedForCurrentProject$ + : of(true), ), - filter(isLoaded => isLoaded), + filter((isLoaded) => isLoaded), take(1), // only ever load once shareReplay(1), @@ -38,9 +43,7 @@ export class DataInitService { private _dataRepairService: DataRepairService, ) { // TODO better construction than this - this.isAllDataLoadedInitially$.pipe( - take(1) - ).subscribe(() => { + this.isAllDataLoadedInitially$.pipe(take(1)).subscribe(() => { // here because to avoid circular dependencies this._store$.dispatch(allDataWasLoaded()); }); @@ -52,17 +55,18 @@ export class DataInitService { const appDataComplete = await this._persistenceService.loadComplete(); const isValid = isValidAppData(appDataComplete); if (isValid) { - this._store$.dispatch(loadAllData({appDataComplete, isOmitTokens})); + this._store$.dispatch(loadAllData({ appDataComplete, isOmitTokens })); } else { if (this._dataRepairService.isRepairPossibleAndConfirmed(appDataComplete)) { const fixedData = this._dataRepairService.repairData(appDataComplete); - this._store$.dispatch(loadAllData({ - appDataComplete: fixedData, - isOmitTokens, - })); + this._store$.dispatch( + loadAllData({ + appDataComplete: fixedData, + isOmitTokens, + }), + ); await this._persistenceService.importComplete(fixedData); } } } - } diff --git a/src/app/core/data-repair/data-repair.service.ts b/src/app/core/data-repair/data-repair.service.ts index 0f27ed53a..2634570c9 100644 --- a/src/app/core/data-repair/data-repair.service.ts +++ b/src/app/core/data-repair/data-repair.service.ts @@ -6,14 +6,10 @@ import { dataRepair } from './data-repair.util'; import { isDataRepairPossible } from './is-data-repair-possible.util'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class DataRepairService { - - constructor( - private _translateService: TranslateService, - ) { - } + constructor(private _translateService: TranslateService) {} repairData(dataIn: AppDataComplete): AppDataComplete { return dataRepair(dataIn); diff --git a/src/app/core/data-repair/data-repair.util.spec.ts b/src/app/core/data-repair/data-repair.util.spec.ts index 1efa1f1df..ae72bc445 100644 --- a/src/app/core/data-repair/data-repair.util.spec.ts +++ b/src/app/core/data-repair/data-repair.util.spec.ts @@ -16,45 +16,55 @@ describe('dataRepair()', () => { beforeEach(() => { mock = createAppDataCompleteMock(); mock.project = { - ...fakeEntityStateFromArray([{ - title: 'FAKE_PROJECT', - id: FAKE_PROJECT_ID, - taskIds: [], - backlogTaskIds: [], - }] as Partial []) + ...fakeEntityStateFromArray([ + { + title: 'FAKE_PROJECT', + id: FAKE_PROJECT_ID, + taskIds: [], + backlogTaskIds: [], + }, + ] as Partial[]), }; mock.tag = { - ...fakeEntityStateFromArray([{ - ...TODAY_TAG, - }] as Partial []) + ...fakeEntityStateFromArray([ + { + ...TODAY_TAG, + }, + ] as Partial[]), }; }); it('should delete tasks with same id in "task" and "taskArchive" from taskArchive', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'TEST', - title: 'TEST', - projectId: FAKE_PROJECT_ID, - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + projectId: FAKE_PROJECT_ID, + }, + ]), } as any; - expect(dataRepair({ - ...mock, - task: taskState, - taskArchive: fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'TEST', - title: 'TEST', - projectId: FAKE_PROJECT_ID, - }]), - } as any)).toEqual({ + expect( + dataRepair({ + ...mock, + task: taskState, + taskArchive: fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + projectId: FAKE_PROJECT_ID, + }, + ]), + } as any), + ).toEqual({ ...mock, task: taskState, taskArchive: { - ...createEmptyEntity() + ...createEmptyEntity(), }, }); }); @@ -62,27 +72,33 @@ describe('dataRepair()', () => { it('should delete missing tasks for tags today list', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'TEST', - title: 'TEST', - projectId: FAKE_PROJECT_ID, - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + projectId: FAKE_PROJECT_ID, + }, + ]), } as any; const tagState: TagState = { - ...fakeEntityStateFromArray([{ - title: 'TEST_TAG', - id: 'TEST_ID_TAG', - taskIds: ['goneTag', 'TEST', 'noneExisting'], - }] as Partial []), + ...fakeEntityStateFromArray([ + { + title: 'TEST_TAG', + id: 'TEST_ID_TAG', + taskIds: ['goneTag', 'TEST', 'noneExisting'], + }, + ] as Partial[]), }; - expect(dataRepair({ - ...mock, - tag: tagState, - task: taskState, - })).toEqual({ + expect( + dataRepair({ + ...mock, + tag: tagState, + task: taskState, + }), + ).toEqual({ ...mock, task: taskState as any, tag: { @@ -93,36 +109,42 @@ describe('dataRepair()', () => { id: 'TEST_ID_TAG', taskIds: ['TEST'], }, - } as any - } + } as any, + }, }); }); it('should delete missing tasks for projects today list', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'TEST', - title: 'TEST', - projectId: 'TEST_ID_PROJECT', - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + projectId: 'TEST_ID_PROJECT', + }, + ]), } as any; const projectState: ProjectState = { - ...fakeEntityStateFromArray([{ - title: 'TEST_PROJECT', - id: 'TEST_ID_PROJECT', - taskIds: ['goneProject', 'TEST', 'noneExisting'], - backlogTaskIds: [], - }] as Partial []), + ...fakeEntityStateFromArray([ + { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['goneProject', 'TEST', 'noneExisting'], + backlogTaskIds: [], + }, + ] as Partial[]), }; - expect(dataRepair({ - ...mock, - project: projectState, - task: taskState, - })).toEqual({ + expect( + dataRepair({ + ...mock, + project: projectState, + task: taskState, + }), + ).toEqual({ ...mock, task: taskState as any, project: { @@ -134,8 +156,8 @@ describe('dataRepair()', () => { taskIds: ['TEST'], backlogTaskIds: [], }, - } as any - } + } as any, + }, }); }); @@ -144,25 +166,29 @@ describe('dataRepair()', () => { ...mock.task, ids: ['EXISTING'], entities: { - EXISTING: {...DEFAULT_TASK, id: 'EXISTING', projectId: 'TEST_PROJECT'}, - nullBacklog: null - } + EXISTING: { ...DEFAULT_TASK, id: 'EXISTING', projectId: 'TEST_PROJECT' }, + nullBacklog: null, + }, } as any; const projectState: ProjectState = { - ...fakeEntityStateFromArray([{ - title: 'TEST_PROJECT', - id: 'TEST_ID_PROJECT', - taskIds: ['EXISTING', 'goneProject', 'TEST', 'noneExisting'], - backlogTaskIds: ['noneExistingBacklog', 'nullBacklog'], - }] as Partial []), + ...fakeEntityStateFromArray([ + { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['EXISTING', 'goneProject', 'TEST', 'noneExisting'], + backlogTaskIds: ['noneExistingBacklog', 'nullBacklog'], + }, + ] as Partial[]), }; - expect(dataRepair({ - ...mock, - project: projectState, - task: taskState, - })).toEqual({ + expect( + dataRepair({ + ...mock, + project: projectState, + task: taskState, + }), + ).toEqual({ ...mock, task: taskState as any, project: { @@ -174,8 +200,8 @@ describe('dataRepair()', () => { taskIds: ['EXISTING'], backlogTaskIds: [], }, - } as any - } + } as any, + }, }); }); @@ -184,25 +210,34 @@ describe('dataRepair()', () => { ...mock.taskArchive, ids: ['PAR_ID', 'SUB_ID'], entities: { - SUB_ID: {...DEFAULT_TASK, id: 'SUB_ID', projectId: 'TEST_PROJECT', parentId: 'PAR_ID'}, - PAR_ID: {...DEFAULT_TASK, id: 'PAR_ID', projectId: 'TEST_PROJECT'}, - } + SUB_ID: { + ...DEFAULT_TASK, + id: 'SUB_ID', + projectId: 'TEST_PROJECT', + parentId: 'PAR_ID', + }, + PAR_ID: { ...DEFAULT_TASK, id: 'PAR_ID', projectId: 'TEST_PROJECT' }, + }, } as any; const projectState: ProjectState = { - ...fakeEntityStateFromArray([{ - title: 'TEST_PROJECT', - id: 'TEST_ID_PROJECT', - taskIds: [], - backlogTaskIds: ['SUB_ID'], - }] as Partial []), + ...fakeEntityStateFromArray([ + { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: [], + backlogTaskIds: ['SUB_ID'], + }, + ] as Partial[]), }; - expect(dataRepair({ - ...mock, - project: projectState, - taskArchive: taskArchiveState, - })).toEqual({ + expect( + dataRepair({ + ...mock, + project: projectState, + taskArchive: taskArchiveState, + }), + ).toEqual({ ...mock, taskArchive: taskArchiveState, project: { @@ -214,35 +249,41 @@ describe('dataRepair()', () => { taskIds: [], backlogTaskIds: [], }, - } as any - } + } as any, + }, }); }); it('should delete missing tasks for projects backlog list', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'TEST', - title: 'TEST', - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + }, + ]), } as any; const projectState: ProjectState = { - ...fakeEntityStateFromArray([{ - title: 'TEST_PROJECT', - id: 'TEST_ID_PROJECT', - taskIds: [], - backlogTaskIds: ['goneProject', 'TEST', 'noneExisting'], - }] as Partial []), + ...fakeEntityStateFromArray([ + { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: [], + backlogTaskIds: ['goneProject', 'TEST', 'noneExisting'], + }, + ] as Partial[]), }; - expect(dataRepair({ - ...mock, - project: projectState, - task: taskState, - })).toEqual({ + expect( + dataRepair({ + ...mock, + project: projectState, + task: taskState, + }), + ).toEqual({ ...mock, task: taskState as any, project: { @@ -254,90 +295,108 @@ describe('dataRepair()', () => { taskIds: [], backlogTaskIds: ['TEST'], }, - } as any - } + } as any, + }, }); }); describe('should fix duplicate entities for', () => { it('task', () => { - expect(dataRepair({ + expect( + dataRepair({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'NO_DUPE', + title: 'NO_DUPE', + projectId: FAKE_PROJECT_ID, + }, + ]), + } as any, + }), + ).toEqual({ ...mock, task: { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'DUPE', - title: 'DUPE', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'DUPE', - title: 'DUPE', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'NO_DUPE', - title: 'NO_DUPE', - projectId: FAKE_PROJECT_ID, - }]) - } as any, - })).toEqual({ - ...mock, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'DUPE', - title: 'DUPE', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'NO_DUPE', - title: 'NO_DUPE', - projectId: FAKE_PROJECT_ID, - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'NO_DUPE', + title: 'NO_DUPE', + projectId: FAKE_PROJECT_ID, + }, + ]), } as any, }); }); it('taskArchive', () => { - expect(dataRepair({ + expect( + dataRepair({ + ...mock, + taskArchive: { + ...mock.taskArchive, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'NO_DUPE', + title: 'NO_DUPE', + projectId: FAKE_PROJECT_ID, + }, + ]), + } as any, + }), + ).toEqual({ ...mock, taskArchive: { ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'DUPE', - title: 'DUPE', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'DUPE', - title: 'DUPE', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'NO_DUPE', - title: 'NO_DUPE', - projectId: FAKE_PROJECT_ID, - }]) - } as any, - })).toEqual({ - ...mock, - taskArchive: { - ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'DUPE', - title: 'DUPE', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'NO_DUPE', - title: 'NO_DUPE', - projectId: FAKE_PROJECT_ID, - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'NO_DUPE', + title: 'NO_DUPE', + projectId: FAKE_PROJECT_ID, + }, + ]), } as any, }); }); @@ -345,44 +404,48 @@ describe('dataRepair()', () => { describe('should fix inconsistent entity states for', () => { it('task', () => { - expect(dataRepair({ - ...mock, - task: { - ids: ['AAA, XXX', 'YYY'], - entities: { - AAA: {...DEFAULT_TASK, id: 'AAA', projectId: FAKE_PROJECT_ID}, - CCC: {...DEFAULT_TASK, id: 'CCC', projectId: FAKE_PROJECT_ID}, - } - } as any, - })).toEqual({ + expect( + dataRepair({ + ...mock, + task: { + ids: ['AAA, XXX', 'YYY'], + entities: { + AAA: { ...DEFAULT_TASK, id: 'AAA', projectId: FAKE_PROJECT_ID }, + CCC: { ...DEFAULT_TASK, id: 'CCC', projectId: FAKE_PROJECT_ID }, + }, + } as any, + }), + ).toEqual({ ...mock, task: { ids: ['AAA', 'CCC'], entities: { - AAA: {...DEFAULT_TASK, id: 'AAA', projectId: FAKE_PROJECT_ID}, - CCC: {...DEFAULT_TASK, id: 'CCC', projectId: FAKE_PROJECT_ID}, - } + AAA: { ...DEFAULT_TASK, id: 'AAA', projectId: FAKE_PROJECT_ID }, + CCC: { ...DEFAULT_TASK, id: 'CCC', projectId: FAKE_PROJECT_ID }, + }, } as any, }); }); it('taskArchive', () => { - expect(dataRepair({ - ...mock, - taskArchive: { - ids: ['AAA, XXX', 'YYY'], - entities: { - AAA: {...DEFAULT_TASK, id: 'AAA', projectId: FAKE_PROJECT_ID}, - CCC: {...DEFAULT_TASK, id: 'CCC', projectId: FAKE_PROJECT_ID}, - } - } as any, - })).toEqual({ + expect( + dataRepair({ + ...mock, + taskArchive: { + ids: ['AAA, XXX', 'YYY'], + entities: { + AAA: { ...DEFAULT_TASK, id: 'AAA', projectId: FAKE_PROJECT_ID }, + CCC: { ...DEFAULT_TASK, id: 'CCC', projectId: FAKE_PROJECT_ID }, + }, + } as any, + }), + ).toEqual({ ...mock, taskArchive: { ids: ['AAA', 'CCC'], entities: { - AAA: {...DEFAULT_TASK, id: 'AAA', projectId: FAKE_PROJECT_ID}, - CCC: {...DEFAULT_TASK, id: 'CCC', projectId: FAKE_PROJECT_ID}, - } + AAA: { ...DEFAULT_TASK, id: 'AAA', projectId: FAKE_PROJECT_ID }, + CCC: { ...DEFAULT_TASK, id: 'CCC', projectId: FAKE_PROJECT_ID }, + }, } as any, }); }); @@ -391,51 +454,61 @@ describe('dataRepair()', () => { it('should restore missing tasks from taskArchive if available', () => { const taskArchiveState = { ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'goneToArchiveToday', - title: 'goneToArchiveToday', - projectId: 'TEST_ID_PROJECT', - }, { - ...DEFAULT_TASK, - id: 'goneToArchiveBacklog', - title: 'goneToArchiveBacklog', - projectId: 'TEST_ID_PROJECT', - }]) - } as any; - - const projectState: ProjectState = { - ...fakeEntityStateFromArray([{ - title: 'TEST_PROJECT', - id: 'TEST_ID_PROJECT', - taskIds: ['goneToArchiveToday', 'GONE'], - backlogTaskIds: ['goneToArchiveBacklog', 'GONE'], - }] as Partial []), - }; - - expect(dataRepair({ - ...mock, - project: projectState, - taskArchive: taskArchiveState, - task: { - ...mock.task, - ...createEmptyEntity() - } as any, - })).toEqual({ - ...mock, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ + ...fakeEntityStateFromArray([ + { ...DEFAULT_TASK, id: 'goneToArchiveToday', title: 'goneToArchiveToday', projectId: 'TEST_ID_PROJECT', - }, { + }, + { ...DEFAULT_TASK, id: 'goneToArchiveBacklog', title: 'goneToArchiveBacklog', projectId: 'TEST_ID_PROJECT', - }]) + }, + ]), + } as any; + + const projectState: ProjectState = { + ...fakeEntityStateFromArray([ + { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['goneToArchiveToday', 'GONE'], + backlogTaskIds: ['goneToArchiveBacklog', 'GONE'], + }, + ] as Partial[]), + }; + + expect( + dataRepair({ + ...mock, + project: projectState, + taskArchive: taskArchiveState, + task: { + ...mock.task, + ...createEmptyEntity(), + } as any, + }), + ).toEqual({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'goneToArchiveToday', + title: 'goneToArchiveToday', + projectId: 'TEST_ID_PROJECT', + }, + { + ...DEFAULT_TASK, + id: 'goneToArchiveBacklog', + title: 'goneToArchiveBacklog', + projectId: 'TEST_ID_PROJECT', + }, + ]), } as any, project: { ...projectState, @@ -446,54 +519,63 @@ describe('dataRepair()', () => { taskIds: ['goneToArchiveToday'], backlogTaskIds: ['goneToArchiveBacklog'], }, - } as any - } + } as any, + }, }); }); it('should add orphan tasks to their project list', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'orphanedTask', - title: 'orphanedTask', - projectId: 'TEST_ID_PROJECT', - parentId: null, - }, { - ...DEFAULT_TASK, - id: 'orphanedTaskOtherProject', - title: 'orphanedTaskOtherProject', - projectId: 'TEST_ID_PROJECT_OTHER', - parentId: null, - }, { - ...DEFAULT_TASK, - id: 'regularTaskOtherProject', - title: 'regularTaskOtherProject', - projectId: 'TEST_ID_PROJECT_OTHER', - parentId: null, - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'orphanedTask', + title: 'orphanedTask', + projectId: 'TEST_ID_PROJECT', + parentId: null, + }, + { + ...DEFAULT_TASK, + id: 'orphanedTaskOtherProject', + title: 'orphanedTaskOtherProject', + projectId: 'TEST_ID_PROJECT_OTHER', + parentId: null, + }, + { + ...DEFAULT_TASK, + id: 'regularTaskOtherProject', + title: 'regularTaskOtherProject', + projectId: 'TEST_ID_PROJECT_OTHER', + parentId: null, + }, + ]), } as any; const projectState: ProjectState = { - ...fakeEntityStateFromArray([{ - title: 'TEST_PROJECT', - id: 'TEST_ID_PROJECT', - taskIds: ['GONE'], - backlogTaskIds: [], - }, { - title: 'TEST_PROJECT_OTHER', - id: 'TEST_ID_PROJECT_OTHER', - taskIds: ['regularTaskOtherProject'], - backlogTaskIds: [], - }] as Partial []), + ...fakeEntityStateFromArray([ + { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['GONE'], + backlogTaskIds: [], + }, + { + title: 'TEST_PROJECT_OTHER', + id: 'TEST_ID_PROJECT_OTHER', + taskIds: ['regularTaskOtherProject'], + backlogTaskIds: [], + }, + ] as Partial[]), }; - expect(dataRepair({ - ...mock, - project: projectState, - task: taskState, - })).toEqual({ + expect( + dataRepair({ + ...mock, + project: projectState, + task: taskState, + }), + ).toEqual({ ...mock, task: taskState, project: { @@ -511,73 +593,84 @@ describe('dataRepair()', () => { taskIds: ['regularTaskOtherProject', 'orphanedTaskOtherProject'], backlogTaskIds: [], }, - } as any - } + } as any, + }, }); }); it('should move archived sub tasks back to their unarchived parents', () => { const taskStateBefore = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskUnarchived', - title: 'subTaskUnarchived', - parentId: 'parent', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'parent', - title: 'parent', - parentId: null, - subTaskIds: ['subTaskUnarchived'], - projectId: FAKE_PROJECT_ID, - }]) - } as any; - - const taskArchiveStateBefore = { - ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskArchived', - title: 'subTaskArchived', - parentId: 'parent', - projectId: FAKE_PROJECT_ID, - }]) - } as any; - - expect(dataRepair({ - ...mock, - task: taskStateBefore, - taskArchive: taskArchiveStateBefore, - })).toEqual({ - ...mock, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ + ...fakeEntityStateFromArray([ + { ...DEFAULT_TASK, id: 'subTaskUnarchived', title: 'subTaskUnarchived', parentId: 'parent', projectId: FAKE_PROJECT_ID, - }, { + }, + { ...DEFAULT_TASK, id: 'parent', title: 'parent', parentId: null, - subTaskIds: ['subTaskUnarchived', 'subTaskArchived'], + subTaskIds: ['subTaskUnarchived'], projectId: FAKE_PROJECT_ID, - }, { + }, + ]), + } as any; + + const taskArchiveStateBefore = { + ...mock.taskArchive, + ...fakeEntityStateFromArray([ + { ...DEFAULT_TASK, id: 'subTaskArchived', title: 'subTaskArchived', parentId: 'parent', projectId: FAKE_PROJECT_ID, - }]) + }, + ]), + } as any; + + expect( + dataRepair({ + ...mock, + task: taskStateBefore, + taskArchive: taskArchiveStateBefore, + }), + ).toEqual({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'subTaskUnarchived', + title: 'subTaskUnarchived', + parentId: 'parent', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'parent', + title: 'parent', + parentId: null, + subTaskIds: ['subTaskUnarchived', 'subTaskArchived'], + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'subTaskArchived', + title: 'subTaskArchived', + parentId: 'parent', + projectId: FAKE_PROJECT_ID, + }, + ]), } as any, taskArchive: { ...mock.taskArchive, - ...fakeEntityStateFromArray([]) + ...fakeEntityStateFromArray([]), } as any, }); }); @@ -585,63 +678,74 @@ describe('dataRepair()', () => { it('should move unarchived sub tasks to their archived parents', () => { const taskStateBefore = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskUnarchived', - title: 'subTaskUnarchived', - parentId: 'parent', - }]) - } as any; - - const taskArchiveStateBefore = { - ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskArchived', - title: 'subTaskArchived', - parentId: 'parent', - }, { - ...DEFAULT_TASK, - id: 'parent', - title: 'parent', - parentId: null, - subTaskIds: ['subTaskArchived'], - projectId: FAKE_PROJECT_ID, - }]) - } as any; - - expect(dataRepair({ - ...mock, - task: taskStateBefore, - taskArchive: taskArchiveStateBefore, - })).toEqual({ - ...mock, - task: { - ...mock.task, - ...fakeEntityStateFromArray([]) - } as any, - taskArchive: { - ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskArchived', - title: 'subTaskArchived', - parentId: 'parent', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'parent', - title: 'parent', - parentId: null, - subTaskIds: ['subTaskArchived', 'subTaskUnarchived'], - projectId: FAKE_PROJECT_ID, - }, { + ...fakeEntityStateFromArray([ + { ...DEFAULT_TASK, id: 'subTaskUnarchived', title: 'subTaskUnarchived', parentId: 'parent', + }, + ]), + } as any; + + const taskArchiveStateBefore = { + ...mock.taskArchive, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'subTaskArchived', + title: 'subTaskArchived', + parentId: 'parent', + }, + { + ...DEFAULT_TASK, + id: 'parent', + title: 'parent', + parentId: null, + subTaskIds: ['subTaskArchived'], projectId: FAKE_PROJECT_ID, - }]) + }, + ]), + } as any; + + expect( + dataRepair({ + ...mock, + task: taskStateBefore, + taskArchive: taskArchiveStateBefore, + }), + ).toEqual({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([]), + } as any, + taskArchive: { + ...mock.taskArchive, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'subTaskArchived', + title: 'subTaskArchived', + parentId: 'parent', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'parent', + title: 'parent', + parentId: null, + subTaskIds: ['subTaskArchived', 'subTaskUnarchived'], + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'subTaskUnarchived', + title: 'subTaskUnarchived', + parentId: 'parent', + projectId: FAKE_PROJECT_ID, + }, + ]), } as any, }); }); @@ -649,65 +753,77 @@ describe('dataRepair()', () => { it('should assign task projectId according to parent', () => { const project = { ...mock.project, - ...fakeEntityStateFromArray([{ - ...DEFAULT_PROJECT, - id: 'p1', - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_PROJECT, + id: 'p1', + }, + ]), } as any; const taskStateBefore = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTask1', - title: 'subTask1', - projectId: null, - parentId: 'parent', - }, { - ...DEFAULT_TASK, - id: 'subTask2', - title: 'subTask2', - projectId: null, - parentId: 'parent', - }, { - ...DEFAULT_TASK, - id: 'parent', - title: 'parent', - parentId: null, - projectId: 'p1', - subTaskIds: ['subTask1', 'subTask2'], - }]) - } as any; - - expect(dataRepair({ - ...mock, - project, - task: taskStateBefore, - })).toEqual({ - ...mock, - project, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ + ...fakeEntityStateFromArray([ + { ...DEFAULT_TASK, id: 'subTask1', title: 'subTask1', + projectId: null, parentId: 'parent', - projectId: 'p1', - }, { + }, + { ...DEFAULT_TASK, id: 'subTask2', title: 'subTask2', + projectId: null, parentId: 'parent', - projectId: 'p1', - }, { + }, + { ...DEFAULT_TASK, id: 'parent', title: 'parent', parentId: null, - subTaskIds: ['subTask1', 'subTask2'], projectId: 'p1', - }]) + subTaskIds: ['subTask1', 'subTask2'], + }, + ]), + } as any; + + expect( + dataRepair({ + ...mock, + project, + task: taskStateBefore, + }), + ).toEqual({ + ...mock, + project, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'subTask1', + title: 'subTask1', + parentId: 'parent', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 'subTask2', + title: 'subTask2', + parentId: 'parent', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 'parent', + title: 'parent', + parentId: null, + subTaskIds: ['subTask1', 'subTask2'], + projectId: 'p1', + }, + ]), } as any, }); }); @@ -715,27 +831,31 @@ describe('dataRepair()', () => { it('should delete non-existent project ids for tasks in "task"', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'TEST', - title: 'TEST', - projectId: 'NON_EXISTENT' - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + projectId: 'NON_EXISTENT', + }, + ]), } as any; - expect(dataRepair({ - ...mock, - task: taskState, - } as any)).toEqual({ + expect( + dataRepair({ + ...mock, + task: taskState, + } as any), + ).toEqual({ ...mock, task: { ...taskState, entities: { TEST: { ...taskState.entities.TEST, - projectId: null - } - } + projectId: null, + }, + }, }, }); }); @@ -743,27 +863,31 @@ describe('dataRepair()', () => { it('should delete non-existent project ids for tasks in "taskArchive"', () => { const taskArchiveState = { ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'TEST', - title: 'TEST', - projectId: 'NON_EXISTENT' - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + projectId: 'NON_EXISTENT', + }, + ]), } as any; - expect(dataRepair({ - ...mock, - taskArchive: taskArchiveState, - } as any)).toEqual({ + expect( + dataRepair({ + ...mock, + taskArchive: taskArchiveState, + } as any), + ).toEqual({ ...mock, taskArchive: { ...taskArchiveState, entities: { TEST: { ...taskArchiveState.entities.TEST, - projectId: null - } - } + projectId: null, + }, + }, }, }); }); @@ -771,47 +895,58 @@ describe('dataRepair()', () => { it('should remove from project list if task has wrong project id', () => { const project = { ...mock.project, - ...fakeEntityStateFromArray([{ - ...DEFAULT_PROJECT, - id: 'p1', - taskIds: ['t1', 't2'] - }, { - ...DEFAULT_PROJECT, - id: 'p2', - taskIds: ['t1'] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_PROJECT, + id: 'p1', + taskIds: ['t1', 't2'], + }, + { + ...DEFAULT_PROJECT, + id: 'p2', + taskIds: ['t1'], + }, + ]), } as any; const task = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 't1', - projectId: 'p1' - }, { - ...DEFAULT_TASK, - id: 't2', - projectId: 'p1' - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 't1', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 't2', + projectId: 'p1', + }, + ]), } as any; - expect(dataRepair({ - ...mock, - project, - task, - })).toEqual({ + expect( + dataRepair({ + ...mock, + project, + task, + }), + ).toEqual({ ...mock, project: { ...project, - ...fakeEntityStateFromArray([{ - ...DEFAULT_PROJECT, - id: 'p1', - taskIds: ['t1', 't2'] - }, { - ...DEFAULT_PROJECT, - id: 'p2', - taskIds: [] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_PROJECT, + id: 'p1', + taskIds: ['t1', 't2'], + }, + { + ...DEFAULT_PROJECT, + id: 'p2', + taskIds: [], + }, + ]), }, task, }); @@ -820,44 +955,54 @@ describe('dataRepair()', () => { it('should move to project if task has no projectId', () => { const project = { ...mock.project, - ...fakeEntityStateFromArray([{ - ...DEFAULT_PROJECT, - id: 'p1', - taskIds: ['t1', 't2'] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_PROJECT, + id: 'p1', + taskIds: ['t1', 't2'], + }, + ]), } as any; const task = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 't1', - projectId: 'p1' - }, { - ...DEFAULT_TASK, - id: 't2', - projectId: null - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 't1', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 't2', + projectId: null, + }, + ]), } as any; - expect(dataRepair({ - ...mock, - project, - task, - })).toEqual({ + expect( + dataRepair({ + ...mock, + project, + task, + }), + ).toEqual({ ...mock, project, task: { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 't1', - projectId: 'p1' - }, { - ...DEFAULT_TASK, - id: 't2', - projectId: 'p1' - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 't1', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 't2', + projectId: 'p1', + }, + ]), } as any, }); }); @@ -865,44 +1010,54 @@ describe('dataRepair()', () => { it('should move to project if backlogTask has no projectId', () => { const project = { ...mock.project, - ...fakeEntityStateFromArray([{ - ...DEFAULT_PROJECT, - id: 'p1', - backlogTaskIds: ['t1', 't2'] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_PROJECT, + id: 'p1', + backlogTaskIds: ['t1', 't2'], + }, + ]), } as any; const task = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 't1', - projectId: 'p1' - }, { - ...DEFAULT_TASK, - id: 't2', - projectId: null - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 't1', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 't2', + projectId: null, + }, + ]), } as any; - expect(dataRepair({ - ...mock, - project, - task, - })).toEqual({ + expect( + dataRepair({ + ...mock, + project, + task, + }), + ).toEqual({ ...mock, project, task: { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 't1', - projectId: 'p1' - }, { - ...DEFAULT_TASK, - id: 't2', - projectId: 'p1' - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 't1', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 't2', + projectId: 'p1', + }, + ]), } as any, }); }); @@ -910,44 +1065,54 @@ describe('dataRepair()', () => { it('should add tagId to task if listed, but task does not contain it', () => { const tag = { ...mock.tag, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TAG, - id: 'tag1', - taskIds: ['task1', 'task2'] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TAG, + id: 'tag1', + taskIds: ['task1', 'task2'], + }, + ]), } as any; const task = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'task1', - tagIds: ['tag1'] - }, { - ...DEFAULT_TASK, - id: 'task2', - tagIds: [] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'task1', + tagIds: ['tag1'], + }, + { + ...DEFAULT_TASK, + id: 'task2', + tagIds: [], + }, + ]), } as any; - expect(dataRepair({ - ...mock, - tag, - task, - })).toEqual({ + expect( + dataRepair({ + ...mock, + tag, + task, + }), + ).toEqual({ ...mock, tag, task: { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'task1', - tagIds: ['tag1'] - }, { - ...DEFAULT_TASK, - id: 'task2', - tagIds: ['tag1'] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'task1', + tagIds: ['tag1'], + }, + { + ...DEFAULT_TASK, + id: 'task2', + tagIds: ['tag1'], + }, + ]), } as any, }); }); @@ -955,128 +1120,156 @@ describe('dataRepair()', () => { it('should cleanup orphaned sub tasks', () => { const task = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'task1', - subTaskIds: ['s1', 's2GONE'], - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 's1', - parentId: 'task1', - projectId: FAKE_PROJECT_ID, - }]), - } as any; - - const taskArchive = { - ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'archiveTask1', - subTaskIds: ['as1', 'as2GONE'], - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'as1', - parentId: 'archiveTask1', - projectId: FAKE_PROJECT_ID, - }]), - } as any; - - expect(dataRepair({ - ...mock, - task, - taskArchive, - })).toEqual({ - ...mock, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ + ...fakeEntityStateFromArray([ + { ...DEFAULT_TASK, id: 'task1', - subTaskIds: ['s1'], + subTaskIds: ['s1', 's2GONE'], projectId: FAKE_PROJECT_ID, - }, { + }, + { ...DEFAULT_TASK, id: 's1', parentId: 'task1', projectId: FAKE_PROJECT_ID, - }]), - } as any, - taskArchive: { - ...mock.taskArchive, - ...fakeEntityStateFromArray([{ + }, + ]), + } as any; + + const taskArchive = { + ...mock.taskArchive, + ...fakeEntityStateFromArray([ + { ...DEFAULT_TASK, id: 'archiveTask1', - subTaskIds: ['as1'], + subTaskIds: ['as1', 'as2GONE'], projectId: FAKE_PROJECT_ID, - }, { + }, + { ...DEFAULT_TASK, id: 'as1', parentId: 'archiveTask1', projectId: FAKE_PROJECT_ID, - }]), - } as any + }, + ]), + } as any; + + expect( + dataRepair({ + ...mock, + task, + taskArchive, + }), + ).toEqual({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'task1', + subTaskIds: ['s1'], + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 's1', + parentId: 'task1', + projectId: FAKE_PROJECT_ID, + }, + ]), + } as any, + taskArchive: { + ...mock.taskArchive, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'archiveTask1', + subTaskIds: ['as1'], + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'as1', + parentId: 'archiveTask1', + projectId: FAKE_PROJECT_ID, + }, + ]), + } as any, }); }); it('should add today tag if no projectId or no tags', () => { const task = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'task1', - subTaskIds: ['sub_task'] - }, { - ...DEFAULT_TASK, - id: 'task2', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'sub_task', - parentId: 'task1' - }]), + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'task1', + subTaskIds: ['sub_task'], + }, + { + ...DEFAULT_TASK, + id: 'task2', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'sub_task', + parentId: 'task1', + }, + ]), } as any; const taskArchive = { ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'archiveTask1', - }]), + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'archiveTask1', + }, + ]), } as any; - expect(dataRepair({ - ...mock, - task, - taskArchive, - })).toEqual({ + expect( + dataRepair({ + ...mock, + task, + taskArchive, + }), + ).toEqual({ ...mock, task: { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'task1', - tagIds: [TODAY_TAG.id], - subTaskIds: ['sub_task'] - }, { - ...DEFAULT_TASK, - id: 'task2', - projectId: FAKE_PROJECT_ID, - }, { - ...DEFAULT_TASK, - id: 'sub_task', - parentId: 'task1' - }]), + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'task1', + tagIds: [TODAY_TAG.id], + subTaskIds: ['sub_task'], + }, + { + ...DEFAULT_TASK, + id: 'task2', + projectId: FAKE_PROJECT_ID, + }, + { + ...DEFAULT_TASK, + id: 'sub_task', + parentId: 'task1', + }, + ]), } as any, taskArchive: { ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'archiveTask1', - tagIds: [TODAY_TAG.id], - }]), - } as any + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'archiveTask1', + tagIds: [TODAY_TAG.id], + }, + ]), + } as any, }); }); }); diff --git a/src/app/core/data-repair/data-repair.util.ts b/src/app/core/data-repair/data-repair.util.ts index a3f45800f..aeefc1867 100644 --- a/src/app/core/data-repair/data-repair.util.ts +++ b/src/app/core/data-repair/data-repair.util.ts @@ -6,7 +6,14 @@ import { TaskArchive, TaskCopy, TaskState } from '../../features/tasks/task.mode import { unique } from '../../util/unique'; import { TODAY_TAG } from '../../features/tag/tag.const'; -const ENTITY_STATE_KEYS: (keyof AppDataComplete)[] = ['task', 'taskArchive', 'taskRepeatCfg', 'tag', 'project', 'simpleCounter']; +const ENTITY_STATE_KEYS: (keyof AppDataComplete)[] = [ + 'task', + 'taskArchive', + 'taskRepeatCfg', + 'tag', + 'project', + 'simpleCounter', +]; export const dataRepair = (data: AppDataComplete): AppDataComplete => { if (!isDataRepairPossible(data)) { @@ -36,7 +43,9 @@ export const dataRepair = (data: AppDataComplete): AppDataComplete => { const _fixEntityStates = (data: AppDataComplete): AppDataComplete => { ENTITY_STATE_KEYS.forEach((key) => { - data[key] = _resetEntityIdsFromObjects(data[key] as AppBaseDataEntityLikeStates) as any; + data[key] = _resetEntityIdsFromObjects( + data[key] as AppBaseDataEntityLikeStates, + ) as any; }); return data; @@ -48,8 +57,8 @@ const _removeDuplicatesFromArchive = (data: AppDataComplete): AppDataComplete => const duplicateIds = taskIds.filter((id) => archiveTaskIds.includes(id)); if (duplicateIds.length) { - data.taskArchive.ids = archiveTaskIds.filter(id => !duplicateIds.includes(id)); - duplicateIds.forEach(id => { + data.taskArchive.ids = archiveTaskIds.filter((id) => !duplicateIds.includes(id)); + duplicateIds.forEach((id) => { if (data.taskArchive.entities[id]) { delete data.taskArchive.entities[id]; } @@ -61,7 +70,9 @@ const _removeDuplicatesFromArchive = (data: AppDataComplete): AppDataComplete => return data; }; -const _moveArchivedSubTasksToUnarchivedParents = (data: AppDataComplete): AppDataComplete => { +const _moveArchivedSubTasksToUnarchivedParents = ( + data: AppDataComplete, +): AppDataComplete => { // to avoid ambiguity const taskState: TaskState = data.task; const taskArchiveState: TaskArchive = data.taskArchive; @@ -73,7 +84,7 @@ const _moveArchivedSubTasksToUnarchivedParents = (data: AppDataComplete): AppDat orhphanedArchivedSubTasks.forEach((t: TaskCopy) => { // delete archived if duplicate if (taskState.ids.includes(t.id as string)) { - taskArchiveState.ids = taskArchiveState.ids.filter(id => t.id !== id); + taskArchiveState.ids = taskArchiveState.ids.filter((id) => t.id !== id); delete taskArchiveState.entities[t.id]; // if entity is empty for some reason if (!taskState.entities[t.id]) { @@ -82,14 +93,14 @@ const _moveArchivedSubTasksToUnarchivedParents = (data: AppDataComplete): AppDat } // copy to today if parent exists else if (taskState.ids.includes(t.parentId as string)) { - taskState.ids.push((t.id)); + taskState.ids.push(t.id); taskState.entities[t.id] = t; const par: TaskCopy = taskState.entities[t.parentId as string] as TaskCopy; par.subTaskIds = unique([...par.subTaskIds, t.id]); // and delete from archive - taskArchiveState.ids = taskArchiveState.ids.filter(id => t.id !== id); + taskArchiveState.ids = taskArchiveState.ids.filter((id) => t.id !== id); delete taskArchiveState.entities[t.id]; } @@ -103,7 +114,9 @@ const _moveArchivedSubTasksToUnarchivedParents = (data: AppDataComplete): AppDat return data; }; -const _moveUnArchivedSubTasksToArchivedParents = (data: AppDataComplete): AppDataComplete => { +const _moveUnArchivedSubTasksToArchivedParents = ( + data: AppDataComplete, +): AppDataComplete => { // to avoid ambiguity const taskState: TaskState = data.task; const taskArchiveState: TaskArchive = data.taskArchive; @@ -115,7 +128,7 @@ const _moveUnArchivedSubTasksToArchivedParents = (data: AppDataComplete): AppDat orhphanedUnArchivedSubTasks.forEach((t: TaskCopy) => { // delete un-archived if duplicate if (taskArchiveState.ids.includes(t.id as string)) { - taskState.ids = taskState.ids.filter(id => t.id !== id); + taskState.ids = taskState.ids.filter((id) => t.id !== id); delete taskState.entities[t.id]; // if entity is empty for some reason if (!taskArchiveState.entities[t.id]) { @@ -124,14 +137,14 @@ const _moveUnArchivedSubTasksToArchivedParents = (data: AppDataComplete): AppDat } // copy to archive if parent exists else if (taskArchiveState.ids.includes(t.parentId as string)) { - taskArchiveState.ids.push((t.id)); + taskArchiveState.ids.push(t.id); taskArchiveState.entities[t.id] = t; const par: TaskCopy = taskArchiveState.entities[t.parentId as string] as TaskCopy; par.subTaskIds = unique([...par.subTaskIds, t.id]); // and delete from today - taskState.ids = taskState.ids.filter(id => t.id !== id); + taskState.ids = taskState.ids.filter((id) => t.id !== id); delete taskState.entities[t.id]; } // make main if it doesn't @@ -144,8 +157,10 @@ const _moveUnArchivedSubTasksToArchivedParents = (data: AppDataComplete): AppDat return data; }; -const _removeMissingTasksFromListsOrRestoreFromArchive = (data: AppDataComplete): AppDataComplete => { - const {task, project, tag, taskArchive} = data; +const _removeMissingTasksFromListsOrRestoreFromArchive = ( + data: AppDataComplete, +): AppDataComplete => { + const { task, project, tag, taskArchive } = data; const taskIds: string[] = task.ids; const taskArchiveIds: string[] = taskArchive.ids as string[]; const taskIdsToRestoreFromArchive: string[] = []; @@ -161,57 +176,70 @@ const _removeMissingTasksFromListsOrRestoreFromArchive = (data: AppDataComplete) return taskIds.includes(id); }); - projectItem.backlogTaskIds = projectItem.backlogTaskIds.filter((id: string): boolean => { - if (taskArchiveIds.includes(id)) { - taskIdsToRestoreFromArchive.push(id); - return true; - } - return taskIds.includes(id); - }); + projectItem.backlogTaskIds = projectItem.backlogTaskIds.filter( + (id: string): boolean => { + if (taskArchiveIds.includes(id)) { + taskIdsToRestoreFromArchive.push(id); + return true; + } + return taskIds.includes(id); + }, + ); }); tag.ids.forEach((tId: string | number) => { const tagItem = tag.entities[tId] as TagCopy; - tagItem.taskIds = tagItem.taskIds.filter(id => taskIds.includes(id)); + tagItem.taskIds = tagItem.taskIds.filter((id) => taskIds.includes(id)); }); - taskIdsToRestoreFromArchive.forEach(id => { + taskIdsToRestoreFromArchive.forEach((id) => { task.entities[id] = taskArchive.entities[id]; delete taskArchive.entities[id]; }); task.ids = [...taskIds, ...taskIdsToRestoreFromArchive]; - taskArchive.ids = taskArchiveIds.filter(id => !taskIdsToRestoreFromArchive.includes(id)); + taskArchive.ids = taskArchiveIds.filter( + (id) => !taskIdsToRestoreFromArchive.includes(id), + ); if (taskIdsToRestoreFromArchive.length > 0) { - console.log(taskIdsToRestoreFromArchive.length + ' missing tasks restored from archive.'); + console.log( + taskIdsToRestoreFromArchive.length + ' missing tasks restored from archive.', + ); } return data; }; -const _resetEntityIdsFromObjects = (data: AppBaseDataEntityLikeStates): AppBaseDataEntityLikeStates => { +const _resetEntityIdsFromObjects = ( + data: AppBaseDataEntityLikeStates, +): AppBaseDataEntityLikeStates => { return { ...data, - ids: Object.keys(data.entities).filter(id => !!data.entities[id]) + ids: Object.keys(data.entities).filter((id) => !!data.entities[id]), }; }; const _addOrphanedTasksToProjectLists = (data: AppDataComplete): AppDataComplete => { - const {task, project} = data; + const { task, project } = data; let allTaskIdsOnProjectLists: string[] = []; project.ids.forEach((pId: string | number) => { const projectItem = project.entities[pId] as ProjectCopy; - allTaskIdsOnProjectLists = allTaskIdsOnProjectLists.concat(projectItem.taskIds, projectItem.backlogTaskIds); + allTaskIdsOnProjectLists = allTaskIdsOnProjectLists.concat( + projectItem.taskIds, + projectItem.backlogTaskIds, + ); }); - const orphanedTaskIds: string[] = task.ids.filter(tid => { + const orphanedTaskIds: string[] = task.ids.filter((tid) => { const taskItem = task.entities[tid]; if (!taskItem) { throw new Error('Missing task'); } - return !taskItem.parentId && !allTaskIdsOnProjectLists.includes(tid) && taskItem.projectId; + return ( + !taskItem.parentId && !allTaskIdsOnProjectLists.includes(tid) && taskItem.projectId + ); }); - orphanedTaskIds.forEach(tid => { + orphanedTaskIds.forEach((tid) => { const taskItem = task.entities[tid]; if (!taskItem) { throw new Error('Missing task'); @@ -227,7 +255,7 @@ const _addOrphanedTasksToProjectLists = (data: AppDataComplete): AppDataComplete }; const _removeNonExistentProjectIds = (data: AppDataComplete): AppDataComplete => { - const {task, project, taskArchive} = data; + const { task, project, taskArchive } = data; const projectIds: string[] = project.ids as string[]; const taskIds: string[] = task.ids; const taskArchiveIds: string[] = taskArchive.ids as string[]; @@ -252,70 +280,76 @@ const _removeNonExistentProjectIds = (data: AppDataComplete): AppDataComplete => const _cleanupNonExistingTasksFromLists = (data: AppDataComplete): AppDataComplete => { const projectIds: string[] = data.project.ids as string[]; - projectIds - .forEach(pid => { - const projectItem = data.project.entities[pid]; - if (!projectItem) { - console.log(data.project); - throw new Error('No project'); - } - (projectItem as ProjectCopy).taskIds = projectItem.taskIds.filter(tid => !!data.task.entities[tid]); - (projectItem as ProjectCopy).backlogTaskIds = projectItem.backlogTaskIds.filter(tid => !!data.task.entities[tid]); - } + projectIds.forEach((pid) => { + const projectItem = data.project.entities[pid]; + if (!projectItem) { + console.log(data.project); + throw new Error('No project'); + } + (projectItem as ProjectCopy).taskIds = projectItem.taskIds.filter( + (tid) => !!data.task.entities[tid], ); + (projectItem as ProjectCopy).backlogTaskIds = projectItem.backlogTaskIds.filter( + (tid) => !!data.task.entities[tid], + ); + }); const tagIds: string[] = data.tag.ids as string[]; tagIds - .map(id => data.tag.entities[id]) - .forEach(tagItem => { - if (!tagItem) { - console.log(data.tag); - throw new Error('No tag'); - } - (tagItem as TagCopy).taskIds = tagItem.taskIds.filter(tid => !!data.task.entities[tid]); + .map((id) => data.tag.entities[id]) + .forEach((tagItem) => { + if (!tagItem) { + console.log(data.tag); + throw new Error('No tag'); } - ); + (tagItem as TagCopy).taskIds = tagItem.taskIds.filter( + (tid) => !!data.task.entities[tid], + ); + }); return data; }; const _fixInconsistentProjectId = (data: AppDataComplete): AppDataComplete => { const projectIds: string[] = data.project.ids as string[]; projectIds - .map(id => data.project.entities[id]) - .forEach(projectItem => { - if (!projectItem) { - console.log(data.project); - throw new Error('No project'); - } - projectItem.taskIds.forEach(tid => { - const task = data.task.entities[tid]; - if (!task) { - throw new Error('No task found'); - } else if (task?.projectId !== projectItem.id) { - // if the task has another projectId leave it there and remove from list - if (task.projectId) { - (projectItem as ProjectCopy).taskIds = projectItem.taskIds.filter(cid => cid !== task.id); - } else { - // if the task has no project id at all, then move it to the project - (task as TaskCopy).projectId = projectItem.id; - } - } - }); - projectItem.backlogTaskIds.forEach(tid => { - const task = data.task.entities[tid]; - if (!task) { - throw new Error('No task found'); - } else if (task?.projectId !== projectItem.id) { - // if the task has another projectId leave it there and remove from list - if (task.projectId) { - (projectItem as ProjectCopy).backlogTaskIds = projectItem.backlogTaskIds.filter(cid => cid !== task.id); - } else { - // if the task has no project id at all, then move it to the project - (task as TaskCopy).projectId = projectItem.id; - } - } - }); + .map((id) => data.project.entities[id]) + .forEach((projectItem) => { + if (!projectItem) { + console.log(data.project); + throw new Error('No project'); } - ); + projectItem.taskIds.forEach((tid) => { + const task = data.task.entities[tid]; + if (!task) { + throw new Error('No task found'); + } else if (task?.projectId !== projectItem.id) { + // if the task has another projectId leave it there and remove from list + if (task.projectId) { + (projectItem as ProjectCopy).taskIds = projectItem.taskIds.filter( + (cid) => cid !== task.id, + ); + } else { + // if the task has no project id at all, then move it to the project + (task as TaskCopy).projectId = projectItem.id; + } + } + }); + projectItem.backlogTaskIds.forEach((tid) => { + const task = data.task.entities[tid]; + if (!task) { + throw new Error('No task found'); + } else if (task?.projectId !== projectItem.id) { + // if the task has another projectId leave it there and remove from list + if (task.projectId) { + (projectItem as ProjectCopy).backlogTaskIds = projectItem.backlogTaskIds.filter( + (cid) => cid !== task.id, + ); + } else { + // if the task has no project id at all, then move it to the project + (task as TaskCopy).projectId = projectItem.id; + } + } + }); + }); return data; }; @@ -323,22 +357,21 @@ const _fixInconsistentProjectId = (data: AppDataComplete): AppDataComplete => { const _fixInconsistentTagId = (data: AppDataComplete): AppDataComplete => { const tagIds: string[] = data.tag.ids as string[]; tagIds - .map(id => data.tag.entities[id]) - .forEach(tagItem => { - if (!tagItem) { - console.log(data.tag); - throw new Error('No tag'); - } - tagItem.taskIds.forEach(tid => { - const task = data.task.entities[tid]; - if (!task) { - throw new Error('No task found'); - } else if (!task?.tagIds.includes(tagItem.id)) { - (task as TaskCopy).tagIds = [...task.tagIds, tagItem.id]; - } - }); + .map((id) => data.tag.entities[id]) + .forEach((tagItem) => { + if (!tagItem) { + console.log(data.tag); + throw new Error('No tag'); } - ); + tagItem.taskIds.forEach((tid) => { + const task = data.task.entities[tid]; + if (!task) { + throw new Error('No task found'); + } else if (!task?.tagIds.includes(tagItem.id)) { + (task as TaskCopy).tagIds = [...task.tagIds, tagItem.id]; + } + }); + }); return data; }; @@ -346,8 +379,8 @@ const _fixInconsistentTagId = (data: AppDataComplete): AppDataComplete => { const _addTodayTagIfNoProjectIdOrTagId = (data: AppDataComplete): AppDataComplete => { const taskIds: string[] = data.task.ids as string[]; taskIds - .map(id => data.task.entities[id]) - .forEach(task => { + .map((id) => data.task.entities[id]) + .forEach((task) => { if (task && !task.parentId && !task.tagIds.length && !task.projectId) { const tag = data.tag.entities[TODAY_TAG.id] as Tag; (task as any).tagIds = [TODAY_TAG.id]; @@ -357,8 +390,8 @@ const _addTodayTagIfNoProjectIdOrTagId = (data: AppDataComplete): AppDataComplet const archivedTaskIds: string[] = data.taskArchive.ids as string[]; archivedTaskIds - .map(id => data.taskArchive.entities[id]) - .forEach(task => { + .map((id) => data.taskArchive.entities[id]) + .forEach((task) => { if (task && !task.parentId && !task.tagIds.length && !task.projectId) { (task as any).tagIds = [TODAY_TAG.id]; } @@ -370,8 +403,8 @@ const _addTodayTagIfNoProjectIdOrTagId = (data: AppDataComplete): AppDataComplet const _setTaskProjectIdAccordingToParent = (data: AppDataComplete): AppDataComplete => { const taskIds: string[] = data.task.ids as string[]; taskIds - .map(id => data.task.entities[id]) - .forEach(taskItem => { + .map((id) => data.task.entities[id]) + .forEach((taskItem) => { if (!taskItem) { console.log(data.task); throw new Error('No task'); @@ -392,8 +425,8 @@ const _setTaskProjectIdAccordingToParent = (data: AppDataComplete): AppDataCompl const archiveTaskIds: string[] = data.taskArchive.ids as string[]; archiveTaskIds - .map(id => data.taskArchive.entities[id]) - .forEach(taskItem => { + .map((id) => data.taskArchive.entities[id]) + .forEach((taskItem) => { if (!taskItem) { console.log(data.taskArchive); throw new Error('No archive task'); @@ -418,8 +451,8 @@ const _setTaskProjectIdAccordingToParent = (data: AppDataComplete): AppDataCompl const _cleanupOrphanedSubTasks = (data: AppDataComplete): AppDataComplete => { const taskIds: string[] = data.task.ids as string[]; taskIds - .map(id => data.task.entities[id]) - .forEach(taskItem => { + .map((id) => data.task.entities[id]) + .forEach((taskItem) => { if (!taskItem) { console.log(data.task); throw new Error('No task'); @@ -440,8 +473,8 @@ const _cleanupOrphanedSubTasks = (data: AppDataComplete): AppDataComplete => { const archiveTaskIds: string[] = data.taskArchive.ids as string[]; archiveTaskIds - .map(id => data.taskArchive.entities[id]) - .forEach(taskItem => { + .map((id) => data.taskArchive.entities[id]) + .forEach((taskItem) => { if (!taskItem) { console.log(data.taskArchive); throw new Error('No archive task'); diff --git a/src/app/core/data-repair/is-data-repair-possible.util.ts b/src/app/core/data-repair/is-data-repair-possible.util.ts index d8f2e9500..c996f4386 100644 --- a/src/app/core/data-repair/is-data-repair-possible.util.ts +++ b/src/app/core/data-repair/is-data-repair-possible.util.ts @@ -2,7 +2,12 @@ import { AppDataComplete } from '../../imex/sync/sync.model'; export const isDataRepairPossible = (data: AppDataComplete): boolean => { const d: any = data as any; - return typeof d === 'object' && d !== null - && typeof d.task === 'object' && d.task !== null - && typeof d.project === 'object' && d.project !== null; + return ( + typeof d === 'object' && + d !== null && + typeof d.task === 'object' && + d.task !== null && + typeof d.project === 'object' && + d.project !== null + ); }; diff --git a/src/app/core/drop-paste-input/drop-paste-input.ts b/src/app/core/drop-paste-input/drop-paste-input.ts index 9bce852c6..38b588baa 100644 --- a/src/app/core/drop-paste-input/drop-paste-input.ts +++ b/src/app/core/drop-paste-input/drop-paste-input.ts @@ -6,9 +6,7 @@ export const createFromDrop = (ev: DragEvent): null | DropPasteInput => { throw new Error('No drop data'); } const text = ev.dataTransfer.getData('text'); - return text - ? (_createTextBookmark(text)) - : (_createFileBookmark(ev.dataTransfer)); + return text ? _createTextBookmark(text) : _createFileBookmark(ev.dataTransfer); }; export const createFromPaste = (ev: ClipboardEvent): null | DropPasteInput => { diff --git a/src/app/core/electron/electron.service.ts b/src/app/core/electron/electron.service.ts index 21d7ab5ba..e4fae06e5 100644 --- a/src/app/core/electron/electron.service.ts +++ b/src/app/core/electron/electron.service.ts @@ -16,11 +16,11 @@ const getResponseChannels = (channel: string) => { return { sendChannel: getSendChannel(channel), dataChannel: `%better-ipc-response-data-channel-${channel}-${id}`, - errorChannel: `%better-ipc-response-error-channel-${channel}-${id}` + errorChannel: `%better-ipc-response-error-channel-${channel}-${id}`, }; }; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class ElectronService { ipcRenderer?: typeof ipcRenderer; webFrame?: typeof webFrame; @@ -74,7 +74,7 @@ export class ElectronService { public callMain(channel: string, data: unknown) { return new Promise((resolve, reject) => { - const {sendChannel, dataChannel, errorChannel} = getResponseChannels(channel); + const { sendChannel, dataChannel, errorChannel } = getResponseChannels(channel); const cleanup = () => { (this.ipcRenderer as typeof ipcRenderer).off(dataChannel, onData); @@ -98,7 +98,7 @@ export class ElectronService { const completeData = { dataChannel, errorChannel, - userData: data + userData: data, }; (this.ipcRenderer as typeof ipcRenderer).send(sendChannel, completeData); diff --git a/src/app/core/electron/exec-before-close.service.ts b/src/app/core/electron/exec-before-close.service.ts index cfffeb5b6..f5d9ea20a 100644 --- a/src/app/core/electron/exec-before-close.service.ts +++ b/src/app/core/electron/exec-before-close.service.ts @@ -6,29 +6,25 @@ import { map } from 'rxjs/operators'; import { IS_ELECTRON } from '../../app.constants'; import { ipcRenderer } from 'electron'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class ExecBeforeCloseService { - ipcRenderer: typeof ipcRenderer = this._electronService.ipcRenderer as typeof ipcRenderer; + ipcRenderer: typeof ipcRenderer = this._electronService + .ipcRenderer as typeof ipcRenderer; onBeforeClose$: Observable = IS_ELECTRON - ? fromEvent(this.ipcRenderer, IPC.NOTIFY_ON_CLOSE).pipe( - map(([, ids]: any) => ids), - ) + ? fromEvent(this.ipcRenderer, IPC.NOTIFY_ON_CLOSE).pipe(map(([, ids]: any) => ids)) : of([]); - constructor( - private _electronService: ElectronService, - ) { - } + constructor(private _electronService: ElectronService) {} schedule(id: string) { - this.ipcRenderer.send(IPC.REGISTER_BEFORE_CLOSE, {id}); + this.ipcRenderer.send(IPC.REGISTER_BEFORE_CLOSE, { id }); } unschedule(id: string) { - this.ipcRenderer.send(IPC.UNREGISTER_BEFORE_CLOSE, {id}); + this.ipcRenderer.send(IPC.UNREGISTER_BEFORE_CLOSE, { id }); } setDone(id: string) { - this.ipcRenderer.send(IPC.BEFORE_CLOSE_DONE, {id}); + this.ipcRenderer.send(IPC.BEFORE_CLOSE_DONE, { id }); } } diff --git a/src/app/core/error-handler/global-error-handler.class.ts b/src/app/core/error-handler/global-error-handler.class.ts index cc19f7ef4..670561c00 100644 --- a/src/app/core/error-handler/global-error-handler.class.ts +++ b/src/app/core/error-handler/global-error-handler.class.ts @@ -3,7 +3,11 @@ import { isObject } from '../../util/is-object'; import { getErrorTxt } from '../../util/get-error-text'; import { IS_ELECTRON } from '../../app.constants'; import { ElectronService } from '../electron/electron.service'; -import { createErrorAlert, isHandledError, logAdvancedStacktrace } from './global-error-handler.util'; +import { + createErrorAlert, + isHandledError, + logAdvancedStacktrace, +} from './global-error-handler.util'; import { remote } from 'electron'; import { saveBeforeLastErrorActionLog } from '../../util/action-logger'; @@ -11,17 +15,17 @@ import { saveBeforeLastErrorActionLog } from '../../util/action-logger'; export class GlobalErrorHandler implements ErrorHandler { private _electronLogger: any; - constructor( - private _electronService: ElectronService, - ) { + constructor(private _electronService: ElectronService) { if (IS_ELECTRON) { - this._electronLogger = (this._electronService.remote as typeof remote).require('electron-log'); + this._electronLogger = (this._electronService.remote as typeof remote).require( + 'electron-log', + ); } } // TODO Cleanup this mess handleError(err: any) { - const errStr = (typeof err === 'string') ? err : err.toString(); + const errStr = typeof err === 'string' ? err : err.toString(); // eslint-disable-next-line const simpleStack = err && err.stack; console.error('GLOBAL_ERROR_HANDLER', err); @@ -33,7 +37,12 @@ export class GlobalErrorHandler implements ErrorHandler { // NOTE: dom exceptions will break all rendering that's why if (err.constructor && err.constructor === DOMException) { - createErrorAlert(this._electronService, 'DOMException: ' + errorStr, simpleStack, err); + createErrorAlert( + this._electronService, + 'DOMException: ' + errorStr, + simpleStack, + err, + ); } else { createErrorAlert(this._electronService, errorStr, simpleStack, err); } @@ -58,7 +67,7 @@ export class GlobalErrorHandler implements ErrorHandler { private _getErrorStr(err: unknown): string { if (isObject(err)) { const str = getErrorTxt(err); - return (typeof str === 'string') + return typeof str === 'string' ? str : 'Unable to parse error string. Please see console error'; } else { diff --git a/src/app/core/error-handler/global-error-handler.util.ts b/src/app/core/error-handler/global-error-handler.util.ts index 96dd3868e..7790f38a5 100644 --- a/src/app/core/error-handler/global-error-handler.util.ts +++ b/src/app/core/error-handler/global-error-handler.util.ts @@ -15,14 +15,14 @@ async function _getStacktrace(err: Error | any): Promise { // Don't try to send stacktraces of HTTP errors as they are already logged on the server if (!isHttpError && isErrorWithStack && !isHandledError(err)) { - return StackTrace.fromError(err) - .then((stackframes) => { - return stackframes - .splice(0, 20) - .map((sf) => { - return sf.toString(); - }).join('\n'); - }); + return StackTrace.fromError(err).then((stackframes) => { + return stackframes + .splice(0, 20) + .map((sf) => { + return sf.toString(); + }) + .join('\n'); + }); } else if (!isHandledError(err)) { console.warn('Error without stack', err); } @@ -31,26 +31,31 @@ async function _getStacktrace(err: Error | any): Promise { const _getStacktraceThrottled = pThrottle(_getStacktrace, 2, 5000); -export const logAdvancedStacktrace = (origErr: unknown, additionalLogFn?: (stack: string) => void) => _getStacktraceThrottled(origErr).then(stack => { +export const logAdvancedStacktrace = ( + origErr: unknown, + additionalLogFn?: (stack: string) => void, +) => + _getStacktraceThrottled(origErr) + .then((stack) => { + if (additionalLogFn) { + additionalLogFn(stack); + } + // append to dialog + const stacktraceEl = document.getElementById('stack-trace'); + if (stacktraceEl) { + stacktraceEl.innerText = stack; + } - if (additionalLogFn) { - additionalLogFn(stack); - } - // append to dialog - const stacktraceEl = document.getElementById('stack-trace'); - if (stacktraceEl) { - stacktraceEl.innerText = stack; - } + const githubIssueLink = document.getElementById('github-issue-url'); - const githubIssueLink = document.getElementById('github-issue-url'); + if (githubIssueLink) { + const errEscaped = _cleanHtml(origErr as string); + githubIssueLink.setAttribute('href', getGithubUrl(errEscaped, stack)); + } - if (githubIssueLink) { - const errEscaped = _cleanHtml(origErr as string); - githubIssueLink.setAttribute('href', getGithubUrl(errEscaped, stack)); - } - -// NOTE: there is an issue with this sometimes -> https://github.com/stacktracejs/stacktrace.js/issues/202 -}).catch(console.error); + // NOTE: there is an issue with this sometimes -> https://github.com/stacktracejs/stacktrace.js/issues/202 + }) + .catch(console.error); const _cleanHtml = (str: string): string => { const div = document.createElement('div'); @@ -58,7 +63,12 @@ const _cleanHtml = (str: string): string => { return div.textContent || div.innerText || ''; }; -export const createErrorAlert = (eSvc: ElectronService, err: string = '', stackTrace: string, origErr: any) => { +export const createErrorAlert = ( + eSvc: ElectronService, + err: string = '', + stackTrace: string, + origErr: any, +) => { if (isWasErrorAlertCreated) { return; } @@ -92,7 +102,9 @@ export const createErrorAlert = (eSvc: ElectronService, err: string = '', stackT } }); document.body.append(errorAlert); - const innerWrapper = document.getElementById('error-alert-inner-wrapper') as HTMLElement; + const innerWrapper = document.getElementById( + 'error-alert-inner-wrapper', + ) as HTMLElement; innerWrapper.append(btnReload); isWasErrorAlertCreated = true; @@ -109,16 +121,25 @@ export const createErrorAlert = (eSvc: ElectronService, err: string = '', stackT export const getSimpleMeta = (): string => { const n = window.navigator; - return `META: SP${environment.version} ${IS_ELECTRON ? 'Electron' : 'Browser'} – ${n.language} – ${n.platform} – ${n.userAgent}`; + return `META: SP${environment.version} ${IS_ELECTRON ? 'Electron' : 'Browser'} – ${ + n.language + } – ${n.platform} – ${n.userAgent}`; }; export const isHandledError = (err: unknown): boolean => { - const errStr = (typeof err === 'string') - ? err - : (typeof err === 'object' && err !== null && typeof (err as any).toString === 'function' && err.toString()); + const errStr = + typeof err === 'string' + ? err + : typeof err === 'object' && + err !== null && + typeof (err as any).toString === 'function' && + err.toString(); // NOTE: for some unknown reason sometimes err is undefined while err.toString is not... // this is why we also check the string value - return (err && (err as any).hasOwnProperty(HANDLED_ERROR_PROP_STR)) || !!((errStr as string).match(HANDLED_ERROR_PROP_STR)); + return ( + (err && (err as any).hasOwnProperty(HANDLED_ERROR_PROP_STR)) || + !!(errStr as string).match(HANDLED_ERROR_PROP_STR) + ); }; const getGithubUrl = (errEscaped: string, stackTrace: string): string => { diff --git a/src/app/core/language/language.service.ts b/src/app/core/language/language.service.ts index ee88c472b..dd1743486 100644 --- a/src/app/core/language/language.service.ts +++ b/src/app/core/language/language.service.ts @@ -3,13 +3,18 @@ import { TranslateService } from '@ngx-translate/core'; import { DateTimeAdapter } from 'ngx-date-time-picker-schedule'; import { DateAdapter } from '@angular/material/core'; import * as moment from 'moment'; -import { AUTO_SWITCH_LNGS, LanguageCode, LanguageCodeMomentMap, RTL_LANGUAGES } from '../../app.constants'; +import { + AUTO_SWITCH_LNGS, + LanguageCode, + LanguageCodeMomentMap, + RTL_LANGUAGES, +} from '../../app.constants'; import { BehaviorSubject, Observable } from 'rxjs'; import { GlobalConfigService } from 'src/app/features/config/global-config.service'; import { map, startWith } from 'rxjs/operators'; import { DEFAULT_GLOBAL_CONFIG } from 'src/app/features/config/default-global-config.const'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class LanguageService { // I think a better approach is to add a field in every [lang].json file to specify the direction of the language private isRTL: BehaviorSubject = new BehaviorSubject(false); @@ -48,15 +53,16 @@ export class LanguageService { private _initMonkeyPatchFirstDayOfWeek() { let firstDayOfWeek = DEFAULT_GLOBAL_CONFIG.misc.firstDayOfWeek; - this._globalConfigService.misc$.pipe( - map(cfg => cfg.firstDayOfWeek), - startWith(1), - ).subscribe((_firstDayOfWeek: number) => { - // default should be monday, if we have an invalid value for some reason - firstDayOfWeek = (_firstDayOfWeek === 0 || _firstDayOfWeek > 0) - ? _firstDayOfWeek - : 1; - }); + this._globalConfigService.misc$ + .pipe( + map((cfg) => cfg.firstDayOfWeek), + startWith(1), + ) + .subscribe((_firstDayOfWeek: number) => { + // default should be monday, if we have an invalid value for some reason + firstDayOfWeek = + _firstDayOfWeek === 0 || _firstDayOfWeek > 0 ? _firstDayOfWeek : 1; + }); // overwrites default method to make this configurable this._dateAdapter.getFirstDayOfWeek = () => firstDayOfWeek; } diff --git a/src/app/core/migration/legacy-models.ts b/src/app/core/migration/legacy-models.ts index 894d35ee5..27d116479 100644 --- a/src/app/core/migration/legacy-models.ts +++ b/src/app/core/migration/legacy-models.ts @@ -50,7 +50,9 @@ export interface LegacyAppDataForProjects { }; } -export interface LegacyAppDataComplete extends LegacyAppBaseData, LegacyAppDataForProjects { +export interface LegacyAppDataComplete + extends LegacyAppBaseData, + LegacyAppDataForProjects { lastActiveTime: number; } diff --git a/src/app/core/migration/legacy-persistence.sevice.ts b/src/app/core/migration/legacy-persistence.sevice.ts index f7eea9c94..0d4858f56 100644 --- a/src/app/core/migration/legacy-persistence.sevice.ts +++ b/src/app/core/migration/legacy-persistence.sevice.ts @@ -13,15 +13,23 @@ import { LS_TASK_ARCHIVE, LS_TASK_ATTACHMENT_STATE, LS_TASK_REPEAT_CFG_STATE, - LS_TASK_STATE + LS_TASK_STATE, } from '../persistence/ls-keys.const'; import { migrateProjectState } from '../../features/project/migrate-projects-state.util'; import { GlobalConfigState } from '../../features/config/global-config.model'; import { migrateGlobalConfigState } from '../../features/config/migrate-global-config.util'; -import { Task, TaskArchive, TaskState, TaskWithSubTasks } from 'src/app/features/tasks/task.model'; +import { + Task, + TaskArchive, + TaskState, + TaskWithSubTasks, +} from 'src/app/features/tasks/task.model'; import { Reminder } from '../../features/reminder/reminder.model'; import { taskReducer } from '../../features/tasks/store/task.reducer'; -import { TaskRepeatCfg, TaskRepeatCfgState } from '../../features/task-repeat-cfg/task-repeat-cfg.model'; +import { + TaskRepeatCfg, + TaskRepeatCfgState, +} from '../../features/task-repeat-cfg/task-repeat-cfg.model'; import { taskRepeatCfgReducer } from '../../features/task-repeat-cfg/store/task-repeat-cfg.reducer'; import { EntityState } from '@ngrx/entity'; import { TaskAttachment } from '../../features/tasks/task-attachment/task-attachment.model'; @@ -30,15 +38,21 @@ import { LegacyAppDataComplete, LegacyAppDataForProjects, LegacyPersistenceBaseModel, - LegacyPersistenceForProjectModel + LegacyPersistenceForProjectModel, } from './legacy-models'; import { BookmarkState } from '../../features/bookmark/store/bookmark.reducer'; import { Bookmark } from '../../features/bookmark/bookmark.model'; import { NoteState } from '../../features/note/store/note.reducer'; import { Note } from '../../features/note/note.model'; import { Metric, MetricState } from '../../features/metric/metric.model'; -import { Improvement, ImprovementState } from '../../features/metric/improvement/improvement.model'; -import { Obstruction, ObstructionState } from '../../features/metric/obstruction/obstruction.model'; +import { + Improvement, + ImprovementState, +} from '../../features/metric/improvement/improvement.model'; +import { + Obstruction, + ObstructionState, +} from '../../features/metric/obstruction/obstruction.model'; import { DatabaseService } from '../persistence/database.service'; import { DEFAULT_PROJECT_ID } from '../../features/project/project.const'; import { Action } from '@ngrx/store'; @@ -48,20 +62,23 @@ import { Injectable } from '@angular/core'; providedIn: 'root', }) export class LegacyPersistenceService { - // handled as private but needs to be assigned before the creations - _baseModels: any [] = []; - _projectModels: any [] = []; + _baseModels: any[] = []; + _projectModels: any[] = []; // TODO auto generate ls keys from appDataKey where possible - project: any = this._cmBase(LS_PROJECT_META_LIST, 'project', migrateProjectState); - globalConfig: any = this._cmBase(LS_GLOBAL_CFG, 'globalConfig', migrateGlobalConfigState); - reminders: any = this._cmBase(LS_REMINDER, 'reminders'); - task: any = this._cmProject( - LS_TASK_STATE, - 'task', - taskReducer, + project: any = this._cmBase( + LS_PROJECT_META_LIST, + 'project', + migrateProjectState, ); + globalConfig: any = this._cmBase( + LS_GLOBAL_CFG, + 'globalConfig', + migrateGlobalConfigState, + ); + reminders: any = this._cmBase(LS_REMINDER, 'reminders'); + task: any = this._cmProject(LS_TASK_STATE, 'task', taskReducer); taskRepeatCfg: any = this._cmProject( LS_TASK_REPEAT_CFG_STATE, 'taskRepeatCfg', @@ -77,18 +94,14 @@ export class LegacyPersistenceService { taskAttachment: any = this._cmProject, TaskAttachment>( LS_TASK_ATTACHMENT_STATE, 'taskAttachment', - (state) => state + (state) => state, ); bookmark: any = this._cmProject( LS_BOOKMARK_STATE, 'bookmark', (state) => state, ); - note: any = this._cmProject( - LS_NOTE_STATE, - 'note', - (state) => state, - ); + note: any = this._cmProject(LS_NOTE_STATE, 'note', (state) => state); metric: any = this._cmProject( LS_METRIC_STATE, 'metric', @@ -106,9 +119,7 @@ export class LegacyPersistenceService { ); private _isBlockSaving: boolean = false; - constructor( - private _databaseService: DatabaseService, - ) { + constructor(private _databaseService: DatabaseService) { // this.loadComplete().then(d => console.log('XXXXXXXXX', d, JSON.stringify(d).length)); // this.loadAllRelatedModelDataForProject('DEFAULT').then(d => console.log(d)); } @@ -198,9 +209,7 @@ export class LegacyPersistenceService { getLastActive(): number { const la = localStorage.getItem(LS_LAST_LOCAL_SYNC_MODEL_CHANGE); // NOTE: we need to parse because new Date('1570549698000') is "Invalid Date" - const laParsed = Number.isNaN(Number(la)) - ? la - : +(la as any); + const laParsed = Number.isNaN(Number(la)) ? la : +(la as any); // NOTE: to account for legacy string dates return new Date(laParsed as any).getTime(); } @@ -208,7 +217,7 @@ export class LegacyPersistenceService { // NOTE: not including backup async loadCompleteLegacy(): Promise { const projectState = await this.project.load(); - const pids = projectState ? projectState.ids as string[] : [DEFAULT_PROJECT_ID]; + const pids = projectState ? (projectState.ids as string[]) : [DEFAULT_PROJECT_ID]; return { lastActiveTime: this.getLastActive(), @@ -255,21 +264,29 @@ export class LegacyPersistenceService { ): LegacyPersistenceForProjectModel { const model = { appDataKey, - load: (projectId: any): Promise => this._loadFromDb(this._makeProjectKey(projectId, lsKey)).then(v => migrateFn(v, projectId)), - save: (projectId: any, data: any, isForce: any) => this._saveToDb(this._makeProjectKey(projectId, lsKey), data, isForce), + load: (projectId: any): Promise => + this._loadFromDb(this._makeProjectKey(projectId, lsKey)).then((v) => + migrateFn(v, projectId), + ), + save: (projectId: any, data: any, isForce: any) => + this._saveToDb(this._makeProjectKey(projectId, lsKey), data, isForce), }; this._projectModels.push(model); return model; } - private async _loadLegacyAppDataForProjects(projectIds: string[]): Promise { - const forProjectsData = await Promise.all(this._projectModels.map(async (modelCfg) => { - const modelState = await this._loadForProjectIds(projectIds, modelCfg.load); - return { - [modelCfg.appDataKey]: modelState, - }; - })); + private async _loadLegacyAppDataForProjects( + projectIds: string[], + ): Promise { + const forProjectsData = await Promise.all( + this._projectModels.map(async (modelCfg) => { + const modelState = await this._loadForProjectIds(projectIds, modelCfg.load); + return { + [modelCfg.appDataKey]: modelState, + }; + }), + ); return Object.assign({}, ...forProjectsData); } @@ -280,18 +297,24 @@ export class LegacyPersistenceService { const dataForProject = await getDataFn(projectId); return { ...prevAcc, - [projectId]: dataForProject + [projectId]: dataForProject, }; }, Promise.resolve({})); } private _makeProjectKey(projectId: string, subKey: string, additional?: string) { - return LS_PROJECT_PREFIX + projectId + '_' + subKey + (additional ? '_' + additional : ''); + return ( + LS_PROJECT_PREFIX + projectId + '_' + subKey + (additional ? '_' + additional : '') + ); } // DATA STORAGE INTERFACE // --------------------- - private async _saveToDb(key: string, data: any, isForce: boolean = false): Promise { + private async _saveToDb( + key: string, + data: any, + isForce: boolean = false, + ): Promise { if (!this._isBlockSaving || isForce === true) { return this._databaseService.save(key, data); } else { diff --git a/src/app/core/migration/migration.service.ts b/src/app/core/migration/migration.service.ts index a88a6e69a..66de8e70a 100644 --- a/src/app/core/migration/migration.service.ts +++ b/src/app/core/migration/migration.service.ts @@ -22,35 +22,38 @@ import { initialMetricState } from '../../features/metric/store/metric.reducer'; import { initialImprovementState } from '../../features/metric/improvement/store/improvement.reducer'; import { initialObstructionState } from '../../features/metric/obstruction/store/obstruction.reducer'; -const EMTPY_ENTITY = () => ({ids: [], entities: {}}); +const EMTPY_ENTITY = () => ({ ids: [], entities: {} }); -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class MigrationService { constructor( private _persistenceService: PersistenceService, private _legacyPersistenceService: LegacyPersistenceService, private _translateService: TranslateService, - ) { - } + ) {} - migrateIfNecessaryToProjectState$(projectState: ProjectState): Observable { + migrateIfNecessaryToProjectState$( + projectState: ProjectState, + ): Observable { const isNeedsMigration = this._isNeedsMigration(projectState); if (isNeedsMigration && this._isConfirmMigrateDialog()) { return from(this._legacyPersistenceService.loadCompleteLegacy()).pipe( map((legacyData) => this._migrate(legacyData)), - concatMap((migratedData) => this._persistenceService.importComplete(migratedData)), + concatMap((migratedData) => + this._persistenceService.importComplete(migratedData), + ), concatMap((migratedData) => this._persistenceService.cleanDatabase()), concatMap(() => this._persistenceService.project.loadState()), ); } - return isNeedsMigration - ? EMPTY - : of(projectState); + return isNeedsMigration ? EMPTY : of(projectState); } - migrateIfNecessary(appDataComplete: LegacyAppDataComplete | AppDataComplete): AppDataComplete { + migrateIfNecessary( + appDataComplete: LegacyAppDataComplete | AppDataComplete, + ): AppDataComplete { const projectState = appDataComplete.project; const isNeedsMigration = this._isNeedsMigration(projectState); if (isNeedsMigration) { @@ -99,46 +102,68 @@ export class MigrationService { return newAppData; } - private _mTaskListsFromTaskToProjectState(legacyAppDataComplete: LegacyAppDataComplete): ProjectState { + private _mTaskListsFromTaskToProjectState( + legacyAppDataComplete: LegacyAppDataComplete, + ): ProjectState { const projectStateBefore = legacyAppDataComplete.project; return { ...projectStateBefore, - entities: (projectStateBefore.ids as string[]).reduce((acc, id): Dictionary => { - const taskState = (legacyAppDataComplete.task as any)[id] || {}; - return { - ...acc, - [id]: { - ...projectStateBefore.entities[id], - taskIds: (taskState as any).todaysTaskIds || [], - backlogTaskIds: (taskState as any).backlogTaskIds || [], - } as Project - }; - }, {}) + entities: (projectStateBefore.ids as string[]).reduce( + (acc, id): Dictionary => { + const taskState = (legacyAppDataComplete.task as any)[id] || {}; + return { + ...acc, + [id]: { + ...projectStateBefore.entities[id], + taskIds: (taskState as any).todaysTaskIds || [], + backlogTaskIds: (taskState as any).backlogTaskIds || [], + } as Project, + }; + }, + {}, + ), }; } private _mTaskState(legacyAppDataComplete: LegacyAppDataComplete): TaskState { const singleState = this._mTaskFromProjectToSingle(legacyAppDataComplete); const standardMigration = migrateTaskState(singleState as TaskState); - return this._mTaskAttachmentsToTaskStates(legacyAppDataComplete, standardMigration) as TaskState; + return this._mTaskAttachmentsToTaskStates( + legacyAppDataComplete, + standardMigration, + ) as TaskState; } private _mTaskArchiveState(legacyAppDataComplete: LegacyAppDataComplete): TaskArchive { - const singleState = this._mTaskArchiveFromProjectToSingle(legacyAppDataComplete) as TaskArchive; + const singleState = this._mTaskArchiveFromProjectToSingle( + legacyAppDataComplete, + ) as TaskArchive; const standardMigration = migrateTaskState(singleState as TaskState); - return this._mTaskAttachmentsToTaskStates(legacyAppDataComplete, standardMigration) as TaskArchive; + return this._mTaskAttachmentsToTaskStates( + legacyAppDataComplete, + standardMigration, + ) as TaskArchive; } - private _mTaskRepeatCfg(legacyAppDataComplete: LegacyAppDataComplete): TaskRepeatCfgState { + private _mTaskRepeatCfg( + legacyAppDataComplete: LegacyAppDataComplete, + ): TaskRepeatCfgState { const pids = legacyAppDataComplete.project.ids as string[]; - const repeatStates = this._addProjectIdToEntity(pids, (legacyAppDataComplete.taskRepeatCfg as any), {tagIds: []}); - return this._mergeEntities(repeatStates, initialTaskRepeatCfgState) as TaskRepeatCfgState; + const repeatStates = this._addProjectIdToEntity( + pids, + legacyAppDataComplete.taskRepeatCfg as any, + { tagIds: [] }, + ); + return this._mergeEntities( + repeatStates, + initialTaskRepeatCfgState, + ) as TaskRepeatCfgState; } private _addProjectIdToEntity( pids: string[], entityProjectStates: { [key: string]: EntityState }, - additionalChanges: Record = {} + additionalChanges: Record = {}, ): EntityState[] { return pids.map((projectId) => { const state = entityProjectStates[projectId]; @@ -149,7 +174,12 @@ export class MigrationService { ...state, entities: (state.ids as string[]).reduce((acc, entityId) => { if (projectId !== state.entities[entityId].projectId) { - console.log('OVERWRITING PROJECT ID', projectId, state.entities[entityId].projectId, state.entities[entityId]); + console.log( + 'OVERWRITING PROJECT ID', + projectId, + state.entities[entityId].projectId, + state.entities[entityId], + ); } return { ...acc, @@ -157,74 +187,102 @@ export class MigrationService { ...state.entities[entityId], projectId, ...additionalChanges, - } + }, }; - }, {}) + }, {}), }; }) as any; } - private _mTaskFromProjectToSingle(legacyAppDataComplete: LegacyAppDataComplete): TaskState { + private _mTaskFromProjectToSingle( + legacyAppDataComplete: LegacyAppDataComplete, + ): TaskState { const pids = legacyAppDataComplete.project.ids as string[]; - const taskStates: TaskState[] = this._addProjectIdToEntity(pids, legacyAppDataComplete.task as any) as TaskState[]; + const taskStates: TaskState[] = this._addProjectIdToEntity( + pids, + legacyAppDataComplete.task as any, + ) as TaskState[]; return this._mergeEntities(taskStates, initialTaskState) as TaskState; } - private _mTaskArchiveFromProjectToSingle(legacyAppDataComplete: LegacyAppDataComplete): TaskArchive { + private _mTaskArchiveFromProjectToSingle( + legacyAppDataComplete: LegacyAppDataComplete, + ): TaskArchive { const pids = legacyAppDataComplete.project.ids as string[]; - const taskStates: TaskArchive[] = this._addProjectIdToEntity(pids, legacyAppDataComplete.taskArchive as any) as TaskArchive[]; + const taskStates: TaskArchive[] = this._addProjectIdToEntity( + pids, + legacyAppDataComplete.taskArchive as any, + ) as TaskArchive[]; return this._mergeEntities(taskStates, EMTPY_ENTITY()) as TaskArchive; } - private _mTaskAttachmentsToTaskStates(legacyAppDataComplete: LegacyAppDataComplete, taskState: (TaskState | TaskArchive)): - TaskState | TaskArchive { - const attachmentStates = Object.keys(legacyAppDataComplete.taskAttachment as any).map(id => (legacyAppDataComplete.taskAttachment as any)[id]); - const allAttachmentState = this._mergeEntities(attachmentStates, initialTaskRepeatCfgState) as EntityState; + private _mTaskAttachmentsToTaskStates( + legacyAppDataComplete: LegacyAppDataComplete, + taskState: TaskState | TaskArchive, + ): TaskState | TaskArchive { + const attachmentStates = Object.keys(legacyAppDataComplete.taskAttachment as any).map( + (id) => (legacyAppDataComplete.taskAttachment as any)[id], + ); + const allAttachmentState = this._mergeEntities( + attachmentStates, + initialTaskRepeatCfgState, + ) as EntityState; return (taskState.ids as string[]).reduce((acc, id) => { - const {attachmentIds, ...tEnt} = acc.entities[id] as any; + const { attachmentIds, ...tEnt } = acc.entities[id] as any; return { ...acc, entities: { ...acc.entities, [id]: { ...tEnt, - attachments: tEnt.attachments || (attachmentIds - ? attachmentIds.map((attachmentId: string) => { - const result = allAttachmentState.entities[attachmentId]; - if (!result) { - console.log('ATTACHMENT NOT FOUND: Will be removed', attachmentIds); - // throw new Error('Attachment not found'); - } else { - console.log('ATTACHMENT FOUND', result.title); - } - return result; - }).filter((v: any) => !!v) - : []) + attachments: + tEnt.attachments || + (attachmentIds + ? attachmentIds + .map((attachmentId: string) => { + const result = allAttachmentState.entities[attachmentId]; + if (!result) { + console.log( + 'ATTACHMENT NOT FOUND: Will be removed', + attachmentIds, + ); + // throw new Error('Attachment not found'); + } else { + console.log('ATTACHMENT FOUND', result.title); + } + return result; + }) + .filter((v: any) => !!v) + : []), }, - } + }, }; }, taskState); } - private _mergeEntities(states: EntityState[], initial: EntityState): EntityState { - return states.reduce( - (acc, s) => { - if (!s || !s.ids) { - return acc; - } - return { - ...acc, - ids: [...acc.ids, ...s.ids] as string[], - // NOTE: that this can lead to overwrite when the ids are the same for some reason - entities: {...acc.entities, ...s.entities} - }; - }, initial - ); + private _mergeEntities( + states: EntityState[], + initial: EntityState, + ): EntityState { + return states.reduce((acc, s) => { + if (!s || !s.ids) { + return acc; + } + return { + ...acc, + ids: [...acc.ids, ...s.ids] as string[], + // NOTE: that this can lead to overwrite when the ids are the same for some reason + entities: { ...acc.entities, ...s.entities }, + }; + }, initial); } private _isNeedsMigration(projectState: ProjectState): boolean { - return (projectState && (!(projectState as any).__modelVersion || (projectState as any).__modelVersion <= 3)); + return ( + projectState && + (!(projectState as any).__modelVersion || (projectState as any).__modelVersion <= 3) + ); } private _isConfirmMigrateDialog(): boolean { diff --git a/src/app/core/notify/notify.service.ts b/src/app/core/notify/notify.service.ts index 4e3d8f5d9..155967b72 100644 --- a/src/app/core/notify/notify.service.ts +++ b/src/app/core/notify/notify.service.ts @@ -15,8 +15,7 @@ export class NotifyService { constructor( private _translateService: TranslateService, private _uiHelperService: UiHelperService, - ) { - } + ) {} async notifyDesktop(options: NotifyModel): Promise { if (!IS_MOBILE) { @@ -26,10 +25,16 @@ export class NotifyService { } async notify(options: NotifyModel): Promise { - const title = options.title && this._translateService.instant(options.title, options.translateParams); - const body = options.body && this._translateService.instant(options.body, options.translateParams); + const title = + options.title && + this._translateService.instant(options.title, options.translateParams); + const body = + options.body && + this._translateService.instant(options.body, options.translateParams); - const svcReg = this._isServiceWorkerAvailable() && await navigator.serviceWorker.getRegistration('ngsw-worker.js'); + const svcReg = + this._isServiceWorkerAvailable() && + (await navigator.serviceWorker.getRegistration('ngsw-worker.js')); if (svcReg && svcReg.showNotification) { // service worker also seems to need to request permission... @@ -43,7 +48,7 @@ export class NotifyService { silent: false, data: { dateOfArrival: Date.now(), - primaryKey: 1 + primaryKey: 1, }, ...options, body, @@ -62,7 +67,7 @@ export class NotifyService { silent: false, data: { dateOfArrival: Date.now(), - primaryKey: 1 + primaryKey: 1, }, ...options, body, @@ -88,6 +93,10 @@ export class NotifyService { } private _isServiceWorkerAvailable(): boolean { - return 'serviceWorker' in navigator && (environment.production || environment.stage) && !IS_ELECTRON; + return ( + 'serviceWorker' in navigator && + (environment.production || environment.stage) && + !IS_ELECTRON + ); } } diff --git a/src/app/core/persistence/database.service.ts b/src/app/core/persistence/database.service.ts index c7963f09b..13a793bd2 100644 --- a/src/app/core/persistence/database.service.ts +++ b/src/app/core/persistence/database.service.ts @@ -25,7 +25,7 @@ export class DatabaseService { isReady$: BehaviorSubject = new BehaviorSubject(false); private _afterReady$: Observable = this.isReady$.pipe( - filter(isReady => isReady), + filter((isReady) => isReady), shareReplay(1), ); @@ -35,9 +35,9 @@ export class DatabaseService { this._init().then(); } - @retry({retries: MAX_RETRY_COUNT, delay: RETRY_DELAY}) + @retry({ retries: MAX_RETRY_COUNT, delay: RETRY_DELAY }) async load(key: string): Promise { - this._lastParams = {a: 'load', key}; + this._lastParams = { a: 'load', key }; await this._afterReady(); try { return await (this.db as IDBPDatabase).get(DB_MAIN_NAME, key); @@ -47,9 +47,9 @@ export class DatabaseService { } } - @retry({retries: MAX_RETRY_COUNT, delay: RETRY_DELAY}) + @retry({ retries: MAX_RETRY_COUNT, delay: RETRY_DELAY }) async save(key: string, data: unknown): Promise { - this._lastParams = {a: 'save', key, data}; + this._lastParams = { a: 'save', key, data }; await this._afterReady(); try { return await (this.db as IDBPDatabase).put(DB_MAIN_NAME, data, key); @@ -59,9 +59,9 @@ export class DatabaseService { } } - @retry({retries: MAX_RETRY_COUNT, delay: RETRY_DELAY}) + @retry({ retries: MAX_RETRY_COUNT, delay: RETRY_DELAY }) async remove(key: string): Promise { - this._lastParams = {a: 'remove', key}; + this._lastParams = { a: 'remove', key }; await this._afterReady(); try { return await (this.db as IDBPDatabase).delete(DB_MAIN_NAME, key); @@ -71,9 +71,9 @@ export class DatabaseService { } } - @retry({retries: MAX_RETRY_COUNT, delay: RETRY_DELAY}) + @retry({ retries: MAX_RETRY_COUNT, delay: RETRY_DELAY }) async clearDatabase(): Promise { - this._lastParams = {a: 'clearDatabase'}; + this._lastParams = { a: 'clearDatabase' }; await this._afterReady(); try { return await (this.db as IDBPDatabase).clear(DB_MAIN_NAME); @@ -83,7 +83,7 @@ export class DatabaseService { } } - @retry({retries: MAX_RETRY_COUNT, delay: RETRY_DELAY}) + @retry({ retries: MAX_RETRY_COUNT, delay: RETRY_DELAY }) private async _init(): Promise> { try { this.db = await openDB(DB_NAME, VERSION, { diff --git a/src/app/core/persistence/local-storage.ts b/src/app/core/persistence/local-storage.ts index c0fcb6055..3182fd9b6 100644 --- a/src/app/core/persistence/local-storage.ts +++ b/src/app/core/persistence/local-storage.ts @@ -10,7 +10,10 @@ export const removeFromRealLs = (key: string) => { localStorage.removeItem(key); }; -export const saveToRealLs = (key: string, state: { [key: string]: unknown } | unknown[]) => { +export const saveToRealLs = ( + key: string, + state: { [key: string]: unknown } | unknown[], +) => { const serializedState = JSON.stringify(state); localStorage.setItem(key, serializedState); }; diff --git a/src/app/core/persistence/ls-keys.const.ts b/src/app/core/persistence/ls-keys.const.ts index 8802689a4..7d858f4e3 100644 --- a/src/app/core/persistence/ls-keys.const.ts +++ b/src/app/core/persistence/ls-keys.const.ts @@ -6,7 +6,7 @@ export type AllowedDBKeys = keyof AppDataComplete | 'SUP_COMPLETE_BACKUP'; export const LS_PREFIX = 'SUP_'; export const LS_PROJECT_PREFIX = LS_PREFIX + 'P_'; export const LS_GLOBAL_CFG = LS_PREFIX + 'GLOBAL_CFG'; -export const LS_BACKUP: AllowedDBKeys = LS_PREFIX + 'COMPLETE_BACKUP' as AllowedDBKeys; +export const LS_BACKUP: AllowedDBKeys = (LS_PREFIX + 'COMPLETE_BACKUP') as AllowedDBKeys; export const LS_REMINDER = LS_PREFIX + 'REMINDER'; export const LS_PROJECT_ARCHIVE = LS_PREFIX + 'ARCHIVE'; @@ -45,7 +45,8 @@ export const LS_GOOGLE_SESSION = LS_PREFIX + 'GOOGLE_SESSION'; export const LS_ACTION_LOG = LS_PREFIX + 'ACTION_LOG'; export const LS_ACTION_BEFORE_LAST_ERROR_LOG = LS_PREFIX + 'LAST_ERROR_ACTION_LOG'; -export const LS_CHECK_STRAY_PERSISTENCE_BACKUP = LS_PREFIX + 'CHECK_STRAY_PERSISTENCE_BACKUP'; +export const LS_CHECK_STRAY_PERSISTENCE_BACKUP = + LS_PREFIX + 'CHECK_STRAY_PERSISTENCE_BACKUP'; export const LS_IS_PROJECT_LIST_EXPANDED = LS_PREFIX + 'IS_PROJECT_LIST_EXPANDED'; export const LS_IS_TAG_LIST_EXPANDED = LS_PREFIX + 'IS_TAG_LIST_EXPANDED'; @@ -56,4 +57,3 @@ export const SS_PROJECT_TMP = SS_PREFIX + 'PROJECT_TMP_EDIT'; export const SS_WEB_APP_INSTALL = LS_PREFIX + 'WEB_APP_INSTALL'; export const SS_JIRA_WONKY_COOKIE = LS_PREFIX + 'JIRA_WONKY_COOKIE'; export const SS_TODO_TMP = SS_PREFIX + 'TODO_TMP_EDIT'; - diff --git a/src/app/core/persistence/persistence-local.service.ts b/src/app/core/persistence/persistence-local.service.ts index 076be6ec0..c84ed62f3 100644 --- a/src/app/core/persistence/persistence-local.service.ts +++ b/src/app/core/persistence/persistence-local.service.ts @@ -1,5 +1,9 @@ import { Injectable } from '@angular/core'; -import { LS_LOCAL_NON_SYNC, LS_SYNC_LAST_LOCAL_REVISION, LS_SYNC_LOCAL_LAST_SYNC } from './ls-keys.const'; +import { + LS_LOCAL_NON_SYNC, + LS_SYNC_LAST_LOCAL_REVISION, + LS_SYNC_LOCAL_LAST_SYNC, +} from './ls-keys.const'; import { DatabaseService } from './database.service'; import { LocalSyncMetaModel } from '../../imex/sync/sync.model'; import { SyncProvider } from '../../imex/sync/sync-provider.model'; @@ -8,18 +12,20 @@ import { SyncProvider } from '../../imex/sync/sync-provider.model'; providedIn: 'root', }) export class PersistenceLocalService { - constructor( - private _databaseService: DatabaseService, - ) { - } + constructor(private _databaseService: DatabaseService) {} async save(data: LocalSyncMetaModel): Promise { return await this._databaseService.save(LS_LOCAL_NON_SYNC, data); } async load(): Promise { - const r = await this._databaseService.load(LS_LOCAL_NON_SYNC) as LocalSyncMetaModel; - if (r && r[SyncProvider.Dropbox] && r[SyncProvider.GoogleDrive] && r[SyncProvider.WebDAV]) { + const r = (await this._databaseService.load(LS_LOCAL_NON_SYNC)) as LocalSyncMetaModel; + if ( + r && + r[SyncProvider.Dropbox] && + r[SyncProvider.GoogleDrive] && + r[SyncProvider.WebDAV] + ) { return r; } return { @@ -44,8 +50,6 @@ export class PersistenceLocalService { private _getLegacyLocalLastSync(p: SyncProvider): number { const it = +(localStorage.getItem(LS_SYNC_LOCAL_LAST_SYNC + p) as any); - return isNaN(it) - ? 0 - : it || 0; + return isNaN(it) ? 0 : it || 0; } } diff --git a/src/app/core/persistence/persistence.actions.ts b/src/app/core/persistence/persistence.actions.ts index f620748af..1c9bb85a7 100644 --- a/src/app/core/persistence/persistence.actions.ts +++ b/src/app/core/persistence/persistence.actions.ts @@ -13,5 +13,3 @@ export const loadFromDb = createAction( '[Persistence] Load from DB', props<{ dbKey: string }>(), ); - - diff --git a/src/app/core/persistence/persistence.model.ts b/src/app/core/persistence/persistence.model.ts index 6eff5333f..867071e0b 100644 --- a/src/app/core/persistence/persistence.model.ts +++ b/src/app/core/persistence/persistence.model.ts @@ -1,8 +1,8 @@ import { AppBaseData, AppDataForProjects } from '../../imex/sync/sync.model'; import { Action } from '@ngrx/store'; -export type ProjectDataLsKey - = 'CFG' +export type ProjectDataLsKey = + | 'CFG' // | 'TASKS_STATE' // | 'TASK_REPEAT_CFG_STATE' // | 'TASK_ATTACHMENT_STATE' @@ -13,15 +13,17 @@ export type ProjectDataLsKey | 'BOOKMARK_STATE' | 'METRIC_STATE' | 'IMPROVEMENT_STATE' - | 'OBSTRUCTION_STATE' - ; + | 'OBSTRUCTION_STATE'; export interface PersistenceBaseModel { appDataKey: keyof AppBaseData; loadState(isSkipMigration?: boolean): Promise; - saveState(state: T, flags: { isDataImport?: boolean; isSyncModelChange?: boolean }): Promise; + saveState( + state: T, + flags: { isDataImport?: boolean; isSyncModelChange?: boolean }, + ): Promise; } export interface PersistenceBaseEntityModel extends PersistenceBaseModel { @@ -42,7 +44,11 @@ export interface PersistenceForProjectModel { load(projectId: string): Promise; - save(projectId: string, state: S, flags: { isDataImport?: boolean; isSyncModelChange?: boolean }): Promise; + save( + projectId: string, + state: S, + flags: { isDataImport?: boolean; isSyncModelChange?: boolean }, + ): Promise; /* @deprecated */ remove(projectId: string): Promise; diff --git a/src/app/core/persistence/persistence.module.ts b/src/app/core/persistence/persistence.module.ts index ae99c8707..10df599ea 100644 --- a/src/app/core/persistence/persistence.module.ts +++ b/src/app/core/persistence/persistence.module.ts @@ -2,10 +2,7 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; @NgModule({ - imports: [ - CommonModule, - ], + imports: [CommonModule], declarations: [], }) -export class PersistenceModule { -} +export class PersistenceModule {} diff --git a/src/app/core/persistence/persistence.service.spec.ts b/src/app/core/persistence/persistence.service.spec.ts index 4b02b4c27..e560bb0d4 100644 --- a/src/app/core/persistence/persistence.service.spec.ts +++ b/src/app/core/persistence/persistence.service.spec.ts @@ -16,51 +16,53 @@ const testScheduler = new TestScheduler((actual, expected) => { describe('PersistenceService', () => { beforeEach(() => { TestBed.configureTestingModule({ - providers: [ - provideMockStore({initialState: {}}), - { - provide: SnackService, useValue: { - open: () => false, - }, + providers: [ + provideMockStore({ initialState: {} }), + { + provide: SnackService, + useValue: { + open: () => false, }, - { - provide: DatabaseService, useValue: { - clearDatabase: () => false, - save: () => false, - remove: () => false, - load: () => false, - }, + }, + { + provide: DatabaseService, + useValue: { + clearDatabase: () => false, + save: () => false, + remove: () => false, + load: () => false, }, - { - provide: CompressionService, useValue: { - decompress: () => false, - compress: () => false, - }, + }, + { + provide: CompressionService, + useValue: { + decompress: () => false, + compress: () => false, }, - ] - } - ); + }, + ], + }); }); it('database update should trigger onAfterSave$', async (done) => { const service: PersistenceService = TestBed.inject(PersistenceService); // once is required to fill up data await service.loadComplete(); - service.onAfterSave$.subscribe(({data}) => { + service.onAfterSave$.subscribe(({ data }) => { expect(data).toEqual(createEmptyEntity()); done(); }); - service.tag.saveState(createEmptyEntity(), {isSyncModelChange: true}); + service.tag.saveState(createEmptyEntity(), { isSyncModelChange: true }); }); describe('inMemoryComplete$', () => { it('should start with loadComplete data', () => { - testScheduler.run(({expectObservable}) => { + testScheduler.run(({ expectObservable }) => { const FAKE_VAL: any = 'VVV'; const a$ = of(FAKE_VAL); spyOn(PersistenceService.prototype, 'loadComplete').and.callFake(() => a$ as any); const service: PersistenceService = TestBed.inject(PersistenceService); - expectObservable(service.inMemoryComplete$).toBe('a', {a: FAKE_VAL}); + expectObservable(service.inMemoryComplete$).toBe('a', { a: FAKE_VAL }); }); }); diff --git a/src/app/core/persistence/persistence.service.ts b/src/app/core/persistence/persistence.service.ts index 7391eab74..4f640eff7 100644 --- a/src/app/core/persistence/persistence.service.ts +++ b/src/app/core/persistence/persistence.service.ts @@ -17,17 +17,25 @@ import { LS_TAG_STATE, LS_TASK_ARCHIVE, LS_TASK_REPEAT_CFG_STATE, - LS_TASK_STATE + LS_TASK_STATE, } from './ls-keys.const'; import { GlobalConfigState } from '../../features/config/global-config.model'; -import { projectReducer, ProjectState } from '../../features/project/store/project.reducer'; -import { ArchiveTask, Task, TaskArchive, TaskState } from '../../features/tasks/task.model'; +import { + projectReducer, + ProjectState, +} from '../../features/project/store/project.reducer'; +import { + ArchiveTask, + Task, + TaskArchive, + TaskState, +} from '../../features/tasks/task.model'; import { AppBaseData, AppDataComplete, AppDataCompleteOptionalSyncModelChange, AppDataForProjects, - DEFAULT_APP_BASE_DATA + DEFAULT_APP_BASE_DATA, } from '../../imex/sync/sync.model'; import { BookmarkState } from '../../features/bookmark/store/bookmark.reducer'; import { NoteState } from '../../features/note/store/note.reducer'; @@ -37,29 +45,48 @@ import { DEFAULT_PROJECT_ID } from '../../features/project/project.const'; import { ExportedProject, ProjectArchive, - ProjectArchivedRelatedData + ProjectArchivedRelatedData, } from '../../features/project/project-archive.model'; import { Project } from '../../features/project/project.model'; import { CompressionService } from '../compression/compression.service'; -import { PersistenceBaseEntityModel, PersistenceBaseModel, PersistenceForProjectModel } from './persistence.model'; +import { + PersistenceBaseEntityModel, + PersistenceBaseModel, + PersistenceForProjectModel, +} from './persistence.model'; import { Metric, MetricState } from '../../features/metric/metric.model'; -import { Improvement, ImprovementState } from '../../features/metric/improvement/improvement.model'; -import { Obstruction, ObstructionState } from '../../features/metric/obstruction/obstruction.model'; -import { TaskRepeatCfg, TaskRepeatCfgState } from '../../features/task-repeat-cfg/task-repeat-cfg.model'; +import { + Improvement, + ImprovementState, +} from '../../features/metric/improvement/improvement.model'; +import { + Obstruction, + ObstructionState, +} from '../../features/metric/obstruction/obstruction.model'; +import { + TaskRepeatCfg, + TaskRepeatCfgState, +} from '../../features/task-repeat-cfg/task-repeat-cfg.model'; import { Bookmark } from '../../features/bookmark/bookmark.model'; import { Note } from '../../features/note/note.model'; import { Action, Store } from '@ngrx/store'; import { taskRepeatCfgReducer } from '../../features/task-repeat-cfg/store/task-repeat-cfg.reducer'; import { Tag, TagState } from '../../features/tag/tag.model'; import { migrateProjectState } from '../../features/project/migrate-projects-state.util'; -import { migrateTaskArchiveState, migrateTaskState } from '../../features/tasks/migrate-task-state.util'; +import { + migrateTaskArchiveState, + migrateTaskState, +} from '../../features/tasks/migrate-task-state.util'; import { migrateGlobalConfigState } from '../../features/config/migrate-global-config.util'; import { taskReducer } from '../../features/tasks/store/task.reducer'; import { tagReducer } from '../../features/tag/store/tag.reducer'; import { migrateTaskRepeatCfgState } from '../../features/task-repeat-cfg/migrate-task-repeat-cfg-state.util'; import { environment } from '../../../environments/environment'; import { checkFixEntityStateConsistency } from '../../util/check-fix-entity-state-consistency'; -import { SimpleCounter, SimpleCounterState } from '../../features/simple-counter/simple-counter.model'; +import { + SimpleCounter, + SimpleCounterState, +} from '../../features/simple-counter/simple-counter.model'; import { simpleCounterReducer } from '../../features/simple-counter/store/simple-counter.reducer'; import { from, merge, Observable, Subject } from 'rxjs'; import { concatMap, debounceTime, shareReplay, skipWhile } from 'rxjs/operators'; @@ -73,54 +100,65 @@ import { obstructionReducer } from '../../features/metric/obstruction/store/obst import { migrateImprovementState, migrateMetricState, - migrateObstructionState + migrateObstructionState, } from '../../features/metric/migrate-metric-states.util'; @Injectable({ providedIn: 'root', }) export class PersistenceService { - // handled as private but needs to be assigned before the creations _baseModels: PersistenceBaseModel[] = []; _projectModels: PersistenceForProjectModel[] = []; // TODO auto generate ls keys from appDataKey where possible - globalConfig: PersistenceBaseModel = this._cmBase(LS_GLOBAL_CFG, 'globalConfig', migrateGlobalConfigState); - reminders: PersistenceBaseModel = this._cmBase(LS_REMINDER, 'reminders'); - - project: PersistenceBaseEntityModel = this._cmBaseEntity( - LS_PROJECT_META_LIST, - 'project', - projectReducer as any, - migrateProjectState, + globalConfig: PersistenceBaseModel = this._cmBase( + LS_GLOBAL_CFG, + 'globalConfig', + migrateGlobalConfigState, ); + reminders: PersistenceBaseModel = this._cmBase( + LS_REMINDER, + 'reminders', + ); + + project: PersistenceBaseEntityModel = this._cmBaseEntity< + ProjectState, + Project + >(LS_PROJECT_META_LIST, 'project', projectReducer as any, migrateProjectState); tag: PersistenceBaseEntityModel = this._cmBaseEntity( LS_TAG_STATE, 'tag', tagReducer, ); - simpleCounter: PersistenceBaseEntityModel = this._cmBaseEntity( + simpleCounter: PersistenceBaseEntityModel< + SimpleCounterState, + SimpleCounter + > = this._cmBaseEntity( LS_SIMPLE_COUNTER_STATE, 'simpleCounter', simpleCounterReducer, ); // METRIC MODELS - metric: PersistenceBaseEntityModel = this._cmBaseEntity( - LS_METRIC_STATE, - 'metric', - metricReducer as any, - migrateMetricState, - ); - improvement: PersistenceBaseEntityModel = this._cmBaseEntity( + metric: PersistenceBaseEntityModel = this._cmBaseEntity< + MetricState, + Metric + >(LS_METRIC_STATE, 'metric', metricReducer as any, migrateMetricState); + improvement: PersistenceBaseEntityModel< + ImprovementState, + Improvement + > = this._cmBaseEntity( LS_IMPROVEMENT_STATE, 'improvement', improvementReducer, migrateImprovementState, ); - obstruction: PersistenceBaseEntityModel = this._cmBaseEntity( + obstruction: PersistenceBaseEntityModel< + ObstructionState, + Obstruction + > = this._cmBaseEntity( LS_OBSTRUCTION_STATE, 'obstruction', obstructionReducer as any, @@ -134,13 +172,14 @@ export class PersistenceService { taskReducer, migrateTaskState, ); - taskArchive: PersistenceBaseEntityModel = this._cmBaseEntity( - LS_TASK_ARCHIVE, - 'taskArchive', - taskReducer as any, - migrateTaskArchiveState, - ); - taskRepeatCfg: PersistenceBaseEntityModel = this._cmBaseEntity( + taskArchive: PersistenceBaseEntityModel = this._cmBaseEntity< + TaskArchive, + ArchiveTask + >(LS_TASK_ARCHIVE, 'taskArchive', taskReducer as any, migrateTaskArchiveState); + taskRepeatCfg: PersistenceBaseEntityModel< + TaskRepeatCfgState, + TaskRepeatCfg + > = this._cmBaseEntity( LS_TASK_REPEAT_CFG_STATE, 'taskRepeatCfg', taskRepeatCfgReducer as any, @@ -148,31 +187,42 @@ export class PersistenceService { ); // PROJECT MODELS - bookmark: PersistenceForProjectModel = this._cmProject( - LS_BOOKMARK_STATE, - 'bookmark', - ); + bookmark: PersistenceForProjectModel = this._cmProject< + BookmarkState, + Bookmark + >(LS_BOOKMARK_STATE, 'bookmark'); note: PersistenceForProjectModel = this._cmProject( LS_NOTE_STATE, 'note', ); // LEGACY PROJECT MODELS - legacyMetric: PersistenceForProjectModel = this._cmProjectLegacy( - LS_METRIC_STATE, - 'metric' as any, - ); - legacyImprovement: PersistenceForProjectModel = this._cmProjectLegacy( + legacyMetric: PersistenceForProjectModel = this._cmProjectLegacy< + MetricState, + Metric + >(LS_METRIC_STATE, 'metric' as any); + legacyImprovement: PersistenceForProjectModel< + ImprovementState, + Improvement + > = this._cmProjectLegacy( LS_IMPROVEMENT_STATE, 'improvement' as any, ); - legacyObstruction: PersistenceForProjectModel = this._cmProjectLegacy( + legacyObstruction: PersistenceForProjectModel< + ObstructionState, + Obstruction + > = this._cmProjectLegacy( LS_OBSTRUCTION_STATE, 'obstruction' as any, ); - onAfterSave$: Subject<{ appDataKey: AllowedDBKeys; data: unknown; isDataImport: boolean; isSyncModelChange: boolean; projectId?: string }> - = new Subject(); + onAfterSave$: Subject<{ + appDataKey: AllowedDBKeys; + data: unknown; + isDataImport: boolean; + isSyncModelChange: boolean; + projectId?: string; + }> = new Subject(); onAfterImport$: Subject = new Subject(); inMemoryComplete$: Observable = merge( @@ -183,11 +233,9 @@ export class PersistenceService { debounceTime(50), concatMap(() => this.loadComplete()), // TODO maybe not necessary - skipWhile(complete => !isValidAppData(complete)), + skipWhile((complete) => !isValidAppData(complete)), ), - ).pipe( - shareReplay(1), - ); + ).pipe(shareReplay(1)); private _inMemoryComplete?: AppDataCompleteOptionalSyncModelChange; private _isBlockSaving: boolean = false; @@ -208,37 +256,58 @@ export class PersistenceService { async loadProjectArchive(): Promise { return await this._loadFromDb({ dbKey: 'archivedProjects', - legacyDBKey: LS_PROJECT_ARCHIVE + legacyDBKey: LS_PROJECT_ARCHIVE, }); } - async saveProjectArchive(data: ProjectArchive, isDataImport: boolean = false): Promise { - return await this._saveToDb({dbKey: 'archivedProjects', data, isDataImport, isSyncModelChange: false}); + async saveProjectArchive( + data: ProjectArchive, + isDataImport: boolean = false, + ): Promise { + return await this._saveToDb({ + dbKey: 'archivedProjects', + data, + isDataImport, + isSyncModelChange: false, + }); } async loadArchivedProject(projectId: string): Promise { - const archive = await this._loadFromDb({dbKey: 'project', legacyDBKey: LS_PROJECT_ARCHIVE, projectId}); + const archive = await this._loadFromDb({ + dbKey: 'project', + legacyDBKey: LS_PROJECT_ARCHIVE, + projectId, + }); const projectDataCompressed = archive[projectId]; const decompressed = await this._compressionService.decompress(projectDataCompressed); const parsed = JSON.parse(decompressed); - console.log(`Decompressed project, size before: ${projectDataCompressed.length}, size after: ${decompressed.length}`, parsed); + console.log( + `Decompressed project, size before: ${projectDataCompressed.length}, size after: ${decompressed.length}`, + parsed, + ); return parsed; } async removeArchivedProject(projectId: string): Promise { const archive = await this._loadFromDb({ dbKey: 'archivedProjects', - legacyDBKey: LS_PROJECT_ARCHIVE + legacyDBKey: LS_PROJECT_ARCHIVE, }); delete archive[projectId]; await this.saveProjectArchive(archive); } - async saveArchivedProject(projectId: string, archivedProject: ProjectArchivedRelatedData) { - const current = await this.loadProjectArchive() || {}; + async saveArchivedProject( + projectId: string, + archivedProject: ProjectArchivedRelatedData, + ) { + const current = (await this.loadProjectArchive()) || {}; const jsonStr = JSON.stringify(archivedProject); const compressedData = await this._compressionService.compress(jsonStr); - console.log(`Compressed project, size before: ${jsonStr.length}, size after: ${compressedData.length}`, archivedProject); + console.log( + `Compressed project, size before: ${jsonStr.length}, size after: ${compressedData.length}`, + archivedProject, + ); return this.saveProjectArchive({ ...current, [projectId]: compressedData, @@ -251,17 +320,21 @@ export class PersistenceService { throw new Error('Project not found'); } return { - ...allProjects.entities[projectId] as Project, + ...(allProjects.entities[projectId] as Project), relatedModels: await this.loadAllRelatedModelDataForProject(projectId), }; } - async loadAllRelatedModelDataForProject(projectId: string): Promise { - const forProjectsData = await Promise.all(this._projectModels.map(async (modelCfg) => { - return { - [modelCfg.appDataKey]: await modelCfg.load(projectId), - }; - })); + async loadAllRelatedModelDataForProject( + projectId: string, + ): Promise { + const forProjectsData = await Promise.all( + this._projectModels.map(async (modelCfg) => { + return { + [modelCfg.appDataKey]: await modelCfg.load(projectId), + }; + }), + ); const projectData = Object.assign({}, ...forProjectsData); return { ...projectData, @@ -269,15 +342,22 @@ export class PersistenceService { } async removeCompleteRelatedDataForProject(projectId: string): Promise { - await Promise.all(this._projectModels.map((modelCfg) => { - return modelCfg.remove(projectId); - })); + await Promise.all( + this._projectModels.map((modelCfg) => { + return modelCfg.remove(projectId); + }), + ); } - async restoreCompleteRelatedDataForProject(projectId: string, data: ProjectArchivedRelatedData): Promise { - await Promise.all(this._projectModels.map((modelCfg) => { - return modelCfg.save(projectId, data[modelCfg.appDataKey], {}); - })); + async restoreCompleteRelatedDataForProject( + projectId: string, + data: ProjectArchivedRelatedData, + ): Promise { + await Promise.all( + this._projectModels.map((modelCfg) => { + return modelCfg.save(projectId, data[modelCfg.appDataKey], {}); + }), + ); } async archiveProject(projectId: string): Promise { @@ -304,9 +384,7 @@ export class PersistenceService { getLastLocalSyncModelChange(): number | null { const la = localStorage.getItem(LS_LAST_LOCAL_SYNC_MODEL_CHANGE); // NOTE: we need to parse because new Date('1570549698000') is "Invalid Date" - const laParsed = Number.isNaN(Number(la)) - ? la - : +(la as string); + const laParsed = Number.isNaN(Number(la)) ? la : +(la as string); if (laParsed === null || laParsed === 0) { return null; @@ -317,16 +395,21 @@ export class PersistenceService { } async loadBackup(): Promise { - return this._loadFromDb({dbKey: LS_BACKUP, legacyDBKey: LS_BACKUP}); + return this._loadFromDb({ dbKey: LS_BACKUP, legacyDBKey: LS_BACKUP }); } async saveBackup(backup?: AppDataComplete): Promise { - const data: AppDataComplete = backup || await this.loadComplete(); - return this._saveToDb({dbKey: LS_BACKUP, data, isDataImport: true, isSyncModelChange: true}); + const data: AppDataComplete = backup || (await this.loadComplete()); + return this._saveToDb({ + dbKey: LS_BACKUP, + data, + isDataImport: true, + isSyncModelChange: true, + }); } async clearBackup(): Promise { - return this._removeFromDb({dbKey: LS_BACKUP}); + return this._removeFromDb({ dbKey: LS_BACKUP }); } // NOTE: not including backup @@ -334,9 +417,7 @@ export class PersistenceService { let r; if (!this._inMemoryComplete) { const projectState = await this.project.loadState(); - const pids = projectState - ? projectState.ids as string[] - : [DEFAULT_PROJECT_ID]; + const pids = projectState ? (projectState.ids as string[]) : [DEFAULT_PROJECT_ID]; if (!pids) { throw new Error('Project State is broken'); } @@ -353,7 +434,7 @@ export class PersistenceService { return { ...r, // TODO remove legacy field - ...({lastActiveTime: this.getLastLocalSyncModelChange()} as any), + ...({ lastActiveTime: this.getLastLocalSyncModelChange() } as any), lastLocalSyncModelChange: this.getLastLocalSyncModelChange(), }; @@ -363,21 +444,26 @@ export class PersistenceService { console.log('IMPORT--->', data); this._isBlockSaving = true; - const forBase = Promise.all(this._baseModels.map(async (modelCfg: PersistenceBaseModel) => { - return await modelCfg.saveState(data[modelCfg.appDataKey], {isDataImport: true}); - })); - const forProject = Promise.all(this._projectModels.map(async (modelCfg: PersistenceForProjectModel) => { - if (!data[modelCfg.appDataKey]) { - devError('No data for ' + modelCfg.appDataKey + ' - ' + data[modelCfg.appDataKey]); - return; - } - return await this._saveForProjectIds(data[modelCfg.appDataKey], modelCfg, true); - })); + const forBase = Promise.all( + this._baseModels.map(async (modelCfg: PersistenceBaseModel) => { + return await modelCfg.saveState(data[modelCfg.appDataKey], { + isDataImport: true, + }); + }), + ); + const forProject = Promise.all( + this._projectModels.map(async (modelCfg: PersistenceForProjectModel) => { + if (!data[modelCfg.appDataKey]) { + devError( + 'No data for ' + modelCfg.appDataKey + ' - ' + data[modelCfg.appDataKey], + ); + return; + } + return await this._saveForProjectIds(data[modelCfg.appDataKey], modelCfg, true); + }), + ); - return await Promise.all([ - forBase, - forProject, - ]) + return await Promise.all([forBase, forProject]) .then(() => { this.updateLastLocalSyncModelChange(data.lastLocalSyncModelChange); this._inMemoryComplete = data; @@ -424,9 +510,10 @@ export class PersistenceService { ): PersistenceBaseModel { const model = { appDataKey, - loadState: (isSkipMigrate = false) => isSkipMigrate - ? this._loadFromDb({dbKey: appDataKey, legacyDBKey: lsKey}) - : this._loadFromDb({dbKey: appDataKey, legacyDBKey: lsKey}).then(migrateFn), + loadState: (isSkipMigrate = false) => + isSkipMigrate + ? this._loadFromDb({ dbKey: appDataKey, legacyDBKey: lsKey }) + : this._loadFromDb({ dbKey: appDataKey, legacyDBKey: lsKey }).then(migrateFn), // In case we want to check on load // loadState: async (isSkipMigrate = false) => { // const data = isSkipMigrate @@ -437,14 +524,22 @@ export class PersistenceService { // } // return data; // }, - saveState: (data: any, { - isDataImport = false, - isSyncModelChange - }: { isDataImport?: boolean; isSyncModelChange: boolean }) => { + saveState: ( + data: any, + { + isDataImport = false, + isSyncModelChange, + }: { isDataImport?: boolean; isSyncModelChange: boolean }, + ) => { if (data && data.ids && data.entities) { data = checkFixEntityStateConsistency(data, appDataKey); } - return this._saveToDb({dbKey: appDataKey, data, isDataImport, isSyncModelChange}); + return this._saveToDb({ + dbKey: appDataKey, + data, + isDataImport, + isSyncModelChange, + }); }, }; if (!isSkipPush) { @@ -463,15 +558,15 @@ export class PersistenceService { ...this._cmBase(lsKey, appDataKey, migrateFn, true), getById: async (id: string): Promise => { - const state = await model.loadState() as any; - return state && state.entities && state.entities[id] || null; + const state = (await model.loadState()) as any; + return (state && state.entities && state.entities[id]) || null; }, // NOTE: side effects are not executed!!! execAction: async (action: Action): Promise => { const state = await model.loadState(); const newState = reducerFn(state, action); - await model.saveState(newState, {isDataImport: false}); + await model.saveState(newState, { isDataImport: false }); return newState; }, }; @@ -486,11 +581,7 @@ export class PersistenceService { appDataKey: keyof AppDataForProjects, migrateFn: (state: S, projectId: string) => S = (v) => v, ): PersistenceForProjectModel { - const model = this._cmProjectLegacy( - lsKey, - appDataKey, - migrateFn, - ); + const model = this._cmProjectLegacy(lsKey, appDataKey, migrateFn); this._projectModels.push(model); return model; } @@ -503,39 +594,49 @@ export class PersistenceService { ): PersistenceForProjectModel { const model = { appDataKey, - load: (projectId: string): Promise => this._loadFromDb({ - dbKey: appDataKey, - projectId, - legacyDBKey: this._makeProjectKey(projectId, lsKey) - }).then(v => migrateFn(v, projectId)), - save: (projectId: string, data: any, { - isDataImport = false, - isSyncModelChange - }: { isDataImport?: boolean; isSyncModelChange?: boolean }) => this._saveToDb({ - dbKey: appDataKey, - data, - isDataImport, - projectId, - isSyncModelChange, - }), - remove: (projectId: string) => this._removeFromDb({dbKey: appDataKey, projectId}), + load: (projectId: string): Promise => + this._loadFromDb({ + dbKey: appDataKey, + projectId, + legacyDBKey: this._makeProjectKey(projectId, lsKey), + }).then((v) => migrateFn(v, projectId)), + save: ( + projectId: string, + data: any, + { + isDataImport = false, + isSyncModelChange, + }: { isDataImport?: boolean; isSyncModelChange?: boolean }, + ) => + this._saveToDb({ + dbKey: appDataKey, + data, + isDataImport, + projectId, + isSyncModelChange, + }), + remove: (projectId: string) => this._removeFromDb({ dbKey: appDataKey, projectId }), ent: { getById: async (projectId: string, id: string): Promise => { - const state = await model.load(projectId) as any; - return state && state.entities && state.entities[id] || null; + const state = (await model.load(projectId)) as any; + return (state && state.entities && state.entities[id]) || null; }, }, }; return model; } - private async _loadAppDataForProjects(projectIds: string[]): Promise { - const forProjectsData = await Promise.all(this._projectModels.map(async (modelCfg) => { - const modelState = await this._loadForProjectIds(projectIds, modelCfg.load); - return { - [modelCfg.appDataKey]: modelState, - }; - })); + private async _loadAppDataForProjects( + projectIds: string[], + ): Promise { + const forProjectsData = await Promise.all( + this._projectModels.map(async (modelCfg) => { + const modelState = await this._loadForProjectIds(projectIds, modelCfg.load); + return { + [modelCfg.appDataKey]: modelState, + }; + }), + ); return Object.assign({}, ...forProjectsData); } @@ -546,35 +647,45 @@ export class PersistenceService { const dataForProject = await getDataFn(projectId); return { ...prevAcc, - [projectId]: dataForProject + [projectId]: dataForProject, }; }, Promise.resolve({})); } // eslint-disable-next-line - private async _saveForProjectIds(data: any, projectModel: PersistenceForProjectModel, isDataImport = false) { + private async _saveForProjectIds( + data: any, + projectModel: PersistenceForProjectModel, + isDataImport = false, + ) { const promises: Promise[] = []; - Object.keys(data).forEach(projectId => { + Object.keys(data).forEach((projectId) => { if (data[projectId]) { - promises.push(projectModel.save(projectId, data[projectId], {isDataImport})); + promises.push(projectModel.save(projectId, data[projectId], { isDataImport })); } }); return await Promise.all(promises); } private _makeProjectKey(projectId: string, subKey: string, additional?: string) { - return LS_PROJECT_PREFIX + projectId + '_' + subKey + (additional ? '_' + additional : ''); + return ( + LS_PROJECT_PREFIX + projectId + '_' + subKey + (additional ? '_' + additional : '') + ); } // DATA STORAGE INTERFACE // --------------------- private _getIDBKey(dbKey: AllowedDBKeys, projectId?: string) { - return projectId - ? 'p__' + projectId + '__' + dbKey - : dbKey; + return projectId ? 'p__' + projectId + '__' + dbKey : dbKey; } - private async _saveToDb({dbKey, data, isDataImport = false, projectId, isSyncModelChange = false}: { + private async _saveToDb({ + dbKey, + data, + isDataImport = false, + projectId, + isSyncModelChange = false, + }: { dbKey: AllowedDBKeys; data: any; projectId?: string; @@ -583,19 +694,25 @@ export class PersistenceService { }): Promise { if (!this._isBlockSaving || isDataImport === true) { const idbKey = this._getIDBKey(dbKey, projectId); - this._store.dispatch(saveToDb({dbKey, data})); + this._store.dispatch(saveToDb({ dbKey, data })); const r = await this._databaseService.save(idbKey, data); this._updateInMemory({ projectId, appDataKey: dbKey, - data + data, }); if (isSyncModelChange) { this.updateLastLocalSyncModelChange(); } - this.onAfterSave$.next({appDataKey: dbKey, data, isDataImport, projectId, isSyncModelChange}); + this.onAfterSave$.next({ + appDataKey: dbKey, + data, + isDataImport, + projectId, + isSyncModelChange, + }); return r; } else { @@ -604,14 +721,18 @@ export class PersistenceService { } } - private async _removeFromDb({dbKey, isDataImport = false, projectId}: { + private async _removeFromDb({ + dbKey, + isDataImport = false, + projectId, + }: { dbKey: AllowedDBKeys; projectId?: string; isDataImport?: boolean; }): Promise { const idbKey = this._getIDBKey(dbKey, projectId); if (!this._isBlockSaving || isDataImport === true) { - this._store.dispatch(removeFromDb({dbKey})); + this._store.dispatch(removeFromDb({ dbKey })); return this._databaseService.remove(idbKey); } else { console.warn('BLOCKED SAVING for ', dbKey); @@ -619,7 +740,11 @@ export class PersistenceService { } } - private async _loadFromDb({legacyDBKey, dbKey, projectId}: { + private async _loadFromDb({ + legacyDBKey, + dbKey, + projectId, + }: { legacyDBKey: string; dbKey: AllowedDBKeys; projectId?: string; @@ -628,10 +753,18 @@ export class PersistenceService { // NOTE: too much clutter // this._store.dispatch(loadFromDb({dbKey})); // TODO remove legacy stuff - return await this._databaseService.load(idbKey) || await this._databaseService.load(legacyDBKey) || undefined; + return ( + (await this._databaseService.load(idbKey)) || + (await this._databaseService.load(legacyDBKey)) || + undefined + ); } - private _updateInMemory({appDataKey, projectId, data}: { + private _updateInMemory({ + appDataKey, + projectId, + data, + }: { appDataKey: AllowedDBKeys; projectId?: string; data: any; @@ -644,11 +777,16 @@ export class PersistenceService { complete: this._inMemoryComplete, projectId, appDataKey, - data + data, }); } - private _extendAppDataComplete({complete, appDataKey, projectId, data}: { + private _extendAppDataComplete({ + complete, + appDataKey, + projectId, + data, + }: { complete: AppDataComplete | AppDataCompleteOptionalSyncModelChange; appDataKey: AllowedDBKeys; projectId?: string; @@ -657,16 +795,14 @@ export class PersistenceService { // console.log(appDataKey, data && data.ids && data.ids.length); return { ...complete, - ...( - projectId - ? { + ...(projectId + ? { [appDataKey]: { - ...((complete as any)[appDataKey]), - [projectId]: data - } + ...(complete as any)[appDataKey], + [projectId]: data, + }, } - : {[appDataKey]: data} - ) + : { [appDataKey]: data }), }; } } diff --git a/src/app/core/snack/snack-custom/snack-custom.component.ts b/src/app/core/snack/snack-custom/snack-custom.component.ts index 41fb5f53a..e6b4266f6 100644 --- a/src/app/core/snack/snack-custom/snack-custom.component.ts +++ b/src/app/core/snack/snack-custom/snack-custom.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + Inject, + OnDestroy, + OnInit, +} from '@angular/core'; import { MAT_SNACK_BAR_DATA, MatSnackBarRef } from '@angular/material/snack-bar'; import { SnackParams } from '../snack.model'; import { Subscription } from 'rxjs'; @@ -8,7 +14,7 @@ import { debounceTime } from 'rxjs/operators'; selector: 'snack-custom', templateUrl: './snack-custom.component.html', styleUrls: ['./snack-custom.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SnackCustomComponent implements OnInit, OnDestroy { private _subs: Subscription = new Subscription(); @@ -16,8 +22,7 @@ export class SnackCustomComponent implements OnInit, OnDestroy { constructor( @Inject(MAT_SNACK_BAR_DATA) public data: SnackParams, public snackBarRef: MatSnackBarRef, - ) { - } + ) {} ngOnInit() { if (this.data.promise) { @@ -31,7 +36,7 @@ export class SnackCustomComponent implements OnInit, OnDestroy { if (!v) { this.snackBarRef.dismiss(); } - }) + }), ); } } diff --git a/src/app/core/snack/snack.module.ts b/src/app/core/snack/snack.module.ts index b8f042a52..26c4cae76 100644 --- a/src/app/core/snack/snack.module.ts +++ b/src/app/core/snack/snack.module.ts @@ -5,15 +5,7 @@ import { SnackCustomComponent } from './snack-custom/snack-custom.component'; import { UiModule } from '../../ui/ui.module'; @NgModule({ - imports: [ - UiModule, - CommonModule, - MatSnackBarModule, - ], - declarations: [ - SnackCustomComponent, - ], + imports: [UiModule, CommonModule, MatSnackBarModule], + declarations: [SnackCustomComponent], }) -export class SnackModule { -} - +export class SnackModule {} diff --git a/src/app/core/snack/snack.service.ts b/src/app/core/snack/snack.service.ts index 957b5a192..b11b1e35d 100644 --- a/src/app/core/snack/snack.service.ts +++ b/src/app/core/snack/snack.service.ts @@ -16,7 +16,9 @@ import { debounce } from 'helpful-decorators'; }) export class SnackService { private _ref?: MatSnackBarRef; - private _onWorkContextChange$: Observable = this._actions$.pipe(ofType(setActiveWorkContext)); + private _onWorkContextChange$: Observable = this._actions$.pipe( + ofType(setActiveWorkContext), + ); constructor( private _store$: Store, @@ -32,7 +34,7 @@ export class SnackService { open(params: SnackParams | string) { if (typeof params === 'string') { - params = {msg: params}; + params = { msg: params }; } this._openSnack(params); } @@ -61,7 +63,7 @@ export class SnackService { translateParams = {}, showWhile$, promise, - isSpinner + isSpinner, } = params; const cfg = { @@ -69,9 +71,10 @@ export class SnackService { ...config, data: { ...params, - msg: (isSkipTranslate) + msg: isSkipTranslate ? msg - : (typeof (msg as unknown) === 'string') && this._translateService.instant(msg, translateParams), + : typeof (msg as unknown) === 'string' && + this._translateService.instant(msg, translateParams), }, }; @@ -94,16 +97,18 @@ export class SnackService { } if (actionStr && actionId && this._ref) { - this._ref.onAction() + this._ref + .onAction() .pipe(takeUntil(_destroy$)) .subscribe(() => { this._store$.dispatch({ type: actionId, - payload: actionPayload + payload: actionPayload, }); destroySubs(); }); - this._ref.afterDismissed() + this._ref + .afterDismissed() .pipe(takeUntil(_destroy$)) .subscribe(() => { destroySubs(); diff --git a/src/app/core/theme/global-theme.service.ts b/src/app/core/theme/global-theme.service.ts index 6e539f468..ff69a5f7a 100644 --- a/src/app/core/theme/global-theme.service.ts +++ b/src/app/core/theme/global-theme.service.ts @@ -16,31 +16,38 @@ import { WorkContextService } from '../../features/work-context/work-context.ser import { combineLatest, Observable } from 'rxjs'; import { remote } from 'electron'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class GlobalThemeService { - isDarkTheme$: Observable = (IS_ELECTRON && this._electronService.isMacOS) - ? new Observable(subscriber => { - subscriber.next((this._electronService.remote as typeof remote).nativeTheme.shouldUseDarkColors); - (this._electronService.remote as typeof remote) - .systemPreferences - .subscribeNotification('AppleInterfaceThemeChangedNotification', () => subscriber.next( - (this._electronService.remote as typeof remote).nativeTheme.shouldUseDarkColors) + isDarkTheme$: Observable = + IS_ELECTRON && this._electronService.isMacOS + ? new Observable((subscriber) => { + subscriber.next( + (this._electronService.remote as typeof remote).nativeTheme + .shouldUseDarkColors, + ); + (this._electronService + .remote as typeof remote).systemPreferences.subscribeNotification( + 'AppleInterfaceThemeChangedNotification', + () => + subscriber.next( + (this._electronService.remote as typeof remote).nativeTheme + .shouldUseDarkColors, + ), + ); + }) + : this._globalConfigService.misc$.pipe( + map((cfg) => cfg.isDarkMode), + distinctUntilChanged(), ); - }) - : this._globalConfigService.misc$.pipe( - map(cfg => cfg.isDarkMode), - distinctUntilChanged() - ); backgroundImg$: Observable = combineLatest([ this._workContextService.currentTheme$, this.isDarkTheme$, ]).pipe( - map(([theme, isDarkMode]) => isDarkMode - ? theme.backgroundImageDark - : theme.backgroundImageLight + map(([theme, isDarkMode]) => + isDarkMode ? theme.backgroundImageDark : theme.backgroundImageLight, ), - distinctUntilChanged() + distinctUntilChanged(), ); constructor( @@ -53,8 +60,7 @@ export class GlobalThemeService { private _domSanitizer: DomSanitizer, private _chartThemeService: NgChartThemeService, private _chromeExtensionInterfaceService: ChromeExtensionInterfaceService, - ) { - } + ) {} init() { // This is here to make web page reloads on non work context pages at least usable @@ -114,14 +120,16 @@ export class GlobalThemeService { icons.forEach(([name, path]) => { this._matIconRegistry.addSvgIcon( name, - this._domSanitizer.bypassSecurityTrustResourceUrl(path) + this._domSanitizer.bypassSecurityTrustResourceUrl(path), ); }); } private _initThemeWatchers() { // init theme watchers - this._workContextService.currentTheme$.subscribe((theme: WorkContextThemeCfg) => this._setColorTheme(theme)); + this._workContextService.currentTheme$.subscribe((theme: WorkContextThemeCfg) => + this._setColorTheme(theme), + ); this.isDarkTheme$.subscribe((isDarkTheme) => this._setDarkTheme(isDarkTheme)); } @@ -155,22 +163,26 @@ export class GlobalThemeService { } private _setChartTheme(isDarkTheme: boolean) { - const overrides = (isDarkTheme) + const overrides = isDarkTheme ? { - legend: { - labels: {fontColor: 'white'} - }, - scales: { - xAxes: [{ - ticks: {fontColor: 'white'}, - gridLines: {color: 'rgba(255,255,255,0.1)'} - }], - yAxes: [{ - ticks: {fontColor: 'white'}, - gridLines: {color: 'rgba(255,255,255,0.1)'} - }] + legend: { + labels: { fontColor: 'white' }, + }, + scales: { + xAxes: [ + { + ticks: { fontColor: 'white' }, + gridLines: { color: 'rgba(255,255,255,0.1)' }, + }, + ], + yAxes: [ + { + ticks: { fontColor: 'white' }, + gridLines: { color: 'rgba(255,255,255,0.1)' }, + }, + ], + }, } - } : {}; this._chartThemeService.setColorschemesOptions(overrides); } diff --git a/src/app/features/bookmark/bookmark-bar/bookmark-bar.component.ts b/src/app/features/bookmark/bookmark-bar/bookmark-bar.component.ts index ab32ba6f8..e69c7ec39 100644 --- a/src/app/features/bookmark/bookmark-bar/bookmark-bar.component.ts +++ b/src/app/features/bookmark/bookmark-bar/bookmark-bar.component.ts @@ -1,4 +1,11 @@ -import { ChangeDetectionStrategy, Component, ElementRef, HostListener, OnDestroy, ViewChild } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + HostListener, + OnDestroy, + ViewChild, +} from '@angular/core'; import { BookmarkService } from '../bookmark.service'; import { MatDialog } from '@angular/material/dialog'; import { DialogEditBookmarkComponent } from '../dialog-edit-bookmark/dialog-edit-bookmark.component'; @@ -14,10 +21,7 @@ import { T } from '../../../t.const'; templateUrl: './bookmark-bar.component.html', styleUrls: ['./bookmark-bar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [ - fadeAnimation, - slideAnimation, - ], + animations: [fadeAnimation, slideAnimation], }) export class BookmarkBarComponent implements OnDestroy { isDragOver: boolean = false; @@ -41,16 +45,16 @@ export class BookmarkBarComponent implements OnDestroy { // } // }); - this._subs.add(this._dragulaService.dropModel(this.LIST_ID) - .subscribe(({targetModel}: any) => { + this._subs.add( + this._dragulaService.dropModel(this.LIST_ID).subscribe(({ targetModel }: any) => { // const {target, source, targetModel, item} = params; const newIds = targetModel.map((m: Bookmark) => m.id); this.bookmarkService.reorderBookmarks(newIds); - }) + }), ); } - @ViewChild('bookmarkBar', {read: ElementRef}) set bookmarkBarEl(content: ElementRef) { + @ViewChild('bookmarkBar', { read: ElementRef }) set bookmarkBarEl(content: ElementRef) { if (content && content.nativeElement) { this.bookmarkBarHeight = content.nativeElement.offsetHeight; } @@ -79,12 +83,14 @@ export class BookmarkBarComponent implements OnDestroy { } openEditDialog(bookmark?: Bookmark) { - this._matDialog.open(DialogEditBookmarkComponent, { - restoreFocus: true, - data: { - bookmark - }, - }).afterClosed() + this._matDialog + .open(DialogEditBookmarkComponent, { + restoreFocus: true, + data: { + bookmark, + }, + }) + .afterClosed() .subscribe((bookmarkIN) => { if (bookmarkIN) { if (bookmarkIN.id) { diff --git a/src/app/features/bookmark/bookmark-link/bookmark-link.directive.ts b/src/app/features/bookmark/bookmark-link/bookmark-link.directive.ts index 49c64d98f..846840bab 100644 --- a/src/app/features/bookmark/bookmark-link/bookmark-link.directive.ts +++ b/src/app/features/bookmark/bookmark-link/bookmark-link.directive.ts @@ -8,18 +8,16 @@ import { ElectronService } from '../../../core/electron/electron.service'; import { ipcRenderer, shell } from 'electron'; @Directive({ - selector: '[bookmarkLink]' + selector: '[bookmarkLink]', }) export class BookmarkLinkDirective { - @Input() type?: BookmarkType; @Input() href?: BookmarkType; constructor( private _electronService: ElectronService, private _snackService: SnackService, - ) { - } + ) {} @HostListener('click', ['$event']) onClick(ev: Event) { if (!this.type || !this.href) { @@ -39,7 +37,7 @@ export class BookmarkLinkDirective { } else if (this.type === 'COMMAND') { this._snackService.open({ msg: T.GLOBAL_SNACK.RUNNING_X, - translateParams: {str: this.href}, + translateParams: { str: this.href }, ico: 'laptop_windows', }); this._exec(this.href); diff --git a/src/app/features/bookmark/bookmark.model.ts b/src/app/features/bookmark/bookmark.model.ts index d59d36e0b..f2318e933 100644 --- a/src/app/features/bookmark/bookmark.model.ts +++ b/src/app/features/bookmark/bookmark.model.ts @@ -1,4 +1,7 @@ -import { DropPasteInput, DropPasteInputType } from '../../core/drop-paste-input/drop-paste.model'; +import { + DropPasteInput, + DropPasteInputType, +} from '../../core/drop-paste-input/drop-paste.model'; export type BookmarkType = DropPasteInputType; diff --git a/src/app/features/bookmark/bookmark.module.ts b/src/app/features/bookmark/bookmark.module.ts index 0cdcb605f..d8870a4e1 100644 --- a/src/app/features/bookmark/bookmark.module.ts +++ b/src/app/features/bookmark/bookmark.module.ts @@ -16,17 +16,13 @@ import { BookmarkLinkDirective } from './bookmark-link/bookmark-link.directive'; UiModule, FormsModule, StoreModule.forFeature(BOOKMARK_FEATURE_NAME, bookmarkReducer), - EffectsModule.forFeature([BookmarkEffects]) + EffectsModule.forFeature([BookmarkEffects]), ], declarations: [ BookmarkBarComponent, DialogEditBookmarkComponent, - BookmarkLinkDirective - ], - exports: [ - BookmarkBarComponent, - DialogEditBookmarkComponent + BookmarkLinkDirective, ], + exports: [BookmarkBarComponent, DialogEditBookmarkComponent], }) -export class BookmarkModule { -} +export class BookmarkModule {} diff --git a/src/app/features/bookmark/bookmark.service.ts b/src/app/features/bookmark/bookmark.service.ts index aa30ef889..aaaaecc17 100644 --- a/src/app/features/bookmark/bookmark.service.ts +++ b/src/app/features/bookmark/bookmark.service.ts @@ -4,7 +4,7 @@ import { BookmarkState, initialBookmarkState, selectAllBookmarks, - selectIsShowBookmarkBar + selectIsShowBookmarkBar, } from './store/bookmark.reducer'; import { AddBookmark, @@ -14,7 +14,7 @@ import { ReorderBookmarks, ShowBookmarks, ToggleBookmarks, - UpdateBookmark + UpdateBookmark, } from './store/bookmark.actions'; import { Observable } from 'rxjs'; import { Bookmark } from './bookmark.model'; @@ -22,7 +22,10 @@ import * as shortid from 'shortid'; import { DialogEditBookmarkComponent } from './dialog-edit-bookmark/dialog-edit-bookmark.component'; import { MatDialog } from '@angular/material/dialog'; import { PersistenceService } from '../../core/persistence/persistence.service'; -import { createFromDrop, createFromPaste } from '../../core/drop-paste-input/drop-paste-input'; +import { + createFromDrop, + createFromPaste, +} from '../../core/drop-paste-input/drop-paste-input'; import { DropPasteInput } from '../../core/drop-paste-input/drop-paste.model'; @Injectable({ @@ -30,14 +33,15 @@ import { DropPasteInput } from '../../core/drop-paste-input/drop-paste.model'; }) export class BookmarkService { bookmarks$: Observable = this._store$.pipe(select(selectAllBookmarks)); - isShowBookmarks$: Observable = this._store$.pipe(select(selectIsShowBookmarkBar)); + isShowBookmarks$: Observable = this._store$.pipe( + select(selectIsShowBookmarkBar), + ); constructor( private _store$: Store, private _matDialog: MatDialog, private _persistenceService: PersistenceService, - ) { - } + ) {} async loadStateForProject(projectId: string) { const lsBookmarkState = await this._persistenceService.bookmark.load(projectId); @@ -45,24 +49,26 @@ export class BookmarkService { } loadState(state: BookmarkState) { - this._store$.dispatch(new LoadBookmarkState({state})); + this._store$.dispatch(new LoadBookmarkState({ state })); } addBookmark(bookmark: Bookmark) { - this._store$.dispatch(new AddBookmark({ - bookmark: { - ...bookmark, - id: shortid() - } - })); + this._store$.dispatch( + new AddBookmark({ + bookmark: { + ...bookmark, + id: shortid(), + }, + }), + ); } deleteBookmark(id: string) { - this._store$.dispatch(new DeleteBookmark({id})); + this._store$.dispatch(new DeleteBookmark({ id })); } updateBookmark(id: string, changes: Partial) { - this._store$.dispatch(new UpdateBookmark({bookmark: {id, changes}})); + this._store$.dispatch(new UpdateBookmark({ bookmark: { id, changes } })); } showBookmarks() { @@ -78,7 +84,7 @@ export class BookmarkService { } reorderBookmarks(ids: string[]) { - this._store$.dispatch(new ReorderBookmarks({ids})); + this._store$.dispatch(new ReorderBookmarks({ ids })); } // HANDLE INPUT @@ -106,12 +112,14 @@ export class BookmarkService { ev.preventDefault(); ev.stopPropagation(); - this._matDialog.open(DialogEditBookmarkComponent, { - restoreFocus: true, - data: { - bookmark: {...bookmark}, - }, - }).afterClosed() + this._matDialog + .open(DialogEditBookmarkComponent, { + restoreFocus: true, + data: { + bookmark: { ...bookmark }, + }, + }) + .afterClosed() .subscribe((bookmarkIN) => { if (bookmarkIN) { if (bookmarkIN.id) { @@ -121,6 +129,5 @@ export class BookmarkService { } } }); - } } diff --git a/src/app/features/bookmark/dialog-edit-bookmark/dialog-edit-bookmark.component.ts b/src/app/features/bookmark/dialog-edit-bookmark/dialog-edit-bookmark.component.ts index a87b7f857..8dd9ace80 100644 --- a/src/app/features/bookmark/dialog-edit-bookmark/dialog-edit-bookmark.component.ts +++ b/src/app/features/bookmark/dialog-edit-bookmark/dialog-edit-bookmark.component.ts @@ -17,7 +17,7 @@ interface BookmarkSelectType { selector: 'dialog-edit-bookmark', templateUrl: './dialog-edit-bookmark.component.html', styleUrls: ['./dialog-edit-bookmark.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogEditBookmarkComponent implements OnInit { T: typeof T = T; @@ -30,29 +30,28 @@ export class DialogEditBookmarkComponent implements OnInit { map((searchTerm) => { // Note: the outer array signifies the observable stream the other is the value return this.customIcons.filter((icoStr) => - icoStr.toLowerCase().includes(searchTerm.toLowerCase()) + icoStr.toLowerCase().includes(searchTerm.toLowerCase()), ); }), ); constructor( private _matDialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: { bookmark: Bookmark } - ) { - } + @Inject(MAT_DIALOG_DATA) public data: { bookmark: Bookmark }, + ) {} ngOnInit() { - this.bookmarkCopy = {...this.data.bookmark} as BookmarkCopy; + this.bookmarkCopy = { ...this.data.bookmark } as BookmarkCopy; if (!this.bookmarkCopy.type) { this.bookmarkCopy.type = 'LINK'; } this.types = [ - {type: 'LINK', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.LINK}, - {type: 'IMG', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.IMG}, + { type: 'LINK', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.LINK }, + { type: 'IMG', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.IMG }, ]; if (IS_ELECTRON) { - this.types.push({type: 'FILE', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.FILE}); - this.types.push({type: 'COMMAND', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.COMMAND}); + this.types.push({ type: 'FILE', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.FILE }); + this.types.push({ type: 'COMMAND', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.COMMAND }); } } @@ -68,7 +67,10 @@ export class DialogEditBookmarkComponent implements OnInit { return; } - if (this.bookmarkCopy.type === 'LINK' && !this.bookmarkCopy.path.match(/(https?|ftp|file):\/\//)) { + if ( + this.bookmarkCopy.type === 'LINK' && + !this.bookmarkCopy.path.match(/(https?|ftp|file):\/\//) + ) { this.bookmarkCopy.path = 'http://' + this.bookmarkCopy.path; } this.close(this.bookmarkCopy); diff --git a/src/app/features/bookmark/store/bookmark.actions.ts b/src/app/features/bookmark/store/bookmark.actions.ts index cd346ce74..c4e9b5d42 100644 --- a/src/app/features/bookmark/store/bookmark.actions.ts +++ b/src/app/features/bookmark/store/bookmark.actions.ts @@ -18,29 +18,25 @@ export enum BookmarkActionTypes { export class LoadBookmarkState implements Action { readonly type: string = BookmarkActionTypes.LoadBookmarkState; - constructor(public payload: { state: BookmarkState }) { - } + constructor(public payload: { state: BookmarkState }) {} } export class AddBookmark implements Action { readonly type: string = BookmarkActionTypes.AddBookmark; - constructor(public payload: { bookmark: Bookmark }) { - } + constructor(public payload: { bookmark: Bookmark }) {} } export class UpdateBookmark implements Action { readonly type: string = BookmarkActionTypes.UpdateBookmark; - constructor(public payload: { bookmark: Update }) { - } + constructor(public payload: { bookmark: Update }) {} } export class DeleteBookmark implements Action { readonly type: string = BookmarkActionTypes.DeleteBookmark; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class ShowBookmarks implements Action { @@ -58,17 +54,15 @@ export class ToggleBookmarks implements Action { export class ReorderBookmarks implements Action { readonly type: string = BookmarkActionTypes.ReorderBookmarks; - constructor(public payload: { ids: string[] }) { - } + constructor(public payload: { ids: string[] }) {} } export type BookmarkActions = - LoadBookmarkState + | LoadBookmarkState | AddBookmark | UpdateBookmark | DeleteBookmark | ShowBookmarks | HideBookmarks | ToggleBookmarks - | ReorderBookmarks - ; + | ReorderBookmarks; diff --git a/src/app/features/bookmark/store/bookmark.effects.ts b/src/app/features/bookmark/store/bookmark.effects.ts index 84d11e36d..61eb06c9f 100644 --- a/src/app/features/bookmark/store/bookmark.effects.ts +++ b/src/app/features/bookmark/store/bookmark.effects.ts @@ -10,38 +10,39 @@ import { WorkContextService } from '../../work-context/work-context.service'; @Injectable() export class BookmarkEffects { - - @Effect({dispatch: false}) updateBookmarks$: Observable = this._actions$ - .pipe( - ofType( - BookmarkActionTypes.AddBookmark, - BookmarkActionTypes.UpdateBookmark, - BookmarkActionTypes.DeleteBookmark, - BookmarkActionTypes.ShowBookmarks, - BookmarkActionTypes.HideBookmarks, - BookmarkActionTypes.ToggleBookmarks, - ), - switchMap(() => combineLatest([ + @Effect({ dispatch: false }) + updateBookmarks$: Observable = this._actions$.pipe( + ofType( + BookmarkActionTypes.AddBookmark, + BookmarkActionTypes.UpdateBookmark, + BookmarkActionTypes.DeleteBookmark, + BookmarkActionTypes.ShowBookmarks, + BookmarkActionTypes.HideBookmarks, + BookmarkActionTypes.ToggleBookmarks, + ), + switchMap(() => + combineLatest([ this._workContextService.activeWorkContextIdIfProject$, this._store$.pipe(select(selectBookmarkFeatureState)), - ]).pipe(first())), - tap(([projectId, state]) => this._saveToLs(projectId, state)), - ); + ]).pipe(first()), + ), + tap(([projectId, state]) => this._saveToLs(projectId, state)), + ); constructor( private _actions$: Actions, private _store$: Store, private _persistenceService: PersistenceService, private _workContextService: WorkContextService, - ) { - } + ) {} private _saveToLs(currentProjectId: string, bookmarkState: BookmarkState) { if (currentProjectId) { - this._persistenceService.bookmark.save(currentProjectId, bookmarkState, {isSyncModelChange: true}); + this._persistenceService.bookmark.save(currentProjectId, bookmarkState, { + isSyncModelChange: true, + }); } else { throw new Error('No current project id'); } } - } diff --git a/src/app/features/bookmark/store/bookmark.reducer.ts b/src/app/features/bookmark/store/bookmark.reducer.ts index 2a208e233..0a6553b46 100644 --- a/src/app/features/bookmark/store/bookmark.reducer.ts +++ b/src/app/features/bookmark/store/bookmark.reducer.ts @@ -6,7 +6,7 @@ import { DeleteBookmark, LoadBookmarkState, ReorderBookmarks, - UpdateBookmark + UpdateBookmark, } from './bookmark.actions'; import { Bookmark } from '../bookmark.model'; import { createFeatureSelector, createSelector } from '@ngrx/store'; @@ -19,19 +19,29 @@ export interface BookmarkState extends EntityState { } export const adapter: EntityAdapter = createEntityAdapter(); -export const selectBookmarkFeatureState = createFeatureSelector(BOOKMARK_FEATURE_NAME); -export const {selectIds, selectEntities, selectAll, selectTotal} = adapter.getSelectors(); +export const selectBookmarkFeatureState = createFeatureSelector( + BOOKMARK_FEATURE_NAME, +); +export const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = adapter.getSelectors(); export const selectAllBookmarks = createSelector(selectBookmarkFeatureState, selectAll); -export const selectIsShowBookmarkBar = createSelector(selectBookmarkFeatureState, state => state.isShowBookmarks); +export const selectIsShowBookmarkBar = createSelector( + selectBookmarkFeatureState, + (state) => state.isShowBookmarks, +); export const initialBookmarkState: BookmarkState = adapter.getInitialState({ // additional entity state properties - isShowBookmarks: false + isShowBookmarks: false, }); export function bookmarkReducer( state: BookmarkState = initialBookmarkState, - action: BookmarkActions + action: BookmarkActions, ): BookmarkState { switch (action.type) { case BookmarkActionTypes.AddBookmark: { @@ -47,16 +57,16 @@ export function bookmarkReducer( } case BookmarkActionTypes.LoadBookmarkState: - return {...(action as LoadBookmarkState).payload.state}; + return { ...(action as LoadBookmarkState).payload.state }; case BookmarkActionTypes.ShowBookmarks: - return {...state, isShowBookmarks: true}; + return { ...state, isShowBookmarks: true }; case BookmarkActionTypes.HideBookmarks: - return {...state, isShowBookmarks: false}; + return { ...state, isShowBookmarks: false }; case BookmarkActionTypes.ToggleBookmarks: - return {...state, isShowBookmarks: !state.isShowBookmarks}; + return { ...state, isShowBookmarks: !state.isShowBookmarks }; case BookmarkActionTypes.ReorderBookmarks: { const oldIds = state.ids as string[]; @@ -67,7 +77,7 @@ export function bookmarkReducer( // check if we have the same values inside the arrays if (oldIds.slice(0).sort().join(',') === newIds.slice(0).sort().join(',')) { - return {...state, ids: newIds}; + return { ...state, ids: newIds }; } else { console.error('Bookmark lost while reordering. Not executing reorder'); return state; @@ -79,5 +89,3 @@ export function bookmarkReducer( } } } - - diff --git a/src/app/features/config/config-form/config-form.component.ts b/src/app/features/config/config-form/config-form.component.ts index 5420c017a..f2b568f63 100644 --- a/src/app/features/config/config-form/config-form.component.ts +++ b/src/app/features/config/config-form/config-form.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; import { FormGroup } from '@angular/forms'; import { GlobalConfigSectionKey } from '../global-config.model'; @@ -13,20 +19,21 @@ import { exists } from '../../../util/exists'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ConfigFormComponent { - T: typeof T = T; config?: Record; @Input() sectionKey?: GlobalConfigSectionKey | ProjectCfgFormKey; - @Output() save: EventEmitter<{ sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; config: unknown }> = new EventEmitter(); + @Output() save: EventEmitter<{ + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; + config: unknown; + }> = new EventEmitter(); fields?: FormlyFieldConfig[]; form: FormGroup = new FormGroup({}); options: FormlyFormOptions = {}; - constructor() { - } + constructor() {} @Input() set cfg(cfg: Record) { - this.config = {...cfg}; + this.config = { ...cfg }; } // somehow needed for the form to work diff --git a/src/app/features/config/config-section/config-section.component.ts b/src/app/features/config/config-section/config-section.component.ts index 68cbd99c9..cf1147c62 100644 --- a/src/app/features/config/config-section/config-section.component.ts +++ b/src/app/features/config/config-section/config-section.component.ts @@ -10,10 +10,14 @@ import { OnInit, Output, ViewChild, - ViewContainerRef + ViewContainerRef, } from '@angular/core'; import { expandAnimation } from '../../../ui/animations/expand.ani'; -import { ConfigFormSection, CustomCfgSection, GlobalConfigSectionKey } from '../global-config.model'; +import { + ConfigFormSection, + CustomCfgSection, + GlobalConfigSectionKey, +} from '../global-config.model'; import { ProjectCfgFormKey } from '../../project/project.model'; import { Subscription } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; @@ -31,8 +35,12 @@ import { exists } from '../../../util/exists'; }) export class ConfigSectionComponent implements OnInit, OnDestroy { @Input() section?: ConfigFormSection<{ [key: string]: any }>; - @Output() save: EventEmitter<{ sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey | TagCfgFormKey; config: any }> = new EventEmitter(); - @ViewChild('customForm', {read: ViewContainerRef, static: true}) customFormRef?: ViewContainerRef; + @Output() save: EventEmitter<{ + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey | TagCfgFormKey; + config: any; + }> = new EventEmitter(); + @ViewChild('customForm', { read: ViewContainerRef, static: true }) + customFormRef?: ViewContainerRef; isExpanded: boolean = false; private _subs: Subscription = new Subscription(); private _instance?: Component; @@ -43,8 +51,7 @@ export class ConfigSectionComponent implements OnInit, OnDestroy { private _componentFactoryResolver: ComponentFactoryResolver, private _workContextService: WorkContextService, private _translateService: TranslateService, - ) { - } + ) {} private _cfg: any; @@ -55,7 +62,7 @@ export class ConfigSectionComponent implements OnInit, OnDestroy { @Input() set cfg(v: any) { this._cfg = v; if (v && this._instance) { - (this._instance as any).cfg = {...v}; + (this._instance as any).cfg = { ...v }; } } @@ -65,23 +72,32 @@ export class ConfigSectionComponent implements OnInit, OnDestroy { } // mark for check manually to make translations work with ngx formly - this._subs.add(this._translateService.onLangChange.subscribe(() => { - this._cd.detectChanges(); - })); + this._subs.add( + this._translateService.onLangChange.subscribe(() => { + this._cd.detectChanges(); + }), + ); // mark for check manually to make it work with ngx formly - this._subs.add(this._workContextService.onWorkContextChange$.subscribe(() => { - this._cd.markForCheck(); + this._subs.add( + this._workContextService.onWorkContextChange$.subscribe(() => { + this._cd.markForCheck(); - if (this.section && this.section.customSection && this.customFormRef && this.section.customSection) { - this.customFormRef.clear(); - // dirty trick to make sure data is actually there - this._viewDestroyTimeout = window.setTimeout(() => { - this._loadCustomSection((this.section as any).customSection); - this._cd.detectChanges(); - }); - } - })); + if ( + this.section && + this.section.customSection && + this.customFormRef && + this.section.customSection + ) { + this.customFormRef.clear(); + // dirty trick to make sure data is actually there + this._viewDestroyTimeout = window.setTimeout(() => { + this._loadCustomSection((this.section as any).customSection); + this._cd.detectChanges(); + }); + } + }), + ); } ngOnDestroy(): void { @@ -91,7 +107,10 @@ export class ConfigSectionComponent implements OnInit, OnDestroy { } } - onSave($event: { sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey | TagCfgFormKey; config: any }) { + onSave($event: { + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey | TagCfgFormKey; + config: any; + }) { this.isExpanded = false; this.save.emit($event); } @@ -104,7 +123,9 @@ export class ConfigSectionComponent implements OnInit, OnDestroy { const componentToRender = customConfigFormSectionComponent(customSection); if (componentToRender) { - const factory: ComponentFactory = this._componentFactoryResolver.resolveComponentFactory(componentToRender as any); + const factory: ComponentFactory = this._componentFactoryResolver.resolveComponentFactory( + componentToRender as any, + ); const ref = exists(this.customFormRef).createComponent(factory); // NOTE: important that this is set only if we actually have a value diff --git a/src/app/features/config/config.module.ts b/src/app/features/config/config.module.ts index 691117ec4..ddd190971 100644 --- a/src/app/features/config/config.module.ts +++ b/src/app/features/config/config.module.ts @@ -21,26 +21,31 @@ import { FormlyMatSliderModule } from '@ngx-formly/material/slider'; FormsModule, ReactiveFormsModule, FormlyModule.forChild({ - types: [{ - name: 'keyboard', - component: KeyboardInputComponent, - extends: 'input', - wrappers: ['form-field'], - }, { - name: 'icon', - component: IconInputComponent, - extends: 'input', - wrappers: ['form-field'], - }, { - name: 'project-select', - component: SelectProjectComponent, - // technically no input, but as the properties get us what we need... - extends: 'input', - wrappers: ['form-field'], - }, { - name: 'repeat', - component: RepeatSectionTypeComponent, - }] + types: [ + { + name: 'keyboard', + component: KeyboardInputComponent, + extends: 'input', + wrappers: ['form-field'], + }, + { + name: 'icon', + component: IconInputComponent, + extends: 'input', + wrappers: ['form-field'], + }, + { + name: 'project-select', + component: SelectProjectComponent, + // technically no input, but as the properties get us what we need... + extends: 'input', + wrappers: ['form-field'], + }, + { + name: 'repeat', + component: RepeatSectionTypeComponent, + }, + ], }), FormlyMatSliderModule, CommonModule, @@ -57,10 +62,6 @@ import { FormlyMatSliderModule } from '@ngx-formly/material/slider'; SelectProjectComponent, RepeatSectionTypeComponent, ], - exports: [ - ConfigSectionComponent, - ConfigFormComponent - ], + exports: [ConfigSectionComponent, ConfigFormComponent], }) -export class ConfigModule { -} +export class ConfigModule {} diff --git a/src/app/features/config/default-global-config.const.ts b/src/app/features/config/default-global-config.const.ts index 452fa0d1b..d4506b27b 100644 --- a/src/app/features/config/default-global-config.const.ts +++ b/src/app/features/config/default-global-config.const.ts @@ -2,12 +2,15 @@ import { GlobalConfigState } from './global-config.model'; import { IS_MAC } from '../../util/is-mac'; import { IS_F_DROID_APP } from '../../util/is-android-web-view'; -export const IS_USE_DARK_THEME_AS_DEFAULT: boolean = !IS_MAC || !window.matchMedia || window.matchMedia('(prefers-color-scheme: dark)').matches; +export const IS_USE_DARK_THEME_AS_DEFAULT: boolean = + !IS_MAC || + !window.matchMedia || + window.matchMedia('(prefers-color-scheme: dark)').matches; const minute = 60 * 1000; export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = { lang: { - lng: null + lng: null, }, misc: { isDarkMode: IS_USE_DARK_THEME_AS_DEFAULT, @@ -43,7 +46,8 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = { isLockScreen: false, isFocusWindow: false, /* eslint-disable-next-line */ - takeABreakMessage: 'Take a break! You have been working for ${duration} without one. Go away from the computer! Take a short walk! Makes you more productive in the long run!', + takeABreakMessage: + 'Take a break! You have been working for ${duration} without one. Go away from the computer! Take a short walk! Makes you more productive in the long run!', takeABreakMinWorkingTime: 60 * minute, motivationalImg: null, }, @@ -141,6 +145,6 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = { userName: null, password: null, syncFilePath: null, - } + }, }, }; diff --git a/src/app/features/config/form-cfgs/automatic-backups-form.const.ts b/src/app/features/config/form-cfgs/automatic-backups-form.const.ts index 587361888..db5e935a2 100644 --- a/src/app/features/config/form-cfgs/automatic-backups-form.const.ts +++ b/src/app/features/config/form-cfgs/automatic-backups-form.const.ts @@ -5,7 +5,9 @@ import { getElectron } from '../../../util/get-electron'; import { IS_ELECTRON } from '../../../app.constants'; import * as ElectronRenderer from 'electron/renderer'; -const backupPath = IS_ELECTRON && `${(getElectron() as typeof ElectronRenderer).remote.app.getPath('userData')}/backups`; +const backupPath = + IS_ELECTRON && + `${(getElectron() as typeof ElectronRenderer).remote.app.getPath('userData')}/backups`; export const AUTOMATIC_BACKUPS_FORM: ConfigFormSection = { isElectronOnly: true, @@ -36,5 +38,5 @@ export const AUTOMATIC_BACKUPS_FORM: ConfigFormSection = { label: T.GCF.AUTO_BACKUPS.LABEL_IS_ENABLED, }, }, - ] + ], }; diff --git a/src/app/features/config/form-cfgs/evaluation-settings-form.const.ts b/src/app/features/config/form-cfgs/evaluation-settings-form.const.ts index 6b91b7403..a8d6cd997 100644 --- a/src/app/features/config/form-cfgs/evaluation-settings-form.const.ts +++ b/src/app/features/config/form-cfgs/evaluation-settings-form.const.ts @@ -13,5 +13,5 @@ export const EVALUATION_SETTINGS_FORM_CFG: ConfigFormSection = label: T.GCF.EVALUATION.IS_HIDE_EVALUATION_SHEET, }, }, - ] + ], }; diff --git a/src/app/features/config/form-cfgs/idle-form.const.ts b/src/app/features/config/form-cfgs/idle-form.const.ts index 1338214ee..9c0dcf941 100644 --- a/src/app/features/config/form-cfgs/idle-form.const.ts +++ b/src/app/features/config/form-cfgs/idle-form.const.ts @@ -52,5 +52,5 @@ export const IDLE_FORM_CFG: ConfigFormSection = { label: T.GCF.IDLE.IS_UN_TRACKED_IDLE_RESETS_BREAK_TIMER, }, }, - ] + ], }; diff --git a/src/app/features/config/form-cfgs/keyboard-form.const.ts b/src/app/features/config/form-cfgs/keyboard-form.const.ts index 224641a28..337e2fd45 100644 --- a/src/app/features/config/form-cfgs/keyboard-form.const.ts +++ b/src/app/features/config/form-cfgs/keyboard-form.const.ts @@ -10,47 +10,47 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection = { help: T.GCF.KEYBOARD.HELP, items: [ // SYSTEM WIDE - ...((IS_ELECTRON) + ...((IS_ELECTRON ? [ - { - type: 'tpl', - className: 'tpl', - templateOptions: { - tag: 'h3', - class: 'sub-section-heading', - text: T.GCF.KEYBOARD.SYSTEM_SHORTCUTS, + { + type: 'tpl', + className: 'tpl', + templateOptions: { + tag: 'h3', + class: 'sub-section-heading', + text: T.GCF.KEYBOARD.SYSTEM_SHORTCUTS, + }, }, - }, - { - key: 'globalShowHide', - type: 'keyboard', - templateOptions: { - label: T.GCF.KEYBOARD.GLOBAL_SHOW_HIDE + { + key: 'globalShowHide', + type: 'keyboard', + templateOptions: { + label: T.GCF.KEYBOARD.GLOBAL_SHOW_HIDE, + }, }, - }, - { - key: 'globalToggleTaskStart', - type: 'keyboard', - templateOptions: { - label: T.GCF.KEYBOARD.GLOBAL_TOGGLE_TASK_START + { + key: 'globalToggleTaskStart', + type: 'keyboard', + templateOptions: { + label: T.GCF.KEYBOARD.GLOBAL_TOGGLE_TASK_START, + }, }, - }, - { - key: 'globalAddNote', - type: 'keyboard', - templateOptions: { - label: T.GCF.KEYBOARD.GLOBAL_ADD_NOTE + { + key: 'globalAddNote', + type: 'keyboard', + templateOptions: { + label: T.GCF.KEYBOARD.GLOBAL_ADD_NOTE, + }, }, - }, - { - key: 'globalAddTask', - type: 'keyboard', - templateOptions: { - label: T.GCF.KEYBOARD.GLOBAL_ADD_TASK + { + key: 'globalAddTask', + type: 'keyboard', + templateOptions: { + label: T.GCF.KEYBOARD.GLOBAL_ADD_TASK, + }, }, - } - ] - : []) as LimitedFormlyFieldConfig[], + ] + : []) as LimitedFormlyFieldConfig[]), // APP WIDE { type: 'tpl', @@ -58,35 +58,35 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection = { templateOptions: { tag: 'h3', class: 'sub-section-heading', - text: T.GCF.KEYBOARD.APP_WIDE_SHORTCUTS + text: T.GCF.KEYBOARD.APP_WIDE_SHORTCUTS, }, }, { key: 'addNewTask', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.ADD_NEW_TASK + label: T.GCF.KEYBOARD.ADD_NEW_TASK, }, }, { key: 'addNewNote', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.ADD_NEW_NOTE + label: T.GCF.KEYBOARD.ADD_NEW_NOTE, }, }, { key: 'toggleSideNav', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TOGGLE_SIDE_NAV + label: T.GCF.KEYBOARD.TOGGLE_SIDE_NAV, }, }, { key: 'openProjectNotes', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.OPEN_PROJECT_NOTES + label: T.GCF.KEYBOARD.OPEN_PROJECT_NOTES, }, }, // { @@ -100,28 +100,28 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection = { key: 'toggleBookmarks', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TOGGLE_BOOKMARKS + label: T.GCF.KEYBOARD.TOGGLE_BOOKMARKS, }, }, { key: 'toggleBacklog', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TOGGLE_BACKLOG + label: T.GCF.KEYBOARD.TOGGLE_BACKLOG, }, }, { key: 'goToWorkView', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.GO_TO_WORK_VIEW + label: T.GCF.KEYBOARD.GO_TO_WORK_VIEW, }, }, { key: 'goToScheduledView', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.GO_TO_SCHEDULED_VIEW + label: T.GCF.KEYBOARD.GO_TO_SCHEDULED_VIEW, }, }, // { @@ -142,28 +142,28 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection = { key: 'goToSettings', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.GO_TO_SETTINGS + label: T.GCF.KEYBOARD.GO_TO_SETTINGS, }, }, { key: 'zoomIn', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.ZOOM_IN + label: T.GCF.KEYBOARD.ZOOM_IN, }, }, { key: 'zoomOut', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.ZOOM_OUT + label: T.GCF.KEYBOARD.ZOOM_OUT, }, }, { key: 'zoomDefault', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.ZOOM_DEFAULT + label: T.GCF.KEYBOARD.ZOOM_DEFAULT, }, }, // TASKS @@ -173,7 +173,7 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection = { templateOptions: { tag: 'h3', class: 'sub-section-heading', - text: T.GCF.KEYBOARD.TASK_SHORTCUTS + text: T.GCF.KEYBOARD.TASK_SHORTCUTS, }, }, { @@ -181,142 +181,142 @@ export const KEYBOARD_SETTINGS_FORM_CFG: ConfigFormSection = { className: 'tpl', templateOptions: { tag: 'p', - text: T.GCF.KEYBOARD.TASK_SHORTCUTS_INFO + text: T.GCF.KEYBOARD.TASK_SHORTCUTS_INFO, }, }, { key: 'taskEditTitle', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_EDIT_TITLE + label: T.GCF.KEYBOARD.TASK_EDIT_TITLE, }, }, { key: 'taskToggleAdditionalInfoOpen', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_TOGGLE_ADDITIONAL_INFO_OPEN + label: T.GCF.KEYBOARD.TASK_TOGGLE_ADDITIONAL_INFO_OPEN, }, }, { key: 'taskOpenEstimationDialog', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_OPEN_ESTIMATION_DIALOG + label: T.GCF.KEYBOARD.TASK_OPEN_ESTIMATION_DIALOG, }, }, { key: 'taskSchedule', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_SCHEDULE + label: T.GCF.KEYBOARD.TASK_SCHEDULE, }, }, { key: 'taskToggleDone', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_TOGGLE_DONE + label: T.GCF.KEYBOARD.TASK_TOGGLE_DONE, }, }, { key: 'taskAddSubTask', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_ADD_SUB_TASK + label: T.GCF.KEYBOARD.TASK_ADD_SUB_TASK, }, }, { key: 'taskDelete', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_DELETE + label: T.GCF.KEYBOARD.TASK_DELETE, }, }, { key: 'taskMoveToProject', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_MOVE_TO_PROJECT + label: T.GCF.KEYBOARD.TASK_MOVE_TO_PROJECT, }, }, { key: 'taskOpenContextMenu', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_OPEN_CONTEXT_MENU + label: T.GCF.KEYBOARD.TASK_OPEN_CONTEXT_MENU, }, }, { key: 'selectPreviousTask', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.SELECT_PREVIOUS_TASK + label: T.GCF.KEYBOARD.SELECT_PREVIOUS_TASK, }, }, { key: 'selectNextTask', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.SELECT_NEXT_TASK + label: T.GCF.KEYBOARD.SELECT_NEXT_TASK, }, }, { key: 'moveTaskUp', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.MOVE_TASK_UP + label: T.GCF.KEYBOARD.MOVE_TASK_UP, }, }, { key: 'moveTaskDown', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.MOVE_TASK_DOWN + label: T.GCF.KEYBOARD.MOVE_TASK_DOWN, }, }, { key: 'moveToBacklog', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.MOVE_TO_BACKLOG + label: T.GCF.KEYBOARD.MOVE_TO_BACKLOG, }, }, { key: 'moveToTodaysTasks', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.MOVE_TO_TODAYS_TASKS + label: T.GCF.KEYBOARD.MOVE_TO_TODAYS_TASKS, }, }, { key: 'expandSubTasks', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.EXPAND_SUB_TASKS + label: T.GCF.KEYBOARD.EXPAND_SUB_TASKS, }, }, { key: 'collapseSubTasks', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.COLLAPSE_SUB_TASKS + label: T.GCF.KEYBOARD.COLLAPSE_SUB_TASKS, }, }, { key: 'togglePlay', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TOGGLE_PLAY + label: T.GCF.KEYBOARD.TOGGLE_PLAY, }, }, { key: 'taskEditTags', type: 'keyboard', templateOptions: { - label: T.GCF.KEYBOARD.TASK_EDIT_TAGS + label: T.GCF.KEYBOARD.TASK_EDIT_TAGS, }, }, - ] + ], }; /* eslint-enable max-len */ diff --git a/src/app/features/config/form-cfgs/language-selection-form.const.ts b/src/app/features/config/form-cfgs/language-selection-form.const.ts index f948c1659..044ecdaa8 100644 --- a/src/app/features/config/form-cfgs/language-selection-form.const.ts +++ b/src/app/features/config/form-cfgs/language-selection-form.const.ts @@ -13,24 +13,24 @@ export const LANGUAGE_SELECTION_FORM_FORM: ConfigFormSection = { templateOptions: { label: T.GCF.LANG.LABEL, options: [ - {label: T.GCF.LANG.AR, value: LanguageCode.ar}, - {label: T.GCF.LANG.ZH, value: LanguageCode.zh}, - {label: T.GCF.LANG.ZH_TW, value: LanguageCode.zh_tw}, - {label: T.GCF.LANG.EN, value: LanguageCode.en}, - {label: T.GCF.LANG.DE, value: LanguageCode.de}, - {label: T.GCF.LANG.FA, value: LanguageCode.fa}, - {label: T.GCF.LANG.FR, value: LanguageCode.fr}, - {label: T.GCF.LANG.JA, value: LanguageCode.ja}, - {label: T.GCF.LANG.KO, value: LanguageCode.ko}, - {label: T.GCF.LANG.RU, value: LanguageCode.ru}, - {label: T.GCF.LANG.ES, value: LanguageCode.es}, - {label: T.GCF.LANG.TR, value: LanguageCode.tr}, - {label: T.GCF.LANG.IT, value: LanguageCode.it}, - {label: T.GCF.LANG.PT, value: LanguageCode.pt}, - {label: T.GCF.LANG.NL, value: LanguageCode.nl}, - {label: T.GCF.LANG.NB, value: LanguageCode.nb}, + { label: T.GCF.LANG.AR, value: LanguageCode.ar }, + { label: T.GCF.LANG.ZH, value: LanguageCode.zh }, + { label: T.GCF.LANG.ZH_TW, value: LanguageCode.zh_tw }, + { label: T.GCF.LANG.EN, value: LanguageCode.en }, + { label: T.GCF.LANG.DE, value: LanguageCode.de }, + { label: T.GCF.LANG.FA, value: LanguageCode.fa }, + { label: T.GCF.LANG.FR, value: LanguageCode.fr }, + { label: T.GCF.LANG.JA, value: LanguageCode.ja }, + { label: T.GCF.LANG.KO, value: LanguageCode.ko }, + { label: T.GCF.LANG.RU, value: LanguageCode.ru }, + { label: T.GCF.LANG.ES, value: LanguageCode.es }, + { label: T.GCF.LANG.TR, value: LanguageCode.tr }, + { label: T.GCF.LANG.IT, value: LanguageCode.it }, + { label: T.GCF.LANG.PT, value: LanguageCode.pt }, + { label: T.GCF.LANG.NL, value: LanguageCode.nl }, + { label: T.GCF.LANG.NB, value: LanguageCode.nb }, ], }, }, - ] + ], }; diff --git a/src/app/features/config/form-cfgs/misc-settings-form.const.ts b/src/app/features/config/form-cfgs/misc-settings-form.const.ts index ea9dd4007..ef6f5a24d 100644 --- a/src/app/features/config/form-cfgs/misc-settings-form.const.ts +++ b/src/app/features/config/form-cfgs/misc-settings-form.const.ts @@ -90,13 +90,13 @@ export const MISC_SETTINGS_FORM_CFG: ConfigFormSection = { templateOptions: { label: T.GCF.MISC.FIRST_DAY_OF_WEEK, options: [ - {label: T.F.TASK_REPEAT.F.SUNDAY, value: 0}, - {label: T.F.TASK_REPEAT.F.MONDAY, value: 1}, - {label: T.F.TASK_REPEAT.F.TUESDAY, value: 2}, - {label: T.F.TASK_REPEAT.F.WEDNESDAY, value: 3}, - {label: T.F.TASK_REPEAT.F.THURSDAY, value: 4}, - {label: T.F.TASK_REPEAT.F.FRIDAY, value: 5}, - {label: T.F.TASK_REPEAT.F.SATURDAY, value: 6}, + { label: T.F.TASK_REPEAT.F.SUNDAY, value: 0 }, + { label: T.F.TASK_REPEAT.F.MONDAY, value: 1 }, + { label: T.F.TASK_REPEAT.F.TUESDAY, value: 2 }, + { label: T.F.TASK_REPEAT.F.WEDNESDAY, value: 3 }, + { label: T.F.TASK_REPEAT.F.THURSDAY, value: 4 }, + { label: T.F.TASK_REPEAT.F.FRIDAY, value: 5 }, + { label: T.F.TASK_REPEAT.F.SATURDAY, value: 6 }, ], }, }, @@ -107,5 +107,5 @@ export const MISC_SETTINGS_FORM_CFG: ConfigFormSection = { label: T.GCF.MISC.TASK_NOTES_TPL, }, }, - ] + ], }; diff --git a/src/app/features/config/form-cfgs/pomodoro-form.const.ts b/src/app/features/config/form-cfgs/pomodoro-form.const.ts index 0c46d93b4..84331f382 100644 --- a/src/app/features/config/form-cfgs/pomodoro-form.const.ts +++ b/src/app/features/config/form-cfgs/pomodoro-form.const.ts @@ -11,63 +11,63 @@ export const POMODORO_FORM_CFG: ConfigFormSection = { key: 'isEnabled', type: 'checkbox', templateOptions: { - label: T.GCF.POMODORO.IS_ENABLED + label: T.GCF.POMODORO.IS_ENABLED, }, }, { key: 'isStopTrackingOnBreak', type: 'checkbox', templateOptions: { - label: T.GCF.POMODORO.IS_STOP_TRACKING_ON_BREAK + label: T.GCF.POMODORO.IS_STOP_TRACKING_ON_BREAK, }, }, { key: 'isManualContinue', type: 'checkbox', templateOptions: { - label: T.GCF.POMODORO.IS_MANUAL_CONTINUE + label: T.GCF.POMODORO.IS_MANUAL_CONTINUE, }, }, { key: 'isPlaySound', type: 'checkbox', templateOptions: { - label: T.GCF.POMODORO.IS_PLAY_SOUND + label: T.GCF.POMODORO.IS_PLAY_SOUND, }, }, { key: 'isPlaySoundAfterBreak', type: 'checkbox', templateOptions: { - label: T.GCF.POMODORO.IS_PLAY_SOUND_AFTER_BREAK + label: T.GCF.POMODORO.IS_PLAY_SOUND_AFTER_BREAK, }, }, { key: 'isPlayTick', type: 'checkbox', templateOptions: { - label: T.GCF.POMODORO.IS_PLAY_TICK + label: T.GCF.POMODORO.IS_PLAY_TICK, }, }, { key: 'duration', type: 'duration', templateOptions: { - label: T.GCF.POMODORO.DURATION + label: T.GCF.POMODORO.DURATION, }, }, { key: 'breakDuration', type: 'duration', templateOptions: { - label: T.GCF.POMODORO.BREAK_DURATION + label: T.GCF.POMODORO.BREAK_DURATION, }, }, { key: 'longerBreakDuration', type: 'duration', templateOptions: { - label: T.GCF.POMODORO.LONGER_BREAK_DURATION + label: T.GCF.POMODORO.LONGER_BREAK_DURATION, }, }, { @@ -79,6 +79,6 @@ export const POMODORO_FORM_CFG: ConfigFormSection = { min: 1, }, }, - ] + ], }; /* eslint-enable max-len */ diff --git a/src/app/features/config/form-cfgs/simple-counter-form.const.ts b/src/app/features/config/form-cfgs/simple-counter-form.const.ts index b4faa664a..4208836b0 100644 --- a/src/app/features/config/form-cfgs/simple-counter-form.const.ts +++ b/src/app/features/config/form-cfgs/simple-counter-form.const.ts @@ -1,6 +1,6 @@ /* eslint-disable max-len */ import { ConfigFormSection } from '../global-config.model'; -import { SimpleCounterConfig, SimpleCounterType } from '../../simple-counter/simple-counter.model'; +import { SimpleCounterConfig, SimpleCounterType, } from '../../simple-counter/simple-counter.model'; import { T } from '../../../t.const'; import { SIMPLE_COUNTER_TRIGGER_ACTIONS } from '../../simple-counter/simple-counter.const'; @@ -39,8 +39,14 @@ export const SIMPLE_COUNTER_FORM: ConfigFormSection = { label: T.F.SIMPLE_COUNTER.FORM.L_TYPE, required: true, options: [ - {label: T.F.SIMPLE_COUNTER.FORM.TYPE_STOPWATCH, value: SimpleCounterType.StopWatch}, - {label: T.F.SIMPLE_COUNTER.FORM.TYPE_CLICK_COUNTER, value: SimpleCounterType.ClickCounter}, + { + label: T.F.SIMPLE_COUNTER.FORM.TYPE_STOPWATCH, + value: SimpleCounterType.StopWatch, + }, + { + label: T.F.SIMPLE_COUNTER.FORM.TYPE_CLICK_COUNTER, + value: SimpleCounterType.ClickCounter, + }, ], }, }, @@ -54,9 +60,9 @@ export const SIMPLE_COUNTER_FORM: ConfigFormSection = { { type: 'icon', key: 'iconOn', - hideExpression: ((model: any) => { + hideExpression: (model: any) => { return model.type !== SimpleCounterType.StopWatch; - }), + }, templateOptions: { label: T.F.SIMPLE_COUNTER.FORM.L_ICON_ON, }, @@ -64,41 +70,50 @@ export const SIMPLE_COUNTER_FORM: ConfigFormSection = { { key: 'triggerOnActions', type: 'select', - hideExpression: ((model: any) => { + hideExpression: (model: any) => { return model.type !== SimpleCounterType.ClickCounter; - }), + }, templateOptions: { label: T.F.SIMPLE_COUNTER.FORM.L_AUTO_COUNT_UP, multiple: true, - options: SIMPLE_COUNTER_TRIGGER_ACTIONS.map(actionStr => ({label: actionStr, value: actionStr})), - } + options: SIMPLE_COUNTER_TRIGGER_ACTIONS.map((actionStr) => ({ + label: actionStr, + value: actionStr, + })), + }, }, { key: 'triggerOnActions', type: 'select', - hideExpression: ((model: any) => { + hideExpression: (model: any) => { return model.type !== SimpleCounterType.StopWatch; - }), + }, templateOptions: { label: T.F.SIMPLE_COUNTER.FORM.L_AUTO_SWITCH_ON, multiple: true, - options: SIMPLE_COUNTER_TRIGGER_ACTIONS.map(actionStr => ({label: actionStr, value: actionStr})), - } + options: SIMPLE_COUNTER_TRIGGER_ACTIONS.map((actionStr) => ({ + label: actionStr, + value: actionStr, + })), + }, }, { key: 'triggerOffActions', type: 'select', - hideExpression: ((model: any) => { + hideExpression: (model: any) => { return model.type !== SimpleCounterType.StopWatch; - }), + }, templateOptions: { label: T.F.SIMPLE_COUNTER.FORM.L_AUTO_SWITCH_OFF, multiple: true, - options: SIMPLE_COUNTER_TRIGGER_ACTIONS.map(actionStr => ({label: actionStr, value: actionStr})), - } - } + options: SIMPLE_COUNTER_TRIGGER_ACTIONS.map((actionStr) => ({ + label: actionStr, + value: actionStr, + })), + }, + }, ], }, }, - ] + ], }; diff --git a/src/app/features/config/form-cfgs/sound-form.const.ts b/src/app/features/config/form-cfgs/sound-form.const.ts index f2e150fab..7dd678940 100644 --- a/src/app/features/config/form-cfgs/sound-form.const.ts +++ b/src/app/features/config/form-cfgs/sound-form.const.ts @@ -31,18 +31,18 @@ export const SOUND_FORM_CFG: ConfigFormSection = { templateOptions: { label: T.GCF.SOUND.DONE_SOUND, options: [ - {label: 'Sound 1', value: 'done1.mp3'}, - {label: 'Sound 2', value: 'done2.mp3'}, - {label: 'Sound 3', value: 'done3.mp3'}, - {label: 'Sound 4', value: 'done4.mp3'}, - {label: 'Sound 5', value: 'done5.mp3'}, - {label: 'Sound 6', value: 'done6.mp3'}, - {label: 'Sound 7', value: 'done7.mp3'}, + { label: 'Sound 1', value: 'done1.mp3' }, + { label: 'Sound 2', value: 'done2.mp3' }, + { label: 'Sound 3', value: 'done3.mp3' }, + { label: 'Sound 4', value: 'done4.mp3' }, + { label: 'Sound 5', value: 'done5.mp3' }, + { label: 'Sound 6', value: 'done6.mp3' }, + { label: 'Sound 7', value: 'done7.mp3' }, ], - hideExpression: ((model: any) => { + hideExpression: (model: any) => { return !model.isPlayDoneSound; - }), - change: ({model}) => playDoneSound(model), + }, + change: ({ model }) => playDoneSound(model), }, }, { @@ -51,9 +51,9 @@ export const SOUND_FORM_CFG: ConfigFormSection = { templateOptions: { label: T.GCF.SOUND.IS_INCREASE_DONE_PITCH, }, - hideExpression: ((model: any) => { + hideExpression: (model: any) => { return !model.isPlayDoneSound; - }), + }, }, - ] + ], }; diff --git a/src/app/features/config/form-cfgs/sync-form.const.ts b/src/app/features/config/form-cfgs/sync-form.const.ts index 68625b4b4..f270b351f 100644 --- a/src/app/features/config/form-cfgs/sync-form.const.ts +++ b/src/app/features/config/form-cfgs/sync-form.const.ts @@ -33,17 +33,20 @@ export const SYNC_FORM: ConfigFormSection = { label: T.F.SYNC.FORM.L_SYNC_PROVIDER, required: true, options: [ - {label: SyncProvider.Dropbox, value: SyncProvider.Dropbox}, - ...(IS_F_DROID_APP ? [] : [{label: SyncProvider.GoogleDrive, value: SyncProvider.GoogleDrive}]), - {label: SyncProvider.WebDAV, value: SyncProvider.WebDAV}, + { label: SyncProvider.Dropbox, value: SyncProvider.Dropbox }, + ...(IS_F_DROID_APP + ? [] + : [{ label: SyncProvider.GoogleDrive, value: SyncProvider.GoogleDrive }]), + { label: SyncProvider.WebDAV, value: SyncProvider.WebDAV }, ], }, }, { // TODO animation maybe - hideExpression: ((m, v, field) => field?.parent?.model.syncProvider !== SyncProvider.Dropbox), + hideExpression: (m, v, field) => + field?.parent?.model.syncProvider !== SyncProvider.Dropbox, key: 'dropboxSync', - templateOptions: {label: 'Address'}, + templateOptions: { label: 'Address' }, fieldGroup: [ { type: 'tpl', @@ -69,7 +72,8 @@ export const SYNC_FORM: ConfigFormSection = { }, { type: 'tpl', - hideExpression: ((model: DropboxSyncConfig) => !!model.accessToken || !model.authCode), + hideExpression: (model: DropboxSyncConfig) => + !!model.accessToken || !model.authCode, templateOptions: { tag: 'button', class: 'mat-raised-button', @@ -79,7 +83,7 @@ export const SYNC_FORM: ConfigFormSection = { { key: 'accessToken', type: 'input', - hideExpression: ((model: DropboxSyncConfig) => !model.accessToken), + hideExpression: (model: DropboxSyncConfig) => !model.accessToken, templateOptions: { label: T.F.SYNC.FORM.DROPBOX.L_ACCESS_TOKEN, }, @@ -87,7 +91,8 @@ export const SYNC_FORM: ConfigFormSection = { ], }, { - hideExpression: ((m, v, field) => field?.parent?.model.syncProvider !== SyncProvider.GoogleDrive), + hideExpression: (m, v, field) => + field?.parent?.model.syncProvider !== SyncProvider.GoogleDrive, key: 'googleDriveSync', // templateOptions: {label: 'Address'}, fieldGroup: [ @@ -109,7 +114,8 @@ export const SYNC_FORM: ConfigFormSection = { ], }, { - hideExpression: ((m, v, field) => field?.parent?.model.syncProvider !== SyncProvider.WebDAV), + hideExpression: (m, v, field) => + field?.parent?.model.syncProvider !== SyncProvider.WebDAV, key: 'webDav', // templateOptions: {label: 'Address'}, fieldGroup: [ @@ -119,7 +125,6 @@ export const SYNC_FORM: ConfigFormSection = { tag: 'p', // text: `

Please open the following link and copy the auth code provided there

`, text: T.F.SYNC.FORM.WEB_DAV.CORS_INFO, - }, }, { @@ -128,7 +133,8 @@ export const SYNC_FORM: ConfigFormSection = { templateOptions: { required: true, label: T.F.SYNC.FORM.WEB_DAV.L_BASE_URL, - description: '* https://your-next-cloud/nextcloud/remote.php/dav/files/yourUserName' + description: + '* https://your-next-cloud/nextcloud/remote.php/dav/files/yourUserName', }, }, { @@ -154,10 +160,10 @@ export const SYNC_FORM: ConfigFormSection = { templateOptions: { required: true, label: T.F.SYNC.FORM.WEB_DAV.L_SYNC_FILE_PATH, - description: '* my-sync-file.json' + description: '* my-sync-file.json', }, }, ], }, - ] + ], }; 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 d1c6995ff..489c0dc72 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 @@ -11,21 +11,21 @@ export const TAKE_A_BREAK_FORM_CFG: ConfigFormSection = { key: 'isTakeABreakEnabled', type: 'checkbox', templateOptions: { - label: T.GCF.TAKE_A_BREAK.IS_ENABLED + label: T.GCF.TAKE_A_BREAK.IS_ENABLED, }, }, { key: 'isLockScreen', type: 'checkbox', templateOptions: { - label: T.GCF.TAKE_A_BREAK.IS_LOCK_SCREEN + label: T.GCF.TAKE_A_BREAK.IS_LOCK_SCREEN, }, }, { key: 'isFocusWindow', type: 'checkbox', templateOptions: { - label: T.GCF.TAKE_A_BREAK.IS_FOCUS_WINDOW + label: T.GCF.TAKE_A_BREAK.IS_FOCUS_WINDOW, }, }, { @@ -33,7 +33,7 @@ export const TAKE_A_BREAK_FORM_CFG: ConfigFormSection = { type: 'duration', hideExpression: '!model.isTakeABreakEnabled', templateOptions: { - label: T.GCF.TAKE_A_BREAK.MIN_WORKING_TIME + label: T.GCF.TAKE_A_BREAK.MIN_WORKING_TIME, }, }, { @@ -41,7 +41,7 @@ export const TAKE_A_BREAK_FORM_CFG: ConfigFormSection = { type: 'textarea', hideExpression: '!model.isTakeABreakEnabled', templateOptions: { - label: T.GCF.TAKE_A_BREAK.MESSAGE + label: T.GCF.TAKE_A_BREAK.MESSAGE, }, }, { @@ -49,8 +49,8 @@ export const TAKE_A_BREAK_FORM_CFG: ConfigFormSection = { type: 'input', hideExpression: '!model.isTakeABreakEnabled', templateOptions: { - label: T.GCF.TAKE_A_BREAK.MOTIVATIONAL_IMG + label: T.GCF.TAKE_A_BREAK.MOTIVATIONAL_IMG, }, }, - ] + ], }; diff --git a/src/app/features/config/form-cfgs/tracking-reminder-form.const.ts b/src/app/features/config/form-cfgs/tracking-reminder-form.const.ts index 583311b2a..bbfb818f7 100644 --- a/src/app/features/config/form-cfgs/tracking-reminder-form.const.ts +++ b/src/app/features/config/form-cfgs/tracking-reminder-form.const.ts @@ -25,8 +25,8 @@ export const TRACKING_REMINDER_FORM_CFG: ConfigFormSection IS_ELECTRON || !cfg.isElectronOnly); export const GLOBAL_SYNC_FORM_CONFIG: ConfigFormConfig = [ - (SYNC_FORM as GenericConfigFormSection), + SYNC_FORM as GenericConfigFormSection, ...(IS_ANDROID_WEB_VIEW ? [] : [IMEX_FORM as GenericConfigFormSection]), - (AUTOMATIC_BACKUPS_FORM as GenericConfigFormSection), + AUTOMATIC_BACKUPS_FORM as GenericConfigFormSection, ].filter((cfg) => IS_ELECTRON || !cfg.isElectronOnly); export const GLOBAL_PRODUCTIVITY_FORM_CONFIG: ConfigFormConfig = [ - (TAKE_A_BREAK_FORM_CFG as GenericConfigFormSection), - (POMODORO_FORM_CFG as GenericConfigFormSection), - (EVALUATION_SETTINGS_FORM_CFG as GenericConfigFormSection), - (SIMPLE_COUNTER_FORM as GenericConfigFormSection), + TAKE_A_BREAK_FORM_CFG as GenericConfigFormSection, + POMODORO_FORM_CFG as GenericConfigFormSection, + EVALUATION_SETTINGS_FORM_CFG as GenericConfigFormSection, + SIMPLE_COUNTER_FORM as GenericConfigFormSection, ].filter((cfg) => IS_ELECTRON || !cfg.isElectronOnly); diff --git a/src/app/features/config/global-config.model.ts b/src/app/features/config/global-config.model.ts index 245d97da8..e3f626717 100644 --- a/src/app/features/config/global-config.model.ts +++ b/src/app/features/config/global-config.model.ts @@ -128,23 +128,20 @@ export type GlobalConfigState = Readonly<{ export type GlobalConfigSectionKey = keyof GlobalConfigState | 'EMPTY'; -export type GlobalSectionConfig - = MiscConfig +export type GlobalSectionConfig = + | MiscConfig | PomodoroConfig | DropboxSyncConfig | KeyboardConfig - | SyncConfig - ; + | SyncConfig; type Omit = Pick>; -export interface LimitedFormlyFieldConfig extends Omit { +export interface LimitedFormlyFieldConfig + extends Omit { key?: keyof FormModel; } -export type CustomCfgSection = - 'FILE_IMPORT_EXPORT' - | 'JIRA_CFG' - | 'SIMPLE_COUNTER_CFG'; +export type CustomCfgSection = 'FILE_IMPORT_EXPORT' | 'JIRA_CFG' | 'SIMPLE_COUNTER_CFG'; // Intermediate model export interface ConfigFormSection { @@ -157,10 +154,9 @@ export interface ConfigFormSection { isElectronOnly?: boolean; } -export interface GenericConfigFormSection extends Omit, 'items'> { +export interface GenericConfigFormSection + extends Omit, 'items'> { items?: FormlyFieldConfig[]; } export type ConfigFormConfig = Readonly; - - diff --git a/src/app/features/config/global-config.service.ts b/src/app/features/config/global-config.service.ts index 5f1b1099f..b61af9125 100644 --- a/src/app/features/config/global-config.service.ts +++ b/src/app/features/config/global-config.service.ts @@ -10,7 +10,7 @@ import { IdleConfig, MiscConfig, SoundConfig, - TakeABreakConfig + TakeABreakConfig, } from './global-config.model'; import { selectConfigFeatureState, @@ -18,7 +18,7 @@ import { selectIdleConfig, selectMiscConfig, selectSoundConfig, - selectTakeABreakConfig + selectTakeABreakConfig, } from './store/global-config.reducer'; import { distinctUntilChanged, shareReplay } from 'rxjs/operators'; import { distinctUntilChangedObject } from '../../util/distinct-until-changed-object'; @@ -59,14 +59,15 @@ export class GlobalConfigService { cfg?: GlobalConfigState; - constructor( - private readonly _store: Store, - ) { + constructor(private readonly _store: Store) { // this.cfg$.subscribe((val) => console.log(val)); - this.cfg$.subscribe((cfg) => this.cfg = cfg); + this.cfg$.subscribe((cfg) => (this.cfg = cfg)); } - updateSection(sectionKey: GlobalConfigSectionKey, sectionCfg: Partial) { + updateSection( + sectionKey: GlobalConfigSectionKey, + sectionCfg: Partial, + ) { this._store.dispatch({ type: GlobalConfigActionTypes.UpdateGlobalConfigSection, payload: { diff --git a/src/app/features/config/icon-input/icon-input.component.ts b/src/app/features/config/icon-input/icon-input.component.ts index 4ab78945c..bf2915a15 100644 --- a/src/app/features/config/icon-input/icon-input.component.ts +++ b/src/app/features/config/icon-input/icon-input.component.ts @@ -8,7 +8,7 @@ import { filter, map, startWith } from 'rxjs/operators'; selector: 'icon-input', templateUrl: './icon-input.component.html', styleUrls: ['./icon-input.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class IconInputComponent extends FieldType implements OnInit { // @ViewChild(MatInput) formFieldControl: MatInput; @@ -25,11 +25,11 @@ export class IconInputComponent extends FieldType implements OnInit { this.filteredIcons$ = this.formControl.valueChanges.pipe( startWith(''), - filter(searchTerm => !!searchTerm), + filter((searchTerm) => !!searchTerm), map((searchTerm) => { // Note: the outer array signifies the observable stream the other is the value - return this.customIcons.filter((icoStr) => - icoStr && icoStr.toLowerCase().includes(searchTerm.toLowerCase()) + return this.customIcons.filter( + (icoStr) => icoStr && icoStr.toLowerCase().includes(searchTerm.toLowerCase()), ); }), ); diff --git a/src/app/features/config/keyboard-input/keyboard-input.component.ts b/src/app/features/config/keyboard-input/keyboard-input.component.ts index b04ab92d1..b685ce57c 100644 --- a/src/app/features/config/keyboard-input/keyboard-input.component.ts +++ b/src/app/features/config/keyboard-input/keyboard-input.component.ts @@ -5,7 +5,7 @@ import { FieldType } from '@ngx-formly/material'; selector: 'keyboard-input', templateUrl: './keyboard-input.component.html', styleUrls: ['./keyboard-input.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class KeyboardInputComponent extends FieldType { // @ViewChild(MatInput, {static: true}) formFieldControl: MatInput; diff --git a/src/app/features/config/migrate-global-config.util.ts b/src/app/features/config/migrate-global-config.util.ts index be4be29e2..a78d05695 100644 --- a/src/app/features/config/migrate-global-config.util.ts +++ b/src/app/features/config/migrate-global-config.util.ts @@ -1,4 +1,9 @@ -import { GlobalConfigState, IdleConfig, SyncConfig, TakeABreakConfig } from './global-config.model'; +import { + GlobalConfigState, + IdleConfig, + SyncConfig, + TakeABreakConfig, +} from './global-config.model'; import { DEFAULT_GLOBAL_CONFIG } from './default-global-config.const'; import { MODEL_VERSION_KEY } from '../../app.constants'; import { isMigrateModel } from '../../util/model-version'; @@ -6,7 +11,9 @@ import { SyncProvider } from '../../imex/sync/sync-provider.model'; const MODEL_VERSION = 2.2001; -export const migrateGlobalConfigState = (globalConfigState: GlobalConfigState): GlobalConfigState => { +export const migrateGlobalConfigState = ( + globalConfigState: GlobalConfigState, +): GlobalConfigState => { if (!isMigrateModel(globalConfigState, MODEL_VERSION, 'GlobalConfig')) { return globalConfigState; } @@ -32,34 +39,38 @@ export const migrateGlobalConfigState = (globalConfigState: GlobalConfigState): }; const _migrateMiscToSeparateKeys = (config: GlobalConfigState): GlobalConfigState => { - const idle: IdleConfig = !!(config.idle) + const idle: IdleConfig = !!config.idle ? config.idle : { - ...DEFAULT_GLOBAL_CONFIG.idle, - // eslint-disable-next-line - isOnlyOpenIdleWhenCurrentTask: (config.misc as any)['isOnlyOpenIdleWhenCurrentTask'], - // eslint-disable-next-line - isEnableIdleTimeTracking: (config.misc as any)['isEnableIdleTimeTracking'], - // eslint-disable-next-line - minIdleTime: (config.misc as any)['minIdleTime'], - // eslint-disable-next-line - isUnTrackedIdleResetsBreakTimer: (config.misc as any)['isUnTrackedIdleResetsBreakTimer'], - }; + ...DEFAULT_GLOBAL_CONFIG.idle, + // eslint-disable-next-line + isOnlyOpenIdleWhenCurrentTask: (config.misc as any)[ + 'isOnlyOpenIdleWhenCurrentTask' + ], + // eslint-disable-next-line + isEnableIdleTimeTracking: (config.misc as any)['isEnableIdleTimeTracking'], + // eslint-disable-next-line + minIdleTime: (config.misc as any)['minIdleTime'], + // eslint-disable-next-line + isUnTrackedIdleResetsBreakTimer: (config.misc as any)[ + 'isUnTrackedIdleResetsBreakTimer' + ], + }; - const takeABreak: TakeABreakConfig = !!(config.takeABreak) + const takeABreak: TakeABreakConfig = !!config.takeABreak ? config.takeABreak : { - ...DEFAULT_GLOBAL_CONFIG.takeABreak, - // eslint-disable-next-line - isTakeABreakEnabled: (config.misc as any)['isTakeABreakEnabled'], - // eslint-disable-next-line - takeABreakMessage: (config.misc as any)['takeABreakMessage'], - // eslint-disable-next-line - takeABreakMinWorkingTime: (config.misc as any)['takeABreakMinWorkingTime'], - }; + ...DEFAULT_GLOBAL_CONFIG.takeABreak, + // eslint-disable-next-line + isTakeABreakEnabled: (config.misc as any)['isTakeABreakEnabled'], + // eslint-disable-next-line + takeABreakMessage: (config.misc as any)['takeABreakMessage'], + // eslint-disable-next-line + takeABreakMinWorkingTime: (config.misc as any)['takeABreakMinWorkingTime'], + }; // we delete the old keys. worst case is, that the default settings are used for outdated versions of the app - const obsoleteMiscKeys: ((keyof TakeABreakConfig) | (keyof IdleConfig))[] = [ + const obsoleteMiscKeys: (keyof TakeABreakConfig | keyof IdleConfig)[] = [ 'isTakeABreakEnabled', 'takeABreakMessage', 'takeABreakMinWorkingTime', @@ -70,7 +81,7 @@ const _migrateMiscToSeparateKeys = (config: GlobalConfigState): GlobalConfigStat 'isUnTrackedIdleResetsBreakTimer', ]; - obsoleteMiscKeys.forEach(key => { + obsoleteMiscKeys.forEach((key) => { if ((config as any)[key]) { delete (config as any)[key]; } @@ -90,7 +101,9 @@ const _extendConfigDefaults = (config: GlobalConfigState): GlobalConfigState => }; }; -const _migrateUndefinedShortcutsToNull = (config: GlobalConfigState): GlobalConfigState => { +const _migrateUndefinedShortcutsToNull = ( + config: GlobalConfigState, +): GlobalConfigState => { const keyboardCopy: any = { // also add new keys ...DEFAULT_GLOBAL_CONFIG.keyboard, @@ -127,26 +140,26 @@ const _migrateSyncCfg = (config: GlobalConfigState): GlobalConfigState => { return { ...config, sync: !!prevProvider - ? { - isEnabled: true, - syncInterval, - syncProvider: prevProvider, - dropboxSync: { - ...DEFAULT_GLOBAL_CONFIG.sync.dropboxSync, - accessToken: (config as any).dropboxSync?.accessToken, - authCode: (config as any).dropboxSync?.authCode, - }, - googleDriveSync: { - ...DEFAULT_GLOBAL_CONFIG.sync.googleDriveSync, - ...(config as any)?.googleDriveSync, - }, - webDav: { - password: null, - syncFilePath: null, - userName: null, - baseUrl: null, - } - } as SyncConfig + ? ({ + isEnabled: true, + syncInterval, + syncProvider: prevProvider, + dropboxSync: { + ...DEFAULT_GLOBAL_CONFIG.sync.dropboxSync, + accessToken: (config as any).dropboxSync?.accessToken, + authCode: (config as any).dropboxSync?.authCode, + }, + googleDriveSync: { + ...DEFAULT_GLOBAL_CONFIG.sync.googleDriveSync, + ...(config as any)?.googleDriveSync, + }, + webDav: { + password: null, + syncFilePath: null, + userName: null, + baseUrl: null, + }, + } as SyncConfig) : DEFAULT_GLOBAL_CONFIG.sync, }; }; @@ -158,7 +171,7 @@ const _fixDefaultProjectId = (config: GlobalConfigState): GlobalConfigState => { misc: { ...config.misc, defaultProjectId: null, - } + }, }; } @@ -166,5 +179,3 @@ const _fixDefaultProjectId = (config: GlobalConfigState): GlobalConfigState => { ...config, }; }; - - diff --git a/src/app/features/config/repeat-section-type/repeat-section-type.component.ts b/src/app/features/config/repeat-section-type/repeat-section-type.component.ts index 742334540..333f7b82e 100644 --- a/src/app/features/config/repeat-section-type/repeat-section-type.component.ts +++ b/src/app/features/config/repeat-section-type/repeat-section-type.component.ts @@ -33,8 +33,6 @@ export class RepeatSectionTypeComponent extends FieldArrayType { } trackByFn(i: number, item: any) { - return item - ? item.id - : i; + return item ? item.id : i; } } diff --git a/src/app/features/config/select-project/select-project.component.ts b/src/app/features/config/select-project/select-project.component.ts index e513667ba..2d50febdb 100644 --- a/src/app/features/config/select-project/select-project.component.ts +++ b/src/app/features/config/select-project/select-project.component.ts @@ -8,16 +8,14 @@ import { T } from 'src/app/t.const'; selector: 'select-project', templateUrl: './select-project.component.html', styleUrls: ['./select-project.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SelectProjectComponent extends FieldType { // @ViewChild(MatInput) formFieldControl: MatInput; T: typeof T = T; - constructor( - public projectService: ProjectService, - ) { + constructor(public projectService: ProjectService) { super(); } diff --git a/src/app/features/config/store/global-config.actions.ts b/src/app/features/config/store/global-config.actions.ts index c9caed7d8..e80fdbe2d 100644 --- a/src/app/features/config/store/global-config.actions.ts +++ b/src/app/features/config/store/global-config.actions.ts @@ -8,11 +8,12 @@ export enum GlobalConfigActionTypes { export class UpdateGlobalConfigSection implements Action { readonly type: string = GlobalConfigActionTypes.UpdateGlobalConfigSection; - constructor(public payload: { - sectionKey: GlobalConfigSectionKey; - sectionCfg: Partial; - }) { - } + constructor( + public payload: { + sectionKey: GlobalConfigSectionKey; + sectionCfg: Partial; + }, + ) {} } export type GlobalConfigActions = UpdateGlobalConfigSection; diff --git a/src/app/features/config/store/global-config.effects.ts b/src/app/features/config/store/global-config.effects.ts index 56a50a4ca..25be13d03 100644 --- a/src/app/features/config/store/global-config.effects.ts +++ b/src/app/features/config/store/global-config.effects.ts @@ -1,7 +1,10 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { filter, tap, withLatestFrom } from 'rxjs/operators'; -import { GlobalConfigActionTypes, UpdateGlobalConfigSection } from './global-config.actions'; +import { + GlobalConfigActionTypes, + UpdateGlobalConfigSection, +} from './global-config.actions'; import { Store } from '@ngrx/store'; import { CONFIG_FEATURE_NAME } from './global-config.reducer'; import { PersistenceService } from '../../../core/persistence/persistence.service'; @@ -18,84 +21,83 @@ import { KeyboardConfig } from '../keyboard-config.model'; @Injectable() export class GlobalConfigEffects { - @Effect({dispatch: false}) updateConfig$: any = this._actions$ - .pipe( - ofType( - GlobalConfigActionTypes.UpdateGlobalConfigSection, - ), - withLatestFrom(this._store), - tap(this._saveToLs.bind(this)) - ); + @Effect({ dispatch: false }) updateConfig$: any = this._actions$.pipe( + ofType(GlobalConfigActionTypes.UpdateGlobalConfigSection), + withLatestFrom(this._store), + tap(this._saveToLs.bind(this)), + ); - @Effect({dispatch: false}) snackUpdate$: any = this._actions$ - .pipe( - ofType( - GlobalConfigActionTypes.UpdateGlobalConfigSection, - ), - tap((action: UpdateGlobalConfigSection) => { - const {sectionKey, sectionCfg} = action.payload; - const isPublicSection = sectionKey.charAt(0) !== '_'; - const isPublicPropUpdated = Object.keys(sectionCfg).find((key) => key.charAt(0) !== '_'); - if (isPublicPropUpdated && isPublicSection) { - this._snackService.open({ - type: 'SUCCESS', - msg: T.F.CONFIG.S.UPDATE_SECTION, - translateParams: {sectionKey} - }); - } - }) - ); + @Effect({ dispatch: false }) snackUpdate$: any = this._actions$.pipe( + ofType(GlobalConfigActionTypes.UpdateGlobalConfigSection), + tap((action: UpdateGlobalConfigSection) => { + const { sectionKey, sectionCfg } = action.payload; + const isPublicSection = sectionKey.charAt(0) !== '_'; + const isPublicPropUpdated = Object.keys(sectionCfg).find( + (key) => key.charAt(0) !== '_', + ); + if (isPublicPropUpdated && isPublicSection) { + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.CONFIG.S.UPDATE_SECTION, + translateParams: { sectionKey }, + }); + } + }), + ); - @Effect({dispatch: false}) updateGlobalShortcut$: any = this._actions$ - .pipe( - ofType( - GlobalConfigActionTypes.UpdateGlobalConfigSection, - ), - filter((action: UpdateGlobalConfigSection) => IS_ELECTRON && action.payload.sectionKey === 'keyboard'), - tap((action: UpdateGlobalConfigSection) => { - const keyboardCfg: KeyboardConfig = action.payload.sectionCfg as KeyboardConfig; - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, keyboardCfg); - }), - ); + @Effect({ dispatch: false }) updateGlobalShortcut$: any = this._actions$.pipe( + ofType(GlobalConfigActionTypes.UpdateGlobalConfigSection), + filter( + (action: UpdateGlobalConfigSection) => + IS_ELECTRON && action.payload.sectionKey === 'keyboard', + ), + tap((action: UpdateGlobalConfigSection) => { + const keyboardCfg: KeyboardConfig = action.payload.sectionCfg as KeyboardConfig; + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, + keyboardCfg, + ); + }), + ); - @Effect({dispatch: false}) registerGlobalShortcutInitially$: any = this._actions$ - .pipe( - ofType( - loadAllData, - ), - filter(() => IS_ELECTRON), - tap((action) => { - const appDataComplete = action.appDataComplete; - const keyboardCfg: KeyboardConfig = (appDataComplete.globalConfig || DEFAULT_GLOBAL_CONFIG).keyboard; - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, keyboardCfg); - }), - ); + @Effect({ dispatch: false }) + registerGlobalShortcutInitially$: any = this._actions$.pipe( + ofType(loadAllData), + filter(() => IS_ELECTRON), + tap((action) => { + const appDataComplete = action.appDataComplete; + const keyboardCfg: KeyboardConfig = ( + appDataComplete.globalConfig || DEFAULT_GLOBAL_CONFIG + ).keyboard; + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, + keyboardCfg, + ); + }), + ); - @Effect({dispatch: false}) selectLanguageOnChange: any = this._actions$ - .pipe( - ofType( - GlobalConfigActionTypes.UpdateGlobalConfigSection, - ), - filter((action: UpdateGlobalConfigSection) => action.payload.sectionKey === 'lang'), + @Effect({ dispatch: false }) selectLanguageOnChange: any = this._actions$.pipe( + ofType(GlobalConfigActionTypes.UpdateGlobalConfigSection), + filter((action: UpdateGlobalConfigSection) => action.payload.sectionKey === 'lang'), + // eslint-disable-next-line + filter( + (action: UpdateGlobalConfigSection) => + action.payload.sectionCfg && (action.payload.sectionCfg as any)['lng'], + ), + tap((action: UpdateGlobalConfigSection) => { // eslint-disable-next-line - filter((action: UpdateGlobalConfigSection) => action.payload.sectionCfg && (action.payload.sectionCfg as any)['lng']), - tap((action: UpdateGlobalConfigSection) => { - // eslint-disable-next-line - this._languageService.setLng((action.payload.sectionCfg as any)['lng']); - }) - ); + this._languageService.setLng((action.payload.sectionCfg as any)['lng']); + }), + ); - @Effect({dispatch: false}) selectLanguageOnLoad: any = this._actions$ - .pipe( - ofType( - loadAllData, - ), - tap((action) => { - const cfg = action.appDataComplete.globalConfig || DEFAULT_GLOBAL_CONFIG; - const lng = cfg && cfg.lang && cfg.lang.lng; - this._languageService.setLng(lng as LanguageCode); - }) - ); + @Effect({ dispatch: false }) selectLanguageOnLoad: any = this._actions$.pipe( + ofType(loadAllData), + tap((action) => { + const cfg = action.appDataComplete.globalConfig || DEFAULT_GLOBAL_CONFIG; + const lng = cfg && cfg.lang && cfg.lang.lng; + this._languageService.setLng(lng as LanguageCode); + }), + ); constructor( private _actions$: Actions, @@ -103,12 +105,13 @@ export class GlobalConfigEffects { private _electronService: ElectronService, private _languageService: LanguageService, private _snackService: SnackService, - private _store: Store - ) { - } + private _store: Store, + ) {} private async _saveToLs([action, completeState]: [any, any]) { const globalConfig = completeState[CONFIG_FEATURE_NAME]; - await this._persistenceService.globalConfig.saveState(globalConfig, {isSyncModelChange: true}); + await this._persistenceService.globalConfig.saveState(globalConfig, { + isSyncModelChange: true, + }); } } diff --git a/src/app/features/config/store/global-config.reducer.ts b/src/app/features/config/store/global-config.reducer.ts index d38d1fbb0..fa53dd84f 100644 --- a/src/app/features/config/store/global-config.reducer.ts +++ b/src/app/features/config/store/global-config.reducer.ts @@ -6,7 +6,7 @@ import { IdleConfig, MiscConfig, SoundConfig, - TakeABreakConfig + TakeABreakConfig, } from '../global-config.model'; import { DEFAULT_GLOBAL_CONFIG } from '../default-global-config.const'; import { loadAllData } from '../../../root-store/meta/load-all-data.action'; @@ -14,39 +14,58 @@ import { AppDataComplete } from '../../../imex/sync/sync.model'; import { migrateGlobalConfigState } from '../migrate-global-config.util'; export const CONFIG_FEATURE_NAME = 'globalConfig'; -export const selectConfigFeatureState = createFeatureSelector(CONFIG_FEATURE_NAME); -export const selectMiscConfig = createSelector(selectConfigFeatureState, (cfg): MiscConfig => cfg.misc); -export const selectSoundConfig = createSelector(selectConfigFeatureState, (cfg): SoundConfig => cfg.sound); -export const selectEvaluationConfig = createSelector(selectConfigFeatureState, (cfg): EvaluationConfig => cfg.evaluation); -export const selectIdleConfig = createSelector(selectConfigFeatureState, (cfg): IdleConfig => cfg.idle); -export const selectTakeABreakConfig = createSelector(selectConfigFeatureState, (cfg): TakeABreakConfig => cfg.takeABreak); +export const selectConfigFeatureState = createFeatureSelector( + CONFIG_FEATURE_NAME, +); +export const selectMiscConfig = createSelector( + selectConfigFeatureState, + (cfg): MiscConfig => cfg.misc, +); +export const selectSoundConfig = createSelector( + selectConfigFeatureState, + (cfg): SoundConfig => cfg.sound, +); +export const selectEvaluationConfig = createSelector( + selectConfigFeatureState, + (cfg): EvaluationConfig => cfg.evaluation, +); +export const selectIdleConfig = createSelector( + selectConfigFeatureState, + (cfg): IdleConfig => cfg.idle, +); +export const selectTakeABreakConfig = createSelector( + selectConfigFeatureState, + (cfg): TakeABreakConfig => cfg.takeABreak, +); export const initialState: GlobalConfigState = DEFAULT_GLOBAL_CONFIG; export function globalConfigReducer( state: GlobalConfigState = initialState, - action: GlobalConfigActions + action: GlobalConfigActions, ): GlobalConfigState { // console.log(action, state); // TODO fix this hackyness once we use the new syntax everywhere if ((action.type as string) === loadAllData.type) { - const {appDataComplete}: { appDataComplete: AppDataComplete; isOmitTokens: boolean } = action as any; + const { + appDataComplete, + }: { appDataComplete: AppDataComplete; isOmitTokens: boolean } = action as any; return appDataComplete.globalConfig - ? migrateGlobalConfigState({...appDataComplete.globalConfig}) + ? migrateGlobalConfigState({ ...appDataComplete.globalConfig }) : state; } switch (action.type) { case GlobalConfigActionTypes.UpdateGlobalConfigSection: - const {sectionKey, sectionCfg} = action.payload; + const { sectionKey, sectionCfg } = action.payload; return { ...state, [sectionKey]: { // @ts-ignore ...state[sectionKey], - ...sectionCfg - } + ...sectionCfg, + }, }; default: diff --git a/src/app/features/initial-dialog/dialog-initial/dialog-initial.component.ts b/src/app/features/initial-dialog/dialog-initial/dialog-initial.component.ts index 22c7f51f6..0e7eada6e 100644 --- a/src/app/features/initial-dialog/dialog-initial/dialog-initial.component.ts +++ b/src/app/features/initial-dialog/dialog-initial/dialog-initial.component.ts @@ -8,7 +8,7 @@ import { version } from '../../../../../package.json'; selector: 'dialog-initial', templateUrl: './dialog-initial.component.html', styleUrls: ['./dialog-initial.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogInitialComponent { T: typeof T = T; @@ -17,8 +17,7 @@ export class DialogInitialComponent { constructor( private _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: InitialDialogResponse, - ) { - } + ) {} close() { this._matDialogRef.close(); diff --git a/src/app/features/initial-dialog/initial-dialog.model.ts b/src/app/features/initial-dialog/initial-dialog.model.ts index 44dbfb6e7..1b064e6a1 100644 --- a/src/app/features/initial-dialog/initial-dialog.model.ts +++ b/src/app/features/initial-dialog/initial-dialog.model.ts @@ -7,11 +7,14 @@ export interface InitialDialogResponse { btnTxt?: string; } -export const instanceOfInitialDialogResponse = (object: any): object is InitialDialogResponse => { - return typeof object === 'object' - && object !== null - && typeof object.dialogNr === 'number' - && typeof object.content === 'string' - && typeof object.showStartingWithVersion === 'string' - ; +export const instanceOfInitialDialogResponse = ( + object: any, +): object is InitialDialogResponse => { + return ( + typeof object === 'object' && + object !== null && + typeof object.dialogNr === 'number' && + typeof object.content === 'string' && + typeof object.showStartingWithVersion === 'string' + ); }; diff --git a/src/app/features/initial-dialog/initial-dialog.module.ts b/src/app/features/initial-dialog/initial-dialog.module.ts index bdf512fc2..1ba6452a9 100644 --- a/src/app/features/initial-dialog/initial-dialog.module.ts +++ b/src/app/features/initial-dialog/initial-dialog.module.ts @@ -5,10 +5,6 @@ import { UiModule } from '../../ui/ui.module'; @NgModule({ declarations: [DialogInitialComponent], - imports: [ - CommonModule, - UiModule, - ] + imports: [CommonModule, UiModule], }) -export class InitialDialogModule { -} +export class InitialDialogModule {} diff --git a/src/app/features/initial-dialog/initial-dialog.service.ts b/src/app/features/initial-dialog/initial-dialog.service.ts index 566822a03..696e3dfe4 100644 --- a/src/app/features/initial-dialog/initial-dialog.service.ts +++ b/src/app/features/initial-dialog/initial-dialog.service.ts @@ -3,7 +3,10 @@ import { LS_INITIAL_DIALOG_NR } from '../../core/persistence/ls-keys.const'; import { HttpClient } from '@angular/common/http'; import { MatDialog } from '@angular/material/dialog'; import { catchError, filter, switchMap, tap, timeout } from 'rxjs/operators'; -import { InitialDialogResponse, instanceOfInitialDialogResponse } from './initial-dialog.model'; +import { + InitialDialogResponse, + instanceOfInitialDialogResponse, +} from './initial-dialog.model'; import { Observable, of } from 'rxjs'; import { DialogInitialComponent } from './dialog-initial/dialog-initial.component'; import { DataInitService } from '../../core/data-init/data-init.service'; @@ -11,18 +14,17 @@ import { version } from '../../../../package.json'; import { lt } from 'semver'; import { GlobalConfigService } from '../config/global-config.service'; -const URL = 'https://app.super-productivity.com/news.json?ngsw-bypass=true&no-cache=' + Date.now(); +const URL = + 'https://app.super-productivity.com/news.json?ngsw-bypass=true&no-cache=' + Date.now(); -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class InitialDialogService { - constructor( private _http: HttpClient, private _matDialog: MatDialog, private _dataInitService: DataInitService, private _globalConfigService: GlobalConfigService, - ) { - } + ) {} showDialogIfNecessary$(): Observable { // if (!environment.production) { @@ -31,7 +33,7 @@ export class InitialDialogService { return this._dataInitService.isAllDataLoadedInitially$.pipe( switchMap(() => this._globalConfigService.misc$), - filter(miscCfg => !miscCfg.isDisableInitialDialog), + filter((miscCfg) => !miscCfg.isDisableInitialDialog), switchMap(() => this._http.get(URL).pipe(timeout(4000))), switchMap((res: InitialDialogResponse | unknown) => { const lastLocalDialogNr = this._loadDialogNr(); @@ -49,7 +51,10 @@ export class InitialDialogService { return of(null); } else if (res.dialogNr <= lastLocalDialogNr) { return of(null); - } else if (res.showStartingWithVersion && lt(version, res.showStartingWithVersion)) { + } else if ( + res.showStartingWithVersion && + lt(version, res.showStartingWithVersion) + ) { return of(null); } else { return this._openDialog$(res).pipe( @@ -69,9 +74,11 @@ export class InitialDialogService { } private _openDialog$(res: InitialDialogResponse): Observable { - return this._matDialog.open(DialogInitialComponent, { - data: res, - }).afterClosed(); + return this._matDialog + .open(DialogInitialComponent, { + data: res, + }) + .afterClosed(); } private _loadDialogNr(): number { diff --git a/src/app/features/issue/cache/get-cache-id.ts b/src/app/features/issue/cache/get-cache-id.ts index 1277ff126..bb1e49fd4 100644 --- a/src/app/features/issue/cache/get-cache-id.ts +++ b/src/app/features/issue/cache/get-cache-id.ts @@ -1,4 +1,3 @@ export const getCacheId = (r: RequestInit, url: string): string => { - return `${url}_${r.method}_${r.body ? r.body : ''}`; }; diff --git a/src/app/features/issue/cache/issue-cache.service.ts b/src/app/features/issue/cache/issue-cache.service.ts index 7b88b8cf6..4cd821e4b 100644 --- a/src/app/features/issue/cache/issue-cache.service.ts +++ b/src/app/features/issue/cache/issue-cache.service.ts @@ -1,26 +1,31 @@ import { Injectable } from '@angular/core'; -import { loadFromRealLs, removeFromRealLs, saveToRealLs } from '../../../core/persistence/local-storage'; +import { + loadFromRealLs, + removeFromRealLs, + saveToRealLs, +} from '../../../core/persistence/local-storage'; import * as moment from 'moment'; import { Duration } from 'moment'; class CacheContent { - constructor( - public expire: Date, - public content: T - ) { - } + constructor(public expire: Date, public content: T) {} } @Injectable({ providedIn: 'root', }) export class IssueCacheService { - async projectCache(pId: string, type: string, expire: Duration, fetch: () => Promise): Promise { + async projectCache( + pId: string, + type: string, + expire: Duration, + fetch: () => Promise, + ): Promise { const key = `SUP_p_${type}_${pId}`; let cachedContent: CacheContent = loadFromRealLs(key) as CacheContent; if (!cachedContent || moment(cachedContent.expire).isBefore(moment())) { cachedContent = new CacheContent(moment().add(expire).toDate(), await fetch()); - saveToRealLs(key, {...cachedContent}); + saveToRealLs(key, { ...cachedContent }); } const realContent: T = cachedContent.content; return realContent; diff --git a/src/app/features/issue/issue-content/issue-content.component.ts b/src/app/features/issue/issue-content/issue-content.component.ts index a85e47493..44dfcdc9f 100644 --- a/src/app/features/issue/issue-content/issue-content.component.ts +++ b/src/app/features/issue/issue-content/issue-content.component.ts @@ -7,7 +7,7 @@ import { IssueData } from '../issue.model'; selector: 'issue-content', templateUrl: './issue-content.component.html', styleUrls: ['./issue-content.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class IssueContentComponent { @Input() task?: TaskWithSubTasks; @@ -17,6 +17,5 @@ export class IssueContentComponent { readonly JIRA_TYPE: string = JIRA_TYPE; readonly CALDAV_TYPE: string = CALDAV_TYPE; - constructor() { - } + constructor() {} } diff --git a/src/app/features/issue/issue-effect-helper.service.ts b/src/app/features/issue/issue-effect-helper.service.ts index 8223a34fd..7ce20aa67 100644 --- a/src/app/features/issue/issue-effect-helper.service.ts +++ b/src/app/features/issue/issue-effect-helper.service.ts @@ -12,31 +12,26 @@ import { SyncService } from '../../imex/sync/sync.service'; }) export class IssueEffectHelperService { pollIssueTaskUpdatesActions$: Observable = this._actions$.pipe( - ofType( - setActiveWorkContext, - ProjectActionTypes.UpdateProjectIssueProviderCfg, - ) + ofType(setActiveWorkContext, ProjectActionTypes.UpdateProjectIssueProviderCfg), ); pollToBacklogActions$: Observable = this._actions$.pipe( - ofType( - setActiveWorkContext, - ProjectActionTypes.UpdateProjectIssueProviderCfg, - ) + ofType(setActiveWorkContext, ProjectActionTypes.UpdateProjectIssueProviderCfg), ); pollToBacklogTriggerToProjectId$: Observable = this._syncService.afterInitialSyncDoneAndDataLoadedInitially$.pipe( concatMap(() => this.pollToBacklogActions$), switchMap(() => this._workContextService.isActiveWorkContextProject$.pipe(first())), // NOTE: it's important that the filter is on top level otherwise the subscription is not canceled - filter(isProject => isProject), - switchMap(() => this._workContextService.activeWorkContextId$.pipe(first()) as Observable), - filter(projectId => !!projectId), + filter((isProject) => isProject), + switchMap( + () => + this._workContextService.activeWorkContextId$.pipe(first()) as Observable, + ), + filter((projectId) => !!projectId), ); constructor( - private _actions$: Actions, + private _actions$: Actions, private _workContextService: WorkContextService, private _syncService: SyncService, - ) { - } + ) {} } - diff --git a/src/app/features/issue/issue-header/issue-header.component.ts b/src/app/features/issue/issue-header/issue-header.component.ts index e6275ae2e..f55124aa0 100644 --- a/src/app/features/issue/issue-header/issue-header.component.ts +++ b/src/app/features/issue/issue-header/issue-header.component.ts @@ -6,7 +6,7 @@ import { CALDAV_TYPE, GITHUB_TYPE, GITLAB_TYPE, JIRA_TYPE } from '../issue.const selector: 'issue-header', templateUrl: './issue-header.component.html', styleUrls: ['./issue-header.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class IssueHeaderComponent { @Input() task?: TaskWithSubTasks; @@ -16,6 +16,5 @@ export class IssueHeaderComponent { readonly JIRA_TYPE: string = JIRA_TYPE; readonly CALDAV_TYPE: string = CALDAV_TYPE; - constructor() { - } + constructor() {} } diff --git a/src/app/features/issue/issue-icon/issue-icon.pipe.ts b/src/app/features/issue/issue-icon/issue-icon.pipe.ts index 57d40185e..c729dff51 100644 --- a/src/app/features/issue/issue-icon/issue-icon.pipe.ts +++ b/src/app/features/issue/issue-icon/issue-icon.pipe.ts @@ -3,7 +3,7 @@ import { IssueProviderKey } from '../issue.model'; import { issueProviderIconMap } from '../issue.const'; @Pipe({ - name: 'issueIcon' + name: 'issueIcon', }) export class IssueIconPipe implements PipeTransform { transform(value: IssueProviderKey, args?: any): any { diff --git a/src/app/features/issue/issue-service-interface.ts b/src/app/features/issue/issue-service-interface.ts index 10c1c6edf..d81ea4c29 100644 --- a/src/app/features/issue/issue-service-interface.ts +++ b/src/app/features/issue/issue-service-interface.ts @@ -8,24 +8,32 @@ export interface IssueServiceInterface { getById$(id: string | number, projectId: string): Observable; - getAddTaskData(issueData: IssueDataReduced): { title: string; additionalFields: Partial }; + getAddTaskData( + issueData: IssueDataReduced, + ): { title: string; additionalFields: Partial }; searchIssues$?(searchTerm: string, projectId: string): Observable; - refreshIssue?(task: Task, + refreshIssue?( + task: Task, isNotifySuccess: boolean, - isNotifyNoUpdateRequired: boolean): Promise<{ + isNotifyNoUpdateRequired: boolean, + ): Promise<{ taskChanges: Partial; issue: IssueData; } | null>; - refreshIssues?(tasks: Task[], + refreshIssues?( + tasks: Task[], isNotifySuccess: boolean, - isNotifyNoUpdateRequired: boolean): Promise<{ - task: Task; - taskChanges: Partial; - issue: IssueData; - }[]>; + isNotifyNoUpdateRequired: boolean, + ): Promise< + { + task: Task; + taskChanges: Partial; + issue: IssueData; + }[] + >; getMappedAttachments?(issueDataIN: IssueData): TaskAttachment[]; } diff --git a/src/app/features/issue/issue.const.ts b/src/app/features/issue/issue.const.ts index 5096c1c97..ab6b07858 100644 --- a/src/app/features/issue/issue.const.ts +++ b/src/app/features/issue/issue.const.ts @@ -1,9 +1,21 @@ -import { ConfigFormConfig, GenericConfigFormSection } from '../config/global-config.model'; +import { + ConfigFormConfig, + GenericConfigFormSection, +} from '../config/global-config.model'; import { DEFAULT_JIRA_CFG, JIRA_CONFIG_FORM_SECTION } from './providers/jira/jira.const'; import { IssueProviderKey } from './issue.model'; -import { DEFAULT_GITHUB_CFG, GITHUB_CONFIG_FORM_SECTION } from './providers/github/github.const'; -import { DEFAULT_GITLAB_CFG, GITLAB_CONFIG_FORM_SECTION } from './providers/gitlab/gitlab.const'; -import { CALDAV_CONFIG_FORM_SECTION, DEFAULT_CALDAV_CFG } from './providers/caldav/caldav.const'; +import { + DEFAULT_GITHUB_CFG, + GITHUB_CONFIG_FORM_SECTION, +} from './providers/github/github.const'; +import { + DEFAULT_GITLAB_CFG, + GITLAB_CONFIG_FORM_SECTION, +} from './providers/gitlab/gitlab.const'; +import { + CALDAV_CONFIG_FORM_SECTION, + DEFAULT_CALDAV_CFG, +} from './providers/caldav/caldav.const'; export const GITLAB_TYPE: IssueProviderKey = 'GITLAB'; export const GITHUB_TYPE: IssueProviderKey = 'GITHUB'; @@ -11,25 +23,29 @@ export const JIRA_TYPE: IssueProviderKey = 'JIRA'; export const CALDAV_TYPE: IssueProviderKey = 'CALDAV'; // TODO uppercase -export const issueProviderKeys: IssueProviderKey[] = [JIRA_TYPE, GITHUB_TYPE, GITLAB_TYPE]; +export const issueProviderKeys: IssueProviderKey[] = [ + JIRA_TYPE, + GITHUB_TYPE, + GITLAB_TYPE, +]; export const issueProviderIconMap = { [JIRA_TYPE]: 'jira', [GITHUB_TYPE]: 'github', [GITLAB_TYPE]: 'gitlab', - [CALDAV_TYPE]: 'caldav' + [CALDAV_TYPE]: 'caldav', }; export const DEFAULT_ISSUE_PROVIDER_CFGS = { [JIRA_TYPE]: DEFAULT_JIRA_CFG, [GITHUB_TYPE]: DEFAULT_GITHUB_CFG, [GITLAB_TYPE]: DEFAULT_GITLAB_CFG, - [CALDAV_TYPE]: DEFAULT_CALDAV_CFG + [CALDAV_TYPE]: DEFAULT_CALDAV_CFG, }; export const ISSUE_PROVIDER_FORM_CFGS: ConfigFormConfig = [ - (GITLAB_CONFIG_FORM_SECTION as GenericConfigFormSection), - (GITHUB_CONFIG_FORM_SECTION as GenericConfigFormSection), - (JIRA_CONFIG_FORM_SECTION as GenericConfigFormSection), - (CALDAV_CONFIG_FORM_SECTION as GenericConfigFormSection), + GITLAB_CONFIG_FORM_SECTION as GenericConfigFormSection, + GITHUB_CONFIG_FORM_SECTION as GenericConfigFormSection, + JIRA_CONFIG_FORM_SECTION as GenericConfigFormSection, + CALDAV_CONFIG_FORM_SECTION as GenericConfigFormSection, ]; diff --git a/src/app/features/issue/issue.model.ts b/src/app/features/issue/issue.model.ts index 56b838da7..7cb16efbc 100644 --- a/src/app/features/issue/issue.model.ts +++ b/src/app/features/issue/issue.model.ts @@ -1,10 +1,19 @@ -import { JiraIssue, JiraIssueReduced } from './providers/jira/jira-issue/jira-issue.model'; +import { + JiraIssue, + JiraIssueReduced, +} from './providers/jira/jira-issue/jira-issue.model'; import { JiraCfg } from './providers/jira/jira.model'; import { GithubCfg } from './providers/github/github.model'; -import { GithubIssue, GithubIssueReduced } from './providers/github/github-issue/github-issue.model'; +import { + GithubIssue, + GithubIssueReduced, +} from './providers/github/github-issue/github-issue.model'; import { GitlabCfg } from './providers/gitlab/gitlab'; import { GitlabIssue } from './providers/gitlab/gitlab-issue/gitlab-issue.model'; -import { CaldavIssue, CaldavIssueReduced } from './providers/caldav/caldav-issue/caldav-issue.model'; +import { + CaldavIssue, + CaldavIssueReduced, +} from './providers/caldav/caldav-issue/caldav-issue.model'; import { CaldavCfg } from './providers/caldav/caldav.model'; export type IssueProviderKey = 'JIRA' | 'GITHUB' | 'GITLAB' | 'CALDAV'; @@ -13,7 +22,7 @@ export type IssueIntegrationCfg = JiraCfg | GithubCfg | GitlabCfg; export enum IssueLocalState { OPEN = 'OPEN', IN_PROGRESS = 'IN_PROGRESS', - DONE = 'DONE' + DONE = 'DONE', } export interface IssueIntegrationCfgs { @@ -25,7 +34,11 @@ export interface IssueIntegrationCfgs { } export type IssueData = JiraIssue | GithubIssue | GitlabIssue | CaldavIssue; -export type IssueDataReduced = GithubIssueReduced | JiraIssueReduced | GitlabIssue | CaldavIssueReduced; +export type IssueDataReduced = + | GithubIssueReduced + | JiraIssueReduced + | GitlabIssue + | CaldavIssueReduced; export interface SearchResultItem { title: string; diff --git a/src/app/features/issue/issue.module.ts b/src/app/features/issue/issue.module.ts index 3e8414dd2..56c77cf4c 100644 --- a/src/app/features/issue/issue.module.ts +++ b/src/app/features/issue/issue.module.ts @@ -16,16 +16,7 @@ import { CaldavIssueModule } from './providers/caldav/caldav-issue/caldav-issue. GitlabIssueModule, CaldavIssueModule, ], - declarations: [ - IssueHeaderComponent, - IssueContentComponent, - IssueIconPipe, - ], - exports: [ - IssueHeaderComponent, - IssueContentComponent, - IssueIconPipe, - ], + declarations: [IssueHeaderComponent, IssueContentComponent, IssueIconPipe], + exports: [IssueHeaderComponent, IssueContentComponent, IssueIconPipe], }) -export class IssueModule { -} +export class IssueModule {} diff --git a/src/app/features/issue/issue.service.ts b/src/app/features/issue/issue.service.ts index 2bc41c118..99a04e37c 100644 --- a/src/app/features/issue/issue.service.ts +++ b/src/app/features/issue/issue.service.ts @@ -1,5 +1,10 @@ import { Injectable } from '@angular/core'; -import { IssueData, IssueDataReduced, IssueProviderKey, SearchResultItem } from './issue.model'; +import { + IssueData, + IssueDataReduced, + IssueProviderKey, + SearchResultItem, +} from './issue.model'; import { TaskAttachment } from '../tasks/task-attachment/task-attachment.model'; import { from, merge, Observable, of, Subject, zip } from 'rxjs'; import { CALDAV_TYPE, GITHUB_TYPE, GITLAB_TYPE, JIRA_TYPE } from './issue.const'; @@ -28,7 +33,7 @@ export class IssueService { [GITLAB_TYPE]: {}, [GITHUB_TYPE]: {}, [JIRA_TYPE]: {}, - [CALDAV_TYPE]: {} + [CALDAV_TYPE]: {}, }; constructor( @@ -37,22 +42,25 @@ export class IssueService { private _githubCommonInterfacesService: GithubCommonInterfacesService, private _gitlabCommonInterfacesService: GitlabCommonInterfacesService, private _caldavCommonInterfaceService: CaldavCommonInterfacesService, - ) { - } + ) {} - getById$(issueType: IssueProviderKey, id: string | number, projectId: string): Observable { + getById$( + issueType: IssueProviderKey, + id: string | number, + projectId: string, + ): Observable { // account for issue refreshment if (this.ISSUE_SERVICE_MAP[issueType].refreshIssue) { if (!this.ISSUE_REFRESH_MAP[issueType][id]) { this.ISSUE_REFRESH_MAP[issueType][id] = new Subject(); } - return this.ISSUE_SERVICE_MAP[issueType].getById$(id, projectId).pipe( - switchMap(issue => merge( - of(issue), - this.ISSUE_REFRESH_MAP[issueType][id], + return this.ISSUE_SERVICE_MAP[issueType] + .getById$(id, projectId) + .pipe( + switchMap((issue) => + merge(of(issue), this.ISSUE_REFRESH_MAP[issueType][id]), ), - ) - ); + ); } else { return this.ISSUE_SERVICE_MAP[issueType].getById$(id, projectId); } @@ -60,19 +68,28 @@ export class IssueService { searchIssues$(searchTerm: string, projectId: string): Observable { const obs = Object.keys(this.ISSUE_SERVICE_MAP) - .map(key => this.ISSUE_SERVICE_MAP[key]) - .filter(provider => typeof provider.searchIssues$ === 'function') - .map(provider => (provider.searchIssues$ as any)(searchTerm, projectId)); + .map((key) => this.ISSUE_SERVICE_MAP[key]) + .filter((provider) => typeof provider.searchIssues$ === 'function') + .map((provider) => (provider.searchIssues$ as any)(searchTerm, projectId)); obs.unshift(from([[]])); - return zip(...obs, (...allResults: any[]) => [].concat(...allResults)) as Observable; + return zip(...obs, (...allResults: any[]) => [].concat(...allResults)) as Observable< + SearchResultItem[] + >; } - issueLink$(issueType: IssueProviderKey, issueId: string | number, projectId: string): Observable { + issueLink$( + issueType: IssueProviderKey, + issueId: string | number, + projectId: string, + ): Observable { return this.ISSUE_SERVICE_MAP[issueType].issueLink$(issueId, projectId); } - getMappedAttachments(issueType: IssueProviderKey, issueDataIN: IssueData): TaskAttachment[] { + getMappedAttachments( + issueType: IssueProviderKey, + issueDataIN: IssueData, + ): TaskAttachment[] { if (!this.ISSUE_SERVICE_MAP[issueType].getMappedAttachments) { return []; } @@ -82,7 +99,7 @@ export class IssueService { async refreshIssue( task: Task, isNotifySuccess: boolean = true, - isNotifyNoUpdateRequired: boolean = false + isNotifyNoUpdateRequired: boolean = false, ): Promise { if (!task.issueId || !task.issueType) { throw new Error('No issue task'); @@ -91,9 +108,16 @@ export class IssueService { throw new Error('Issue method not available'); } - const update = await (this.ISSUE_SERVICE_MAP[task.issueType].refreshIssue as any)(task, isNotifySuccess, isNotifyNoUpdateRequired); + const update = await (this.ISSUE_SERVICE_MAP[task.issueType].refreshIssue as any)( + task, + isNotifySuccess, + isNotifyNoUpdateRequired, + ); if (update) { - if (this.ISSUE_SERVICE_MAP[task.issueType].getById$ && this.ISSUE_REFRESH_MAP[task.issueType][task.issueId]) { + if ( + this.ISSUE_SERVICE_MAP[task.issueType].getById$ && + this.ISSUE_REFRESH_MAP[task.issueType][task.issueId] + ) { this.ISSUE_REFRESH_MAP[task.issueType][task.issueId].next(update.issue); } this._taskService.update(task.id, update.taskChanges); @@ -103,7 +127,7 @@ export class IssueService { async refreshIssues( tasks: Task[], isNotifySuccess: boolean = true, - isNotifyNoUpdateRequired: boolean = false + isNotifyNoUpdateRequired: boolean = false, ): Promise { // dynamic map that has a list of tasks for every entry where the entry is an issue type const tasksIssueIdsByIssueType: any = {}; @@ -127,7 +151,7 @@ export class IssueService { const updates = await (this.ISSUE_SERVICE_MAP[issuesType].refreshIssues as any)( tasksIssueIdsByIssueType[issuesType], isNotifySuccess, - isNotifyNoUpdateRequired + isNotifyNoUpdateRequired, ); if (updates) { for (const update of updates) { @@ -156,21 +180,26 @@ export class IssueService { if (!this.ISSUE_SERVICE_MAP[issueType].getAddTaskData) { throw new Error('Issue method not available'); } - const {issueId, issueData} = (typeof issueIdOrData === 'number' || typeof issueIdOrData === 'string') - ? { - issueId: issueIdOrData, - issueData: await this.ISSUE_SERVICE_MAP[issueType].getById$(issueIdOrData, projectId).toPromise() - } - : { - issueId: issueIdOrData.id, - issueData: issueIdOrData - }; + const { issueId, issueData } = + typeof issueIdOrData === 'number' || typeof issueIdOrData === 'string' + ? { + issueId: issueIdOrData, + issueData: await this.ISSUE_SERVICE_MAP[issueType] + .getById$(issueIdOrData, projectId) + .toPromise(), + } + : { + issueId: issueIdOrData.id, + issueData: issueIdOrData, + }; - const {title = null, additionalFields = {}} = this.ISSUE_SERVICE_MAP[issueType].getAddTaskData(issueData); + const { title = null, additionalFields = {} } = this.ISSUE_SERVICE_MAP[ + issueType + ].getAddTaskData(issueData); return this._taskService.add(title, isAddToBacklog, { issueType, - issueId: (issueId as string), + issueId: issueId as string, issueWasUpdated: false, issueLastUpdated: Date.now(), ...additionalFields, diff --git a/src/app/features/issue/providers/caldav/caldav-client.service.ts b/src/app/features/issue/providers/caldav/caldav-client.service.ts index 97fb67d67..5316d13eb 100644 --- a/src/app/features/issue/providers/caldav/caldav-client.service.ts +++ b/src/app/features/issue/providers/caldav/caldav-client.service.ts @@ -26,20 +26,22 @@ interface ClientCache { providedIn: 'root', }) export class CaldavClientService { - private _clientCache = new Map(); - constructor( - private readonly _snackService: SnackService - ) { - } + constructor(private readonly _snackService: SnackService) {} private static _isValidSettings(cfg: CaldavCfg): boolean { - return !!cfg - && !!cfg.caldavUrl && cfg.caldavUrl.length > 0 - && !!cfg.resourceName && cfg.resourceName.length > 0 - && !!cfg.username && cfg.username.length > 0 - && !!cfg.password && cfg.password.length > 0; + return ( + !!cfg && + !!cfg.caldavUrl && + cfg.caldavUrl.length > 0 && + !!cfg.resourceName && + cfg.resourceName.length > 0 && + !!cfg.username && + cfg.username.length > 0 && + !!cfg.password && + cfg.password.length > 0 + ); } private static _getCalendarUriFromUrl(url: string) { @@ -53,28 +55,28 @@ export class CaldavClientService { private static async _getAllTodos(calendar: any, filterOpen: boolean) { const query = { name: [NS.IETF_CALDAV, 'comp-filter'], - attributes: [ - ['name', 'VCALENDAR'], + attributes: [['name', 'VCALENDAR']], + children: [ + { + name: [NS.IETF_CALDAV, 'comp-filter'], + attributes: [['name', 'VTODO']], + }, ], - children: [{ - name: [NS.IETF_CALDAV, 'comp-filter'], - attributes: [ - ['name', 'VTODO'], - ], - }], }; if (filterOpen) { // @ts-ignore - query.children[0].children = [{ - name: [NS.IETF_CALDAV, 'prop-filter'], - attributes: [ - ['name', 'completed'], - ], - children: [{ - name: [NS.IETF_CALDAV, 'is-not-defined'], - }] - }]; + query.children[0].children = [ + { + name: [NS.IETF_CALDAV, 'prop-filter'], + attributes: [['name', 'completed']], + children: [ + { + name: [NS.IETF_CALDAV, 'is-not-defined'], + }, + ], + }, + ]; } return await calendar.calendarQuery([query]); @@ -83,25 +85,25 @@ export class CaldavClientService { private static async _findTaskByUid(calendar: any, taskUid: string) { const query = { name: [NS.IETF_CALDAV, 'comp-filter'], - attributes: [ - ['name', 'VCALENDAR'], - ], - children: [{ - name: [NS.IETF_CALDAV, 'comp-filter'], - attributes: [ - ['name', 'VTODO'], - ], - children: [{ - name: [NS.IETF_CALDAV, 'prop-filter'], - attributes: [ - ['name', 'uid'], + attributes: [['name', 'VCALENDAR']], + children: [ + { + name: [NS.IETF_CALDAV, 'comp-filter'], + attributes: [['name', 'VTODO']], + children: [ + { + name: [NS.IETF_CALDAV, 'prop-filter'], + attributes: [['name', 'uid']], + children: [ + { + name: [NS.IETF_CALDAV, 'text-match'], + value: taskUid, + }, + ], + }, ], - children: [{ - name: [NS.IETF_CALDAV, 'text-match'], - value: taskUid, - }], - }] - }], + }, + ], }; return await calendar.calendarQuery([query]); } @@ -142,7 +144,7 @@ export class CaldavClientService { } for (i = 0; i < this.length; i++) { chr = etag.charCodeAt(i); - hash = ((hash << 5) - hash) + chr; //eslint-disable-line no-bitwise + hash = (hash << 5) - hash + chr; //eslint-disable-line no-bitwise // Convert to 32bit integer hash |= 0; //eslint-disable-line no-bitwise } @@ -157,15 +159,20 @@ export class CaldavClientService { if (this._clientCache.has(client_key)) { return this._clientCache.get(client_key) as ClientCache; } else { - const client = new DavClient({ - rootUrl: cfg.caldavUrl - }, this._getXhrProvider(cfg)); + const client = new DavClient( + { + rootUrl: cfg.caldavUrl, + }, + this._getXhrProvider(cfg), + ); - await client.connect({enableCalDAV: true}).catch((err: any) => this._handleNetErr(err)); + await client + .connect({ enableCalDAV: true }) + .catch((err: any) => this._handleNetErr(err)); const cache = { client, - calendars: new Map() + calendars: new Map(), }; this._clientCache.set(client_key, cache); @@ -181,10 +188,15 @@ export class CaldavClientService { return clientCache.calendars.get(resource); } - const calendars = await clientCache.client.calendarHomes[0].findAllCalendars() + const calendars = await clientCache.client.calendarHomes[0] + .findAllCalendars() .catch((err: any) => this._handleNetErr(err)); - const calendar = calendars.find((item: Calendar) => (item.displayname || CaldavClientService._getCalendarUriFromUrl(item.url)) === resource); + const calendar = calendars.find( + (item: Calendar) => + (item.displayname || CaldavClientService._getCalendarUriFromUrl(item.url)) === + resource, + ); if (calendar !== undefined) { clientCache.calendars.set(resource, calendar); @@ -194,31 +206,35 @@ export class CaldavClientService { this._snackService.open({ type: 'ERROR', translateParams: { - calendarName: cfg.resourceName as string + calendarName: cfg.resourceName as string, }, - msg: T.F.CALDAV.S.CALENDAR_NOT_FOUND + msg: T.F.CALDAV.S.CALENDAR_NOT_FOUND, }); throw new Error('CALENDAR NOT FOUND: ' + cfg.resourceName); } getOpenTasks$(cfg: CaldavCfg): Observable { return from(this._getTasks(cfg, true, true)).pipe( - catchError((err) => throwError({[HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err}))); + catchError((err) => throwError({ [HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err })), + ); } searchOpenTasks$(text: string, cfg: CaldavCfg): Observable { - return from(this._getTasks(cfg, true, true) - .then(tasks => - tasks.filter(todo => todo.summary.includes(text)) - .map(todo => { + return from( + this._getTasks(cfg, true, true).then((tasks) => + tasks + .filter((todo) => todo.summary.includes(text)) + .map((todo) => { return { title: todo.summary, issueType: CALDAV_TYPE, issueData: todo, }; - }) - )).pipe( - catchError((err) => throwError({[HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err}))); + }), + ), + ).pipe( + catchError((err) => throwError({ [HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err })), + ); } getById$(id: string | number, caldavCfg: CaldavCfg): Observable { @@ -227,19 +243,28 @@ export class CaldavClientService { id = id.toString(10); } return from(this._getTask(caldavCfg, id)).pipe( - catchError((err) => throwError({[HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err}))); + catchError((err) => throwError({ [HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err })), + ); } getByIds$(ids: string[], cfg: CaldavCfg): Observable { - return from(this._getTasks(cfg, false, false) - .then(tasks => tasks - .filter(task => task.id in ids))).pipe( - catchError((err) => throwError({[HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err}))); + return from( + this._getTasks(cfg, false, false).then((tasks) => + tasks.filter((task) => task.id in ids), + ), + ).pipe( + catchError((err) => throwError({ [HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err })), + ); } - updateCompletedState$(caldavCfg: CaldavCfg, issueId: string, completed: boolean): Observable { + updateCompletedState$( + caldavCfg: CaldavCfg, + issueId: string, + completed: boolean, + ): Observable { return from(this._updateCompletedState(caldavCfg, issueId, completed)).pipe( - catchError((err) => throwError({[HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err}))); + catchError((err) => throwError({ [HANDLED_ERROR_PROP_STR]: 'Caldav: ' + err })), + ); } private _getXhrProvider(cfg: CaldavCfg) { @@ -248,12 +273,15 @@ export class CaldavClientService { const oldOpen = xhr.open; // override open() method to add headers - xhr.open = function() { + xhr.open = function () { // @ts-ignore const result = oldOpen.apply(this, arguments); // @ts-ignore xhr.setRequestHeader('X-Requested-With', 'SuperProductivity'); - xhr.setRequestHeader('Authorization', 'Basic ' + btoa(cfg.username + ':' + cfg.password)); + xhr.setRequestHeader( + 'Authorization', + 'Basic ' + btoa(cfg.username + ':' + cfg.password), + ); return result; }; return xhr; @@ -265,7 +293,7 @@ export class CaldavClientService { private _handleNetErr(err: any) { this._snackService.open({ type: 'ERROR', - msg: T.F.CALDAV.S.ERR_NETWORK + msg: T.F.CALDAV.S.ERR_NETWORK, }); throw new Error('CALDAV NETWORK ERROR: ' + err); } @@ -274,27 +302,40 @@ export class CaldavClientService { if (!CaldavClientService._isValidSettings(cfg)) { this._snackService.open({ type: 'ERROR', - msg: T.F.CALDAV.S.ERR_NOT_CONFIGURED + msg: T.F.CALDAV.S.ERR_NOT_CONFIGURED, }); throwHandledError('CalDav: Not enough settings'); } } - private async _getTasks(cfg: CaldavCfg, filterOpen: boolean, filterCategory: boolean): Promise { + private async _getTasks( + cfg: CaldavCfg, + filterOpen: boolean, + filterCategory: boolean, + ): Promise { const cal = await this._getCalendar(cfg); - const tasks = await CaldavClientService._getAllTodos(cal, filterOpen).catch((err: any) => this._handleNetErr(err)); - return tasks.map((t: any) => CaldavClientService._mapTask(t)) - .filter((t: CaldavIssue) => !filterCategory || !cfg.categoryFilter || (t.labels.includes(cfg.categoryFilter))); + const tasks = await CaldavClientService._getAllTodos( + cal, + filterOpen, + ).catch((err: any) => this._handleNetErr(err)); + return tasks + .map((t: any) => CaldavClientService._mapTask(t)) + .filter( + (t: CaldavIssue) => + !filterCategory || !cfg.categoryFilter || t.labels.includes(cfg.categoryFilter), + ); } private async _getTask(cfg: CaldavCfg, uid: string): Promise { const cal = await this._getCalendar(cfg); - const task = await CaldavClientService._findTaskByUid(cal, uid).catch((err: any) => this._handleNetErr(err)); + const task = await CaldavClientService._findTaskByUid(cal, uid).catch((err: any) => + this._handleNetErr(err), + ); if (task.length < 1) { this._snackService.open({ type: 'ERROR', - msg: T.F.CALDAV.S.ISSUE_NOT_FOUND + msg: T.F.CALDAV.S.ISSUE_NOT_FOUND, }); throw new Error('ISSUE NOT FOUND: ' + uid); } @@ -309,22 +350,24 @@ export class CaldavClientService { this._snackService.open({ type: 'ERROR', translateParams: { - calendarName: cfg.resourceName as string + calendarName: cfg.resourceName as string, }, - msg: T.F.CALDAV.S.CALENDAR_READ_ONLY + msg: T.F.CALDAV.S.CALENDAR_READ_ONLY, }); throw new Error('CALENDAR READ ONLY: ' + cfg.resourceName); } - const tasks = await CaldavClientService._findTaskByUid(cal, uid).catch((err: any) => this._handleNetErr(err)); + const tasks = await CaldavClientService._findTaskByUid(cal, uid).catch((err: any) => + this._handleNetErr(err), + ); if (tasks.length < 1) { this._snackService.open({ type: 'ERROR', translateParams: { - issueId: uid + issueId: uid, }, - msg: T.F.CALDAV.S.ISSUE_NOT_FOUND + msg: T.F.CALDAV.S.ISSUE_NOT_FOUND, }); throw new Error('ISSUE NOT FOUND: ' + uid); } @@ -351,6 +394,5 @@ export class CaldavClientService { task.data = ICAL.stringify(jCal); await task.update().catch((err: any) => this._handleNetErr(err)); - } } diff --git a/src/app/features/issue/providers/caldav/caldav-common-interfaces.service.ts b/src/app/features/issue/providers/caldav/caldav-common-interfaces.service.ts index 925677d28..bc384d17a 100644 --- a/src/app/features/issue/providers/caldav/caldav-common-interfaces.service.ts +++ b/src/app/features/issue/providers/caldav/caldav-common-interfaces.service.ts @@ -20,20 +20,24 @@ export class CaldavCommonInterfacesService implements IssueServiceInterface { private readonly _projectService: ProjectService, private readonly _caldavClientService: CaldavClientService, private readonly _snackService: SnackService, - ) { - } + ) {} private static _formatIssueTitleForSnack(title: string): string { return truncate(title); } - getAddTaskData(issueData: CaldavIssueReduced): { title: string; additionalFields: Partial } { - return {additionalFields: {issueLastUpdated: issueData.etag_hash}, title: issueData.summary}; + getAddTaskData( + issueData: CaldavIssueReduced, + ): { title: string; additionalFields: Partial } { + return { + additionalFields: { issueLastUpdated: issueData.etag_hash }, + title: issueData.summary, + }; } getById$(id: string | number, projectId: string): Observable { return this._getCfgOnce$(projectId).pipe( - concatMap(caldavCfg => this._caldavClientService.getById$(id, caldavCfg)) + concatMap((caldavCfg) => this._caldavClientService.getById$(id, caldavCfg)), ); } @@ -41,11 +45,11 @@ export class CaldavCommonInterfacesService implements IssueServiceInterface { return of(''); } - async refreshIssue(task: Task, + async refreshIssue( + task: Task, isNotifySuccess: boolean, - isNotifyNoUpdateRequired: boolean): - Promise<{ taskChanges: Partial; issue: CaldavIssue } | null> { - + isNotifyNoUpdateRequired: boolean, + ): Promise<{ taskChanges: Partial; issue: CaldavIssue } | null> { if (!task.projectId) { throw new Error('No projectId'); } @@ -62,7 +66,9 @@ export class CaldavCommonInterfacesService implements IssueServiceInterface { this._snackService.open({ ico: 'cloud_download', translateParams: { - issueText: CaldavCommonInterfacesService._formatIssueTitleForSnack(issue.summary) + issueText: CaldavCommonInterfacesService._formatIssueTitleForSnack( + issue.summary, + ), }, msg: T.F.CALDAV.S.ISSUE_UPDATE, }); @@ -89,7 +95,7 @@ export class CaldavCommonInterfacesService implements IssueServiceInterface { async refreshIssues( tasks: Task[], isNotifySuccess: boolean = true, - isNotifyNoUpdateRequired: boolean = false + isNotifyNoUpdateRequired: boolean = false, ): Promise<{ task: Task; taskChanges: Partial; issue: CaldavIssue }[]> { // First sort the tasks by the issueId // because the API returns it in a desc order by issue iid(issueId) @@ -100,25 +106,44 @@ export class CaldavCommonInterfacesService implements IssueServiceInterface { } const cfg = await this._getCfgOnce$(projectId).toPromise(); - const issues: CaldavIssue[] = await this._caldavClientService.getByIds$(tasks.map(t => t.id), cfg).toPromise(); - const issueMap = new Map(issues.map(item => [item.id, item])); + const issues: CaldavIssue[] = await this._caldavClientService + .getByIds$( + tasks.map((t) => t.id), + cfg, + ) + .toPromise(); + const issueMap = new Map(issues.map((item) => [item.id, item])); if (isNotifyNoUpdateRequired) { - tasks.filter(task => issueMap.has(task.id) && issueMap.get(task.id)?.etag_hash === task.issueLastUpdated) - .forEach(_ => this._snackService.open({ - msg: T.F.CALDAV.S.ISSUE_NO_UPDATE_REQUIRED, - ico: 'cloud_download', - })); + tasks + .filter( + (task) => + issueMap.has(task.id) && + issueMap.get(task.id)?.etag_hash === task.issueLastUpdated, + ) + .forEach((_) => + this._snackService.open({ + msg: T.F.CALDAV.S.ISSUE_NO_UPDATE_REQUIRED, + ico: 'cloud_download', + }), + ); } - return tasks.filter(task => issueMap.has(task.id) && issueMap.get(task.id)?.etag_hash !== task.issueLastUpdated) - .map(task => { + return tasks + .filter( + (task) => + issueMap.has(task.id) && + issueMap.get(task.id)?.etag_hash !== task.issueLastUpdated, + ) + .map((task) => { const issue = issueMap.get(task.id) as CaldavIssue; if (isNotifySuccess) { this._snackService.open({ ico: 'cloud_download', translateParams: { - issueText: CaldavCommonInterfacesService._formatIssueTitleForSnack(issue.summary) + issueText: CaldavCommonInterfacesService._formatIssueTitleForSnack( + issue.summary, + ), }, msg: T.F.CALDAV.S.ISSUE_UPDATE, }); @@ -137,15 +162,17 @@ export class CaldavCommonInterfacesService implements IssueServiceInterface { searchIssues$(searchTerm: string, projectId: string): Observable { return this._getCfgOnce$(projectId).pipe( - switchMap((caldavCfg) => (caldavCfg && caldavCfg.isSearchIssuesFromCaldav) - ? this._caldavClientService.searchOpenTasks$(searchTerm, caldavCfg).pipe(catchError(() => [])) - : of([]) - ) + switchMap((caldavCfg) => + caldavCfg && caldavCfg.isSearchIssuesFromCaldav + ? this._caldavClientService + .searchOpenTasks$(searchTerm, caldavCfg) + .pipe(catchError(() => [])) + : of([]), + ), ); } private _getCfgOnce$(projectId: string): Observable { return this._projectService.getCaldavCfgForProject$(projectId).pipe(first()); } - } diff --git a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue-content/caldav-issue-content.component.ts b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue-content/caldav-issue-content.component.ts index 568601e5b..2e04b5862 100644 --- a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue-content/caldav-issue-content.component.ts +++ b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue-content/caldav-issue-content.component.ts @@ -10,7 +10,7 @@ import { CaldavIssue } from '../caldav-issue.model'; templateUrl: './caldav-issue-content.component.html', styleUrls: ['./caldav-issue-content.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation] + animations: [expandAnimation], }) export class CaldavIssueContentComponent { @Input() issue?: CaldavIssue; @@ -18,10 +18,7 @@ export class CaldavIssueContentComponent { T: typeof T = T; - constructor( - private readonly _taskService: TaskService, - ) { - } + constructor(private readonly _taskService: TaskService) {} hideUpdates() { this._taskService.markIssueUpdatesAsRead((this.task as TaskWithSubTasks).id); diff --git a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue-header/caldav-issue-header.component.ts b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue-header/caldav-issue-header.component.ts index b2eea3b04..ac61baf11 100644 --- a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue-header/caldav-issue-header.component.ts +++ b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue-header/caldav-issue-header.component.ts @@ -6,12 +6,11 @@ import { TaskWithSubTasks } from 'src/app/features/tasks/task.model'; selector: 'caldav-issue-header', templateUrl: './caldav-issue-header.component.html', styleUrls: ['./caldav-issue-header.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class CaldavIssueHeaderComponent { T: typeof T = T; @Input() public task?: TaskWithSubTasks; - constructor() { - } + constructor() {} } diff --git a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.effects.ts b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.effects.ts index ceacd46c5..4a68573b3 100644 --- a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.effects.ts +++ b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.effects.ts @@ -3,7 +3,16 @@ import { Actions, Effect, ofType } from '@ngrx/effects'; import { SnackService } from '../../../../../core/snack/snack.service'; import { TaskService } from '../../../../tasks/task.service'; import { ProjectService } from '../../../../project/project.service'; -import { concatMap, filter, first, map, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators'; +import { + concatMap, + filter, + first, + map, + switchMap, + takeUntil, + tap, + withLatestFrom, +} from 'rxjs/operators'; import { IssueService } from '../../../issue.service'; import { forkJoin, Observable, timer } from 'rxjs'; import { Task, TaskWithSubTasks } from 'src/app/features/tasks/task.model'; @@ -20,87 +29,107 @@ import { TaskActionTypes, UpdateTask } from '../../../../tasks/store/task.action @Injectable() export class CaldavIssueEffects { + @Effect({ dispatch: false }) + checkForDoneTransition$: Observable = this._actions$.pipe( + ofType(TaskActionTypes.UpdateTask), + filter((a: UpdateTask): boolean => 'isDone' in a.payload.task.changes), + concatMap((a: UpdateTask) => + this._taskService.getByIdOnce$(a.payload.task.id as string), + ), + filter((task: Task) => task && task.issueType === CALDAV_TYPE), + concatMap((task: Task) => { + if (!task.projectId) { + throw new Error('No projectId for task'); + } + return this._getCfgOnce$(task.projectId).pipe( + map((caldavCfg) => ({ caldavCfg, task })), + ); + }), + filter( + ({ caldavCfg: caldavCfg, task }) => + isCaldavEnabled(caldavCfg) && caldavCfg.isTransitionIssuesEnabled, + ), + concatMap(({ caldavCfg: caldavCfg, task }) => { + return this._handleTransitionForIssue$(caldavCfg, task); + }), + ); - @Effect({dispatch: false}) - checkForDoneTransition$: Observable = this._actions$ - .pipe( - ofType(TaskActionTypes.UpdateTask), - filter((a: UpdateTask): boolean => 'isDone' in a.payload.task.changes), - concatMap((a: UpdateTask) => this._taskService.getByIdOnce$(a.payload.task.id as string)), - filter((task: Task) => (task && task.issueType === CALDAV_TYPE)), - concatMap((task: Task) => { - if (!task.projectId) { - throw new Error('No projectId for task'); - } - return this._getCfgOnce$(task.projectId).pipe( - map((caldavCfg) => ({caldavCfg, task})), - ); - }), - filter(({caldavCfg: caldavCfg, task}) => - isCaldavEnabled(caldavCfg) && caldavCfg.isTransitionIssuesEnabled - ), - concatMap(({caldavCfg: caldavCfg, task}) => { - return this._handleTransitionForIssue$(caldavCfg, task); - }) - ); + private _pollTimer$: Observable = timer( + CALDAV_INITIAL_POLL_DELAY, + CALDAV_POLL_INTERVAL, + ); - private _pollTimer$: Observable = timer(CALDAV_INITIAL_POLL_DELAY, CALDAV_POLL_INTERVAL); - - @Effect({dispatch: false}) + @Effect({ dispatch: false }) pollNewIssuesToBacklog$: Observable = this._issueEffectHelperService.pollToBacklogTriggerToProjectId$.pipe( - switchMap((pId) => this._projectService.getCaldavCfgForProject$(pId).pipe( - first(), - filter(caldavCfg => isCaldavEnabled(caldavCfg) && caldavCfg.isAutoAddToBacklog), - switchMap(caldavCfg => this._pollTimer$.pipe( - // NOTE: required otherwise timer stays alive for filtered actions - takeUntil(this._issueEffectHelperService.pollToBacklogActions$), - tap(() => console.log('CALDAV_POLL_BACKLOG_CHANGES')), - withLatestFrom( - this._caldavClientService.getOpenTasks$(caldavCfg), - this._taskService.getAllIssueIdsForProject(pId, CALDAV_TYPE) as Promise + switchMap((pId) => + this._projectService.getCaldavCfgForProject$(pId).pipe( + first(), + filter((caldavCfg) => isCaldavEnabled(caldavCfg) && caldavCfg.isAutoAddToBacklog), + switchMap((caldavCfg) => + this._pollTimer$.pipe( + // NOTE: required otherwise timer stays alive for filtered actions + takeUntil(this._issueEffectHelperService.pollToBacklogActions$), + tap(() => console.log('CALDAV_POLL_BACKLOG_CHANGES')), + withLatestFrom( + this._caldavClientService.getOpenTasks$(caldavCfg), + this._taskService.getAllIssueIdsForProject(pId, CALDAV_TYPE) as Promise< + string[] + >, + ), + tap( + ([, issues, allTaskCaldavIssueIds]: [ + any, + CaldavIssueReduced[], + string[], + ]) => { + const issuesToAdd = issues.filter( + (issue) => !allTaskCaldavIssueIds.includes(issue.id), + ); + console.log('issuesToAdd', issuesToAdd); + if (issuesToAdd?.length) { + this._importNewIssuesToBacklog(pId, issuesToAdd); + } + }, + ), + ), ), - tap(([, issues, allTaskCaldavIssueIds]: [any, CaldavIssueReduced[], string[]]) => { - const issuesToAdd = issues.filter(issue => !allTaskCaldavIssueIds.includes(issue.id)); - console.log('issuesToAdd', issuesToAdd); - if (issuesToAdd?.length) { - this._importNewIssuesToBacklog(pId, issuesToAdd); - } - }) - )), - )), + ), + ), ); private _updateIssuesForCurrentContext$: Observable = this._workContextService.allTasksForCurrentContext$.pipe( first(), switchMap((tasks) => { - const caldavIssueTasks = tasks.filter(task => task.issueType === CALDAV_TYPE); - return forkJoin(caldavIssueTasks.map(task => { + const caldavIssueTasks = tasks.filter((task) => task.issueType === CALDAV_TYPE); + return forkJoin( + caldavIssueTasks.map((task) => { if (!task.projectId) { throw new Error('No project for task'); } return this._projectService.getCaldavCfgForProject$(task.projectId).pipe( first(), - map(cfg => ({ + map((cfg) => ({ cfg, task, - })) + })), ); - }) + }), ); }), - map((cos) => cos - .filter(({cfg, task}: { cfg: CaldavCfg; task: TaskWithSubTasks }): boolean => - isCaldavEnabled(cfg) && cfg.isAutoPoll - ) - .map(({task}: { cfg: CaldavCfg; task: TaskWithSubTasks }) => task) + map((cos) => + cos + .filter( + ({ cfg, task }: { cfg: CaldavCfg; task: TaskWithSubTasks }): boolean => + isCaldavEnabled(cfg) && cfg.isAutoPoll, + ) + .map(({ task }: { cfg: CaldavCfg; task: TaskWithSubTasks }) => task), ), tap((caldavTasks: TaskWithSubTasks[]) => this._refreshIssues(caldavTasks)), ); - @Effect({dispatch: false}) - pollIssueChangesForCurrentContext$: Observable = this._issueEffectHelperService.pollIssueTaskUpdatesActions$ - .pipe( - switchMap(() => this._pollTimer$), - switchMap(() => this._updateIssuesForCurrentContext$), - ); + @Effect({ dispatch: false }) + pollIssueChangesForCurrentContext$: Observable = this._issueEffectHelperService.pollIssueTaskUpdatesActions$.pipe( + switchMap(() => this._pollTimer$), + switchMap(() => this._updateIssuesForCurrentContext$), + ); constructor( private readonly _actions$: Actions, @@ -111,8 +140,7 @@ export class CaldavIssueEffects { private readonly _taskService: TaskService, private readonly _workContextService: WorkContextService, private readonly _issueEffectHelperService: IssueEffectHelperService, - ) { - } + ) {} private _refreshIssues(caldavTasks: TaskWithSubTasks[]) { if (caldavTasks && caldavTasks.length > 0) { @@ -125,7 +153,10 @@ export class CaldavIssueEffects { } } - private _importNewIssuesToBacklog(projectId: string, issuesToAdd: CaldavIssueReduced[]) { + private _importNewIssuesToBacklog( + projectId: string, + issuesToAdd: CaldavIssueReduced[], + ) { issuesToAdd.forEach((issue) => { this._issueService.addTaskWithIssue(CALDAV_TYPE, issue, projectId, true); }); @@ -134,7 +165,7 @@ export class CaldavIssueEffects { this._snackService.open({ ico: 'cloud_download', translateParams: { - issueText: issuesToAdd[0].summary + issueText: issuesToAdd[0].summary, }, msg: T.F.CALDAV.S.IMPORTED_SINGLE_ISSUE, }); @@ -142,7 +173,7 @@ export class CaldavIssueEffects { this._snackService.open({ ico: 'cloud_download', translateParams: { - issuesLength: issuesToAdd.length + issuesLength: issuesToAdd.length, }, msg: T.F.CALDAV.S.IMPORTED_MULTIPLE_ISSUES, }); @@ -150,7 +181,8 @@ export class CaldavIssueEffects { } private _handleTransitionForIssue$(caldavCfg: CaldavCfg, task: Task): Observable { - return this._caldavClientService.updateCompletedState$(caldavCfg, task.issueId as string, task.isDone) + return this._caldavClientService + .updateCompletedState$(caldavCfg, task.issueId as string, task.isDone) .pipe(concatMap(() => this._issueService.refreshIssue(task, true))); } @@ -158,4 +190,3 @@ export class CaldavIssueEffects { return this._projectService.getCaldavCfgForProject$(projectId).pipe(first()); } } - diff --git a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.model.ts b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.model.ts index f517e272c..c188caf5c 100644 --- a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.model.ts +++ b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.model.ts @@ -9,6 +9,7 @@ export type CaldavIssueReduced = Readonly<{ etag_hash: number; }>; -export type CaldavIssue = CaldavIssueReduced & Readonly<{ - note: string; -}>; +export type CaldavIssue = CaldavIssueReduced & + Readonly<{ + note: string; + }>; diff --git a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.module.ts b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.module.ts index f77ca58c1..818de3dc8 100644 --- a/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.module.ts +++ b/src/app/features/issue/providers/caldav/caldav-issue/caldav-issue.module.ts @@ -20,5 +20,4 @@ import { CaldavIssueEffects } from './caldav-issue.effects'; ], exports: [CaldavIssueHeaderComponent, CaldavIssueContentComponent], }) -export class CaldavIssueModule { -} +export class CaldavIssueModule {} diff --git a/src/app/features/issue/providers/caldav/caldav.const.ts b/src/app/features/issue/providers/caldav/caldav.const.ts index 876a02aa8..5a4cdc3ca 100644 --- a/src/app/features/issue/providers/caldav/caldav.const.ts +++ b/src/app/features/issue/providers/caldav/caldav.const.ts @@ -1,5 +1,8 @@ import { CaldavCfg } from './caldav.model'; -import { ConfigFormSection, LimitedFormlyFieldConfig } from '../../../config/global-config.model'; +import { + ConfigFormSection, + LimitedFormlyFieldConfig, +} from '../../../config/global-config.model'; import { T } from '../../../../t.const'; export const DEFAULT_CALDAV_CFG: CaldavCfg = { @@ -24,8 +27,8 @@ export const CALDAV_CONFIG_FORM: LimitedFormlyFieldConfig[] = [ templateOptions: { label: T.F.CALDAV.FORM.CALDAV_URL, type: 'text', - pattern: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ - } + pattern: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/, + }, }, { key: 'resourceName', @@ -64,28 +67,28 @@ export const CALDAV_CONFIG_FORM: LimitedFormlyFieldConfig[] = [ key: 'isSearchIssuesFromCaldav', type: 'checkbox', templateOptions: { - label: T.F.CALDAV.FORM.IS_SEARCH_ISSUES_FROM_CALDAV + label: T.F.CALDAV.FORM.IS_SEARCH_ISSUES_FROM_CALDAV, }, }, { key: 'isAutoPoll', type: 'checkbox', templateOptions: { - label: T.F.CALDAV.FORM.IS_AUTO_POLL + label: T.F.CALDAV.FORM.IS_AUTO_POLL, }, }, { key: 'isAutoAddToBacklog', type: 'checkbox', templateOptions: { - label: T.F.CALDAV.FORM.IS_AUTO_ADD_TO_BACKLOG + label: T.F.CALDAV.FORM.IS_AUTO_ADD_TO_BACKLOG, }, }, { key: 'isTransitionIssuesEnabled', type: 'checkbox', templateOptions: { - label: T.F.CALDAV.FORM.IS_TRANSITION_ISSUES_ENABLED + label: T.F.CALDAV.FORM.IS_TRANSITION_ISSUES_ENABLED, }, }, { diff --git a/src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.component.ts b/src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.component.ts index ba6a28e97..a4e36aa16 100644 --- a/src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.component.ts +++ b/src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.component.ts @@ -10,7 +10,7 @@ import { CaldavCfg } from '../caldav.model'; selector: 'dialog-caldav-initial-setup', templateUrl: './dialog-caldav-initial-setup.component.html', styleUrls: ['./dialog-caldav-initial-setup.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogCaldavInitialSetupComponent { T: typeof T = T; diff --git a/src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.module.ts b/src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.module.ts index e0a22f1ac..2fc197893 100644 --- a/src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.module.ts +++ b/src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.module.ts @@ -4,12 +4,8 @@ import { UiModule } from '../../../../../ui/ui.module'; import { DialogCaldavInitialSetupComponent } from './dialog-caldav-initial-setup.component'; @NgModule({ - imports: [ - CommonModule, - UiModule, - ], + imports: [CommonModule, UiModule], declarations: [DialogCaldavInitialSetupComponent], exports: [], }) -export class DialogCaldavInitialSetupModule { -} +export class DialogCaldavInitialSetupModule {} diff --git a/src/app/features/issue/providers/github/github-api.service.ts b/src/app/features/issue/providers/github/github-api.service.ts index 1257c18c4..d13883ebe 100644 --- a/src/app/features/issue/providers/github/github-api.service.ts +++ b/src/app/features/issue/providers/github/github-api.service.ts @@ -1,13 +1,26 @@ import { Injectable } from '@angular/core'; import { GithubCfg } from './github.model'; import { SnackService } from '../../../../core/snack/snack.service'; -import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams, HttpRequest } from '@angular/common/http'; +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, + HttpParams, + HttpRequest, +} from '@angular/common/http'; import { GITHUB_API_BASE_URL } from './github.const'; import { Observable, ObservableInput, of, throwError } from 'rxjs'; import { GithubIssueSearchResult, GithubOriginalIssue } from './github-api-responses'; import { catchError, filter, map, switchMap } from 'rxjs/operators'; -import { mapGithubIssue, mapGithubIssueToSearchResult } from './github-issue/github-issue-map.util'; -import { GithubComment, GithubIssue, GithubIssueReduced } from './github-issue/github-issue.model'; +import { + mapGithubIssue, + mapGithubIssueToSearchResult, +} from './github-issue/github-issue-map.util'; +import { + GithubComment, + GithubIssue, + GithubIssueReduced, +} from './github-issue/github-issue.model'; import { SearchResultItem } from '../../issue.model'; import { HANDLED_ERROR_PROP_STR } from '../../../../app.constants'; import { T } from '../../../../t.const'; @@ -19,49 +32,57 @@ const BASE = GITHUB_API_BASE_URL; providedIn: 'root', }) export class GithubApiService { - constructor( - private _snackService: SnackService, - private _http: HttpClient, - ) { - } + constructor(private _snackService: SnackService, private _http: HttpClient) {} - getById$(issueId: number, cfg: GithubCfg, isGetComments: boolean = true): Observable { - return this._sendRequest$({ - url: `${BASE}repos/${cfg.repo}/issues/${issueId}` - }, cfg) - .pipe( - switchMap(issue => isGetComments + getById$( + issueId: number, + cfg: GithubCfg, + isGetComments: boolean = true, + ): Observable { + return this._sendRequest$( + { + url: `${BASE}repos/${cfg.repo}/issues/${issueId}`, + }, + cfg, + ).pipe( + switchMap((issue) => + isGetComments ? this.getCommentListForIssue$(issueId, cfg).pipe( - map(comments => ({...issue, comments})) - ) + map((comments) => ({ ...issue, comments })), + ) : of(issue), - ), - ); + ), + ); } getCommentListForIssue$(issueId: number, cfg: GithubCfg): Observable { - return this._sendRequest$({ - url: `${BASE}repos/${cfg.repo}/issues/${issueId}/comments` - }, cfg) - .pipe( - ); + return this._sendRequest$( + { + url: `${BASE}repos/${cfg.repo}/issues/${issueId}/comments`, + }, + cfg, + ).pipe(); } - searchIssueForRepo$(searchText: string, cfg: GithubCfg, isSearchAllGithub: boolean = false): Observable { - const repoQuery = isSearchAllGithub - ? '' : - `+repo:${cfg.repo}`; + searchIssueForRepo$( + searchText: string, + cfg: GithubCfg, + isSearchAllGithub: boolean = false, + ): Observable { + const repoQuery = isSearchAllGithub ? '' : `+repo:${cfg.repo}`; - return this._sendRequest$({ - url: `${BASE}search/issues?q=${encodeURIComponent(searchText + repoQuery)}` - }, cfg) - .pipe( - map((res: GithubIssueSearchResult) => { - return (res && res.items) - ? res.items.map(mapGithubIssue).map(mapGithubIssueToSearchResult) - : []; - }), - ); + return this._sendRequest$( + { + url: `${BASE}search/issues?q=${encodeURIComponent(searchText + repoQuery)}`, + }, + cfg, + ).pipe( + map((res: GithubIssueSearchResult) => { + return res && res.items + ? res.items.map(mapGithubIssue).map(mapGithubIssueToSearchResult) + : []; + }), + ); } getLast100IssuesForRepo$(cfg: GithubCfg): Observable { @@ -75,12 +96,13 @@ export class GithubApiService { // ? res && res.items.map(mapGithubIssue) // : []), // ); - return this._sendRequest$({ - url: `${BASE}repos/${repo}/issues?per_page=100&sort=updated`, - }, cfg).pipe( - map((issues: GithubOriginalIssue[]) => issues - ? issues.map(mapGithubIssue) - : []), + return this._sendRequest$( + { + url: `${BASE}repos/${repo}/issues?per_page=100&sort=updated`, + }, + cfg, + ).pipe( + map((issues: GithubOriginalIssue[]) => (issues ? issues.map(mapGithubIssue) : [])), ); } @@ -88,7 +110,7 @@ export class GithubApiService { if (!this._isValidSettings(cfg)) { this._snackService.open({ type: 'ERROR', - msg: T.F.GITHUB.S.ERR_NOT_CONFIGURED + msg: T.F.GITHUB.S.ERR_NOT_CONFIGURED, }); throwHandledError('Github: Not enough settings'); } @@ -98,41 +120,46 @@ export class GithubApiService { return !!cfg && !!cfg.repo && cfg.repo.length > 0; } - private _sendRequest$(params: HttpRequest | any, cfg: GithubCfg): Observable { + private _sendRequest$( + params: HttpRequest | any, + cfg: GithubCfg, + ): Observable { this._checkSettings(cfg); const p: HttpRequest | any = { ...params, method: params.method || 'GET', headers: { - ...(cfg.token ? {Authorization: 'token ' + cfg.token} : {}), + ...(cfg.token ? { Authorization: 'token ' + cfg.token } : {}), ...(params.headers ? params.headers : {}), - } + }, }; - const bodyArg = params.data - ? [params.data] - : []; + const bodyArg = params.data ? [params.data] : []; - const allArgs = [...bodyArg, { - headers: new HttpHeaders(p.headers), - params: new HttpParams({fromObject: p.params}), - reportProgress: false, - observe: 'response', - responseType: params.responseType, - }]; + const allArgs = [ + ...bodyArg, + { + headers: new HttpHeaders(p.headers), + params: new HttpParams({ fromObject: p.params }), + reportProgress: false, + observe: 'response', + responseType: params.responseType, + }, + ]; const req = new HttpRequest(p.method, p.url, ...allArgs); return this._http.request(req).pipe( // TODO remove type: 0 @see https://brianflove.com/2018/09/03/angular-http-client-observe-response/ - filter(res => !(res === Object(res) && res.type === 0)), - map((res: any) => (res && res.body) - ? res.body - : res), + filter((res) => !(res === Object(res) && res.type === 0)), + map((res: any) => (res && res.body ? res.body : res)), catchError(this._handleRequestError$.bind(this)), ); } - private _handleRequestError$(error: HttpErrorResponse, caught: Observable): ObservableInput { + private _handleRequestError$( + error: HttpErrorResponse, + caught: Observable, + ): ObservableInput { if (error.error instanceof ErrorEvent) { // A client-side or network error occurred. Handle it accordingly. this._snackService.open({ @@ -142,23 +169,25 @@ export class GithubApiService { } else if (error.error && error.error.message) { this._snackService.open({ type: 'ERROR', - msg: 'Github: ' + error.error.message + msg: 'Github: ' + error.error.message, }); } else { // The backend returned an unsuccessful response code. this._snackService.open({ type: 'ERROR', translateParams: { - errorMsg: error.error && (error.error.name || error.error.statusText) || error.toString(), + errorMsg: + (error.error && (error.error.name || error.error.statusText)) || + error.toString(), statusCode: error.status, }, msg: T.F.GITHUB.S.ERR_UNKNOWN, }); } if (error && error.message) { - return throwError({[HANDLED_ERROR_PROP_STR]: 'Github: ' + error.message}); + return throwError({ [HANDLED_ERROR_PROP_STR]: 'Github: ' + error.message }); } - return throwError({[HANDLED_ERROR_PROP_STR]: 'Github: Api request failed.'}); + return throwError({ [HANDLED_ERROR_PROP_STR]: 'Github: Api request failed.' }); } } diff --git a/src/app/features/issue/providers/github/github-common-interfaces.service.ts b/src/app/features/issue/providers/github/github-common-interfaces.service.ts index 8c8e88f92..e97f3c228 100644 --- a/src/app/features/issue/providers/github/github-common-interfaces.service.ts +++ b/src/app/features/issue/providers/github/github-common-interfaces.service.ts @@ -20,27 +20,29 @@ export class GithubCommonInterfacesService implements IssueServiceInterface { private readonly _githubApiService: GithubApiService, private readonly _projectService: ProjectService, private readonly _snackService: SnackService, - ) { - } + ) {} issueLink$(issueId: number, projectId: string): Observable { return this._getCfgOnce$(projectId).pipe( - map((cfg) => `https://github.com/${cfg.repo}/issues/${issueId}`) + map((cfg) => `https://github.com/${cfg.repo}/issues/${issueId}`), ); } getById$(issueId: number, projectId: string) { return this._getCfgOnce$(projectId).pipe( - concatMap(githubCfg => this._githubApiService.getById$(issueId, githubCfg)) + concatMap((githubCfg) => this._githubApiService.getById$(issueId, githubCfg)), ); } searchIssues$(searchTerm: string, projectId: string): Observable { return this._getCfgOnce$(projectId).pipe( - switchMap((githubCfg) => (githubCfg && githubCfg.isSearchIssuesFromGithub) - ? this._githubApiService.searchIssueForRepo$(searchTerm, githubCfg).pipe(catchError(() => [])) - : of([]) - ) + switchMap((githubCfg) => + githubCfg && githubCfg.isSearchIssuesFromGithub + ? this._githubApiService + .searchIssueForRepo$(searchTerm, githubCfg) + .pipe(catchError(() => [])) + : of([]), + ), ); } @@ -61,13 +63,16 @@ export class GithubCommonInterfacesService implements IssueServiceInterface { // const issueUpdate: number = new Date(issue.updated_at).getTime(); const filterUserName = cfg.filterUsername && cfg.filterUsername.toLowerCase(); - const commentsByOthers = (filterUserName && filterUserName.length > 1) - ? issue.comments.filter(comment => comment.user.login.toLowerCase() !== cfg.filterUsername) - : issue.comments; + const commentsByOthers = + filterUserName && filterUserName.length > 1 + ? issue.comments.filter( + (comment) => comment.user.login.toLowerCase() !== cfg.filterUsername, + ) + : issue.comments; // TODO: we also need to handle the case when the user himself updated the issue, to also update the issue... const updates: number[] = [ - ...(commentsByOthers.map(comment => new Date(comment.created_at).getTime())), + ...commentsByOthers.map((comment) => new Date(comment.created_at).getTime()), // todo check if this can be re-implemented // issueUpdate ].sort(); @@ -86,7 +91,7 @@ export class GithubCommonInterfacesService implements IssueServiceInterface { this._snackService.open({ ico: 'cloud_download', translateParams: { - issueText: this._formatIssueTitleForSnack(issue.number, issue.title) + issueText: this._formatIssueTitleForSnack(issue.number, issue.title), }, msg: T.F.GITHUB.S.ISSUE_UPDATE, }); @@ -110,14 +115,16 @@ export class GithubCommonInterfacesService implements IssueServiceInterface { return null; } - getAddTaskData(issue: GithubIssueReduced): { title: string; additionalFields: Partial } { + getAddTaskData( + issue: GithubIssueReduced, + ): { title: string; additionalFields: Partial } { return { title: this._formatIssueTitle(issue.number, issue.title), additionalFields: { // issueWasUpdated: false, // NOTE: we use Date.now() instead to because updated does not account for comments // issueLastUpdated: new Date(issue.updated_at).getTime() - } + }, }; } diff --git a/src/app/features/issue/providers/github/github-issue/github-issue-content/github-issue-content.component.ts b/src/app/features/issue/providers/github/github-issue/github-issue-content/github-issue-content.component.ts index 7d3bed3cb..91a2c8d04 100644 --- a/src/app/features/issue/providers/github/github-issue/github-issue-content/github-issue-content.component.ts +++ b/src/app/features/issue/providers/github/github-issue/github-issue-content/github-issue-content.component.ts @@ -10,7 +10,7 @@ import { TaskService } from '../../../../../tasks/task.service'; templateUrl: './github-issue-content.component.html', styleUrls: ['./github-issue-content.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation] + animations: [expandAnimation], }) export class GithubIssueContentComponent { @Input() issue?: GithubIssue; @@ -18,10 +18,7 @@ export class GithubIssueContentComponent { T: typeof T = T; - constructor( - private readonly _taskService: TaskService, - ) { - } + constructor(private readonly _taskService: TaskService) {} hideUpdates() { if (!this.task) { diff --git a/src/app/features/issue/providers/github/github-issue/github-issue-header/github-issue-header.component.ts b/src/app/features/issue/providers/github/github-issue/github-issue-header/github-issue-header.component.ts index 43fdf25c3..1e5bbb664 100644 --- a/src/app/features/issue/providers/github/github-issue/github-issue-header/github-issue-header.component.ts +++ b/src/app/features/issue/providers/github/github-issue/github-issue-header/github-issue-header.component.ts @@ -6,12 +6,11 @@ import { T } from '../../../../../../t.const'; selector: 'github-issue-header', templateUrl: './github-issue-header.component.html', styleUrls: ['./github-issue-header.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class GithubIssueHeaderComponent { T: typeof T = T; @Input() task?: TaskWithSubTasks; - constructor() { - } + constructor() {} } diff --git a/src/app/features/issue/providers/github/github-issue/github-issue-map.util.ts b/src/app/features/issue/providers/github/github-issue/github-issue-map.util.ts index 0a0d93ab1..8c96da042 100644 --- a/src/app/features/issue/providers/github/github-issue/github-issue-map.util.ts +++ b/src/app/features/issue/providers/github/github-issue/github-issue-map.util.ts @@ -34,7 +34,7 @@ export const mapGithubIssue = (issue: GithubOriginalIssue): GithubIssue => { apiUrl: issue.url, // transformed - comments: [] + comments: [], }; }; @@ -45,4 +45,3 @@ export const mapGithubIssueToSearchResult = (issue: GithubIssue): SearchResultIt issueData: issue, }; }; - diff --git a/src/app/features/issue/providers/github/github-issue/github-issue.effects.ts b/src/app/features/issue/providers/github/github-issue/github-issue.effects.ts index a6eacd85d..920a53465 100644 --- a/src/app/features/issue/providers/github/github-issue/github-issue.effects.ts +++ b/src/app/features/issue/providers/github/github-issue/github-issue.effects.ts @@ -4,7 +4,15 @@ import { GithubApiService } from '../github-api.service'; import { SnackService } from '../../../../../core/snack/snack.service'; import { TaskService } from '../../../../tasks/task.service'; import { ProjectService } from '../../../../project/project.service'; -import { filter, first, map, switchMap, takeUntil, tap, withLatestFrom } from 'rxjs/operators'; +import { + filter, + first, + map, + switchMap, + takeUntil, + tap, + withLatestFrom, +} from 'rxjs/operators'; import { IssueService } from '../../../issue.service'; import { forkJoin, Observable, timer } from 'rxjs'; import { GITHUB_INITIAL_POLL_DELAY, GITHUB_POLL_INTERVAL } from '../github.const'; @@ -19,63 +27,81 @@ import { IssueEffectHelperService } from '../../../issue-effect-helper.service'; @Injectable() export class GithubIssueEffects { - - private _pollTimer$: Observable = timer(GITHUB_INITIAL_POLL_DELAY, GITHUB_POLL_INTERVAL); - @Effect({dispatch: false}) + private _pollTimer$: Observable = timer( + GITHUB_INITIAL_POLL_DELAY, + GITHUB_POLL_INTERVAL, + ); + @Effect({ dispatch: false }) pollNewIssuesToBacklog$: Observable = this._issueEffectHelperService.pollToBacklogTriggerToProjectId$.pipe( - switchMap((pId) => this._projectService.getGithubCfgForProject$(pId).pipe( - first(), - filter(githubCfg => isGithubEnabled(githubCfg) && githubCfg.isAutoAddToBacklog), - switchMap(githubCfg => this._pollTimer$.pipe( - // NOTE: required otherwise timer stays alive for filtered actions - takeUntil(this._issueEffectHelperService.pollToBacklogActions$), - tap(() => console.log('GITHUB_POLL_BACKLOG_CHANGES')), - withLatestFrom( - this._githubApiService.getLast100IssuesForRepo$(githubCfg), - this._taskService.getAllIssueIdsForProject(pId, GITHUB_TYPE) as Promise + switchMap((pId) => + this._projectService.getGithubCfgForProject$(pId).pipe( + first(), + filter((githubCfg) => isGithubEnabled(githubCfg) && githubCfg.isAutoAddToBacklog), + switchMap((githubCfg) => + this._pollTimer$.pipe( + // NOTE: required otherwise timer stays alive for filtered actions + takeUntil(this._issueEffectHelperService.pollToBacklogActions$), + tap(() => console.log('GITHUB_POLL_BACKLOG_CHANGES')), + withLatestFrom( + this._githubApiService.getLast100IssuesForRepo$(githubCfg), + this._taskService.getAllIssueIdsForProject(pId, GITHUB_TYPE) as Promise< + number[] + >, + ), + tap( + ([, issues, allTaskGithubIssueIds]: [ + any, + GithubIssueReduced[], + number[], + ]) => { + const issuesToAdd = issues.filter( + (issue) => !allTaskGithubIssueIds.includes(issue.id), + ); + console.log('issuesToAdd', issuesToAdd); + if (issuesToAdd?.length) { + this._importNewIssuesToBacklog(pId, issuesToAdd); + } + }, + ), + ), ), - tap(([, issues, allTaskGithubIssueIds]: [any, GithubIssueReduced[], number[]]) => { - const issuesToAdd = issues.filter(issue => !allTaskGithubIssueIds.includes(issue.id)); - console.log('issuesToAdd', issuesToAdd); - if (issuesToAdd?.length) { - this._importNewIssuesToBacklog(pId, issuesToAdd); - } - }) - )), - )), + ), + ), ); private _updateIssuesForCurrentContext$: Observable = this._workContextService.allTasksForCurrentContext$.pipe( first(), switchMap((tasks) => { - const gitIssueTasks = tasks.filter(task => task.issueType === GITHUB_TYPE); - return forkJoin(gitIssueTasks.map(task => { + const gitIssueTasks = tasks.filter((task) => task.issueType === GITHUB_TYPE); + return forkJoin( + gitIssueTasks.map((task) => { if (!task.projectId) { throw new Error('No project for task'); } return this._projectService.getGithubCfgForProject$(task.projectId).pipe( first(), - map(cfg => ({ + map((cfg) => ({ cfg, task, - })) + })), ); - }) + }), ); }), - map((cos) => cos - .filter(({cfg, task}: { cfg: GithubCfg; task: TaskWithSubTasks }): boolean => - isGithubEnabled(cfg) && cfg.isAutoPoll - ) - .map(({task}: { cfg: GithubCfg; task: TaskWithSubTasks }) => task) + map((cos) => + cos + .filter( + ({ cfg, task }: { cfg: GithubCfg; task: TaskWithSubTasks }): boolean => + isGithubEnabled(cfg) && cfg.isAutoPoll, + ) + .map(({ task }: { cfg: GithubCfg; task: TaskWithSubTasks }) => task), ), tap((githubTasks: TaskWithSubTasks[]) => this._refreshIssues(githubTasks)), ); - @Effect({dispatch: false}) - pollIssueChangesForCurrentContext$: Observable = this._issueEffectHelperService.pollIssueTaskUpdatesActions$ - .pipe( - switchMap(() => this._pollTimer$), - switchMap(() => this._updateIssuesForCurrentContext$), - ); + @Effect({ dispatch: false }) + pollIssueChangesForCurrentContext$: Observable = this._issueEffectHelperService.pollIssueTaskUpdatesActions$.pipe( + switchMap(() => this._pollTimer$), + switchMap(() => this._updateIssuesForCurrentContext$), + ); constructor( private readonly _snackService: SnackService, @@ -85,8 +111,7 @@ export class GithubIssueEffects { private readonly _taskService: TaskService, private readonly _workContextService: WorkContextService, private readonly _issueEffectHelperService: IssueEffectHelperService, - ) { - } + ) {} private _refreshIssues(githubTasks: TaskWithSubTasks[]) { if (githubTasks && githubTasks.length > 0) { @@ -99,7 +124,10 @@ export class GithubIssueEffects { } } - private _importNewIssuesToBacklog(projectId: string, issuesToAdd: GithubIssueReduced[]) { + private _importNewIssuesToBacklog( + projectId: string, + issuesToAdd: GithubIssueReduced[], + ) { issuesToAdd.forEach((issue) => { this._issueService.addTaskWithIssue(GITHUB_TYPE, issue, projectId, true); }); @@ -108,7 +136,7 @@ export class GithubIssueEffects { this._snackService.open({ ico: 'cloud_download', translateParams: { - issueText: `#${issuesToAdd[0].number} ${issuesToAdd[0].title}` + issueText: `#${issuesToAdd[0].number} ${issuesToAdd[0].title}`, }, msg: T.F.GITHUB.S.IMPORTED_SINGLE_ISSUE, }); @@ -116,11 +144,10 @@ export class GithubIssueEffects { this._snackService.open({ ico: 'cloud_download', translateParams: { - issuesLength: issuesToAdd.length + issuesLength: issuesToAdd.length, }, msg: T.F.GITHUB.S.IMPORTED_MULTIPLE_ISSUES, }); } } } - diff --git a/src/app/features/issue/providers/github/github-issue/github-issue.model.ts b/src/app/features/issue/providers/github/github-issue/github-issue.model.ts index ee241446a..803477c79 100644 --- a/src/app/features/issue/providers/github/github-issue/github-issue.model.ts +++ b/src/app/features/issue/providers/github/github-issue/github-issue.model.ts @@ -4,7 +4,7 @@ import { GithubOriginalMileStone, GithubOriginalPullRequest, GithubOriginalState, - GithubOriginalUser + GithubOriginalUser, } from '../github-api-responses'; export type GithubState = GithubOriginalState; @@ -52,7 +52,7 @@ export type GithubIssueReduced = Readonly<{ // repository: GithubOriginalRepository; }>; -export type GithubIssue = GithubIssueReduced & Readonly<{ - comments: GithubComment[]; -}>; - +export type GithubIssue = GithubIssueReduced & + Readonly<{ + comments: GithubComment[]; + }>; diff --git a/src/app/features/issue/providers/github/github-issue/github-issue.module.ts b/src/app/features/issue/providers/github/github-issue/github-issue.module.ts index 7ca10f636..addac9cfa 100644 --- a/src/app/features/issue/providers/github/github-issue/github-issue.module.ts +++ b/src/app/features/issue/providers/github/github-issue/github-issue.module.ts @@ -15,14 +15,7 @@ import { GithubIssueEffects } from './github-issue.effects'; ReactiveFormsModule, EffectsModule.forFeature([GithubIssueEffects]), ], - declarations: [ - GithubIssueHeaderComponent, - GithubIssueContentComponent, - ], - exports: [ - GithubIssueHeaderComponent, - GithubIssueContentComponent, - ], + declarations: [GithubIssueHeaderComponent, GithubIssueContentComponent], + exports: [GithubIssueHeaderComponent, GithubIssueContentComponent], }) -export class GithubIssueModule { -} +export class GithubIssueModule {} diff --git a/src/app/features/issue/providers/github/github-view-components/dialog-github-initial-setup/dialog-github-initial-setup.component.ts b/src/app/features/issue/providers/github/github-view-components/dialog-github-initial-setup/dialog-github-initial-setup.component.ts index ca7a0bc48..4c3bc55fb 100644 --- a/src/app/features/issue/providers/github/github-view-components/dialog-github-initial-setup/dialog-github-initial-setup.component.ts +++ b/src/app/features/issue/providers/github/github-view-components/dialog-github-initial-setup/dialog-github-initial-setup.component.ts @@ -10,7 +10,7 @@ import { T } from '../../../../../../t.const'; selector: 'dialog-github-initial-setup', templateUrl: './dialog-github-initial-setup.component.html', styleUrls: ['./dialog-github-initial-setup.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogGithubInitialSetupComponent implements OnInit { T: typeof T = T; @@ -25,8 +25,7 @@ export class DialogGithubInitialSetupComponent implements OnInit { this.githubCfg = this.data.githubCfg || DEFAULT_GITHUB_CFG; } - ngOnInit() { - } + ngOnInit() {} saveGithubCfg(gitCfg: GithubCfg) { this._matDialogRef.close(gitCfg); diff --git a/src/app/features/issue/providers/github/github-view-components/github-view-components.module.ts b/src/app/features/issue/providers/github/github-view-components/github-view-components.module.ts index 433fb4d5c..01deabe53 100644 --- a/src/app/features/issue/providers/github/github-view-components/github-view-components.module.ts +++ b/src/app/features/issue/providers/github/github-view-components/github-view-components.module.ts @@ -4,12 +4,8 @@ import { UiModule } from '../../../../../ui/ui.module'; import { DialogGithubInitialSetupComponent } from './dialog-github-initial-setup/dialog-github-initial-setup.component'; @NgModule({ - imports: [ - CommonModule, - UiModule, - ], + imports: [CommonModule, UiModule], declarations: [DialogGithubInitialSetupComponent], exports: [], }) -export class GithubViewComponentsModule { -} +export class GithubViewComponentsModule {} diff --git a/src/app/features/issue/providers/github/github.const.ts b/src/app/features/issue/providers/github/github.const.ts index 145edb07e..7b8fef9ec 100644 --- a/src/app/features/issue/providers/github/github.const.ts +++ b/src/app/features/issue/providers/github/github.const.ts @@ -1,7 +1,10 @@ // TODO use as a checklist import { GithubCfg } from './github.model'; import { T } from '../../../../t.const'; -import { ConfigFormSection, LimitedFormlyFieldConfig } from '../../../config/global-config.model'; +import { + ConfigFormSection, + LimitedFormlyFieldConfig, +} from '../../../config/global-config.model'; export const DEFAULT_GITHUB_CFG: GithubCfg = { repo: null, @@ -36,35 +39,35 @@ export const GITHUB_CONFIG_FORM: LimitedFormlyFieldConfig[] = [ type: 'input', templateOptions: { label: T.F.GITHUB.FORM.TOKEN, - description: T.F.GITHUB.FORM.TOKEN_DESCRIPTION + description: T.F.GITHUB.FORM.TOKEN_DESCRIPTION, }, }, { key: 'isSearchIssuesFromGithub', type: 'checkbox', templateOptions: { - label: T.F.GITHUB.FORM.IS_SEARCH_ISSUES_FROM_GITHUB + label: T.F.GITHUB.FORM.IS_SEARCH_ISSUES_FROM_GITHUB, }, }, { key: 'isAutoPoll', type: 'checkbox', templateOptions: { - label: T.F.GITHUB.FORM.IS_AUTO_POLL + label: T.F.GITHUB.FORM.IS_AUTO_POLL, }, }, { key: 'isAutoAddToBacklog', type: 'checkbox', templateOptions: { - label: T.F.GITHUB.FORM.IS_AUTO_ADD_TO_BACKLOG + label: T.F.GITHUB.FORM.IS_AUTO_ADD_TO_BACKLOG, }, }, { key: 'filterUsername', type: 'input', templateOptions: { - label: T.F.GITHUB.FORM.FILTER_USER + label: T.F.GITHUB.FORM.FILTER_USER, }, }, ]; diff --git a/src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.component.ts b/src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.component.ts index 17941f238..f5dfa1cde 100644 --- a/src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.component.ts +++ b/src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.component.ts @@ -10,7 +10,7 @@ import { DEFAULT_GITLAB_CFG, GITLAB_CONFIG_FORM } from '../gitlab.const'; selector: 'dialog-gitlab-initial-setup', templateUrl: './dialog-gitlab-initial-setup.component.html', styleUrls: ['./dialog-gitlab-initial-setup.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogGitlabInitialSetupComponent implements OnInit { T: typeof T = T; @@ -25,8 +25,7 @@ export class DialogGitlabInitialSetupComponent implements OnInit { this.gitlabCfg = this.data.gitlabCfg || DEFAULT_GITLAB_CFG; } - ngOnInit() { - } + ngOnInit() {} saveGitlabCfg(gitlabCfg: GitlabCfg) { this._matDialogRef.close(gitlabCfg); diff --git a/src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.module.ts b/src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.module.ts index 19b80350c..220900e4b 100644 --- a/src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.module.ts +++ b/src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.module.ts @@ -4,12 +4,8 @@ import { UiModule } from '../../../../../ui/ui.module'; import { DialogGitlabInitialSetupComponent } from './dialog-gitlab-initial-setup.component'; @NgModule({ - imports: [ - CommonModule, - UiModule, - ], + imports: [CommonModule, UiModule], declarations: [DialogGitlabInitialSetupComponent], exports: [], }) -export class DialogGitlabInitialSetupModule { -} +export class DialogGitlabInitialSetupModule {} diff --git a/src/app/features/issue/providers/gitlab/gitlab-api/gitlab-api.service.ts b/src/app/features/issue/providers/gitlab/gitlab-api/gitlab-api.service.ts index e9fa9bd20..3d449fbdf 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-api/gitlab-api.service.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-api/gitlab-api.service.ts @@ -1,5 +1,11 @@ import { Injectable } from '@angular/core'; -import { HttpClient, HttpErrorResponse, HttpHeaders, HttpParams, HttpRequest } from '@angular/common/http'; +import { + HttpClient, + HttpErrorResponse, + HttpHeaders, + HttpParams, + HttpRequest, +} from '@angular/common/http'; import { EMPTY, forkJoin, Observable, ObservableInput, of, throwError } from 'rxjs'; import { SnackService } from 'src/app/core/snack/snack.service'; @@ -10,45 +16,45 @@ import { GITLAB_API_BASE_URL, GITLAB_PROJECT_REGEX } from '../gitlab.const'; import { T } from 'src/app/t.const'; import { catchError, filter, map, mergeMap, take } from 'rxjs/operators'; import { GitlabIssue } from '../gitlab-issue/gitlab-issue.model'; -import { mapGitlabIssue, mapGitlabIssueToSearchResult } from '../gitlab-issue/gitlab-issue-map.util'; +import { + mapGitlabIssue, + mapGitlabIssueToSearchResult, +} from '../gitlab-issue/gitlab-issue-map.util'; import { SearchResultItem } from '../../../issue.model'; @Injectable({ providedIn: 'root', }) export class GitlabApiService { - constructor( - private _snackService: SnackService, - private _http: HttpClient, - ) { - } + constructor(private _snackService: SnackService, private _http: HttpClient) {} getProjectData$(cfg: GitlabCfg): Observable { if (!this._isValidSettings(cfg)) { return EMPTY; } return this._getProjectIssues$(1, cfg).pipe( - mergeMap( - (issues: GitlabIssue[]) => { - if (issues && issues.length) { - return forkJoin([ - ...issues.map(issue => this.getIssueWithComments$(issue, cfg)) - ]); - } else { - return of([]); - } - }), + mergeMap((issues: GitlabIssue[]) => { + if (issues && issues.length) { + return forkJoin([ + ...issues.map((issue) => this.getIssueWithComments$(issue, cfg)), + ]); + } else { + return of([]); + } + }), ); } getById$(id: number, cfg: GitlabCfg): Observable { - return this._sendRequest$({ - url: `${this.apiLink(cfg)}/issues/${id}` - }, cfg).pipe( - mergeMap( - (issue: GitlabOriginalIssue) => { - return this.getIssueWithComments$(mapGitlabIssue(issue), cfg); - }) + return this._sendRequest$( + { + url: `${this.apiLink(cfg)}/issues/${id}`, + }, + cfg, + ).pipe( + mergeMap((issue: GitlabOriginalIssue) => { + return this.getIssueWithComments$(mapGitlabIssue(issue), cfg); + }), ); } @@ -61,66 +67,82 @@ export class GitlabApiService { queryParams += `${ids[i]}&iids[]=`; } } - return this._sendRequest$({ - url: `${this.apiLink(cfg)}/issues?${queryParams}&per_page=100` - }, cfg).pipe( + return this._sendRequest$( + { + url: `${this.apiLink(cfg)}/issues?${queryParams}&per_page=100`, + }, + cfg, + ).pipe( map((issues: GitlabOriginalIssue[]) => { return issues ? issues.map(mapGitlabIssue) : []; }), mergeMap((issues: GitlabIssue[]) => { if (issues && issues.length) { return forkJoin([ - ...issues.map(issue => this.getIssueWithComments$(issue, cfg)) + ...issues.map((issue) => this.getIssueWithComments$(issue, cfg)), ]); } else { return of([]); } - }) + }), ); } getIssueWithComments$(issue: GitlabIssue, cfg: GitlabCfg): Observable { return this._getIssueComments$(issue.id, 1, cfg).pipe( map((comments) => { - return { - ...issue, - comments, - commentsNr: comments.length, - }; - } - )); - } - - searchIssueInProject$(searchText: string, cfg: GitlabCfg): Observable { - if (!this._isValidSettings(cfg)) { - return EMPTY; - } - return this._sendRequest$({ - url: `${this.apiLink(cfg)}/issues?search=${searchText}&order_by=updated_at` - }, cfg).pipe( - map((issues: GitlabOriginalIssue[]) => { - return issues ? issues.map(mapGitlabIssue) : []; + return { + ...issue, + comments, + commentsNr: comments.length, + }; }), - mergeMap( - (issues: GitlabIssue[]) => { - if (issues && issues.length) { - return forkJoin([ - ...issues.map(issue => this.getIssueWithComments$(issue, cfg)) - ]); - } else { - return of([]); - } - }), - map((issues: GitlabIssue[]) => { - return issues ? issues.map(mapGitlabIssueToSearchResult) : []; - }) ); } - private _getProjectIssues$(pageNumber: number, cfg: GitlabCfg): Observable { - return this._sendRequest$({ - url: `${this.apiLink(cfg)}/issues?state=opened&order_by=updated_at&per_page=100&page=${pageNumber}` - }, cfg).pipe( + searchIssueInProject$( + searchText: string, + cfg: GitlabCfg, + ): Observable { + if (!this._isValidSettings(cfg)) { + return EMPTY; + } + return this._sendRequest$( + { + url: `${this.apiLink(cfg)}/issues?search=${searchText}&order_by=updated_at`, + }, + cfg, + ).pipe( + map((issues: GitlabOriginalIssue[]) => { + return issues ? issues.map(mapGitlabIssue) : []; + }), + mergeMap((issues: GitlabIssue[]) => { + if (issues && issues.length) { + return forkJoin([ + ...issues.map((issue) => this.getIssueWithComments$(issue, cfg)), + ]); + } else { + return of([]); + } + }), + map((issues: GitlabIssue[]) => { + return issues ? issues.map(mapGitlabIssueToSearchResult) : []; + }), + ); + } + + private _getProjectIssues$( + pageNumber: number, + cfg: GitlabCfg, + ): Observable { + return this._sendRequest$( + { + url: `${this.apiLink( + cfg, + )}/issues?state=opened&order_by=updated_at&per_page=100&page=${pageNumber}`, + }, + cfg, + ).pipe( take(1), map((issues: GitlabOriginalIssue[]) => { return issues ? issues.map(mapGitlabIssue) : []; @@ -132,9 +154,14 @@ export class GitlabApiService { if (!this._isValidSettings(cfg)) { return EMPTY; } - return this._sendRequest$({ - url: `${this.apiLink(cfg)}/issues/${issueid}/notes?per_page=100&page=${pageNumber}`, - }, cfg).pipe( + return this._sendRequest$( + { + url: `${this.apiLink( + cfg, + )}/issues/${issueid}/notes?per_page=100&page=${pageNumber}`, + }, + cfg, + ).pipe( map((comments: GitlabOriginalComment[]) => { return comments ? comments : []; }), @@ -147,46 +174,51 @@ export class GitlabApiService { } this._snackService.open({ type: 'ERROR', - msg: T.F.GITLAB.S.ERR_NOT_CONFIGURED + msg: T.F.GITLAB.S.ERR_NOT_CONFIGURED, }); return false; } - private _sendRequest$(params: HttpRequest | any, cfg: GitlabCfg): Observable { + private _sendRequest$( + params: HttpRequest | any, + cfg: GitlabCfg, + ): Observable { this._isValidSettings(cfg); const p: HttpRequest | any = { ...params, method: params.method || 'GET', headers: { - ...(cfg.token ? {Authorization: 'Bearer ' + cfg.token} : {}), + ...(cfg.token ? { Authorization: 'Bearer ' + cfg.token } : {}), ...(params.headers ? params.headers : {}), - } + }, }; - const bodyArg = params.data - ? [params.data] - : []; + const bodyArg = params.data ? [params.data] : []; - const allArgs = [...bodyArg, { - headers: new HttpHeaders(p.headers), - params: new HttpParams({fromObject: p.params}), - reportProgress: false, - observe: 'response', - responseType: params.responseType, - }]; + const allArgs = [ + ...bodyArg, + { + headers: new HttpHeaders(p.headers), + params: new HttpParams({ fromObject: p.params }), + reportProgress: false, + observe: 'response', + responseType: params.responseType, + }, + ]; const req = new HttpRequest(p.method, p.url, ...allArgs); return this._http.request(req).pipe( // TODO remove type: 0 @see https://brianflove.com/2018/09/03/angular-http-client-observe-response/ - filter(res => !(res === Object(res) && res.type === 0)), - map((res: any) => (res && res.body) - ? res.body - : res), + filter((res) => !(res === Object(res) && res.type === 0)), + map((res: any) => (res && res.body ? res.body : res)), catchError(this._handleRequestError$.bind(this)), ); } - private _handleRequestError$(error: HttpErrorResponse, caught: Observable): ObservableInput { + private _handleRequestError$( + error: HttpErrorResponse, + caught: Observable, + ): ObservableInput { console.error(error); if (error.error instanceof ErrorEvent) { // A client-side or network error occurred. Handle it accordingly. @@ -206,23 +238,25 @@ export class GitlabApiService { }); } if (error && error.message) { - return throwError({[HANDLED_ERROR_PROP_STR]: 'Gitlab: ' + error.message}); + return throwError({ [HANDLED_ERROR_PROP_STR]: 'Gitlab: ' + error.message }); } - return throwError({[HANDLED_ERROR_PROP_STR]: 'Gitlab: Api request failed.'}); + return throwError({ [HANDLED_ERROR_PROP_STR]: 'Gitlab: Api request failed.' }); } private apiLink(projectConfig: GitlabCfg): string { let apiURL: string = ''; let projectURL: string = projectConfig.project ? projectConfig.project : ''; if (projectConfig.gitlabBaseUrl) { - const fixedUrl = projectConfig.gitlabBaseUrl.match(/.*\/$/) ? projectConfig.gitlabBaseUrl : `${projectConfig.gitlabBaseUrl}/`; + const fixedUrl = projectConfig.gitlabBaseUrl.match(/.*\/$/) + ? projectConfig.gitlabBaseUrl + : `${projectConfig.gitlabBaseUrl}/`; apiURL = fixedUrl + 'api/v4/projects/'; } else { apiURL = GITLAB_API_BASE_URL + '/'; } const projectPath = projectURL.match(GITLAB_PROJECT_REGEX); if (projectPath) { - projectURL = projectURL.replace(/\//ig, '%2F'); + projectURL = projectURL.replace(/\//gi, '%2F'); } else { // Should never enter here throwError('Gitlab Project URL'); diff --git a/src/app/features/issue/providers/gitlab/gitlab-common-interfaces.service.ts b/src/app/features/issue/providers/gitlab/gitlab-common-interfaces.service.ts index e5e849a21..f44f52264 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-common-interfaces.service.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-common-interfaces.service.ts @@ -21,41 +21,48 @@ export class GitlabCommonInterfacesService implements IssueServiceInterface { private readonly _gitlabApiService: GitlabApiService, private readonly _projectService: ProjectService, private readonly _snackService: SnackService, - ) { - } + ) {} issueLink$(issueId: number, projectId: string): Observable { return this._getCfgOnce$(projectId).pipe( map((cfg) => { if (cfg.gitlabBaseUrl) { - const fixedUrl = cfg.gitlabBaseUrl.match(/.*\/$/) ? cfg.gitlabBaseUrl : `${cfg.gitlabBaseUrl}/`; + const fixedUrl = cfg.gitlabBaseUrl.match(/.*\/$/) + ? cfg.gitlabBaseUrl + : `${cfg.gitlabBaseUrl}/`; return `${fixedUrl}${cfg.project}issues/${issueId}`; } else { - return `${GITLAB_BASE_URL}${cfg.project?.replace(/%2F/g, '/')}/issues/${issueId}`; + return `${GITLAB_BASE_URL}${cfg.project?.replace( + /%2F/g, + '/', + )}/issues/${issueId}`; } - }) + }), ); } getById$(issueId: number, projectId: string) { return this._getCfgOnce$(projectId).pipe( - concatMap(gitlabCfg => this._gitlabApiService.getById$(issueId, gitlabCfg)), + concatMap((gitlabCfg) => this._gitlabApiService.getById$(issueId, gitlabCfg)), ); } searchIssues$(searchTerm: string, projectId: string): Observable { return this._getCfgOnce$(projectId).pipe( - switchMap((gitlabCfg) => (gitlabCfg && gitlabCfg.isSearchIssuesFromGitlab) - ? this._gitlabApiService.searchIssueInProject$(searchTerm, gitlabCfg).pipe(catchError(() => [])) - : of([]) - ) + switchMap((gitlabCfg) => + gitlabCfg && gitlabCfg.isSearchIssuesFromGitlab + ? this._gitlabApiService + .searchIssueInProject$(searchTerm, gitlabCfg) + .pipe(catchError(() => [])) + : of([]), + ), ); } async refreshIssue( task: Task, isNotifySuccess: boolean = true, - isNotifyNoUpdateRequired: boolean = false + isNotifyNoUpdateRequired: boolean = false, ): Promise<{ taskChanges: Partial; issue: GitlabIssue } | null> { if (!task.projectId) { throw new Error('No projectId'); @@ -68,14 +75,17 @@ export class GitlabCommonInterfacesService implements IssueServiceInterface { const issue = await this._gitlabApiService.getById$(+task.issueId, cfg).toPromise(); const issueUpdate: number = new Date(issue.updated_at).getTime(); - const commentsByOthers = (cfg.filterUsername && cfg.filterUsername.length > 1) - ? issue.comments.filter(comment => comment.author.username !== cfg.filterUsername) - : issue.comments; + const commentsByOthers = + cfg.filterUsername && cfg.filterUsername.length > 1 + ? issue.comments.filter( + (comment) => comment.author.username !== cfg.filterUsername, + ) + : issue.comments; // TODO: we also need to handle the case when the user himself updated the issue, to also update the issue... const updates: number[] = [ - ...(commentsByOthers.map(comment => new Date(comment.created_at).getTime())), - issueUpdate + ...commentsByOthers.map((comment) => new Date(comment.created_at).getTime()), + issueUpdate, ].sort(); const lastRemoteUpdate = updates[updates.length - 1]; @@ -85,7 +95,7 @@ export class GitlabCommonInterfacesService implements IssueServiceInterface { this._snackService.open({ ico: 'cloud_download', translateParams: { - issueText: this._formatIssueTitleForSnack(issue.number, issue.title) + issueText: this._formatIssueTitleForSnack(issue.number, issue.title), }, msg: T.F.GITLAB.S.ISSUE_UPDATE, }); @@ -112,7 +122,7 @@ export class GitlabCommonInterfacesService implements IssueServiceInterface { async refreshIssues( tasks: Task[], isNotifySuccess: boolean = true, - isNotifyNoUpdateRequired: boolean = false + isNotifyNoUpdateRequired: boolean = false, ): Promise<{ task: Task; taskChanges: Partial; issue: GitlabIssue }[]> { // First sort the tasks by the issueId // because the API returns it in a desc order by issue iid(issueId) @@ -133,20 +143,29 @@ export class GitlabCommonInterfacesService implements IssueServiceInterface { for (let j = 0; j < paramsCount && i < tasks.length; j++, i++) { ids.push(tasks[i].issueId); } - issues.push(...(await this._gitlabApiService.getByIds$(ids as string[], cfg).toPromise())); + issues.push( + ...(await this._gitlabApiService.getByIds$(ids as string[], cfg).toPromise()), + ); } - const updatedIssues: { task: Task; taskChanges: Partial; issue: GitlabIssue }[] = []; + const updatedIssues: { + task: Task; + taskChanges: Partial; + issue: GitlabIssue; + }[] = []; for (i = 0; i < tasks.length; i++) { const issueUpdate: number = new Date(issues[i].updated_at).getTime(); - const commentsByOthers = (cfg.filterUsername && cfg.filterUsername.length > 1) - ? issues[i].comments.filter(comment => comment.author.username !== cfg.filterUsername) - : issues[i].comments; + const commentsByOthers = + cfg.filterUsername && cfg.filterUsername.length > 1 + ? issues[i].comments.filter( + (comment) => comment.author.username !== cfg.filterUsername, + ) + : issues[i].comments; const updates: number[] = [ - ...(commentsByOthers.map(comment => new Date(comment.created_at).getTime())), - issueUpdate + ...commentsByOthers.map((comment) => new Date(comment.created_at).getTime()), + issueUpdate, ].sort(); const lastRemoteUpdate = updates[updates.length - 1]; const wasUpdated = lastRemoteUpdate > (tasks[i].issueLastUpdated || 0); @@ -167,7 +186,7 @@ export class GitlabCommonInterfacesService implements IssueServiceInterface { this._snackService.open({ ico: 'cloud_download', translateParams: { - issueText: this._formatIssueTitleForSnack(issues[i].number, issues[i].title) + issueText: this._formatIssueTitleForSnack(issues[i].number, issues[i].title), }, msg: T.F.GITLAB.S.ISSUE_UPDATE, }); @@ -181,12 +200,14 @@ export class GitlabCommonInterfacesService implements IssueServiceInterface { return updatedIssues; } - getAddTaskData(issue: GitlabIssue): { title: string; additionalFields: Partial } { + getAddTaskData( + issue: GitlabIssue, + ): { title: string; additionalFields: Partial } { return { title: this._formatIssueTitle(issue.number, issue.title), additionalFields: { - issuePoints: issue.weight - } + issuePoints: issue.weight, + }, }; } diff --git a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-content/gitlab-issue-content.component.ts b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-content/gitlab-issue-content.component.ts index 46a311fc8..f67aa9049 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-content/gitlab-issue-content.component.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-content/gitlab-issue-content.component.ts @@ -10,7 +10,7 @@ import { TaskService } from '../../../../../tasks/task.service'; templateUrl: './gitlab-issue-content.component.html', styleUrls: ['./gitlab-issue-content.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation] + animations: [expandAnimation], }) export class GitlabIssueContentComponent { @Input() issue?: GitlabIssue; @@ -18,10 +18,7 @@ export class GitlabIssueContentComponent { T: typeof T = T; - constructor( - private readonly _taskService: TaskService, - ) { - } + constructor(private readonly _taskService: TaskService) {} hideUpdates() { this._taskService.markIssueUpdatesAsRead((this.task as TaskWithSubTasks).id); diff --git a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-header/gitlab-issue-header.component.ts b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-header/gitlab-issue-header.component.ts index 464cdba9b..6f14c1e73 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-header/gitlab-issue-header.component.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-header/gitlab-issue-header.component.ts @@ -6,12 +6,11 @@ import { TaskWithSubTasks } from 'src/app/features/tasks/task.model'; selector: 'gitlab-issue-header', templateUrl: './gitlab-issue-header.component.html', styleUrls: ['./gitlab-issue-header.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class GitlabIssueHeaderComponent { T: typeof T = T; @Input() public task?: TaskWithSubTasks; - constructor() { - } + constructor() {} } diff --git a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-map.util.ts b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-map.util.ts index 29a4c8f9e..1398ccd62 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-map.util.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue-map.util.ts @@ -38,4 +38,3 @@ export const mapGitlabIssueToSearchResult = (issue: GitlabIssue): SearchResultIt issueData: issue, }; }; - diff --git a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue.effects.ts b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue.effects.ts index 832e39002..b6c967aaa 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue.effects.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue.effects.ts @@ -15,28 +15,34 @@ import { T } from 'src/app/t.const'; import { TaskWithSubTasks } from '../../../../tasks/task.model'; import { WorkContextService } from '../../../../work-context/work-context.service'; -const isGitlabEnabled = (gitlabCfg: GitlabCfg): boolean => !!gitlabCfg && !!gitlabCfg.project; +const isGitlabEnabled = (gitlabCfg: GitlabCfg): boolean => + !!gitlabCfg && !!gitlabCfg.project; @Injectable() export class GitlabIssueEffects { private _updateIssuesForCurrentContext$: Observable = this._workContextService.allTasksForCurrentContext$.pipe( first(), switchMap((tasks) => { - const gitIssueTasks = tasks.filter(task => task.issueType === GITLAB_TYPE); - return forkJoin(gitIssueTasks.map(task => this._projectService.getGitlabCfgForProject$(task.projectId as string).pipe( - first(), - map(cfg => ({ - cfg, - task, - })) - )) + const gitIssueTasks = tasks.filter((task) => task.issueType === GITLAB_TYPE); + return forkJoin( + gitIssueTasks.map((task) => + this._projectService.getGitlabCfgForProject$(task.projectId as string).pipe( + first(), + map((cfg) => ({ + cfg, + task, + })), + ), + ), ); }), - map((cos: any) => cos - .filter(({cfg, task}: { cfg: GitlabCfg; task: TaskWithSubTasks }) => - isGitlabEnabled(cfg) && cfg.isAutoPoll - ) - .map(({task}: { cfg: GitlabCfg; task: TaskWithSubTasks }) => task) + map((cos: any) => + cos + .filter( + ({ cfg, task }: { cfg: GitlabCfg; task: TaskWithSubTasks }) => + isGitlabEnabled(cfg) && cfg.isAutoPoll, + ) + .map(({ task }: { cfg: GitlabCfg; task: TaskWithSubTasks }) => task), ), tap((gitlabTasks: TaskWithSubTasks[]) => { if (gitlabTasks && gitlabTasks.length > 0) { @@ -50,27 +56,33 @@ export class GitlabIssueEffects { }), ); - private _pollTimer$: Observable = timer(GITLAB_INITIAL_POLL_DELAY, GITLAB_POLL_INTERVAL); - - @Effect({dispatch: false}) - pollNewIssuesToBacklog$: Observable = this._issueEffectHelperService.pollToBacklogTriggerToProjectId$.pipe( - switchMap((pId) => this._projectService.getGitlabCfgForProject$(pId).pipe( - first(), - filter(gitlabCfg => isGitlabEnabled(gitlabCfg) && gitlabCfg.isAutoAddToBacklog), - switchMap(gitlabCfg => this._pollTimer$.pipe( - // NOTE: required otherwise timer stays alive for filtered actions - takeUntil(this._issueEffectHelperService.pollToBacklogActions$), - tap(() => console.log('GITLAB!_POLL_BACKLOG_CHANGES')), - tap(() => this._importNewIssuesToBacklog(pId, gitlabCfg)), - )), - )), + private _pollTimer$: Observable = timer( + GITLAB_INITIAL_POLL_DELAY, + GITLAB_POLL_INTERVAL, + ); + + @Effect({ dispatch: false }) + pollNewIssuesToBacklog$: Observable = this._issueEffectHelperService.pollToBacklogTriggerToProjectId$.pipe( + switchMap((pId) => + this._projectService.getGitlabCfgForProject$(pId).pipe( + first(), + filter((gitlabCfg) => isGitlabEnabled(gitlabCfg) && gitlabCfg.isAutoAddToBacklog), + switchMap((gitlabCfg) => + this._pollTimer$.pipe( + // NOTE: required otherwise timer stays alive for filtered actions + takeUntil(this._issueEffectHelperService.pollToBacklogActions$), + tap(() => console.log('GITLAB!_POLL_BACKLOG_CHANGES')), + tap(() => this._importNewIssuesToBacklog(pId, gitlabCfg)), + ), + ), + ), + ), + ); + @Effect({ dispatch: false }) + pollIssueChangesForCurrentContext$: Observable = this._issueEffectHelperService.pollIssueTaskUpdatesActions$.pipe( + switchMap(() => this._pollTimer$), + switchMap(() => this._updateIssuesForCurrentContext$), ); - @Effect({dispatch: false}) - pollIssueChangesForCurrentContext$: Observable = this._issueEffectHelperService.pollIssueTaskUpdatesActions$ - .pipe( - switchMap(() => this._pollTimer$), - switchMap(() => this._updateIssuesForCurrentContext$), - ); constructor( private readonly _snackService: SnackService, @@ -80,13 +92,17 @@ export class GitlabIssueEffects { private readonly _taskService: TaskService, private readonly _workContextService: WorkContextService, private readonly _issueEffectHelperService: IssueEffectHelperService, - ) { - } + ) {} private async _importNewIssuesToBacklog(projectId: string, gitlabCfg: GitlabCfg) { const issues = await this._gitlabApiService.getProjectData$(gitlabCfg).toPromise(); - const allTaskGitlabIssueIds = await this._taskService.getAllIssueIdsForProject(projectId, GITLAB_TYPE) as number[]; - const issuesToAdd = issues.filter(issue => !allTaskGitlabIssueIds.includes(issue.id)); + const allTaskGitlabIssueIds = (await this._taskService.getAllIssueIdsForProject( + projectId, + GITLAB_TYPE, + )) as number[]; + const issuesToAdd = issues.filter( + (issue) => !allTaskGitlabIssueIds.includes(issue.id), + ); issuesToAdd.forEach((issue) => { this._issueService.addTaskWithIssue(GITLAB_TYPE, issue, projectId, true); @@ -96,7 +112,7 @@ export class GitlabIssueEffects { this._snackService.open({ ico: 'cloud_download', translateParams: { - issueText: `#${issuesToAdd[0].number} ${issuesToAdd[0].title}` + issueText: `#${issuesToAdd[0].number} ${issuesToAdd[0].title}`, }, msg: T.F.GITLAB.S.IMPORTED_SINGLE_ISSUE, }); @@ -104,7 +120,7 @@ export class GitlabIssueEffects { this._snackService.open({ ico: 'cloud_download', translateParams: { - issuesLength: issuesToAdd.length + issuesLength: issuesToAdd.length, }, msg: T.F.GITLAB.S.IMPORTED_MULTIPLE_ISSUES, }); diff --git a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue.module.ts b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue.module.ts index 63efab8de..cec881eec 100644 --- a/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue.module.ts +++ b/src/app/features/issue/providers/gitlab/gitlab-issue/gitlab-issue.module.ts @@ -18,5 +18,4 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; ], exports: [GitlabIssueHeaderComponent, GitlabIssueContentComponent], }) -export class GitlabIssueModule { -} +export class GitlabIssueModule {} diff --git a/src/app/features/issue/providers/gitlab/gitlab.const.ts b/src/app/features/issue/providers/gitlab/gitlab.const.ts index b6a4da400..ccbc54de6 100644 --- a/src/app/features/issue/providers/gitlab/gitlab.const.ts +++ b/src/app/features/issue/providers/gitlab/gitlab.const.ts @@ -1,7 +1,10 @@ // TODO use as a checklist import { GitlabCfg } from './gitlab'; import { T } from '../../../../t.const'; -import { ConfigFormSection, LimitedFormlyFieldConfig } from '../../../config/global-config.model'; +import { + ConfigFormSection, + LimitedFormlyFieldConfig, +} from '../../../config/global-config.model'; import { GITHUB_INITIAL_POLL_DELAY } from '../github/github.const'; export const DEFAULT_GITLAB_CFG: GitlabCfg = { @@ -33,8 +36,8 @@ export const GITLAB_CONFIG_FORM: LimitedFormlyFieldConfig[] = [ templateOptions: { label: T.F.GITLAB.FORM.GITLAB_BASE_URL, type: 'text', - pattern: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/ - } + pattern: /((([A-Za-z]{3,9}:(?:\/\/)?)(?:[\-;:&=\+\$,\w]+@)?[A-Za-z0-9\.\-]+|(?:www\.|[\-;:&=\+\$,\w]+@)[A-Za-z0-9\.\-]+)((?:\/[\+~%\/\.\w\-_]*)?\??(?:[\-\+=&;%@\.\w_]*)#?(?:[\.\!\/\\\w]*))?)/, + }, }, { key: 'project', @@ -65,28 +68,28 @@ export const GITLAB_CONFIG_FORM: LimitedFormlyFieldConfig[] = [ key: 'isSearchIssuesFromGitlab', type: 'checkbox', templateOptions: { - label: T.F.GITLAB.FORM.IS_SEARCH_ISSUES_FROM_GITLAB + label: T.F.GITLAB.FORM.IS_SEARCH_ISSUES_FROM_GITLAB, }, }, { key: 'isAutoPoll', type: 'checkbox', templateOptions: { - label: T.F.GITLAB.FORM.IS_AUTO_POLL + label: T.F.GITLAB.FORM.IS_AUTO_POLL, }, }, { key: 'isAutoAddToBacklog', type: 'checkbox', templateOptions: { - label: T.F.GITLAB.FORM.IS_AUTO_ADD_TO_BACKLOG + label: T.F.GITLAB.FORM.IS_AUTO_ADD_TO_BACKLOG, }, }, { key: 'filterUsername', type: 'input', templateOptions: { - label: T.F.GITLAB.FORM.FILTER_USER + label: T.F.GITLAB.FORM.FILTER_USER, }, }, ]; diff --git a/src/app/features/issue/providers/jira/jira-api.service.ts b/src/app/features/issue/providers/jira/jira-api.service.ts index e3b00bdad..874ee7899 100644 --- a/src/app/features/issue/providers/jira/jira-api.service.ts +++ b/src/app/features/issue/providers/jira/jira-api.service.ts @@ -5,23 +5,35 @@ import { JIRA_ADDITIONAL_ISSUE_FIELDS, JIRA_DATETIME_FORMAT, JIRA_MAX_RESULTS, - JIRA_REQUEST_TIMEOUT_DURATION + JIRA_REQUEST_TIMEOUT_DURATION, } from './jira.const'; import { mapIssueResponse, mapIssuesResponse, mapResponse, mapToSearchResults, - mapTransitionResponse + mapTransitionResponse, } from './jira-issue/jira-issue-map.util'; -import { JiraOriginalStatus, JiraOriginalTransition, JiraOriginalUser } from './jira-api-responses'; +import { + JiraOriginalStatus, + JiraOriginalTransition, + JiraOriginalUser, +} from './jira-api-responses'; import { JiraCfg } from './jira.model'; import { IPC } from '../../../../../../electron/ipc-events.const'; import { SnackService } from '../../../../core/snack/snack.service'; import { HANDLED_ERROR_PROP_STR, IS_ELECTRON } from '../../../../app.constants'; import { Observable, of, throwError } from 'rxjs'; import { SearchResultItem } from '../../issue.model'; -import { catchError, concatMap, finalize, first, mapTo, shareReplay, take } from 'rxjs/operators'; +import { + catchError, + concatMap, + finalize, + first, + mapTo, + shareReplay, + take, +} from 'rxjs/operators'; import { JiraIssue, JiraIssueReduced } from './jira-issue/jira-issue.model'; import * as moment from 'moment'; import { BannerService } from '../../../../core/banner/banner.service'; @@ -73,10 +85,7 @@ export class JiraApiService { private _isExtension: boolean = false; private _isInterfacesReadyIfNeeded$: Observable = IS_ELECTRON ? of(true).pipe() - : this._chromeExtensionInterfaceService.onReady$.pipe( - mapTo(true), - shareReplay(1) - ); + : this._chromeExtensionInterfaceService.onReady$.pipe(mapTo(true), shareReplay(1)); constructor( private _chromeExtensionInterfaceService: ChromeExtensionInterfaceService, @@ -88,19 +97,23 @@ export class JiraApiService { ) { // set up callback listener for electron if (IS_ELECTRON) { - (this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.JIRA_CB_EVENT, (ev: IpcRendererEvent, - res: any) => { - this._handleResponse(res); - }); + (this._electronService.ipcRenderer as typeof ipcRenderer).on( + IPC.JIRA_CB_EVENT, + (ev: IpcRendererEvent, res: any) => { + this._handleResponse(res); + }, + ); } - this._chromeExtensionInterfaceService.onReady$ - .subscribe(() => { - this._isExtension = true; - this._chromeExtensionInterfaceService.addEventListener('SP_JIRA_RESPONSE', (ev: unknown, data: any) => { + this._chromeExtensionInterfaceService.onReady$.subscribe(() => { + this._isExtension = true; + this._chromeExtensionInterfaceService.addEventListener( + 'SP_JIRA_RESPONSE', + (ev: unknown, data: any) => { this._handleResponse(data); - }); - }); + }, + ); + }); } unblockAccess() { @@ -119,12 +132,12 @@ export class JiraApiService { showSubTasks: true, showSubTaskParent: true, query: searchStr, - currentJQL: cfg.searchJqlQuery + currentJQL: cfg.searchJqlQuery, }, - transform: mapToSearchResults + transform: mapToSearchResults, // NOTE: we pass the cfg as well to avoid race conditions }, - cfg + cfg, }); } @@ -133,17 +146,20 @@ export class JiraApiService { jiraReqCfg: { pathname: 'field', }, - cfg + cfg, }); } - findAutoImportIssues$(cfg: JiraCfg, isFetchAdditional?: boolean, - maxResults: number = JIRA_MAX_RESULTS): Observable { + findAutoImportIssues$( + cfg: JiraCfg, + isFetchAdditional?: boolean, + maxResults: number = JIRA_MAX_RESULTS, + ): Observable { const options = { maxResults, fields: [ ...JIRA_ADDITIONAL_ISSUE_FIELDS, - ...(cfg.storyPointFieldId ? [cfg.storyPointFieldId] : []) + ...(cfg.storyPointFieldId ? [cfg.storyPointFieldId] : []), ], }; const searchQuery = cfg.autoAddBacklogJqlQuery; @@ -153,7 +169,9 @@ export class JiraApiService { type: 'ERROR', msg: T.F.JIRA.S.NO_AUTO_IMPORT_JQL, }); - return throwError({[HANDLED_ERROR_PROP_STR]: 'JiraApi: No search query for auto import'}); + return throwError({ + [HANDLED_ERROR_PROP_STR]: 'JiraApi: No search query for auto import', + }); } return this._sendRequest$({ @@ -163,10 +181,10 @@ export class JiraApiService { method: 'POST', body: { ...options, - jql: searchQuery + jql: searchQuery, }, }, - cfg + cfg, }); } @@ -185,7 +203,7 @@ export class JiraApiService { transform: mapResponse, }, cfg, - isForce + isForce, }); } @@ -195,21 +213,24 @@ export class JiraApiService { pathname: `status`, transform: mapResponse, }, - cfg + cfg, }); } - getTransitionsForIssue$(issueId: string, cfg: JiraCfg): Observable { + getTransitionsForIssue$( + issueId: string, + cfg: JiraCfg, + ): Observable { return this._sendRequest$({ jiraReqCfg: { pathname: `issue/${issueId}/transitions`, method: 'GET', query: { - expand: 'transitions.fields' + expand: 'transitions.fields', }, transform: mapTransitionResponse, }, - cfg + cfg, }); } @@ -221,11 +242,11 @@ export class JiraApiService { body: { transition: { id: transitionId, - } + }, }, transform: mapResponse, }, - cfg + cfg, }); } @@ -238,7 +259,7 @@ export class JiraApiService { accountId, }, }, - cfg + cfg, }); } @@ -247,7 +268,7 @@ export class JiraApiService { started, timeSpent, comment, - cfg + cfg, }: { issueId: string; started: string; @@ -267,20 +288,24 @@ export class JiraApiService { body: worklog, transform: mapResponse, }, - cfg + cfg, }); } - private _getIssueById$(issueId: string, cfg: JiraCfg, isGetChangelog: boolean = false): Observable { + private _getIssueById$( + issueId: string, + cfg: JiraCfg, + isGetChangelog: boolean = false, + ): Observable { return this._sendRequest$({ jiraReqCfg: { transform: mapIssueResponse as (res: any, cfg?: JiraCfg) => any, pathname: `issue/${issueId}`, query: { - expand: isGetChangelog ? ['changelog', 'description'] : ['description'] - } + expand: isGetChangelog ? ['changelog', 'description'] : ['description'], + }, }, - cfg + cfg, }); } @@ -288,8 +313,13 @@ export class JiraApiService { // -------- private _isMinimalSettings(settings: JiraCfg) { - return settings && settings.host && settings.userName && settings.password - && (IS_ELECTRON || this._isExtension); + return ( + settings && + settings.host && + settings.userName && + settings.password && + (IS_ELECTRON || this._isExtension) + ); } private _sendRequest$({ @@ -303,31 +333,35 @@ export class JiraApiService { }): Observable { return this._isInterfacesReadyIfNeeded$.pipe( take(1), - concatMap(() => (IS_ELECTRON && cfg.isWonkyCookieMode) - ? this._checkSetWonkyCookie(cfg) - : of(true) + concatMap(() => + IS_ELECTRON && cfg.isWonkyCookieMode ? this._checkSetWonkyCookie(cfg) : of(true), ), concatMap(() => { // assign uuid to request to know which responsive belongs to which promise - const requestId = `${jiraReqCfg.pathname}__${jiraReqCfg.method || 'GET'}__${shortid()}`; + const requestId = `${jiraReqCfg.pathname}__${ + jiraReqCfg.method || 'GET' + }__${shortid()}`; if (!isOnline()) { this._snackService.open({ type: 'CUSTOM', msg: T.G.NO_CON, - ico: 'cloud_off' + ico: 'cloud_off', }); - return throwError({[HANDLED_ERROR_PROP_STR]: 'Jira Offline ' + requestId}); + return throwError({ [HANDLED_ERROR_PROP_STR]: 'Jira Offline ' + requestId }); } if (!this._isMinimalSettings(cfg)) { this._snackService.open({ type: 'ERROR', - msg: (!IS_ELECTRON && !this._isExtension) - ? T.F.JIRA.S.EXTENSION_NOT_LOADED - : T.F.JIRA.S.INSUFFICIENT_SETTINGS, + msg: + !IS_ELECTRON && !this._isExtension + ? T.F.JIRA.S.EXTENSION_NOT_LOADED + : T.F.JIRA.S.INSUFFICIENT_SETTINGS, + }); + return throwError({ + [HANDLED_ERROR_PROP_STR]: 'Insufficient Settings for Jira ' + requestId, }); - return throwError({[HANDLED_ERROR_PROP_STR]: 'Insufficient Settings for Jira ' + requestId}); } if (this._isBlockAccess && !isForce) { @@ -338,10 +372,13 @@ export class JiraApiService { svgIco: 'jira', action: { label: T.F.JIRA.BANNER.BLOCK_ACCESS_UNBLOCK, - fn: () => this.unblockAccess() - } + fn: () => this.unblockAccess(), + }, + }); + return throwError({ + [HANDLED_ERROR_PROP_STR]: + 'Blocked access to prevent being shut out ' + requestId, }); - return throwError({[HANDLED_ERROR_PROP_STR]: 'Blocked access to prevent being shut out ' + requestId}); } // BUILD REQUEST START @@ -349,20 +386,32 @@ export class JiraApiService { const requestInit = this._makeRequestInit(jiraReqCfg, cfg); const queryStr = jiraReqCfg.query - ? `?${stringify(jiraReqCfg.query, {arrayFormat: 'comma'})}` + ? `?${stringify(jiraReqCfg.query, { arrayFormat: 'comma' })}` : ''; const base = `${stripTrailing(cfg.host || 'null', '/')}/rest/api/${API_VERSION}`; const url = `${base}/${jiraReqCfg.pathname}${queryStr}`.trim(); - return this._sendRequestToExecutor$(requestId, url, requestInit, jiraReqCfg.transform, cfg); + return this._sendRequestToExecutor$( + requestId, + url, + requestInit, + jiraReqCfg.transform, + cfg, + ); // NOTE: offline is sexier & easier than cache, but in case we change our mind... // const args = [requestId, url, requestInit, jiraReqCfg.transform]; // return this._issueCacheService.cache(url, requestInit, this._sendRequestToExecutor$.bind(this), args); - })); + }), + ); } - private _sendRequestToExecutor$(requestId: string, url: string, requestInit: RequestInit, transform: any, - jiraCfg: JiraCfg): Observable { + private _sendRequestToExecutor$( + requestId: string, + url: string, + requestInit: RequestInit, + transform: any, + jiraCfg: JiraCfg, + ): Observable { // TODO refactor to observable for request canceling etc let promiseResolve; let promiseReject; @@ -378,49 +427,57 @@ export class JiraApiService { requestId, requestInit, transform, - jiraCfg + jiraCfg, }); - const requestToSend = {requestId, requestInit, url}; + const requestToSend = { requestId, requestInit, url }; if (this._electronService.isElectronApp) { - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.JIRA_MAKE_REQUEST_EVENT, { - ...requestToSend, - jiraCfg - }); + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.JIRA_MAKE_REQUEST_EVENT, + { + ...requestToSend, + jiraCfg, + }, + ); } else if (this._isExtension) { - this._chromeExtensionInterfaceService.dispatchEvent('SP_JIRA_REQUEST', requestToSend); + this._chromeExtensionInterfaceService.dispatchEvent( + 'SP_JIRA_REQUEST', + requestToSend, + ); } this._globalProgressBarService.countUp(url); - return fromPromise(promise) - .pipe( - catchError((err) => { - console.log(err); - console.log(getErrorTxt(err)); - const errTxt = `Jira: ${getErrorTxt(err)}`; - this._snackService.open({type: 'ERROR', msg: errTxt}); - return throwError({[HANDLED_ERROR_PROP_STR]: errTxt}); - }), - first(), - finalize(() => this._globalProgressBarService.countDown()) - ); + return fromPromise(promise).pipe( + catchError((err) => { + console.log(err); + console.log(getErrorTxt(err)); + const errTxt = `Jira: ${getErrorTxt(err)}`; + this._snackService.open({ type: 'ERROR', msg: errTxt }); + return throwError({ [HANDLED_ERROR_PROP_STR]: errTxt }); + }), + first(), + finalize(() => this._globalProgressBarService.countDown()), + ); } private _makeRequestInit(jr: JiraRequestCfg, cfg: JiraCfg): RequestInit { return { method: jr.method || 'GET', - ...(jr.body ? {body: JSON.stringify(jr.body)} : {}), + ...(jr.body ? { body: JSON.stringify(jr.body) } : {}), - headers: (IS_ELECTRON && cfg.isWonkyCookieMode) - ? { - Cookie: sessionStorage.getItem(SS_JIRA_WONKY_COOKIE) as string, - } - : { - authorization: `Basic ${this._b64EncodeUnicode(`${cfg.userName}:${cfg.password}`)}`, - Cookie: '', - 'Content-Type': 'application/json' - } + headers: + IS_ELECTRON && cfg.isWonkyCookieMode + ? { + Cookie: sessionStorage.getItem(SS_JIRA_WONKY_COOKIE) as string, + } + : { + authorization: `Basic ${this._b64EncodeUnicode( + `${cfg.userName}:${cfg.password}`, + )}`, + Cookie: '', + 'Content-Type': 'application/json', + }, }; } @@ -432,20 +489,23 @@ export class JiraApiService { const loginUrl = `${cfg.host}`; const apiUrl = `${cfg.host}/rest/api/${API_VERSION}/myself`; - const val = await this._matDialog.open(DialogPromptComponent, { - data: { - // TODO add message to translations - placeholder: 'Insert Cookie String', - message: `

Jira Wonky Cookie Authentication

+ const val = await this._matDialog + .open(DialogPromptComponent, { + data: { + // TODO add message to translations + placeholder: 'Insert Cookie String', + message: `

Jira Wonky Cookie Authentication

  1. Log into Jira from your browser
  2. Go to this api url
  3. Open up the dev tools
  4. Navigate to "network" and reload page
  5. Copy all request header cookies from the api request and enter them here
  6. -
` - } - }).afterClosed().toPromise(); +`, + }, + }) + .afterClosed() + .toPromise(); if (typeof val === 'string') { sessionStorage.setItem(SS_JIRA_WONKY_COOKIE, val); @@ -463,7 +523,7 @@ export class JiraApiService { requestId, requestInit, transform, - jiraCfg + jiraCfg, }: { promiseResolve: any; promiseReject: any; @@ -490,7 +550,7 @@ export class JiraApiService { }); this._requestsLog[requestId].reject('Request timed out'); delete this._requestsLog[requestId]; - }, JIRA_REQUEST_TIMEOUT_DURATION) + }, JIRA_REQUEST_TIMEOUT_DURATION), }; } @@ -505,12 +565,13 @@ export class JiraApiService { if (!res || res.error) { console.error('JIRA_RESPONSE_ERROR', res, currentRequest); // let msg = - if (res?.error && ( - res.error.statusCode === 401 - || res.error === 401 - || res.error.message === 'Forbidden' - || res.error.message === 'Unauthorized' - )) { + if ( + res?.error && + (res.error.statusCode === 401 || + res.error === 401 || + res.error.message === 'Forbidden' || + res.error.message === 'Unauthorized') + ) { this._blockAccess(); } @@ -526,7 +587,7 @@ export class JiraApiService { // delete entry for promise afterwards delete this._requestsLog[res.requestId]; } else { - console.warn('Jira: Response Request ID not existing', (res && res.requestId)); + console.warn('Jira: Response Request ID not existing', res && res.requestId); } } diff --git a/src/app/features/issue/providers/jira/jira-common-interfaces.service.ts b/src/app/features/issue/providers/jira/jira-common-interfaces.service.ts index 3d4aa4442..592b84ae5 100644 --- a/src/app/features/issue/providers/jira/jira-common-interfaces.service.ts +++ b/src/app/features/issue/providers/jira/jira-common-interfaces.service.ts @@ -17,35 +17,38 @@ import { JiraCfg } from './jira.model'; providedIn: 'root', }) export class JiraCommonInterfacesService implements IssueServiceInterface { - constructor( private readonly _jiraApiService: JiraApiService, private readonly _snackService: SnackService, private readonly _projectService: ProjectService, - ) { - } + ) {} // NOTE: we're using the issueKey instead of the real issueId getById$(issueId: string | number, projectId: string) { return this._getCfgOnce$(projectId).pipe( - switchMap(jiraCfg => this._jiraApiService.getIssueById$(issueId as string, jiraCfg)) + switchMap((jiraCfg) => + this._jiraApiService.getIssueById$(issueId as string, jiraCfg), + ), ); } // NOTE: this gives back issueKey instead of issueId searchIssues$(searchTerm: string, projectId: string): Observable { return this._getCfgOnce$(projectId).pipe( - switchMap((jiraCfg) => (jiraCfg && jiraCfg.isEnabled) - ? this._jiraApiService.issuePicker$(searchTerm, jiraCfg).pipe(catchError(() => [])) - : of([]) - ) + switchMap((jiraCfg) => + jiraCfg && jiraCfg.isEnabled + ? this._jiraApiService + .issuePicker$(searchTerm, jiraCfg) + .pipe(catchError(() => [])) + : of([]), + ), ); } async refreshIssue( task: Task, isNotifySuccess: boolean = true, - isNotifyNoUpdateRequired: boolean = false + isNotifyNoUpdateRequired: boolean = false, ): Promise<{ taskChanges: Partial; issue: JiraIssue } | null> { if (!task.projectId) { throw new Error('No projectId'); @@ -55,7 +58,9 @@ export class JiraCommonInterfacesService implements IssueServiceInterface { } const cfg = await this._getCfgOnce$(task.projectId).toPromise(); - const issue = await this._jiraApiService.getIssueById$(task.issueId, cfg).toPromise() as JiraIssue; + const issue = (await this._jiraApiService + .getIssueById$(task.issueId, cfg) + .toPromise()) as JiraIssue; // @see https://developer.atlassian.com/cloud/jira/platform/jira-expressions-type-reference/#date const newUpdated = new Date(issue.updated).getTime(); @@ -66,7 +71,7 @@ export class JiraCommonInterfacesService implements IssueServiceInterface { this._snackService.open({ msg: T.F.JIRA.S.ISSUE_UPDATE, translateParams: { - issueText: `${issue.key}` + issueText: `${issue.key}`, }, ico: 'cloud_download', }); @@ -74,7 +79,7 @@ export class JiraCommonInterfacesService implements IssueServiceInterface { this._snackService.open({ msg: T.F.JIRA.S.ISSUE_NO_UPDATE_REQUIRED, translateParams: { - issueText: `${issue.key}` + issueText: `${issue.key}`, }, ico: 'cloud_download', }); @@ -88,7 +93,7 @@ export class JiraCommonInterfacesService implements IssueServiceInterface { issueWasUpdated: wasUpdated, // circumvent errors for old jira versions #652 issueAttachmentNr: issue.attachments?.length, - issuePoints: issue.storyPoints + issuePoints: issue.storyPoints, }, issue, }; @@ -96,15 +101,17 @@ export class JiraCommonInterfacesService implements IssueServiceInterface { return null; } - getAddTaskData(issue: JiraIssueReduced): { title: string; additionalFields: Partial } { + getAddTaskData( + issue: JiraIssueReduced, + ): { title: string; additionalFields: Partial } { return { title: `${issue.key} ${issue.summary}`, additionalFields: { issuePoints: issue.storyPoints, issueAttachmentNr: issue.attachments ? issue.attachments.length : 0, issueWasUpdated: false, - issueLastUpdated: new Date(issue.updated).getTime() - } + issueLastUpdated: new Date(issue.updated).getTime(), + }, }; } @@ -115,12 +122,16 @@ export class JiraCommonInterfacesService implements IssueServiceInterface { // const isIssueKey = isNaN(Number(issueId)); return this._projectService.getJiraCfgForProject$(projectId).pipe( first(), - map((jiraCfg) => jiraCfg.host + '/browse/' + issueId) + map((jiraCfg) => jiraCfg.host + '/browse/' + issueId), ); } getMappedAttachments(issueData: JiraIssue): TaskAttachment[] { - return issueData && issueData.attachments && issueData.attachments.map(mapJiraAttachmentToAttachment); + return ( + issueData && + issueData.attachments && + issueData.attachments.map(mapJiraAttachmentToAttachment) + ); } private _getCfgOnce$(projectId: string): Observable { diff --git a/src/app/features/issue/providers/jira/jira-issue/jira-issue-content/jira-issue-content.component.ts b/src/app/features/issue/providers/jira/jira-issue/jira-issue-content/jira-issue-content.component.ts index be6c907ad..10abf7ed5 100644 --- a/src/app/features/issue/providers/jira/jira-issue/jira-issue-content/jira-issue-content.component.ts +++ b/src/app/features/issue/providers/jira/jira-issue/jira-issue-content/jira-issue-content.component.ts @@ -16,7 +16,7 @@ import { switchMap } from 'rxjs/operators'; templateUrl: './jira-issue-content.component.html', styleUrls: ['./jira-issue-content.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation] + animations: [expandAnimation], }) export class JiraIssueContentComponent { description?: string; @@ -26,14 +26,18 @@ export class JiraIssueContentComponent { task?: TaskWithSubTasks; private _task$: ReplaySubject = new ReplaySubject(1); issueUrl$: Observable = this._task$.pipe( - switchMap((task) => this._jiraCommonInterfacesService.issueLink$(task.issueId as string, task.projectId as string)) + switchMap((task) => + this._jiraCommonInterfacesService.issueLink$( + task.issueId as string, + task.projectId as string, + ), + ), ); constructor( - private readonly _taskService: TaskService, - private readonly _jiraCommonInterfacesService: JiraCommonInterfacesService, - ) { - } + private readonly _taskService: TaskService, + private readonly _jiraCommonInterfacesService: JiraCommonInterfacesService, + ) {} @Input('issue') set issueIn(i: JiraIssue) { this.issue = i; diff --git a/src/app/features/issue/providers/jira/jira-issue/jira-issue-header/jira-issue-header.component.ts b/src/app/features/issue/providers/jira/jira-issue/jira-issue-header/jira-issue-header.component.ts index 3bdda85b8..44c95593b 100644 --- a/src/app/features/issue/providers/jira/jira-issue/jira-issue-header/jira-issue-header.component.ts +++ b/src/app/features/issue/providers/jira/jira-issue/jira-issue-header/jira-issue-header.component.ts @@ -7,12 +7,11 @@ import { Observable } from 'rxjs'; selector: 'jira-issue-header', templateUrl: './jira-issue-header.component.html', styleUrls: ['./jira-issue-header.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class JiraIssueHeaderComponent { @Input() task?: TaskWithSubTasks; isOnline$: Observable = isOnline$; - constructor() { - } + constructor() {} } diff --git a/src/app/features/issue/providers/jira/jira-issue/jira-issue-map.util.ts b/src/app/features/issue/providers/jira/jira-issue/jira-issue-map.util.ts index 4d249650c..92247f40e 100644 --- a/src/app/features/issue/providers/jira/jira-issue/jira-issue-map.util.ts +++ b/src/app/features/issue/providers/jira/jira-issue/jira-issue-map.util.ts @@ -1,33 +1,44 @@ -import { JiraAttachment, JiraAuthor, JiraChangelogEntry, JiraComment, JiraIssue } from './jira-issue.model'; +import { + JiraAttachment, + JiraAuthor, + JiraChangelogEntry, + JiraComment, + JiraIssue, +} from './jira-issue.model'; import { JiraIssueOriginal, JiraOriginalAttachment, JiraOriginalAuthor, JiraOriginalChangelog, - JiraOriginalComment + JiraOriginalComment, } from '../jira-api-responses'; import { JiraCfg } from '../jira.model'; -import { DropPasteIcons, DropPasteInputType } from '../../../../../core/drop-paste-input/drop-paste.model'; +import { + DropPasteIcons, + DropPasteInputType, +} from '../../../../../core/drop-paste-input/drop-paste.model'; import { IssueProviderKey, SearchResultItem } from '../../../issue.model'; import { TaskAttachment } from '../../../../tasks/task-attachment/task-attachment.model'; import { dedupeByKey } from '../../../../../util/de-dupe-by-key'; import { JIRA_TYPE } from '../../../issue.const'; export const mapToSearchResults = (res: any): SearchResultItem[] => { - const issues = dedupeByKey(res.response.sections.map((sec: any) => sec.issues).flat(), 'key') - .map((issue: any) => { - return { - title: issue.key + ' ' + issue.summaryText, - titleHighlighted: issue.key + ' ' + issue.summary, - issueType: JIRA_TYPE as IssueProviderKey, - issueData: { - ...issue, - summary: issue.summaryText, - // NOTE: we always use the key, because it allows us to create the right link - id: issue.key, - }, - }; - }); + const issues = dedupeByKey( + res.response.sections.map((sec: any) => sec.issues).flat(), + 'key', + ).map((issue: any) => { + return { + title: issue.key + ' ' + issue.summaryText, + titleHighlighted: issue.key + ' ' + issue.summary, + issueType: JIRA_TYPE as IssueProviderKey, + issueData: { + ...issue, + summary: issue.summaryText, + // NOTE: we always use the key, because it allows us to create the right link + id: issue.key, + }, + }; + }); return issues; }; @@ -39,7 +50,8 @@ export const mapIssuesResponse = (res: any, cfg: JiraCfg): JiraIssue[] => { export const mapResponse = (res: any): unknown => res.response; -export const mapIssueResponse = (res: any, cfg: JiraCfg): JiraIssue => mapIssue(res.response, cfg); +export const mapIssueResponse = (res: any, cfg: JiraCfg): JiraIssue => + mapIssue(res.response, cfg); export const mapIssue = (issue: JiraIssueOriginal, cfg: JiraCfg): JiraIssue => { const issueCopy = Object.assign({}, issue); @@ -56,20 +68,25 @@ export const mapIssue = (issue: JiraIssueOriginal, cfg: JiraCfg): JiraIssue => { summary: fields.summary, updated: fields.updated, status: fields.status, - storyPoints: (!!cfg.storyPointFieldId && !!(fields as any)[cfg.storyPointFieldId]) - ? (fields as any)[cfg.storyPointFieldId] as number - : undefined, + storyPoints: + !!cfg.storyPointFieldId && !!(fields as any)[cfg.storyPointFieldId] + ? ((fields as any)[cfg.storyPointFieldId] as number) + : undefined, attachments: fields.attachment && fields.attachment.map(mapAttachment), - comments: (!!fields.comment && !!fields.comment.comments) - ? fields.comment.comments.map(mapComments) - : [], + comments: + !!fields.comment && !!fields.comment.comments + ? fields.comment.comments.map(mapComments) + : [], changelog: mapChangelog(issueCopy.changelog as JiraOriginalChangelog), assignee: mapAuthor(fields.assignee, true), // url: makeIssueUrl(cfg.host, issueCopy.key) }; }; -export const mapAuthor = (author: JiraOriginalAuthor, isOptional: boolean = false): JiraAuthor | null => { +export const mapAuthor = ( + author: JiraOriginalAuthor, + isOptional: boolean = false, +): JiraAuthor | null => { if (!author) { return null; } @@ -82,18 +99,20 @@ export const mapAuthor = (author: JiraOriginalAuthor, isOptional: boolean = fals export const mapAttachment = (attachment: JiraOriginalAttachment): JiraAttachment => { return Object.assign({}, attachment, { self: undefined, - author: undefined + author: undefined, }); }; export const mapComments = (comment: JiraOriginalComment): JiraComment => { return Object.assign({}, comment, { self: undefined, updateAuthor: undefined, - author: mapAuthor(comment.author) + author: mapAuthor(comment.author), }); }; -export const mapJiraAttachmentToAttachment = (jiraAttachment: JiraAttachment): TaskAttachment => { +export const mapJiraAttachmentToAttachment = ( + jiraAttachment: JiraAttachment, +): TaskAttachment => { const type = mapAttachmentType(jiraAttachment.mimeType); return { id: null, @@ -101,7 +120,7 @@ export const mapJiraAttachmentToAttachment = (jiraAttachment: JiraAttachment): T path: jiraAttachment.thumbnail || jiraAttachment.content, originalImgPath: jiraAttachment.content, type, - icon: DropPasteIcons[type] + icon: DropPasteIcons[type], }; }; @@ -111,8 +130,8 @@ export const mapChangelog = (changelog: JiraOriginalChangelog): JiraChangelogEnt return []; } - changelog.histories.forEach(entry => { - entry.items.forEach(item => { + changelog.histories.forEach((entry) => { + entry.items.forEach((item) => { newChangelog.push({ author: mapAuthor(entry.author, true), created: entry.created, @@ -137,5 +156,4 @@ const mapAttachmentType = (mimeType: string): DropPasteInputType => { default: return 'LINK'; } - }; 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 9abae445e..bc92bc3dc 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 @@ -12,14 +12,22 @@ import { takeUntil, tap, throttleTime, - withLatestFrom + withLatestFrom, } from 'rxjs/operators'; import { JiraApiService } from '../jira-api.service'; import { JiraIssueReduced } from './jira-issue.model'; import { SnackService } from '../../../../../core/snack/snack.service'; import { Task, TaskWithSubTasks } from '../../../../tasks/task.model'; import { TaskService } from '../../../../tasks/task.service'; -import { BehaviorSubject, EMPTY, forkJoin, Observable, of, throwError, timer } from 'rxjs'; +import { + BehaviorSubject, + EMPTY, + forkJoin, + Observable, + of, + throwError, + timer, +} from 'rxjs'; import { MatDialog } from '@angular/material/dialog'; import { DialogJiraTransitionComponent } from '../jira-view-components/dialog-jira-transition/dialog-jira-transition.component'; import { IssueLocalState } from '../../../issue.model'; @@ -32,9 +40,16 @@ import { truncate } from '../../../../../util/truncate'; import { WorkContextService } from '../../../../work-context/work-context.service'; import { JiraCfg, JiraTransitionOption } from '../jira.model'; import { IssueEffectHelperService } from '../../../issue-effect-helper.service'; -import { SetCurrentTask, TaskActionTypes, UpdateTask } from '../../../../tasks/store/task.actions'; +import { + SetCurrentTask, + TaskActionTypes, + UpdateTask, +} from '../../../../tasks/store/task.actions'; import { DialogJiraAddWorklogComponent } from '../jira-view-components/dialog-jira-add-worklog/dialog-jira-add-worklog.component'; -import { selectCurrentTaskParentOrCurrent, selectTaskEntities } from '../../../../tasks/store/task.selectors'; +import { + selectCurrentTaskParentOrCurrent, + selectTaskEntities, +} from '../../../../tasks/store/task.selectors'; import { Dictionary } from '@ngrx/entity'; import { HANDLED_ERROR_PROP_STR } from '../../../../../app.constants'; import { DialogConfirmComponent } from '../../../../../ui/dialog-confirm/dialog-confirm.component'; @@ -45,49 +60,60 @@ import { isJiraEnabled } from '../is-jira-enabled.util'; @Injectable() export class JiraIssueEffects { // ----- - @Effect({dispatch: false}) + @Effect({ dispatch: false }) addWorklog$: any = this._actions$.pipe( ofType(TaskActionTypes.UpdateTask), filter((act: UpdateTask) => act.payload.task.changes.isDone === true), withLatestFrom( this._workContextService.isActiveWorkContextProject$, - this._workContextService.activeWorkContextId$ + this._workContextService.activeWorkContextId$, ), filter(([, isActiveContextProject]) => isActiveContextProject), - concatMap(([act, , projectId]) => this._getCfgOnce$(projectId as string).pipe( - map(jiraCfg => ({ - act, - projectId, - jiraCfg - })), - )), - filter(({jiraCfg}) => isJiraEnabled(jiraCfg)), + concatMap(([act, , projectId]) => + this._getCfgOnce$(projectId as string).pipe( + map((jiraCfg) => ({ + act, + projectId, + jiraCfg, + })), + ), + ), + filter(({ jiraCfg }) => isJiraEnabled(jiraCfg)), withLatestFrom(this._store$.pipe(select(selectTaskEntities))), - tap(([{act, projectId, jiraCfg}, taskEntities]: [{ - act: UpdateTask; - projectId: string | null; - jiraCfg: JiraCfg; - }, Dictionary]) => { - const taskId = act.payload.task.id; - const task = taskEntities[taskId]; - if (!task) { - throw new Error('No task'); - } + tap( + ([{ act, projectId, jiraCfg }, taskEntities]: [ + { + act: UpdateTask; + projectId: string | null; + jiraCfg: JiraCfg; + }, + Dictionary, + ]) => { + const taskId = act.payload.task.id; + const task = taskEntities[taskId]; + if (!task) { + throw new Error('No task'); + } - if (jiraCfg.isAddWorklogOnSubTaskDone && jiraCfg.isWorklogEnabled) { - if (task && task.issueType === JIRA_TYPE && task.issueId - && !(jiraCfg.isAddWorklogOnSubTaskDone && task.subTaskIds.length > 0)) { - this._openWorklogDialog(task, task.issueId, jiraCfg); - } else if (task.parentId) { - const parent = taskEntities[task.parentId]; - if (parent && parent.issueId && parent.issueType === JIRA_TYPE) { - // NOTE we're still sending the sub task for the meta data we need - this._openWorklogDialog(task, parent.issueId, jiraCfg); + if (jiraCfg.isAddWorklogOnSubTaskDone && jiraCfg.isWorklogEnabled) { + if ( + task && + task.issueType === JIRA_TYPE && + task.issueId && + !(jiraCfg.isAddWorklogOnSubTaskDone && task.subTaskIds.length > 0) + ) { + this._openWorklogDialog(task, task.issueId, jiraCfg); + } else if (task.parentId) { + const parent = taskEntities[task.parentId]; + if (parent && parent.issueId && parent.issueType === JIRA_TYPE) { + // NOTE we're still sending the sub task for the meta data we need + this._openWorklogDialog(task, parent.issueId, jiraCfg); + } } } - } - return undefined; - }) + return undefined; + }, + ), ); // CHECK CONNECTION @@ -100,57 +126,71 @@ export class JiraIssueEffects { // from a project with a working jira cfg to one with a non working one, but on the other hand // this is already complicated enough as is... // I am sorry future me O:) - @Effect({dispatch: false}) - checkForReassignment: any = this._actions$ - .pipe( - ofType(TaskActionTypes.SetCurrentTask), - // only if a task is started - filter((a: SetCurrentTask) => !!a.payload), - withLatestFrom( - this._store$.pipe(select(selectCurrentTaskParentOrCurrent)), - ), - filter(([, currentTaskOrParent]) => ( - !!currentTaskOrParent - && currentTaskOrParent.issueType === JIRA_TYPE - && !!currentTaskOrParent.issueId - )), - concatMap(([, currentTaskOrParent]) => { - if (!currentTaskOrParent.projectId) { - throw new Error('No projectId for task'); - } - return this._getCfgOnce$(currentTaskOrParent.projectId).pipe( - map((jiraCfg) => ({jiraCfg, currentTaskOrParent})), - ); - }), - filter(({jiraCfg, currentTaskOrParent}) => isJiraEnabled(jiraCfg) && jiraCfg.isCheckToReAssignTicketOnTaskStart), - // show every 15s max to give time for updates - throttleTime(15000), - // TODO there is probably a better way to to do this - // TODO refactor to actions - switchMap(({jiraCfg, currentTaskOrParent}) => { - return this._jiraApiService.getReducedIssueById$(currentTaskOrParent.issueId as string, jiraCfg).pipe( + @Effect({ dispatch: false }) + checkForReassignment: any = this._actions$.pipe( + ofType(TaskActionTypes.SetCurrentTask), + // only if a task is started + filter((a: SetCurrentTask) => !!a.payload), + withLatestFrom(this._store$.pipe(select(selectCurrentTaskParentOrCurrent))), + filter( + ([, currentTaskOrParent]) => + !!currentTaskOrParent && + currentTaskOrParent.issueType === JIRA_TYPE && + !!currentTaskOrParent.issueId, + ), + concatMap(([, currentTaskOrParent]) => { + if (!currentTaskOrParent.projectId) { + throw new Error('No projectId for task'); + } + return this._getCfgOnce$(currentTaskOrParent.projectId).pipe( + map((jiraCfg) => ({ jiraCfg, currentTaskOrParent })), + ); + }), + filter( + ({ jiraCfg, currentTaskOrParent }) => + isJiraEnabled(jiraCfg) && jiraCfg.isCheckToReAssignTicketOnTaskStart, + ), + // show every 15s max to give time for updates + throttleTime(15000), + // TODO there is probably a better way to to do this + // TODO refactor to actions + switchMap(({ jiraCfg, currentTaskOrParent }) => { + return this._jiraApiService + .getReducedIssueById$(currentTaskOrParent.issueId as string, jiraCfg) + .pipe( withLatestFrom(this._jiraApiService.getCurrentUser$(jiraCfg)), concatMap(([issue, currentUser]) => { const assignee = issue.assignee; if (!issue) { - return throwError({[HANDLED_ERROR_PROP_STR]: 'Jira: Issue Data not found'}); - } else if (!issue.assignee || issue.assignee.accountId !== currentUser.accountId) { - return this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - okTxt: T.F.JIRA.DIALOG_CONFIRM_ASSIGNMENT.OK, - translateParams: { - summary: issue.summary, - assignee: assignee ? assignee.displayName : 'nobody' + return throwError({ + [HANDLED_ERROR_PROP_STR]: 'Jira: Issue Data not found', + }); + } else if ( + !issue.assignee || + issue.assignee.accountId !== currentUser.accountId + ) { + return this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + okTxt: T.F.JIRA.DIALOG_CONFIRM_ASSIGNMENT.OK, + translateParams: { + summary: issue.summary, + assignee: assignee ? assignee.displayName : 'nobody', + }, + message: T.F.JIRA.DIALOG_CONFIRM_ASSIGNMENT.MSG, }, - message: T.F.JIRA.DIALOG_CONFIRM_ASSIGNMENT.MSG, - } - }).afterClosed() + }) + .afterClosed() .pipe( switchMap((isConfirm) => { return isConfirm - ? this._jiraApiService.updateAssignee$(issue.id, currentUser.accountId, jiraCfg) + ? this._jiraApiService.updateAssignee$( + issue.id, + currentUser.accountId, + jiraCfg, + ) : EMPTY; }), // tap(() => { @@ -161,134 +201,157 @@ export class JiraIssueEffects { } else { return EMPTY; } - }) + }), ); - }) - ); + }), + ); // POLLING & UPDATES - @Effect({dispatch: false}) - checkForStartTransition$: Observable = this._actions$ - .pipe( - ofType(TaskActionTypes.SetCurrentTask), - // only if a task is started - filter((a: SetCurrentTask) => !!a.payload), - withLatestFrom( - this._store$.pipe(select(selectCurrentTaskParentOrCurrent)), + @Effect({ dispatch: false }) + checkForStartTransition$: Observable = this._actions$.pipe( + ofType(TaskActionTypes.SetCurrentTask), + // only if a task is started + filter((a: SetCurrentTask) => !!a.payload), + withLatestFrom(this._store$.pipe(select(selectCurrentTaskParentOrCurrent))), + filter( + ([, currentTaskOrParent]) => + currentTaskOrParent && currentTaskOrParent.issueType === JIRA_TYPE, + ), + concatMap(([, currentTaskOrParent]) => { + if (!currentTaskOrParent.projectId) { + throw new Error('No projectId for task'); + } + return this._getCfgOnce$(currentTaskOrParent.projectId).pipe( + map((jiraCfg) => ({ jiraCfg, currentTaskOrParent })), + ); + }), + filter( + ({ jiraCfg, currentTaskOrParent }) => + isJiraEnabled(jiraCfg) && jiraCfg.isTransitionIssuesEnabled, + ), + concatMap(({ jiraCfg, currentTaskOrParent }) => + this._handleTransitionForIssue( + IssueLocalState.IN_PROGRESS, + jiraCfg, + currentTaskOrParent, ), - filter(([, currentTaskOrParent]) => - (currentTaskOrParent && currentTaskOrParent.issueType === JIRA_TYPE) - ), - concatMap(([, currentTaskOrParent]) => { - if (!currentTaskOrParent.projectId) { - throw new Error('No projectId for task'); - } - return this._getCfgOnce$(currentTaskOrParent.projectId).pipe( - map((jiraCfg) => ({jiraCfg, currentTaskOrParent})) - ); - }), - filter(({jiraCfg, currentTaskOrParent}) => - isJiraEnabled(jiraCfg) && jiraCfg.isTransitionIssuesEnabled - ), - concatMap(({jiraCfg, currentTaskOrParent}) => - this._handleTransitionForIssue(IssueLocalState.IN_PROGRESS, jiraCfg, currentTaskOrParent) - ), - ); - @Effect({dispatch: false}) - checkForDoneTransition$: Observable = this._actions$ - .pipe( - ofType(TaskActionTypes.UpdateTask), - filter((a: UpdateTask): boolean => !!a.payload.task.changes.isDone), - concatMap((a: UpdateTask) => this._taskService.getByIdOnce$(a.payload.task.id as string)), - filter((task: Task) => (task && task.issueType === JIRA_TYPE)), - concatMap((task: Task) => { - if (!task.projectId) { - throw new Error('No projectId for task'); - } - return this._getCfgOnce$(task.projectId).pipe( - map((jiraCfg) => ({jiraCfg, task})), - ); - }), - filter(({jiraCfg, task}) => - isJiraEnabled(jiraCfg) && jiraCfg.isTransitionIssuesEnabled - ), - concatMap(({jiraCfg, task}) => { - return this._handleTransitionForIssue(IssueLocalState.DONE, jiraCfg, task); - }) - ); + ), + ); + @Effect({ dispatch: false }) + checkForDoneTransition$: Observable = this._actions$.pipe( + ofType(TaskActionTypes.UpdateTask), + filter((a: UpdateTask): boolean => !!a.payload.task.changes.isDone), + concatMap((a: UpdateTask) => + this._taskService.getByIdOnce$(a.payload.task.id as string), + ), + filter((task: Task) => task && task.issueType === JIRA_TYPE), + concatMap((task: Task) => { + if (!task.projectId) { + throw new Error('No projectId for task'); + } + return this._getCfgOnce$(task.projectId).pipe( + map((jiraCfg) => ({ jiraCfg, task })), + ); + }), + filter( + ({ jiraCfg, task }) => isJiraEnabled(jiraCfg) && jiraCfg.isTransitionIssuesEnabled, + ), + concatMap(({ jiraCfg, task }) => { + return this._handleTransitionForIssue(IssueLocalState.DONE, jiraCfg, task); + }), + ); // HOOKS - private _isInitialRequestForProjectDone$: BehaviorSubject = new BehaviorSubject(false); + private _isInitialRequestForProjectDone$: BehaviorSubject = new BehaviorSubject( + false, + ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) checkConnection$: Observable = this._actions$.pipe( ofType(setActiveWorkContext), tap(() => this._isInitialRequestForProjectDone$.next(false)), - filter(({activeType}) => (activeType === WorkContextType.PROJECT)), - concatMap(({activeId}) => this._getCfgOnce$(activeId)), + filter(({ activeType }) => activeType === WorkContextType.PROJECT), + concatMap(({ activeId }) => this._getCfgOnce$(activeId)), // NOTE: might not be loaded yet - filter(jiraCfg => isJiraEnabled(jiraCfg)), + filter((jiraCfg) => isJiraEnabled(jiraCfg)), // just fire any single request concatMap((jiraCfg) => this._jiraApiService.getCurrentUser$(jiraCfg)), tap(() => this._isInitialRequestForProjectDone$.next(true)), ); - private _pollTimer$: Observable = timer(JIRA_INITIAL_POLL_BACKLOG_DELAY, JIRA_POLL_INTERVAL); + private _pollTimer$: Observable = timer( + JIRA_INITIAL_POLL_BACKLOG_DELAY, + JIRA_POLL_INTERVAL, + ); // ----------------- - @Effect({dispatch: false}) + @Effect({ dispatch: false }) pollNewIssuesToBacklog$: any = this._issueEffectHelperService.pollToBacklogTriggerToProjectId$.pipe( switchMap(this._afterInitialRequestCheckForProjectJiraSuccessfull$.bind(this)), - switchMap((pId: string) => this._getCfgOnce$(pId).pipe( - filter(jiraCfg => isJiraEnabled(jiraCfg) && jiraCfg.isAutoAddToBacklog), - // tap(() => console.log('POLL TIMER STARTED')), - switchMap(jiraCfg => this._pollTimer$.pipe( - // NOTE: required otherwise timer stays alive for filtered actions - takeUntil(this._issueEffectHelperService.pollToBacklogActions$), - tap(() => console.log('JIRA_POLL_BACKLOG_CHANGES')), - tap(() => this._importNewIssuesToBacklog(pId, jiraCfg)) - )), - )), - ); - @Effect({dispatch: false}) - pollIssueChangesForCurrentContext$: any = this._issueEffectHelperService.pollIssueTaskUpdatesActions$.pipe( - switchMap((inVal) => this._workContextService.isActiveWorkContextProject$.pipe( - take(1), - switchMap(isProject => isProject - ? this._afterInitialRequestCheckForProjectJiraSuccessfull$(inVal) - : of(inVal) + switchMap((pId: string) => + this._getCfgOnce$(pId).pipe( + filter((jiraCfg) => isJiraEnabled(jiraCfg) && jiraCfg.isAutoAddToBacklog), + // tap(() => console.log('POLL TIMER STARTED')), + switchMap((jiraCfg) => + this._pollTimer$.pipe( + // NOTE: required otherwise timer stays alive for filtered actions + takeUntil(this._issueEffectHelperService.pollToBacklogActions$), + tap(() => console.log('JIRA_POLL_BACKLOG_CHANGES')), + tap(() => this._importNewIssuesToBacklog(pId, jiraCfg)), + ), + ), ), - )), + ), + ); + @Effect({ dispatch: false }) + pollIssueChangesForCurrentContext$: any = this._issueEffectHelperService.pollIssueTaskUpdatesActions$.pipe( + switchMap((inVal) => + this._workContextService.isActiveWorkContextProject$.pipe( + take(1), + switchMap((isProject) => + isProject + ? this._afterInitialRequestCheckForProjectJiraSuccessfull$(inVal) + : of(inVal), + ), + ), + ), switchMap(() => this._pollTimer$), - switchMap(() => this._workContextService.allTasksForCurrentContext$.pipe( - first(), - switchMap((tasks) => { - const jiraIssueTasks = tasks.filter(task => task.issueType === JIRA_TYPE); - return forkJoin(jiraIssueTasks.map(task => { - if (!task.projectId) { - throw new Error('No projectId for task'); - } - return this._getCfgOnce$(task.projectId).pipe( - map(cfg => ({cfg, task})) + switchMap(() => + this._workContextService.allTasksForCurrentContext$.pipe( + first(), + switchMap((tasks) => { + const jiraIssueTasks = tasks.filter((task) => task.issueType === JIRA_TYPE); + return forkJoin( + jiraIssueTasks.map((task) => { + if (!task.projectId) { + throw new Error('No projectId for task'); + } + return this._getCfgOnce$(task.projectId).pipe( + map((cfg) => ({ cfg, task })), + ); + }), + ); + }), + map((cos) => + cos + .filter( + ({ cfg, task }: { cfg: JiraCfg; task: TaskWithSubTasks }) => + isJiraEnabled(cfg) && cfg.isAutoPollTickets, + ) + .map(({ task }: { cfg: JiraCfg; task: TaskWithSubTasks }) => task), + ), + tap((jiraTasks: TaskWithSubTasks[]) => { + if (jiraTasks && jiraTasks.length > 0) { + this._snackService.open({ + msg: T.F.JIRA.S.POLLING, + svgIco: 'jira', + isSpinner: true, + }); + jiraTasks.forEach((task) => + this._issueService.refreshIssue(task, true, false), ); } - )); - }), - map((cos) => cos - .filter(({cfg, task}: { cfg: JiraCfg; task: TaskWithSubTasks }) => - isJiraEnabled(cfg) && cfg.isAutoPollTickets - ) - .map(({task}: { cfg: JiraCfg; task: TaskWithSubTasks }) => task) + }), ), - tap((jiraTasks: TaskWithSubTasks[]) => { - if (jiraTasks && jiraTasks.length > 0) { - this._snackService.open({ - msg: T.F.JIRA.S.POLLING, - svgIco: 'jira', - isSpinner: true, - }); - jiraTasks.forEach((task) => this._issueService.refreshIssue(task, true, false)); - } - }), - )), + ), ); constructor( @@ -302,18 +365,23 @@ export class JiraIssueEffects { private readonly _issueService: IssueService, private readonly _matDialog: MatDialog, private readonly _issueEffectHelperService: IssueEffectHelperService, - ) { - } + ) {} - private _afterInitialRequestCheckForProjectJiraSuccessfull$(args: TY): Observable { + private _afterInitialRequestCheckForProjectJiraSuccessfull$( + args: TY, + ): Observable { return this._isInitialRequestForProjectDone$.pipe( - filter(isDone => isDone), + filter((isDone) => isDone), take(1), mapTo(args), ); } - private _handleTransitionForIssue(localState: IssueLocalState, jiraCfg: JiraCfg, task: Task): Observable { + private _handleTransitionForIssue( + localState: IssueLocalState, + jiraCfg: JiraCfg, + task: Task, + ): Observable { const chosenTransition: JiraTransitionOption = jiraCfg.transitionConfig[localState]; if (!task.issueId) { @@ -324,10 +392,11 @@ export class JiraIssueEffects { case 'DO_NOT': return EMPTY; case 'ALWAYS_ASK': - return this._jiraApiService.getReducedIssueById$(task.issueId, jiraCfg).pipe( - concatMap((issue) => this._openTransitionDialog(issue, localState, task) - ) - ); + return this._jiraApiService + .getReducedIssueById$(task.issueId, jiraCfg) + .pipe( + concatMap((issue) => this._openTransitionDialog(issue, localState, task)), + ); default: if (!chosenTransition || !chosenTransition.id) { this._snackService.open({ @@ -337,98 +406,118 @@ export class JiraIssueEffects { // NOTE: we would kill the whole effect chain if we do this // return throwError({[HANDLED_ERROR_PROP_STR]: 'Jira: No valid transition configured'}); return timer(2000).pipe( - concatMap(() => this._jiraApiService.getReducedIssueById$(task.issueId as string, jiraCfg)), - concatMap((issue: JiraIssueReduced) => this._openTransitionDialog(issue, localState, task)) + concatMap(() => + this._jiraApiService.getReducedIssueById$(task.issueId as string, jiraCfg), + ), + concatMap((issue: JiraIssueReduced) => + this._openTransitionDialog(issue, localState, task), + ), ); } return this._jiraApiService.getReducedIssueById$(task.issueId, jiraCfg).pipe( concatMap((issue) => { if (!issue.status || issue.status.name !== chosenTransition.name) { - return this._jiraApiService.transitionIssue$(issue.id, chosenTransition.id, jiraCfg).pipe( - concatMap(() => { - this._snackService.open({ - type: 'SUCCESS', - msg: T.F.JIRA.S.TRANSITION_SUCCESS, - translateParams: { - issueKey: `${issue.key}`, - chosenTransition: `${chosenTransition.name}`, - }, - }); - return this._issueService.refreshIssue(task, false, false); - }) - ); + return this._jiraApiService + .transitionIssue$(issue.id, chosenTransition.id, jiraCfg) + .pipe( + concatMap(() => { + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.JIRA.S.TRANSITION_SUCCESS, + translateParams: { + issueKey: `${issue.key}`, + chosenTransition: `${chosenTransition.name}`, + }, + }); + return this._issueService.refreshIssue(task, false, false); + }), + ); } else { // no transition required return EMPTY; } - }) + }), ); } } private _openWorklogDialog(task: Task, issueId: string, jiraCfg: JiraCfg) { - return this._jiraApiService.getReducedIssueById$(issueId, jiraCfg).pipe(take(1)).subscribe(issue => { - this._matDialog.open(DialogJiraAddWorklogComponent, { + return this._jiraApiService + .getReducedIssueById$(issueId, jiraCfg) + .pipe(take(1)) + .subscribe((issue) => { + this._matDialog.open(DialogJiraAddWorklogComponent, { + restoreFocus: true, + data: { + issue, + task, + }, + }); + }); + } + + private _openTransitionDialog( + issue: JiraIssueReduced, + localState: IssueLocalState, + task: Task, + ): Observable { + return this._matDialog + .open(DialogJiraTransitionComponent, { restoreFocus: true, data: { issue, + localState, task, - } - }); - }); - } - - private _openTransitionDialog(issue: JiraIssueReduced, localState: IssueLocalState, task: Task): Observable { - return this._matDialog.open(DialogJiraTransitionComponent, { - restoreFocus: true, - data: { - issue, - localState, - task, - } - }).afterClosed(); + }, + }) + .afterClosed(); } private _importNewIssuesToBacklog(projectId: string, cfg: JiraCfg) { - this._jiraApiService.findAutoImportIssues$(cfg).subscribe(async (issues: JiraIssueReduced[]) => { + this._jiraApiService + .findAutoImportIssues$(cfg) + .subscribe(async (issues: JiraIssueReduced[]) => { + if (!Array.isArray(issues)) { + return; + } + const allTaskJiraIssueIds = (await this._taskService.getAllIssueIdsForProject( + projectId, + JIRA_TYPE, + )) as string[]; - if (!Array.isArray(issues)) { - return; - } - const allTaskJiraIssueIds = await this._taskService.getAllIssueIdsForProject(projectId, JIRA_TYPE) as string[]; + // NOTE: we check for key as well as id although normally the key should suffice + const issuesToAdd = issues.filter( + (issue) => + !allTaskJiraIssueIds.includes(issue.id) && + !allTaskJiraIssueIds.includes(issue.key), + ); - // NOTE: we check for key as well as id although normally the key should suffice - const issuesToAdd = issues.filter( - issue => !allTaskJiraIssueIds.includes(issue.id) && !allTaskJiraIssueIds.includes(issue.key) - ); + issuesToAdd.forEach((issue) => { + this._issueService.addTaskWithIssue(JIRA_TYPE, issue, projectId, true); + }); - issuesToAdd.forEach((issue) => { - this._issueService.addTaskWithIssue(JIRA_TYPE, issue, projectId, true); + if (issuesToAdd.length === 1) { + this._snackService.open({ + translateParams: { + issueText: truncate(`${issuesToAdd[0].key} ${issuesToAdd[0].summary}`), + }, + msg: T.F.JIRA.S.IMPORTED_SINGLE_ISSUE, + ico: 'cloud_download', + }); + } else if (issuesToAdd.length > 1) { + this._snackService.open({ + translateParams: { + issuesLength: issuesToAdd.length, + }, + msg: T.F.JIRA.S.IMPORTED_MULTIPLE_ISSUES, + ico: 'cloud_download', + }); + } }); - - if (issuesToAdd.length === 1) { - this._snackService.open({ - translateParams: { - issueText: truncate(`${issuesToAdd[0].key} ${issuesToAdd[0].summary}`), - }, - msg: T.F.JIRA.S.IMPORTED_SINGLE_ISSUE, - ico: 'cloud_download', - }); - } else if (issuesToAdd.length > 1) { - this._snackService.open({ - translateParams: { - issuesLength: issuesToAdd.length - }, - msg: T.F.JIRA.S.IMPORTED_MULTIPLE_ISSUES, - ico: 'cloud_download', - }); - } - }); } private _getCfgOnce$(projectId: string): Observable { return this._projectService.getJiraCfgForProject$(projectId).pipe(first()); } } - diff --git a/src/app/features/issue/providers/jira/jira-issue/jira-issue.model.ts b/src/app/features/issue/providers/jira/jira-issue/jira-issue.model.ts index 7e5841de7..3c2f19bd2 100644 --- a/src/app/features/issue/providers/jira/jira-issue/jira-issue.model.ts +++ b/src/app/features/issue/providers/jira/jira-issue/jira-issue.model.ts @@ -45,7 +45,7 @@ export type JiraChangelogEntry = Readonly<{ // NOTE this is NOT equal to JiraIssueOriginalReduced export type JiraIssueReduced = Readonly<{ -// copied data + // copied data key: string; id: string; summary: string; @@ -66,7 +66,7 @@ export type JiraIssueReduced = Readonly<{ storyPoints?: number; }>; -export type JiraIssue = JiraIssueReduced & Readonly<{ - changelog: JiraChangelogEntry[]; -}>; - +export type JiraIssue = JiraIssueReduced & + Readonly<{ + changelog: JiraChangelogEntry[]; + }>; diff --git a/src/app/features/issue/providers/jira/jira-issue/jira-issue.module.ts b/src/app/features/issue/providers/jira/jira-issue/jira-issue.module.ts index 0d710aa9b..556a4ff00 100644 --- a/src/app/features/issue/providers/jira/jira-issue/jira-issue.module.ts +++ b/src/app/features/issue/providers/jira/jira-issue/jira-issue.module.ts @@ -15,14 +15,7 @@ import { JiraIssueContentComponent } from './jira-issue-content/jira-issue-conte ReactiveFormsModule, EffectsModule.forFeature([JiraIssueEffects]), ], - declarations: [ - JiraIssueHeaderComponent, - JiraIssueContentComponent, - ], - exports: [ - JiraIssueHeaderComponent, - JiraIssueContentComponent, - ], + declarations: [JiraIssueHeaderComponent, JiraIssueContentComponent], + exports: [JiraIssueHeaderComponent, JiraIssueContentComponent], }) -export class JiraIssueModule { -} +export class JiraIssueModule {} diff --git a/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-add-worklog/dialog-jira-add-worklog.component.ts b/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-add-worklog/dialog-jira-add-worklog.component.ts index b70f63ee1..a407c3c39 100644 --- a/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-add-worklog/dialog-jira-add-worklog.component.ts +++ b/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-add-worklog/dialog-jira-add-worklog.component.ts @@ -13,7 +13,7 @@ import * as moment from 'moment'; selector: 'dialog-jira-add-worklog', templateUrl: './dialog-jira-add-worklog.component.html', styleUrls: ['./dialog-jira-add-worklog.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogJiraAddWorklogComponent { T: typeof T = T; @@ -27,10 +27,11 @@ export class DialogJiraAddWorklogComponent { private _matDialogRef: MatDialogRef, private _snackService: SnackService, private _projectService: ProjectService, - @Inject(MAT_DIALOG_DATA) public data: { + @Inject(MAT_DIALOG_DATA) + public data: { issue: JiraIssue; task: Task; - } + }, ) { this.timeSpent = this.data.task.timeSpent; this.issue = this.data.issue; @@ -44,21 +45,26 @@ export class DialogJiraAddWorklogComponent { async submitWorklog() { if (this.issue.id && this.started && this.timeSpent && this.data.task.projectId) { - const cfg = await this._projectService.getJiraCfgForProject$(this.data.task.projectId).pipe(first()).toPromise(); - this._jiraApiService.addWorklog$({ - issueId: this.issue.id, - started: this.started, - timeSpent: this.timeSpent, - comment: this.comment, - cfg, - }).subscribe(res => { - this._snackService.open({ - type: 'SUCCESS', - msg: T.F.JIRA.S.ADDED_WORKLOG_FOR, - translateParams: {issueKey: this.issue.key} + const cfg = await this._projectService + .getJiraCfgForProject$(this.data.task.projectId) + .pipe(first()) + .toPromise(); + this._jiraApiService + .addWorklog$({ + issueId: this.issue.id, + started: this.started, + timeSpent: this.timeSpent, + comment: this.comment, + cfg, + }) + .subscribe((res) => { + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.JIRA.S.ADDED_WORKLOG_FOR, + translateParams: { issueKey: this.issue.key }, + }); + this.close(); }); - this.close(); - }); } } diff --git a/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-initial-setup/dialog-jira-initial-setup.component.ts b/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-initial-setup/dialog-jira-initial-setup.component.ts index 039da62d3..dfc6711ff 100644 --- a/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-initial-setup/dialog-jira-initial-setup.component.ts +++ b/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-initial-setup/dialog-jira-initial-setup.component.ts @@ -7,7 +7,7 @@ import { T } from '../../../../../../t.const'; selector: 'dialog-jira-initial-setup', templateUrl: './dialog-jira-initial-setup.component.html', styleUrls: ['./dialog-jira-initial-setup.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogJiraInitialSetupComponent { T: typeof T = T; diff --git a/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-transition/dialog-jira-transition.component.ts b/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-transition/dialog-jira-transition.component.ts index ea27d641c..3aea158fc 100644 --- a/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-transition/dialog-jira-transition.component.ts +++ b/src/app/features/issue/providers/jira/jira-view-components/dialog-jira-transition/dialog-jira-transition.component.ts @@ -17,16 +17,20 @@ import { JiraCfg } from '../../jira.model'; selector: 'dialog-jira-transition', templateUrl: './dialog-jira-transition.component.html', styleUrls: ['./dialog-jira-transition.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogJiraTransitionComponent { T: typeof T = T; - _jiraCfg$: Observable = this._projectService.getJiraCfgForProject$(this.data.task.projectId as string); + _jiraCfg$: Observable = this._projectService.getJiraCfgForProject$( + this.data.task.projectId as string, + ); availableTransitions$: Observable = this._jiraCfg$.pipe( first(), - switchMap((cfg) => this._jiraApiService.getTransitionsForIssue$(this.data.issue.id, cfg)) + switchMap((cfg) => + this._jiraApiService.getTransitionsForIssue$(this.data.issue.id, cfg), + ), ); chosenTransition?: JiraOriginalTransition; @@ -37,11 +41,12 @@ export class DialogJiraTransitionComponent { private _jiraCommonInterfacesService: JiraCommonInterfacesService, private _matDialogRef: MatDialogRef, private _snackService: SnackService, - @Inject(MAT_DIALOG_DATA) public data: { + @Inject(MAT_DIALOG_DATA) + public data: { issue: JiraIssueReduced; localState: IssueLocalState; task: Task; - } + }, ) { if (!this.data.task.projectId) { throw new Error('No projectId for task'); @@ -56,18 +61,22 @@ export class DialogJiraTransitionComponent { if (this.chosenTransition && this.chosenTransition.id) { const trans: JiraOriginalTransition = this.chosenTransition; - this._jiraCfg$.pipe( - concatMap((jiraCfg) => this._jiraApiService.transitionIssue$(this.data.issue.id, trans.id, jiraCfg)), - first(), - ).subscribe(() => { - this._jiraCommonInterfacesService.refreshIssue(this.data.task, false, false); - this._snackService.open({ - type: 'SUCCESS', - msg: T.F.JIRA.S.TRANSITION, - translateParams: {issueKey: this.data.issue.key, name: trans.name} + this._jiraCfg$ + .pipe( + concatMap((jiraCfg) => + this._jiraApiService.transitionIssue$(this.data.issue.id, trans.id, jiraCfg), + ), + first(), + ) + .subscribe(() => { + this._jiraCommonInterfacesService.refreshIssue(this.data.task, false, false); + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.JIRA.S.TRANSITION, + translateParams: { issueKey: this.data.issue.key, name: trans.name }, + }); + this.close(); }); - this.close(); - }); } } diff --git a/src/app/features/issue/providers/jira/jira-view-components/jira-cfg-stepper/jira-cfg-stepper.component.ts b/src/app/features/issue/providers/jira/jira-view-components/jira-cfg-stepper/jira-cfg-stepper.component.ts index 348d4df16..41e025267 100644 --- a/src/app/features/issue/providers/jira/jira-view-components/jira-cfg-stepper/jira-cfg-stepper.component.ts +++ b/src/app/features/issue/providers/jira/jira-view-components/jira-cfg-stepper/jira-cfg-stepper.component.ts @@ -5,11 +5,15 @@ import { EventEmitter, Input, OnDestroy, - Output + Output, } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { JiraCfg } from '../../jira.model'; -import { DEFAULT_JIRA_CFG, JIRA_ADVANCED_FORM_CFG, JIRA_CREDENTIALS_FORM_CFG } from '../../jira.const'; +import { + DEFAULT_JIRA_CFG, + JIRA_ADVANCED_FORM_CFG, + JIRA_CREDENTIALS_FORM_CFG, +} from '../../jira.const'; import { FormlyFieldConfig } from '@ngx-formly/core'; import { JiraApiService } from '../../jira-api.service'; import { JiraOriginalUser } from '../../jira-api-responses'; @@ -24,7 +28,7 @@ import { HANDLED_ERROR_PROP_STR, HelperClasses } from '../../../../../../app.con templateUrl: './jira-cfg-stepper.component.html', styleUrls: ['./jira-cfg-stepper.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation] + animations: [expandAnimation], }) export class JiraCfgStepperComponent implements OnDestroy { T: typeof T = T; @@ -37,7 +41,7 @@ export class JiraCfgStepperComponent implements OnDestroy { isTestCredentialsSuccess: boolean = false; user?: JiraOriginalUser; - jiraCfg: JiraCfg = Object.assign({}, DEFAULT_JIRA_CFG, {isEnabled: true}); + jiraCfg: JiraCfg = Object.assign({}, DEFAULT_JIRA_CFG, { isEnabled: true }); @Output() saveCfg: EventEmitter = new EventEmitter(); private _subs: Subscription = new Subscription(); @@ -52,7 +56,7 @@ export class JiraCfgStepperComponent implements OnDestroy { @Input() set cfg(cfg: JiraCfg) { if (cfg) { - this.jiraCfg = {...cfg}; + this.jiraCfg = { ...cfg }; } } @@ -71,19 +75,22 @@ export class JiraCfgStepperComponent implements OnDestroy { testCredentials() { this.isTestCredentialsSuccess = false; this._subs.add( - this._jiraApiService.getCurrentUser$(this.jiraCfg, true) - .pipe(catchError((err) => { - this.isTestCredentialsSuccess = false; - this.user = undefined; - this._changeDetectorRef.detectChanges(); - return throwError({[HANDLED_ERROR_PROP_STR]: err}); - })) + this._jiraApiService + .getCurrentUser$(this.jiraCfg, true) + .pipe( + catchError((err) => { + this.isTestCredentialsSuccess = false; + this.user = undefined; + this._changeDetectorRef.detectChanges(); + return throwError({ [HANDLED_ERROR_PROP_STR]: err }); + }), + ) .subscribe((user: JiraOriginalUser) => { this.user = user; this.isTestCredentialsSuccess = true; this._changeDetectorRef.detectChanges(); - }) + }), ); } } diff --git a/src/app/features/issue/providers/jira/jira-view-components/jira-cfg/jira-cfg.component.ts b/src/app/features/issue/providers/jira/jira-view-components/jira-cfg/jira-cfg.component.ts index f321da126..75f832b8d 100644 --- a/src/app/features/issue/providers/jira/jira-view-components/jira-cfg/jira-cfg.component.ts +++ b/src/app/features/issue/providers/jira/jira-view-components/jira-cfg/jira-cfg.component.ts @@ -1,5 +1,16 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { ConfigFormSection, GlobalConfigSectionKey } from '../../../../../config/global-config.model'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; +import { + ConfigFormSection, + GlobalConfigSectionKey, +} from '../../../../../config/global-config.model'; import { ProjectCfgFormKey } from '../../../../../project/project.model'; import { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core'; import { FormControl, FormGroup } from '@angular/forms'; @@ -7,7 +18,15 @@ import { JiraCfg, JiraTransitionConfig, JiraTransitionOption } from '../../jira. import { expandAnimation } from '../../../../../../ui/animations/expand.ani'; import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { SearchResultItem } from '../../../../issue.model'; -import { catchError, concatMap, debounceTime, first, map, switchMap, tap } from 'rxjs/operators'; +import { + catchError, + concatMap, + debounceTime, + first, + map, + switchMap, + tap, +} from 'rxjs/operators'; import { JiraApiService } from '../../jira-api.service'; import { DEFAULT_JIRA_CFG } from '../../jira.const'; import { JiraIssue } from '../../jira-issue/jira-issue.model'; @@ -24,46 +43,57 @@ import { JIRA_TYPE } from '../../../../issue.const'; templateUrl: './jira-cfg.component.html', styleUrls: ['./jira-cfg.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation] + animations: [expandAnimation], }) export class JiraCfgComponent implements OnInit, OnDestroy { @Input() section?: ConfigFormSection; - @Output() save: EventEmitter<{ sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; config: any }> = new EventEmitter(); + @Output() save: EventEmitter<{ + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; + config: any; + }> = new EventEmitter(); T: typeof T = T; HelperClasses: typeof HelperClasses = HelperClasses; issueSuggestionsCtrl: FormControl = new FormControl(); customFieldSuggestionsCtrl: FormControl = new FormControl(); - customFields: any [] = []; + customFields: any[] = []; customFieldsPromise?: Promise; isLoading$: BehaviorSubject = new BehaviorSubject(false); fields?: FormlyFieldConfig[]; form: FormGroup = new FormGroup({}); options: FormlyFormOptions = {}; - filteredIssueSuggestions$: Observable = this.issueSuggestionsCtrl.valueChanges.pipe( + filteredIssueSuggestions$: Observable< + SearchResultItem[] + > = this.issueSuggestionsCtrl.valueChanges.pipe( debounceTime(300), tap(() => this.isLoading$.next(true)), switchMap((searchTerm: string) => { - return (searchTerm && searchTerm.length > 1) - ? this._projectService.getJiraCfgForProject$(this._workContextService.activeWorkContextId as string) - .pipe( - first(), - switchMap((cfg) => this._jiraApiService.issuePicker$(searchTerm, cfg)), - catchError(() => { - return []; - }) - ) - // Note: the outer array signifies the observable stream the other is the value - : [[]]; + return searchTerm && searchTerm.length > 1 + ? this._projectService + .getJiraCfgForProject$(this._workContextService.activeWorkContextId as string) + .pipe( + first(), + switchMap((cfg) => this._jiraApiService.issuePicker$(searchTerm, cfg)), + catchError(() => { + return []; + }), + ) + : // Note: the outer array signifies the observable stream the other is the value + [[]]; // TODO fix type }), tap((suggestions) => { this.isLoading$.next(false); }), ); - filteredCustomFieldSuggestions$: Observable = this.customFieldSuggestionsCtrl.valueChanges.pipe( - map(value => this._filterCustomFieldSuggestions(value)), + filteredCustomFieldSuggestions$: Observable< + any[] + > = this.customFieldSuggestionsCtrl.valueChanges.pipe( + map((value) => this._filterCustomFieldSuggestions(value)), ); - transitionConfigOpts: { key: keyof JiraTransitionConfig; val: JiraTransitionOption }[] = []; + transitionConfigOpts: { + key: keyof JiraTransitionConfig; + val: JiraTransitionOption; + }[] = []; private _subs: Subscription = new Subscription(); @@ -72,8 +102,7 @@ export class JiraCfgComponent implements OnInit, OnDestroy { private _snackService: SnackService, private _projectService: ProjectService, private _workContextService: WorkContextService, - ) { - } + ) {} private _cfg?: JiraCfg; @@ -83,9 +112,7 @@ export class JiraCfgComponent implements OnInit, OnDestroy { // NOTE: this is legit because it might be that there is no issue provider cfg yet @Input() set cfg(cfg: JiraCfg) { - const newCfg: JiraCfg = cfg - ? {...cfg} - : DEFAULT_JIRA_CFG; + const newCfg: JiraCfg = cfg ? { ...cfg } : DEFAULT_JIRA_CFG; if (!newCfg.transitionConfig) { newCfg.transitionConfig = DEFAULT_JIRA_CFG.transitionConfig; @@ -106,10 +133,10 @@ export class JiraCfgComponent implements OnInit, OnDestroy { this.transitionConfigOpts = Object.keys(newCfg.transitionConfig).map((k: string) => { const key = k as keyof JiraTransitionConfig; - return ({ + return { key, - val: newCfg.transitionConfig[key] - }); + val: newCfg.transitionConfig[key], + }; }); } @@ -126,7 +153,7 @@ export class JiraCfgComponent implements OnInit, OnDestroy { } setTransition(key: keyof JiraTransitionConfig, value: JiraTransitionOption) { - return this.cfg.transitionConfig[key] = value; + return (this.cfg.transitionConfig[key] = value); } toggleEnabled(isEnabled: boolean) { @@ -141,7 +168,9 @@ export class JiraCfgComponent implements OnInit, OnDestroy { submit() { if (!this.cfg) { - throw new Error('No config for ' + (this.section as ConfigFormSection).key); + throw new Error( + 'No config for ' + (this.section as ConfigFormSection).key, + ); } else { this.save.emit({ sectionKey: (this.section as ConfigFormSection).key, @@ -163,10 +192,13 @@ export class JiraCfgComponent implements OnInit, OnDestroy { } loadCustomFields() { - this.customFieldsPromise = this._projectService.getJiraCfgForProject$(this._workContextService.activeWorkContextId as string).pipe( - first(), - concatMap((jiraCfg) => this._jiraApiService.listFields$(jiraCfg)) - ).toPromise(); + this.customFieldsPromise = this._projectService + .getJiraCfgForProject$(this._workContextService.activeWorkContextId as string) + .pipe( + first(), + concatMap((jiraCfg) => this._jiraApiService.listFields$(jiraCfg)), + ) + .toPromise(); this.customFieldsPromise.then((v: any) => { if (v && Array.isArray(v.response)) { this.customFields = v.response; @@ -182,24 +214,26 @@ export class JiraCfgComponent implements OnInit, OnDestroy { } else { const issueId = searchResultItem.issueData.id as string; this._subs.add( - this._jiraApiService.getTransitionsForIssue$(issueId, this.cfg) + this._jiraApiService + .getTransitionsForIssue$(issueId, this.cfg) .subscribe((val) => { this.cfg.availableTransitions = val; this._snackService.open({ type: 'SUCCESS', msg: T.F.JIRA.S.TRANSITIONS_LOADED, }); - }) + }), ); } } private _filterCustomFieldSuggestions(value: string): string[] { const filterValue = value && value.toLowerCase(); - return this.customFields.filter(field => field - && ( - field.name.toLowerCase().includes(filterValue) - || field.id.includes(filterValue) - )); + return this.customFields.filter( + (field) => + field && + (field.name.toLowerCase().includes(filterValue) || + field.id.includes(filterValue)), + ); } } diff --git a/src/app/features/issue/providers/jira/jira-view-components/jira-view-components.module.ts b/src/app/features/issue/providers/jira/jira-view-components/jira-view-components.module.ts index 94605d1c0..b83a6fb42 100644 --- a/src/app/features/issue/providers/jira/jira-view-components/jira-view-components.module.ts +++ b/src/app/features/issue/providers/jira/jira-view-components/jira-view-components.module.ts @@ -9,11 +9,7 @@ import { DialogJiraTransitionComponent } from './dialog-jira-transition/dialog-j import { DialogJiraAddWorklogComponent } from './dialog-jira-add-worklog/dialog-jira-add-worklog.component'; @NgModule({ - imports: [ - CommonModule, - UiModule, - FormsModule, - ], + imports: [CommonModule, UiModule, FormsModule], declarations: [ DialogJiraTransitionComponent, DialogJiraInitialSetupComponent, @@ -21,9 +17,6 @@ import { DialogJiraAddWorklogComponent } from './dialog-jira-add-worklog/dialog- JiraCfgStepperComponent, JiraCfgComponent, ], - exports: [ - JiraCfgStepperComponent - ], + exports: [JiraCfgStepperComponent], }) -export class JiraViewComponentsModule { -} +export class JiraViewComponentsModule {} diff --git a/src/app/features/issue/providers/jira/jira.const.ts b/src/app/features/issue/providers/jira/jira.const.ts index 799e4c06b..f0a012317 100644 --- a/src/app/features/issue/providers/jira/jira.const.ts +++ b/src/app/features/issue/providers/jira/jira.const.ts @@ -2,7 +2,10 @@ import { JiraCfg } from './jira.model'; import { GITHUB_INITIAL_POLL_DELAY } from '../github/github.const'; import { T } from '../../../../t.const'; -import { ConfigFormSection, LimitedFormlyFieldConfig } from '../../../config/global-config.model'; +import { + ConfigFormSection, + LimitedFormlyFieldConfig, +} from '../../../config/global-config.model'; export const JIRA_DATETIME_FORMAT = 'YYYY-MM-DDTHH:mm:ss.SSZZ'; @@ -18,7 +21,8 @@ export const DEFAULT_JIRA_CFG: JiraCfg = { searchJqlQuery: '', isAutoAddToBacklog: true, - autoAddBacklogJqlQuery: 'assignee = currentUser() AND sprint in openSprints() AND resolution = Unresolved', + autoAddBacklogJqlQuery: + 'assignee = currentUser() AND sprint in openSprints() AND resolution = Unresolved', isWorklogEnabled: true, isAutoWorklog: false, @@ -38,9 +42,9 @@ export const DEFAULT_JIRA_CFG: JiraCfg = { transitionConfig: { OPEN: 'DO_NOT', IN_PROGRESS: 'ALWAYS_ASK', - DONE: 'ALWAYS_ASK' + DONE: 'ALWAYS_ASK', }, - userToAssignOnDone: null + userToAssignOnDone: null, }; // export const JIRA_POLL_INTERVAL = 10 * 1000; @@ -104,21 +108,21 @@ export const JIRA_CREDENTIALS_FORM_CFG: LimitedFormlyFieldConfig[] = [ required: true, label: T.F.JIRA.FORM_CRED.PASSWORD, type: 'password', - description: '* https://confluence.atlassian.com/cloud/api-tokens-938839638.html' + description: '* https://confluence.atlassian.com/cloud/api-tokens-938839638.html', }, }, { key: 'isAllowSelfSignedCertificate', type: 'checkbox', templateOptions: { - label: T.F.JIRA.FORM_CRED.ALLOW_SELF_SIGNED + label: T.F.JIRA.FORM_CRED.ALLOW_SELF_SIGNED, }, }, { key: 'isWonkyCookieMode', type: 'checkbox', templateOptions: { - label: T.F.JIRA.FORM_CRED.WONKY_COOKIE_MODE + label: T.F.JIRA.FORM_CRED.WONKY_COOKIE_MODE, }, }, ]; @@ -128,49 +132,49 @@ export const JIRA_ADVANCED_FORM_CFG: LimitedFormlyFieldConfig[] = [ key: 'isAutoPollTickets', type: 'checkbox', templateOptions: { - label: T.F.JIRA.FORM_ADV.IS_AUTO_POLL_TICKETS + label: T.F.JIRA.FORM_ADV.IS_AUTO_POLL_TICKETS, }, }, { key: 'isCheckToReAssignTicketOnTaskStart', type: 'checkbox', templateOptions: { - label: T.F.JIRA.FORM_ADV.IS_CHECK_TO_RE_ASSIGN_TICKET_ON_TASK_START + label: T.F.JIRA.FORM_ADV.IS_CHECK_TO_RE_ASSIGN_TICKET_ON_TASK_START, }, }, { key: 'isAutoAddToBacklog', type: 'checkbox', templateOptions: { - label: T.F.JIRA.FORM_ADV.IS_AUTO_ADD_TO_BACKLOG + label: T.F.JIRA.FORM_ADV.IS_AUTO_ADD_TO_BACKLOG, }, }, { key: 'autoAddBacklogJqlQuery', type: 'input', templateOptions: { - label: T.F.JIRA.FORM_ADV.AUTO_ADD_BACKLOG_JQL_QUERY + label: T.F.JIRA.FORM_ADV.AUTO_ADD_BACKLOG_JQL_QUERY, }, }, { key: 'searchJqlQuery', type: 'input', templateOptions: { - label: T.F.JIRA.FORM_ADV.SEARCH_JQL_QUERY + label: T.F.JIRA.FORM_ADV.SEARCH_JQL_QUERY, }, }, { key: 'isWorklogEnabled', type: 'checkbox', templateOptions: { - label: T.F.JIRA.FORM_ADV.IS_WORKLOG_ENABLED + label: T.F.JIRA.FORM_ADV.IS_WORKLOG_ENABLED, }, }, { key: 'isAddWorklogOnSubTaskDone', type: 'checkbox', templateOptions: { - label: T.F.JIRA.FORM_ADV.IS_ADD_WORKLOG_ON_SUB_TASK_DONE + label: T.F.JIRA.FORM_ADV.IS_ADD_WORKLOG_ON_SUB_TASK_DONE, }, }, ]; @@ -205,7 +209,7 @@ export const JIRA_CONFIG_FORM_SECTION: ConfigFormSection = { templateOptions: { tag: 'h3', class: 'sub-section-heading', - text: T.F.JIRA.FORM_SECTION.CREDENTIALS + text: T.F.JIRA.FORM_SECTION.CREDENTIALS, }, }, ...JIRA_CREDENTIALS_FORM_CFG, @@ -215,9 +219,9 @@ export const JIRA_CONFIG_FORM_SECTION: ConfigFormSection = { templateOptions: { tag: 'h3', class: 'sub-section-heading', - text: T.F.JIRA.FORM_SECTION.ADV_CFG + text: T.F.JIRA.FORM_SECTION.ADV_CFG, }, }, ...JIRA_ADVANCED_FORM_CFG, - ] + ], }; diff --git a/src/app/features/issue/providers/jira/jira.model.ts b/src/app/features/issue/providers/jira/jira.model.ts index 9bf7fb0a8..3abf26eba 100644 --- a/src/app/features/issue/providers/jira/jira.model.ts +++ b/src/app/features/issue/providers/jira/jira.model.ts @@ -66,4 +66,3 @@ isCheckToReAssignTicket ==> via change current task transitioning ==> via time tracking state */ - diff --git a/src/app/features/metric/evaluation-sheet/evaluation-sheet.component.ts b/src/app/features/metric/evaluation-sheet/evaluation-sheet.component.ts index 70c9a0c5c..aae69d933 100644 --- a/src/app/features/metric/evaluation-sheet/evaluation-sheet.component.ts +++ b/src/app/features/metric/evaluation-sheet/evaluation-sheet.component.ts @@ -6,7 +6,7 @@ import { Input, OnDestroy, OnInit, - Output + Output, } from '@angular/core'; import { MetricCopy } from '../metric.model'; import { MetricService } from '../metric.service'; @@ -23,7 +23,7 @@ import { MatDialog } from '@angular/material/dialog'; selector: 'evaluation-sheet', templateUrl: './evaluation-sheet.component.html', styleUrls: ['./evaluation-sheet.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class EvaluationSheetComponent implements OnDestroy, OnInit { @Output() save: EventEmitter = new EventEmitter(); @@ -31,7 +31,9 @@ export class EvaluationSheetComponent implements OnDestroy, OnInit { metricForDay?: MetricCopy; day$: BehaviorSubject = new BehaviorSubject(getWorklogStr()); private _metricForDay$: Observable = this.day$.pipe( - switchMap((day) => this._metricService.getMetricForDayOrDefaultWithCheckedImprovements$(day)), + switchMap((day) => + this._metricService.getMetricForDayOrDefaultWithCheckedImprovements$(day), + ), ); // isForToday$: Observable = this.day$.pipe(map(day => day === getWorklogStr())); private _subs: Subscription = new Subscription(); @@ -42,18 +44,19 @@ export class EvaluationSheetComponent implements OnDestroy, OnInit { private _metricService: MetricService, private _matDialog: MatDialog, private _cd: ChangeDetectorRef, - ) { - } + ) {} @Input() set day(val: string) { this.day$.next(val); } ngOnInit(): void { - this._subs.add(this._metricForDay$.subscribe(metric => { - this.metricForDay = metric; - this._cd.detectChanges(); - })); + this._subs.add( + this._metricForDay$.subscribe((metric) => { + this.metricForDay = metric; + this._cd.detectChanges(); + }), + ); } ngOnDestroy(): void { @@ -61,51 +64,79 @@ export class EvaluationSheetComponent implements OnDestroy, OnInit { } updateMood(mood: number) { - this._update({mood}); + this._update({ mood }); } updateProductivity(productivity: number) { - this._update({productivity}); + this._update({ productivity }); } addObstruction(v: string) { - this._update({obstructions: [...(this.metricForDay as MetricCopy).obstructions, v]}); + this._update({ + obstructions: [...(this.metricForDay as MetricCopy).obstructions, v], + }); } addNewObstruction(v: string) { const id = this.obstructionService.addObstruction(v); - this._update({obstructions: [...(this.metricForDay as MetricCopy).obstructions, id]}); + this._update({ + obstructions: [...(this.metricForDay as MetricCopy).obstructions, id], + }); } removeObstruction(idToRemove: string) { - this._update({obstructions: (this.metricForDay as MetricCopy).obstructions.filter(id => id !== idToRemove)}); + this._update({ + obstructions: (this.metricForDay as MetricCopy).obstructions.filter( + (id) => id !== idToRemove, + ), + }); } addImprovement(v: string) { - this._update({improvements: [...(this.metricForDay as MetricCopy).improvements, v]}); + this._update({ + improvements: [...(this.metricForDay as MetricCopy).improvements, v], + }); } addNewImprovement(v: string) { const id = this.improvementService.addImprovement(v); - this._update({improvements: [...(this.metricForDay as MetricCopy).improvements, id]}); + this._update({ + improvements: [...(this.metricForDay as MetricCopy).improvements, id], + }); } removeImprovement(idToRemove: string) { - this._update({improvements: (this.metricForDay as MetricCopy).improvements.filter(id => id !== idToRemove)}); + this._update({ + improvements: (this.metricForDay as MetricCopy).improvements.filter( + (id) => id !== idToRemove, + ), + }); } addImprovementTomorrow(v: string) { - this._update({improvementsTomorrow: [...(this.metricForDay as MetricCopy).improvementsTomorrow, v]}); + this._update({ + improvementsTomorrow: [ + ...(this.metricForDay as MetricCopy).improvementsTomorrow, + v, + ], + }); } addNewImprovementTomorrow(v: string) { const id = this.improvementService.addImprovement(v); - this._update({improvementsTomorrow: [...(this.metricForDay as MetricCopy).improvementsTomorrow, id]}); + this._update({ + improvementsTomorrow: [ + ...(this.metricForDay as MetricCopy).improvementsTomorrow, + id, + ], + }); } removeImprovementTomorrow(idToRemove: string) { this._update({ - improvementsTomorrow: (this.metricForDay as MetricCopy).improvementsTomorrow.filter(id => id !== idToRemove), + improvementsTomorrow: (this.metricForDay as MetricCopy).improvementsTomorrow.filter( + (id) => id !== idToRemove, + ), }); // this.improvementService.disableImprovementRepeat(idToRemove); } @@ -123,6 +154,6 @@ export class EvaluationSheetComponent implements OnDestroy, OnInit { ...(this.metricForDay as MetricCopy), ...updateData, } as MetricCopy; - this._metricService.upsertMetric((this.metricForDay as MetricCopy)); + this._metricService.upsertMetric(this.metricForDay as MetricCopy); } } diff --git a/src/app/features/metric/improvement-banner/improvement-banner.ani.ts b/src/app/features/metric/improvement-banner/improvement-banner.ani.ts index 6785d8ca4..b56356789 100644 --- a/src/app/features/metric/improvement-banner/improvement-banner.ani.ts +++ b/src/app/features/metric/improvement-banner/improvement-banner.ani.ts @@ -1,23 +1,40 @@ -import { animate, keyframes, query, stagger, style, transition, trigger } from '@angular/animations'; +import { + animate, + keyframes, + query, + stagger, + style, + transition, + trigger, +} from '@angular/animations'; import { ANI_FAST_TIMING } from '../../../ui/animations/animation.const'; const ANI = [ - query(':enter', style({opacity: 0, width: 0}), {optional: true}), + query(':enter', style({ opacity: 0, width: 0 }), { optional: true }), - query(':enter', stagger('40ms', [ - animate(ANI_FAST_TIMING, keyframes([ - style({opacity: 0, width: 0, transform: 'scale(0)', offset: 0}), - style({opacity: 1, width: '*', transform: 'scale(1)', offset: 0.99}), - style({width: 'auto', offset: 1.0}), - ]))]), {optional: true} + query( + ':enter', + stagger('40ms', [ + animate( + ANI_FAST_TIMING, + keyframes([ + style({ opacity: 0, width: 0, transform: 'scale(0)', offset: 0 }), + style({ opacity: 1, width: '*', transform: 'scale(1)', offset: 0.99 }), + style({ width: 'auto', offset: 1.0 }), + ]), + ), + ]), + { optional: true }, ), query( - ':leave', stagger('-40ms', [ - style({transform: 'scale(1)', opacity: 1, width: '*'}), - animate(ANI_FAST_TIMING, style({transform: 'scale(0)', width: 0})) - ], - ), {optional: true}), + ':leave', + stagger('-40ms', [ + style({ transform: 'scale(1)', opacity: 1, width: '*' }), + animate(ANI_FAST_TIMING, style({ transform: 'scale(0)', width: 0 })), + ]), + { optional: true }, + ), ]; export const improvementBannerAnimation = trigger('improvementBanner', [ diff --git a/src/app/features/metric/improvement-banner/improvement-banner.component.ts b/src/app/features/metric/improvement-banner/improvement-banner.component.ts index 1d8a77741..73905ac7c 100644 --- a/src/app/features/metric/improvement-banner/improvement-banner.component.ts +++ b/src/app/features/metric/improvement-banner/improvement-banner.component.ts @@ -11,7 +11,7 @@ import { T } from '../../../t.const'; templateUrl: './improvement-banner.component.html', styleUrls: ['./improvement-banner.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [improvementBannerAnimation] + animations: [improvementBannerAnimation], }) export class ImprovementBannerComponent implements OnDestroy { T: typeof T = T; @@ -19,10 +19,12 @@ export class ImprovementBannerComponent implements OnDestroy { private _subs: Subscription = new Subscription(); - constructor( - public improvementService: ImprovementService, - ) { - this._subs.add(this.improvementService.improvementBannerImprovements$.subscribe(val => this.improvements = val || [])); + constructor(public improvementService: ImprovementService) { + this._subs.add( + this.improvementService.improvementBannerImprovements$.subscribe( + (val) => (this.improvements = val || []), + ), + ); } ngOnDestroy(): void { @@ -41,5 +43,4 @@ export class ImprovementBannerComponent implements OnDestroy { trackById(i: number, improvement: Improvement): string { return improvement.id; } - } diff --git a/src/app/features/metric/improvement/improvement.module.ts b/src/app/features/metric/improvement/improvement.module.ts index 97fd9c137..358b3ebaa 100644 --- a/src/app/features/metric/improvement/improvement.module.ts +++ b/src/app/features/metric/improvement/improvement.module.ts @@ -3,16 +3,18 @@ import { CommonModule } from '@angular/common'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { ImprovementEffects } from './store/improvement.effects'; -import { IMPROVEMENT_FEATURE_NAME, improvementReducer } from './store/improvement.reducer'; +import { + IMPROVEMENT_FEATURE_NAME, + improvementReducer, +} from './store/improvement.reducer'; @NgModule({ imports: [ CommonModule, StoreModule.forFeature(IMPROVEMENT_FEATURE_NAME, improvementReducer), - EffectsModule.forFeature([ImprovementEffects]) + EffectsModule.forFeature([ImprovementEffects]), ], declarations: [], exports: [], }) -export class ImprovementModule { -} +export class ImprovementModule {} diff --git a/src/app/features/metric/improvement/improvement.service.ts b/src/app/features/metric/improvement/improvement.service.ts index 01a85b8a4..f1157d0ba 100644 --- a/src/app/features/metric/improvement/improvement.service.ts +++ b/src/app/features/metric/improvement/improvement.service.ts @@ -1,6 +1,9 @@ import { Injectable } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { selectAllImprovements, selectRepeatedImprovementIds } from './store/improvement.reducer'; +import { + selectAllImprovements, + selectRepeatedImprovementIds, +} from './store/improvement.reducer'; import { AddImprovement, AddImprovementCheckedDay, @@ -10,70 +13,82 @@ import { DisableImprovementRepeat, HideImprovement, ToggleImprovementRepeat, - UpdateImprovement + UpdateImprovement, } from './store/improvement.actions'; import { Observable } from 'rxjs'; import { Improvement, ImprovementState } from './improvement.model'; import * as shortid from 'shortid'; -import { selectHasLastTrackedImprovements, selectImprovementBannerImprovements } from '../store/metric.selectors'; +import { + selectHasLastTrackedImprovements, + selectImprovementBannerImprovements, +} from '../store/metric.selectors'; import { getWorklogStr } from '../../../util/get-work-log-str'; @Injectable({ providedIn: 'root', }) export class ImprovementService { - improvements$: Observable = this._store$.pipe(select(selectAllImprovements)); - repeatedImprovementIds$: Observable = this._store$.pipe(select(selectRepeatedImprovementIds)); - improvementBannerImprovements$: Observable = this._store$.pipe(select(selectImprovementBannerImprovements)); - hasLastTrackedImprovements$: Observable = this._store$.pipe(select(selectHasLastTrackedImprovements)); + improvements$: Observable = this._store$.pipe( + select(selectAllImprovements), + ); + repeatedImprovementIds$: Observable = this._store$.pipe( + select(selectRepeatedImprovementIds), + ); + improvementBannerImprovements$: Observable = this._store$.pipe( + select(selectImprovementBannerImprovements), + ); + hasLastTrackedImprovements$: Observable = this._store$.pipe( + select(selectHasLastTrackedImprovements), + ); - constructor( - private _store$: Store, - ) { - } + constructor(private _store$: Store) {} addImprovement(title: string): string { const id = shortid(); - this._store$.dispatch(new AddImprovement({ - improvement: { - title, - id, - isRepeat: false, - checkedDays: [] - } - })); + this._store$.dispatch( + new AddImprovement({ + improvement: { + title, + id, + isRepeat: false, + checkedDays: [], + }, + }), + ); return id; } addCheckedDay(id: string, checkedDay: string = getWorklogStr()) { - this._store$.dispatch(new AddImprovementCheckedDay({ - id, - checkedDay, - })); + this._store$.dispatch( + new AddImprovementCheckedDay({ + id, + checkedDay, + }), + ); } deleteImprovement(id: string) { - this._store$.dispatch(new DeleteImprovement({id})); + this._store$.dispatch(new DeleteImprovement({ id })); } deleteImprovements(ids: string[]) { - this._store$.dispatch(new DeleteImprovements({ids})); + this._store$.dispatch(new DeleteImprovements({ ids })); } updateImprovement(id: string, changes: Partial) { - this._store$.dispatch(new UpdateImprovement({improvement: {id, changes}})); + this._store$.dispatch(new UpdateImprovement({ improvement: { id, changes } })); } hideImprovement(id: string) { - this._store$.dispatch(new HideImprovement({id})); + this._store$.dispatch(new HideImprovement({ id })); } toggleImprovementRepeat(id: string) { - this._store$.dispatch(new ToggleImprovementRepeat({id})); + this._store$.dispatch(new ToggleImprovementRepeat({ id })); } disableImprovementRepeat(id: string) { - this._store$.dispatch(new DisableImprovementRepeat({id})); + this._store$.dispatch(new DisableImprovementRepeat({ id })); } clearHiddenImprovements() { diff --git a/src/app/features/metric/improvement/store/improvement.actions.ts b/src/app/features/metric/improvement/store/improvement.actions.ts index 963bad293..148b45405 100644 --- a/src/app/features/metric/improvement/store/improvement.actions.ts +++ b/src/app/features/metric/improvement/store/improvement.actions.ts @@ -17,57 +17,49 @@ export enum ImprovementActionTypes { export class AddImprovement implements Action { readonly type: string = ImprovementActionTypes.AddImprovement; - constructor(public payload: { improvement: Improvement }) { - } + constructor(public payload: { improvement: Improvement }) {} } export class UpdateImprovement implements Action { readonly type: string = ImprovementActionTypes.UpdateImprovement; - constructor(public payload: { improvement: Update }) { - } + constructor(public payload: { improvement: Update }) {} } export class DeleteImprovement implements Action { readonly type: string = ImprovementActionTypes.DeleteImprovement; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class DeleteImprovements implements Action { readonly type: string = ImprovementActionTypes.DeleteImprovements; - constructor(public payload: { ids: string[] }) { - } + constructor(public payload: { ids: string[] }) {} } export class HideImprovement implements Action { readonly type: string = ImprovementActionTypes.HideImprovement; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class AddImprovementCheckedDay implements Action { readonly type: string = ImprovementActionTypes.AddImprovementCheckedDay; - constructor(public payload: { id: string; checkedDay: string }) { - } + constructor(public payload: { id: string; checkedDay: string }) {} } export class ToggleImprovementRepeat implements Action { readonly type: string = ImprovementActionTypes.ToggleImprovementRepeat; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class DisableImprovementRepeat implements Action { readonly type: string = ImprovementActionTypes.DisableImprovementRepeat; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class ClearHiddenImprovements implements Action { @@ -75,7 +67,7 @@ export class ClearHiddenImprovements implements Action { } export type ImprovementActions = - AddImprovement + | AddImprovement | UpdateImprovement | DeleteImprovement | DeleteImprovements @@ -83,5 +75,4 @@ export type ImprovementActions = | ToggleImprovementRepeat | DisableImprovementRepeat | AddImprovementCheckedDay - | ClearHiddenImprovements - ; + | ClearHiddenImprovements; diff --git a/src/app/features/metric/improvement/store/improvement.effects.ts b/src/app/features/metric/improvement/store/improvement.effects.ts index f0b5e8f02..609d54fca 100644 --- a/src/app/features/metric/improvement/store/improvement.effects.ts +++ b/src/app/features/metric/improvement/store/improvement.effects.ts @@ -2,8 +2,15 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { filter, first, map, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { select, Store } from '@ngrx/store'; -import { ClearHiddenImprovements, DeleteImprovements, ImprovementActionTypes } from './improvement.actions'; -import { selectImprovementFeatureState, selectImprovementHideDay } from './improvement.reducer'; +import { + ClearHiddenImprovements, + DeleteImprovements, + ImprovementActionTypes, +} from './improvement.actions'; +import { + selectImprovementFeatureState, + selectImprovementHideDay, +} from './improvement.reducer'; import { PersistenceService } from '../../../../core/persistence/persistence.service'; import { selectUnusedImprovementIds } from '../../store/metric.selectors'; import { ProjectActionTypes } from '../../../project/store/project.actions'; @@ -12,51 +19,44 @@ import { ImprovementState } from '../improvement.model'; @Injectable() export class ImprovementEffects { + @Effect({ dispatch: false }) updateImprovements$: any = this._actions$.pipe( + ofType( + ImprovementActionTypes.AddImprovement, + ImprovementActionTypes.UpdateImprovement, + ImprovementActionTypes.ToggleImprovementRepeat, + ImprovementActionTypes.DisableImprovementRepeat, + ImprovementActionTypes.HideImprovement, + ImprovementActionTypes.DeleteImprovement, + ImprovementActionTypes.AddImprovementCheckedDay, + ), + switchMap(() => + this._store$.pipe(select(selectImprovementFeatureState)).pipe(first()), + ), + tap((state) => this._saveToLs(state)), + ); - @Effect({dispatch: false}) updateImprovements$: any = this._actions$ - .pipe( - ofType( - ImprovementActionTypes.AddImprovement, - ImprovementActionTypes.UpdateImprovement, - ImprovementActionTypes.ToggleImprovementRepeat, - ImprovementActionTypes.DisableImprovementRepeat, - ImprovementActionTypes.HideImprovement, - ImprovementActionTypes.DeleteImprovement, - ImprovementActionTypes.AddImprovementCheckedDay, - ), - switchMap(() => this._store$.pipe(select(selectImprovementFeatureState)).pipe(first())), - tap((state) => this._saveToLs(state)), - ); + @Effect() clearImprovements$: any = this._actions$.pipe( + ofType(ProjectActionTypes.LoadProjectRelatedDataSuccess), + withLatestFrom(this._store$.pipe(select(selectImprovementHideDay))), + filter(([, hideDay]) => hideDay !== getWorklogStr()), + map(() => new ClearHiddenImprovements()), + ); - @Effect() clearImprovements$: any = this._actions$ - .pipe( - ofType( - ProjectActionTypes.LoadProjectRelatedDataSuccess, - ), - withLatestFrom(this._store$.pipe(select(selectImprovementHideDay))), - filter(([, hideDay]) => hideDay !== getWorklogStr()), - map(() => new ClearHiddenImprovements()), - ); - - @Effect() clearUnusedImprovements$: any = this._actions$ - .pipe( - ofType( - ProjectActionTypes.LoadProjectRelatedDataSuccess, - ), - withLatestFrom( - this._store$.pipe(select(selectUnusedImprovementIds)), - ), - map(([a, unusedIds]) => new DeleteImprovements({ids: unusedIds})), - ); + @Effect() clearUnusedImprovements$: any = this._actions$.pipe( + ofType(ProjectActionTypes.LoadProjectRelatedDataSuccess), + withLatestFrom(this._store$.pipe(select(selectUnusedImprovementIds))), + map(([a, unusedIds]) => new DeleteImprovements({ ids: unusedIds })), + ); constructor( private _actions$: Actions, private _store$: Store, private _persistenceService: PersistenceService, - ) { - } + ) {} private _saveToLs(improvementState: ImprovementState) { - this._persistenceService.improvement.saveState(improvementState, {isSyncModelChange: true}); + this._persistenceService.improvement.saveState(improvementState, { + isSyncModelChange: true, + }); } } diff --git a/src/app/features/metric/improvement/store/improvement.reducer.ts b/src/app/features/metric/improvement/store/improvement.reducer.ts index 46705bc78..874446dd2 100644 --- a/src/app/features/metric/improvement/store/improvement.reducer.ts +++ b/src/app/features/metric/improvement/store/improvement.reducer.ts @@ -8,7 +8,7 @@ import { ImprovementActions, ImprovementActionTypes, ToggleImprovementRepeat, - UpdateImprovement + UpdateImprovement, } from './improvement.actions'; import { Improvement, ImprovementState } from '../improvement.model'; import { createFeatureSelector, createSelector } from '@ngrx/store'; @@ -20,25 +20,46 @@ import { migrateImprovementState } from '../../migrate-metric-states.util'; export const IMPROVEMENT_FEATURE_NAME = 'improvement'; export const adapter: EntityAdapter = createEntityAdapter(); -export const selectImprovementFeatureState = createFeatureSelector(IMPROVEMENT_FEATURE_NAME); -export const {selectIds, selectEntities, selectAll, selectTotal} = adapter.getSelectors(); -export const selectAllImprovements = createSelector(selectImprovementFeatureState, selectAll); -export const selectAllImprovementIds = createSelector(selectImprovementFeatureState, selectIds); -export const selectImprovementHideDay = createSelector(selectImprovementFeatureState, (s) => s.hideDay); +export const selectImprovementFeatureState = createFeatureSelector( + IMPROVEMENT_FEATURE_NAME, +); +export const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = adapter.getSelectors(); +export const selectAllImprovements = createSelector( + selectImprovementFeatureState, + selectAll, +); +export const selectAllImprovementIds = createSelector( + selectImprovementFeatureState, + selectIds, +); +export const selectImprovementHideDay = createSelector( + selectImprovementFeatureState, + (s) => s.hideDay, +); export const selectRepeatedImprovementIds = createSelector( selectAllImprovements, (improvements: Improvement[]): string[] => { - return improvements && improvements.filter(i => i.isRepeat).map(i => i.id); - } + return improvements && improvements.filter((i) => i.isRepeat).map((i) => i.id); + }, ); export const selectCheckedImprovementIdsForDay = createSelector( selectAllImprovements, (improvements: Improvement[], props: { day: string }): string[] => { - return improvements.filter(improvement => improvement.checkedDays && improvement.checkedDays.includes(props.day)) - .map(improvement => improvement.id); - }); + return improvements + .filter( + (improvement) => + improvement.checkedDays && improvement.checkedDays.includes(props.day), + ) + .map((improvement) => improvement.id); + }, +); export const initialImprovementState: ImprovementState = adapter.getInitialState({ // additional entity state properties @@ -48,11 +69,11 @@ export const initialImprovementState: ImprovementState = adapter.getInitialState export function improvementReducer( state: ImprovementState = initialImprovementState, - action: ImprovementActions + action: ImprovementActions, ): ImprovementState { // TODO fix this hackyness once we use the new syntax everywhere if ((action.type as string) === loadAllData.type) { - const {appDataComplete}: { appDataComplete: AppDataComplete } = action as any; + const { appDataComplete }: { appDataComplete: AppDataComplete } = action as any; return appDataComplete.improvement?.ids ? appDataComplete.improvement : migrateImprovementState(state); @@ -80,45 +101,53 @@ export function improvementReducer( return { ...state, hideDay: getWorklogStr(), - hiddenImprovementBannerItems: [...items, (action as HideImprovement).payload.id] + hiddenImprovementBannerItems: [...items, (action as HideImprovement).payload.id], }; case ImprovementActionTypes.ToggleImprovementRepeat: const itemI = state.entities[(action as ToggleImprovementRepeat).payload.id]; - return adapter.updateOne({ - id: (action as ToggleImprovementRepeat).payload.id, - changes: { - isRepeat: !(itemI as Improvement).isRepeat + return adapter.updateOne( + { + id: (action as ToggleImprovementRepeat).payload.id, + changes: { + isRepeat: !(itemI as Improvement).isRepeat, + }, }, - }, state); + state, + ); case ImprovementActionTypes.DisableImprovementRepeat: - return adapter.updateOne({ - id: (action as DisableImprovementRepeat).payload.id, - changes: { - isRepeat: false + return adapter.updateOne( + { + id: (action as DisableImprovementRepeat).payload.id, + changes: { + isRepeat: false, + }, }, - }, state); + state, + ); case ImprovementActionTypes.ClearHiddenImprovements: return { ...state, - hiddenImprovementBannerItems: [] + hiddenImprovementBannerItems: [], }; case ImprovementActionTypes.AddImprovementCheckedDay: { - const {id, checkedDay} = (action as AddImprovementCheckedDay).payload; + const { id, checkedDay } = (action as AddImprovementCheckedDay).payload; const allCheckedDays = (state.entities[id] as Improvement).checkedDays || []; - return (allCheckedDays.includes(checkedDay) && checkedDay) + return allCheckedDays.includes(checkedDay) && checkedDay ? state - : adapter.updateOne({ - id, - changes: { - checkedDays: [...allCheckedDays, checkedDay] - } - }, state); - + : adapter.updateOne( + { + id, + changes: { + checkedDays: [...allCheckedDays, checkedDay], + }, + }, + state, + ); } default: { @@ -126,5 +155,3 @@ export function improvementReducer( } } } - - diff --git a/src/app/features/metric/metric.component.ts b/src/app/features/metric/metric.component.ts index a0e818550..ad07dc191 100644 --- a/src/app/features/metric/metric.component.ts +++ b/src/app/features/metric/metric.component.ts @@ -40,6 +40,5 @@ export class MetricComponent { constructor( public metricService: MetricService, public projectMetricsService: ProjectMetricsService, - ) { - } + ) {} } diff --git a/src/app/features/metric/metric.module.ts b/src/app/features/metric/metric.module.ts index 050961809..eee88d956 100644 --- a/src/app/features/metric/metric.module.ts +++ b/src/app/features/metric/metric.module.ts @@ -27,16 +27,8 @@ import { MigrateMetricService } from './migrate-metric.service'; EffectsModule.forFeature([MetricEffects]), RouterModule, ], - declarations: [ - EvaluationSheetComponent, - MetricComponent, - ImprovementBannerComponent, - ], - exports: [ - EvaluationSheetComponent, - MetricComponent, - ImprovementBannerComponent, - ], + declarations: [EvaluationSheetComponent, MetricComponent, ImprovementBannerComponent], + exports: [EvaluationSheetComponent, MetricComponent, ImprovementBannerComponent], }) export class MetricModule { constructor(private _migrateMetricService: MigrateMetricService) { @@ -83,5 +75,4 @@ export class MetricModule { // }); // }, 300); // } - } diff --git a/src/app/features/metric/metric.service.ts b/src/app/features/metric/metric.service.ts index f364f9bda..d3dceb132 100644 --- a/src/app/features/metric/metric.service.ts +++ b/src/app/features/metric/metric.service.ts @@ -1,21 +1,27 @@ import { Injectable } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { AddMetric, DeleteMetric, UpdateMetric, UpsertMetric } from './store/metric.actions'; +import { + AddMetric, + DeleteMetric, + UpdateMetric, + UpsertMetric, +} from './store/metric.actions'; import { combineLatest, Observable, of } from 'rxjs'; import { LineChartData, Metric, MetricState, PieChartData } from './metric.model'; import { getWorklogStr } from '../../util/get-work-log-str'; import { selectImprovementCountsPieChartData, - selectMetricById, selectMetricFeatureState, + selectMetricById, + selectMetricFeatureState, selectMetricHasData, selectObstructionCountsPieChartData, - selectProductivityHappinessLineChartData + selectProductivityHappinessLineChartData, } from './store/metric.selectors'; import { map, switchMap } from 'rxjs/operators'; import { DEFAULT_METRIC_FOR_DAY } from './metric.const'; import { selectCheckedImprovementIdsForDay, - selectRepeatedImprovementIds + selectRepeatedImprovementIds, } from './improvement/store/improvement.reducer'; @Injectable({ @@ -26,15 +32,16 @@ export class MetricService { hasData$: Observable = this._store$.pipe(select(selectMetricHasData)); state$: Observable = this._store$.pipe(select(selectMetricFeatureState)); // lastTrackedMetric$: Observable = this._store$.pipe(select(selectLastTrackedMetric)); - improvementCountsPieChartData$: Observable = this._store$.pipe(select(selectImprovementCountsPieChartData)); - obstructionCountsPieChartData$: Observable = this._store$.pipe(select(selectObstructionCountsPieChartData)); + improvementCountsPieChartData$: Observable = this._store$.pipe( + select(selectImprovementCountsPieChartData), + ); + obstructionCountsPieChartData$: Observable = this._store$.pipe( + select(selectObstructionCountsPieChartData), + ); // productivityHappinessLineChartData$: Observable = this._store$.pipe(select(selectProductivityHappinessLineChartDataComplete)); - constructor( - private _store$: Store, - ) { - } + constructor(private _store$: Store) {} // getMetricForDay$(id: string = getWorklogStr()): Observable { // if (!id) { @@ -43,47 +50,51 @@ export class MetricService { // return this._store$.pipe(select(selectMetricById, {id}), take(1)); // } - getMetricForDayOrDefaultWithCheckedImprovements$(day: string = getWorklogStr()): Observable { - return this._store$.pipe(select(selectMetricById, {id: day})).pipe( + getMetricForDayOrDefaultWithCheckedImprovements$( + day: string = getWorklogStr(), + ): Observable { + return this._store$.pipe(select(selectMetricById, { id: day })).pipe( switchMap((metric) => { return metric ? of(metric) : combineLatest([ - this._store$.pipe(select(selectCheckedImprovementIdsForDay, {day})), - this._store$.pipe(select(selectRepeatedImprovementIds)), - ]).pipe( - map(([checkedImprovementIds, repeatedImprovementIds]) => { - return { - id: day, - ...DEFAULT_METRIC_FOR_DAY, - improvements: checkedImprovementIds || [], - improvementsTomorrow: repeatedImprovementIds || [], - }; - }) - ); + this._store$.pipe(select(selectCheckedImprovementIdsForDay, { day })), + this._store$.pipe(select(selectRepeatedImprovementIds)), + ]).pipe( + map(([checkedImprovementIds, repeatedImprovementIds]) => { + return { + id: day, + ...DEFAULT_METRIC_FOR_DAY, + improvements: checkedImprovementIds || [], + improvementsTomorrow: repeatedImprovementIds || [], + }; + }), + ); }), ); } addMetric(metric: Metric) { - this._store$.dispatch(new AddMetric({ - metric: { - ...metric, - id: metric.id || getWorklogStr(), - } - })); + this._store$.dispatch( + new AddMetric({ + metric: { + ...metric, + id: metric.id || getWorklogStr(), + }, + }), + ); } deleteMetric(id: string) { - this._store$.dispatch(new DeleteMetric({id})); + this._store$.dispatch(new DeleteMetric({ id })); } updateMetric(id: string, changes: Partial) { - this._store$.dispatch(new UpdateMetric({metric: {id, changes}})); + this._store$.dispatch(new UpdateMetric({ metric: { id, changes } })); } upsertMetric(metric: Metric) { - this._store$.dispatch(new UpsertMetric({metric})); + this._store$.dispatch(new UpsertMetric({ metric })); } upsertTodayMetric(metricIn: Partial) { @@ -93,12 +104,13 @@ export class MetricService { ...DEFAULT_METRIC_FOR_DAY, ...metricIn, } as Metric; - this._store$.dispatch(new UpsertMetric({metric})); + this._store$.dispatch(new UpsertMetric({ metric })); } // STATISTICS getProductivityHappinessChartData$(howMany: number = 20): Observable { - return this._store$.pipe(select(selectProductivityHappinessLineChartData, {howMany})); + return this._store$.pipe( + select(selectProductivityHappinessLineChartData, { howMany }), + ); } - } diff --git a/src/app/features/metric/metric.util.ts b/src/app/features/metric/metric.util.ts index 6bdd77319..066c7cd0e 100644 --- a/src/app/features/metric/metric.util.ts +++ b/src/app/features/metric/metric.util.ts @@ -6,10 +6,13 @@ import { BreakNr, BreakTime } from '../work-context/work-context.model'; import { exists } from '../../util/exists'; // really TaskWithSubTasks? -export const mapSimpleMetrics = ( - [breakNr, breakTime, worklog, totalTimeSpent, allTasks]: - [BreakNr, BreakTime, Worklog, number, TaskWithSubTasks[]]): SimpleMetrics => { - +export const mapSimpleMetrics = ([ + breakNr, + breakTime, + worklog, + totalTimeSpent, + allTasks, +]: [BreakNr, BreakTime, Worklog, number, TaskWithSubTasks[]]): SimpleMetrics => { const s = { start: 99999999999999999999999, end: getWorklogStr(), @@ -28,7 +31,7 @@ export const mapSimpleMetrics = ( }; allTasks.forEach((task) => { - if ((task.created < s.start)) { + if (task.created < s.start) { s.start = task.created; } @@ -58,7 +61,7 @@ export const mapSimpleMetrics = ( avgTasksPerDay: s.nrOfMainTasks / s.daysWorked, avgTimeSpentOnDay: s.timeSpent / s.daysWorked, avgTimeSpentOnTask: s.timeSpent / s.nrOfMainTasks, - avgTimeSpentOnTaskIncludingSubTasks: s.timeSpent / (s.nrOfAllTasks - s.nrOfParentTasks), - + avgTimeSpentOnTaskIncludingSubTasks: + s.timeSpent / (s.nrOfAllTasks - s.nrOfParentTasks), }; }; diff --git a/src/app/features/metric/migrate-metric-states.util.ts b/src/app/features/metric/migrate-metric-states.util.ts index 96c9770f6..3bd67669a 100644 --- a/src/app/features/metric/migrate-metric-states.util.ts +++ b/src/app/features/metric/migrate-metric-states.util.ts @@ -18,6 +18,12 @@ const migrateMetricStatesUtil = (modelName: string) => (metricState: any): any = }; }; -export const migrateMetricState: (metricState: MetricState) => MetricState = migrateMetricStatesUtil('Metric'); -export const migrateImprovementState: (improvementState: ImprovementState) => ImprovementState = migrateMetricStatesUtil('Improvement'); -export const migrateObstructionState: (obstructionState: ObstructionState) => ObstructionState = migrateMetricStatesUtil('Obstruction'); +export const migrateMetricState: ( + metricState: MetricState, +) => MetricState = migrateMetricStatesUtil('Metric'); +export const migrateImprovementState: ( + improvementState: ImprovementState, +) => ImprovementState = migrateMetricStatesUtil('Improvement'); +export const migrateObstructionState: ( + obstructionState: ObstructionState, +) => ObstructionState = migrateMetricStatesUtil('Obstruction'); diff --git a/src/app/features/metric/migrate-metric.service.ts b/src/app/features/metric/migrate-metric.service.ts index 288aacd6f..bcfd13715 100644 --- a/src/app/features/metric/migrate-metric.service.ts +++ b/src/app/features/metric/migrate-metric.service.ts @@ -15,82 +15,91 @@ import { SyncService } from '../../imex/sync/sync.service'; import { unique } from '../../util/unique'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class MigrateMetricService { - constructor( private _metricService: MetricService, private _persistenceService: PersistenceService, private _dataImportService: DataImportService, private _syncService: SyncService, - ) { - - } + ) {} checkMigrate() { // TODO check for model version number instead - this._syncService.afterInitialSyncDoneAndDataLoadedInitially$.pipe( - first(), - concatMap(() => this._metricService.state$), - first(), - ).subscribe(async (metricState: MetricState) => { - console.log('Migrating Legacy Metric State to new model'); - console.log('metricMigration:', metricState [MODEL_VERSION_KEY], {metricState}); - if (!metricState [MODEL_VERSION_KEY]) { - const projectState = await this._persistenceService.project.loadState(); - // For new instances - if (!projectState?.ids?.length) { - return; - } + this._syncService.afterInitialSyncDoneAndDataLoadedInitially$ + .pipe( + first(), + concatMap(() => this._metricService.state$), + first(), + ) + .subscribe(async (metricState: MetricState) => { + console.log('Migrating Legacy Metric State to new model'); + console.log('metricMigration:', metricState[MODEL_VERSION_KEY], { metricState }); + if (!metricState[MODEL_VERSION_KEY]) { + const projectState = await this._persistenceService.project.loadState(); + // For new instances + if (!projectState?.ids?.length) { + return; + } - console.log(projectState); - let newM = initialMetricState; - let newI = initialImprovementState; - let newO = initialObstructionState; + console.log(projectState); + let newM = initialMetricState; + let newI = initialImprovementState; + let newO = initialObstructionState; - for (const id of (projectState.ids as string[])) { - const mForProject = await this._persistenceService.legacyMetric.load(id); - const iForProject = await this._persistenceService.legacyImprovement.load(id); - const oForProject = await this._persistenceService.legacyObstruction.load(id); - if (mForProject && (oForProject || iForProject)) { - console.log('metricMigration:', {mForProject, iForProject, oForProject}); - newM = this._mergeMetricsState(newM, mForProject); - if (iForProject) { - newI = this._mergeIntoState(newI, iForProject) as ImprovementState; - } - if (oForProject) { - newO = this._mergeIntoState(newO, oForProject); + for (const id of projectState.ids as string[]) { + const mForProject = await this._persistenceService.legacyMetric.load(id); + const iForProject = await this._persistenceService.legacyImprovement.load(id); + const oForProject = await this._persistenceService.legacyObstruction.load(id); + if (mForProject && (oForProject || iForProject)) { + console.log('metricMigration:', { mForProject, iForProject, oForProject }); + newM = this._mergeMetricsState(newM, mForProject); + if (iForProject) { + newI = this._mergeIntoState(newI, iForProject) as ImprovementState; + } + if (oForProject) { + newO = this._mergeIntoState(newO, oForProject); + } } } - } - console.log('metricMigration:', {newM, newI, newO}); + console.log('metricMigration:', { newM, newI, newO }); - await this._persistenceService.improvement.saveState(newI, {isSyncModelChange: false}); - await this._persistenceService.obstruction.saveState(newO, {isSyncModelChange: false}); - await this._persistenceService.metric.saveState({ - ...newM, - [MODEL_VERSION_KEY]: METRIC_MODEL_VERSION - }, {isSyncModelChange: false}); - const data = await this._persistenceService.loadComplete(); - await this._dataImportService.importCompleteSyncData(data); - } - }); + await this._persistenceService.improvement.saveState(newI, { + isSyncModelChange: false, + }); + await this._persistenceService.obstruction.saveState(newO, { + isSyncModelChange: false, + }); + await this._persistenceService.metric.saveState( + { + ...newM, + [MODEL_VERSION_KEY]: METRIC_MODEL_VERSION, + }, + { isSyncModelChange: false }, + ); + const data = await this._persistenceService.loadComplete(); + await this._dataImportService.importCompleteSyncData(data); + } + }); } - private _mergeMetricsState(completeState: MetricState, newState: MetricState): MetricState { + private _mergeMetricsState( + completeState: MetricState, + newState: MetricState, + ): MetricState { const s = { ...completeState, ...newState, // NOTE: we need to make them unique, because we're possibly merging multiple entities into one - ids: unique([...completeState.ids as string[], ...newState.ids as string[]]), + ids: unique([...(completeState.ids as string[]), ...(newState.ids as string[])]), entities: { ...completeState.entities, ...newState.entities, - } + }, }; - Object.keys(newState.entities).forEach(dayStr => { + Object.keys(newState.entities).forEach((dayStr) => { const mOld = completeState.entities[dayStr] as MetricCopy; const mNew = newState.entities[dayStr] as MetricCopy; // merge same entry into one @@ -99,22 +108,28 @@ export class MigrateMetricService { ...mOld, obstructions: [...mOld.obstructions, ...mNew.obstructions], improvements: [...mOld.improvements, ...mNew.improvements], - improvementsTomorrow: [...mOld.improvementsTomorrow, ...mNew.improvementsTomorrow], + improvementsTomorrow: [ + ...mOld.improvementsTomorrow, + ...mNew.improvementsTomorrow, + ], }; } }); return s; } - private _mergeIntoState(completeState: EntityState, newState: EntityState): EntityState { + private _mergeIntoState( + completeState: EntityState, + newState: EntityState, + ): EntityState { return { ...completeState, ...newState, - ids: [...completeState.ids as string[], ...newState.ids as string[]], + ids: [...(completeState.ids as string[]), ...(newState.ids as string[])], entities: { ...completeState.entities, ...newState.entities, - } + }, }; } } diff --git a/src/app/features/metric/obstruction/obstruction.module.ts b/src/app/features/metric/obstruction/obstruction.module.ts index 8077217c0..a28299d2b 100644 --- a/src/app/features/metric/obstruction/obstruction.module.ts +++ b/src/app/features/metric/obstruction/obstruction.module.ts @@ -3,16 +3,18 @@ import { CommonModule } from '@angular/common'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { ObstructionEffects } from './store/obstruction.effects'; -import { OBSTRUCTION_FEATURE_NAME, obstructionReducer } from './store/obstruction.reducer'; +import { + OBSTRUCTION_FEATURE_NAME, + obstructionReducer, +} from './store/obstruction.reducer'; @NgModule({ imports: [ CommonModule, StoreModule.forFeature(OBSTRUCTION_FEATURE_NAME, obstructionReducer), - EffectsModule.forFeature([ObstructionEffects]) + EffectsModule.forFeature([ObstructionEffects]), ], declarations: [], exports: [], }) -export class ObstructionModule { -} +export class ObstructionModule {} diff --git a/src/app/features/metric/obstruction/obstruction.service.ts b/src/app/features/metric/obstruction/obstruction.service.ts index a88496348..71a8a745a 100644 --- a/src/app/features/metric/obstruction/obstruction.service.ts +++ b/src/app/features/metric/obstruction/obstruction.service.ts @@ -1,7 +1,12 @@ import { Injectable } from '@angular/core'; import { select, Store } from '@ngrx/store'; import { selectAllObstructions } from './store/obstruction.reducer'; -import { AddObstruction, DeleteObstruction, DeleteObstructions, UpdateObstruction } from './store/obstruction.actions'; +import { + AddObstruction, + DeleteObstruction, + DeleteObstructions, + UpdateObstruction, +} from './store/obstruction.actions'; import { Observable } from 'rxjs'; import { Obstruction, ObstructionState } from './obstruction.model'; import * as shortid from 'shortid'; @@ -10,33 +15,34 @@ import * as shortid from 'shortid'; providedIn: 'root', }) export class ObstructionService { - obstructions$: Observable = this._store$.pipe(select(selectAllObstructions)); + obstructions$: Observable = this._store$.pipe( + select(selectAllObstructions), + ); - constructor( - private _store$: Store, - ) { - } + constructor(private _store$: Store) {} addObstruction(title: string): string { const id = shortid(); - this._store$.dispatch(new AddObstruction({ - obstruction: { - title, - id, - } - })); + this._store$.dispatch( + new AddObstruction({ + obstruction: { + title, + id, + }, + }), + ); return id; } deleteObstruction(id: string) { - this._store$.dispatch(new DeleteObstruction({id})); + this._store$.dispatch(new DeleteObstruction({ id })); } deleteObstructions(ids: string[]) { - this._store$.dispatch(new DeleteObstructions({ids})); + this._store$.dispatch(new DeleteObstructions({ ids })); } updateObstruction(id: string, changes: Partial) { - this._store$.dispatch(new UpdateObstruction({obstruction: {id, changes}})); + this._store$.dispatch(new UpdateObstruction({ obstruction: { id, changes } })); } } diff --git a/src/app/features/metric/obstruction/store/obstruction.actions.ts b/src/app/features/metric/obstruction/store/obstruction.actions.ts index 54c45908d..e5ffd2470 100644 --- a/src/app/features/metric/obstruction/store/obstruction.actions.ts +++ b/src/app/features/metric/obstruction/store/obstruction.actions.ts @@ -12,34 +12,29 @@ export enum ObstructionActionTypes { export class AddObstruction implements Action { readonly type: string = ObstructionActionTypes.AddObstruction; - constructor(public payload: { obstruction: Obstruction }) { - } + constructor(public payload: { obstruction: Obstruction }) {} } export class UpdateObstruction implements Action { readonly type: string = ObstructionActionTypes.UpdateObstruction; - constructor(public payload: { obstruction: Update }) { - } + constructor(public payload: { obstruction: Update }) {} } export class DeleteObstruction implements Action { readonly type: string = ObstructionActionTypes.DeleteObstruction; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class DeleteObstructions implements Action { readonly type: string = ObstructionActionTypes.DeleteObstructions; - constructor(public payload: { ids: string[] }) { - } + constructor(public payload: { ids: string[] }) {} } export type ObstructionActions = - AddObstruction + | AddObstruction | UpdateObstruction | DeleteObstruction - | DeleteObstructions - ; + | DeleteObstructions; diff --git a/src/app/features/metric/obstruction/store/obstruction.effects.ts b/src/app/features/metric/obstruction/store/obstruction.effects.ts index 0d1338cbd..4f2bf52cc 100644 --- a/src/app/features/metric/obstruction/store/obstruction.effects.ts +++ b/src/app/features/metric/obstruction/store/obstruction.effects.ts @@ -11,39 +11,37 @@ import { ObstructionState } from '../obstruction.model'; @Injectable() export class ObstructionEffects { + @Effect({ dispatch: false }) updateObstructions$: any = this._actions$.pipe( + ofType( + ObstructionActionTypes.AddObstruction, + ObstructionActionTypes.UpdateObstruction, + ObstructionActionTypes.DeleteObstruction, + ), + switchMap(() => + this._store$.pipe(select(selectObstructionFeatureState)).pipe(first()), + ), + tap((state) => this._saveToLs(state)), + ); - @Effect({dispatch: false}) updateObstructions$: any = this._actions$ - .pipe( - ofType( - ObstructionActionTypes.AddObstruction, - ObstructionActionTypes.UpdateObstruction, - ObstructionActionTypes.DeleteObstruction, - ), - switchMap(() => this._store$.pipe(select(selectObstructionFeatureState)).pipe(first())), - tap((state) => this._saveToLs(state)), - ); - - @Effect() clearUnusedObstructions$: any = this._actions$ - .pipe( - ofType( - MetricActionTypes.AddMetric, - MetricActionTypes.UpsertMetric, - MetricActionTypes.UpdateMetric, - ), - withLatestFrom( - this._store$.pipe(select(selectUnusedObstructionIds)), - ), - map(([a, unusedIds]) => new DeleteObstructions({ids: unusedIds})), - ); + @Effect() clearUnusedObstructions$: any = this._actions$.pipe( + ofType( + MetricActionTypes.AddMetric, + MetricActionTypes.UpsertMetric, + MetricActionTypes.UpdateMetric, + ), + withLatestFrom(this._store$.pipe(select(selectUnusedObstructionIds))), + map(([a, unusedIds]) => new DeleteObstructions({ ids: unusedIds })), + ); constructor( private _actions$: Actions, private _store$: Store, private _persistenceService: PersistenceService, - ) { - } + ) {} private _saveToLs(obstructionState: ObstructionState) { - this._persistenceService.obstruction.saveState(obstructionState, {isSyncModelChange: true}); + this._persistenceService.obstruction.saveState(obstructionState, { + isSyncModelChange: true, + }); } } diff --git a/src/app/features/metric/obstruction/store/obstruction.reducer.ts b/src/app/features/metric/obstruction/store/obstruction.reducer.ts index fc2c8661c..e95bc6eb3 100644 --- a/src/app/features/metric/obstruction/store/obstruction.reducer.ts +++ b/src/app/features/metric/obstruction/store/obstruction.reducer.ts @@ -5,7 +5,7 @@ import { DeleteObstructions, ObstructionActions, ObstructionActionTypes, - UpdateObstruction + UpdateObstruction, } from './obstruction.actions'; import { Obstruction, ObstructionState } from '../obstruction.model'; import { createFeatureSelector, createSelector } from '@ngrx/store'; @@ -16,10 +16,23 @@ import { migrateObstructionState } from '../../migrate-metric-states.util'; export const OBSTRUCTION_FEATURE_NAME = 'obstruction'; export const adapter: EntityAdapter = createEntityAdapter(); -export const selectObstructionFeatureState = createFeatureSelector(OBSTRUCTION_FEATURE_NAME); -export const {selectIds, selectEntities, selectAll, selectTotal} = adapter.getSelectors(); -export const selectAllObstructions = createSelector(selectObstructionFeatureState, selectAll); -export const selectAllObstructionIds = createSelector(selectObstructionFeatureState, selectIds); +export const selectObstructionFeatureState = createFeatureSelector( + OBSTRUCTION_FEATURE_NAME, +); +export const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = adapter.getSelectors(); +export const selectAllObstructions = createSelector( + selectObstructionFeatureState, + selectAll, +); +export const selectAllObstructionIds = createSelector( + selectObstructionFeatureState, + selectIds, +); export const initialObstructionState: ObstructionState = adapter.getInitialState({ // additional entity state properties @@ -27,12 +40,11 @@ export const initialObstructionState: ObstructionState = adapter.getInitialState export function obstructionReducer( state: ObstructionState = initialObstructionState, - action: ObstructionActions + action: ObstructionActions, ): ObstructionState { - // TODO fix this hackyness once we use the new syntax everywhere if ((action.type as string) === loadAllData.type) { - const {appDataComplete}: { appDataComplete: AppDataComplete } = action as any; + const { appDataComplete }: { appDataComplete: AppDataComplete } = action as any; return appDataComplete.obstruction?.ids ? appDataComplete.obstruction : migrateObstructionState(state); @@ -60,5 +72,3 @@ export function obstructionReducer( } } } - - diff --git a/src/app/features/metric/project-metrics.service.ts b/src/app/features/metric/project-metrics.service.ts index 5bfc58c49..9832b7737 100644 --- a/src/app/features/metric/project-metrics.service.ts +++ b/src/app/features/metric/project-metrics.service.ts @@ -10,32 +10,31 @@ import { WorklogService } from '../worklog/worklog.service'; import { WorkContextService } from '../work-context/work-context.service'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class ProjectMetricsService { simpleMetrics$: Observable = this._workContextService.activeWorkContextTypeAndId$.pipe( - switchMap(({activeType, activeId}) => { - return (activeType === WorkContextType.PROJECT) - + switchMap(({ activeType, activeId }) => { + return activeType === WorkContextType.PROJECT ? combineLatest([ - this._projectService.getBreakNrForProject$(activeId), - this._projectService.getBreakTimeForProject$(activeId), - this._worklogService.worklog$, - this._worklogService.totalTimeSpent$, - from(this._taskService.getAllTasksForProject(activeId)) - ]).pipe( - map(mapSimpleMetrics), - // because otherwise the page is always redrawn if a task is active - take(1), - ) - + this._projectService.getBreakNrForProject$(activeId), + this._projectService.getBreakTimeForProject$(activeId), + this._worklogService.worklog$, + this._worklogService.totalTimeSpent$, + from(this._taskService.getAllTasksForProject(activeId)), + ]).pipe( + map(mapSimpleMetrics), + // because otherwise the page is always redrawn if a task is active + take(1), + ) : EMPTY; }), ); + constructor( private _taskService: TaskService, private _projectService: ProjectService, private _worklogService: WorklogService, private _workContextService: WorkContextService, - ) { } + ) {} } diff --git a/src/app/features/metric/store/metric.actions.ts b/src/app/features/metric/store/metric.actions.ts index f00b76030..067b20c39 100644 --- a/src/app/features/metric/store/metric.actions.ts +++ b/src/app/features/metric/store/metric.actions.ts @@ -12,34 +12,25 @@ export enum MetricActionTypes { export class AddMetric implements Action { readonly type: string = MetricActionTypes.AddMetric; - constructor(public payload: { metric: Metric }) { - } + constructor(public payload: { metric: Metric }) {} } export class UpdateMetric implements Action { readonly type: string = MetricActionTypes.UpdateMetric; - constructor(public payload: { metric: Update }) { - } + constructor(public payload: { metric: Update }) {} } export class UpsertMetric implements Action { readonly type: string = MetricActionTypes.UpsertMetric; - constructor(public payload: { metric: Metric }) { - } + constructor(public payload: { metric: Metric }) {} } export class DeleteMetric implements Action { readonly type: string = MetricActionTypes.DeleteMetric; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } -export type MetricActions = - AddMetric - | UpdateMetric - | DeleteMetric - | UpsertMetric - ; +export type MetricActions = AddMetric | UpdateMetric | DeleteMetric | UpsertMetric; diff --git a/src/app/features/metric/store/metric.effects.ts b/src/app/features/metric/store/metric.effects.ts index 953ca1e18..307b9820c 100644 --- a/src/app/features/metric/store/metric.effects.ts +++ b/src/app/features/metric/store/metric.effects.ts @@ -9,18 +9,16 @@ import { MetricState } from '../metric.model'; @Injectable() export class MetricEffects { - - @Effect({dispatch: false}) updateMetrics$: any = this._actions$ - .pipe( - ofType( - MetricActionTypes.AddMetric, - MetricActionTypes.UpdateMetric, - MetricActionTypes.DeleteMetric, - MetricActionTypes.UpsertMetric, - ), - switchMap(() => this._store$.pipe(select(selectMetricFeatureState)).pipe(first())), - tap((state) => this._saveToLs(state)), - ); + @Effect({ dispatch: false }) updateMetrics$: any = this._actions$.pipe( + ofType( + MetricActionTypes.AddMetric, + MetricActionTypes.UpdateMetric, + MetricActionTypes.DeleteMetric, + MetricActionTypes.UpsertMetric, + ), + switchMap(() => this._store$.pipe(select(selectMetricFeatureState)).pipe(first())), + tap((state) => this._saveToLs(state)), + ); // @Effect({dispatch: false}) saveMetrics$: any = this._actions$ // .pipe( @@ -39,11 +37,9 @@ export class MetricEffects { private _actions$: Actions, private _store$: Store, private _persistenceService: PersistenceService, - ) { - } + ) {} private _saveToLs(metricState: MetricState) { - this._persistenceService.metric.saveState(metricState, {isSyncModelChange: true}); + this._persistenceService.metric.saveState(metricState, { isSyncModelChange: true }); } - } diff --git a/src/app/features/metric/store/metric.reducer.ts b/src/app/features/metric/store/metric.reducer.ts index 1c6df698c..93e419d2f 100644 --- a/src/app/features/metric/store/metric.reducer.ts +++ b/src/app/features/metric/store/metric.reducer.ts @@ -5,7 +5,7 @@ import { MetricActions, MetricActionTypes, UpdateMetric, - UpsertMetric + UpsertMetric, } from './metric.actions'; import { Metric, MetricState } from '../metric.model'; import { loadAllData } from '../../../root-store/meta/load-all-data.action'; @@ -21,13 +21,12 @@ export const initialMetricState: MetricState = metricAdapter.getInitialState({ export function metricReducer( state: MetricState = initialMetricState, - action: MetricActions + action: MetricActions, ): MetricState { - // TODO fix this hackyness once we use the new syntax everywhere if ((action.type as string) === loadAllData.type) { - const {appDataComplete}: { appDataComplete: AppDataComplete } = action as any; - return (appDataComplete.metric?.ids) + const { appDataComplete }: { appDataComplete: AppDataComplete } = action as any; + return appDataComplete.metric?.ids ? appDataComplete.metric : migrateMetricState(state); } @@ -54,5 +53,3 @@ export function metricReducer( } } } - - diff --git a/src/app/features/metric/store/metric.selectors.ts b/src/app/features/metric/store/metric.selectors.ts index 7e4d487cf..54b9647d2 100644 --- a/src/app/features/metric/store/metric.selectors.ts +++ b/src/app/features/metric/store/metric.selectors.ts @@ -5,88 +5,116 @@ import { METRIC_FEATURE_NAME, metricAdapter } from './metric.reducer'; import { selectAllImprovementIds, selectImprovementFeatureState, - selectRepeatedImprovementIds + selectRepeatedImprovementIds, } from '../improvement/store/improvement.reducer'; import { Improvement, ImprovementState } from '../improvement/improvement.model'; -import { selectAllObstructionIds, selectObstructionFeatureState } from '../obstruction/store/obstruction.reducer'; +import { + selectAllObstructionIds, + selectObstructionFeatureState, +} from '../obstruction/store/obstruction.reducer'; import { ObstructionState } from '../obstruction/obstruction.model'; import { unique } from '../../../util/unique'; -export const selectMetricFeatureState = createFeatureSelector(METRIC_FEATURE_NAME); -export const {selectIds, selectEntities, selectAll, selectTotal} = metricAdapter.getSelectors(); +export const selectMetricFeatureState = createFeatureSelector( + METRIC_FEATURE_NAME, +); +export const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = metricAdapter.getSelectors(); export const selectAllMetrics = createSelector(selectMetricFeatureState, selectAll); -export const selectLastTrackedMetric = createSelector(selectMetricFeatureState, (state: MetricState): Metric | null => { - const ids = state.ids as string[]; - const sorted = sortWorklogDates(ids); - const id = sorted[sorted.length - 1]; - return state.entities[id] || null; -}); +export const selectLastTrackedMetric = createSelector( + selectMetricFeatureState, + (state: MetricState): Metric | null => { + const ids = state.ids as string[]; + const sorted = sortWorklogDates(ids); + const id = sorted[sorted.length - 1]; + return state.entities[id] || null; + }, +); -export const selectMetricHasData = createSelector(selectMetricFeatureState, (state) => state && !!state.ids.length); +export const selectMetricHasData = createSelector( + selectMetricFeatureState, + (state) => state && !!state.ids.length, +); export const selectImprovementBannerImprovements = createSelector( selectLastTrackedMetric, selectImprovementFeatureState, selectRepeatedImprovementIds, - (metric: Metric | null, improvementState: ImprovementState, repeatedImprovementIds: string[]): Improvement[] | null => { + ( + metric: Metric | null, + improvementState: ImprovementState, + repeatedImprovementIds: string[], + ): Improvement[] | null => { if (!improvementState.ids.length) { return null; } const hiddenIds = improvementState.hiddenImprovementBannerItems || []; - const selectedTomorrowIds = metric && metric.improvementsTomorrow || []; - const all = unique(repeatedImprovementIds.concat(selectedTomorrowIds)) - .filter((id: string) => !hiddenIds.includes(id)); - return all.map((id: string) => improvementState.entities[id] as Improvement) - // NOTE: we need to check, because metric and improvement state might be out of sync for some milliseconds - // @see #978 - .filter(improvement => !!improvement); - }); + const selectedTomorrowIds = (metric && metric.improvementsTomorrow) || []; + const all = unique(repeatedImprovementIds.concat(selectedTomorrowIds)).filter( + (id: string) => !hiddenIds.includes(id), + ); + return ( + all + .map((id: string) => improvementState.entities[id] as Improvement) + // NOTE: we need to check, because metric and improvement state might be out of sync for some milliseconds + // @see #978 + .filter((improvement) => !!improvement) + ); + }, +); export const selectHasLastTrackedImprovements = createSelector( selectImprovementBannerImprovements, - (improvements): boolean => !!improvements && improvements.length > 0 + (improvements): boolean => !!improvements && improvements.length > 0, ); export const selectAllUsedImprovementIds = createSelector( selectAllMetrics, (metrics: Metric[]): string[] => { return unique( - metrics.reduce((acc: string[], metric: Metric): string[] => [ - ...acc, - ...metric.improvements, - ...metric.improvementsTomorrow, - ], []) + metrics.reduce( + (acc: string[], metric: Metric): string[] => [ + ...acc, + ...metric.improvements, + ...metric.improvementsTomorrow, + ], + [], + ), ); - } + }, ); export const selectUnusedImprovementIds = createSelector( selectAllUsedImprovementIds, selectAllImprovementIds as any, (usedIds: string[], allIds: string[]): string[] => { - return allIds.filter(id => !usedIds.includes(id)); - } + return allIds.filter((id) => !usedIds.includes(id)); + }, ); export const selectAllUsedObstructionIds = createSelector( selectAllMetrics, (metrics: Metric[]): string[] => { return unique( - metrics.reduce((acc: string[], metric: Metric): string[] => [ - ...acc, - ...metric.obstructions, - ], []) + metrics.reduce( + (acc: string[], metric: Metric): string[] => [...acc, ...metric.obstructions], + [], + ), ); - } + }, ); export const selectUnusedObstructionIds = createSelector( selectAllUsedObstructionIds, selectAllObstructionIds as any, (usedIds: string[], allIds: string[]): string[] => { - return allIds.filter(id => !usedIds.includes(id)); - } + return allIds.filter((id) => !usedIds.includes(id)); + }, ); // DYNAMIC @@ -98,7 +126,7 @@ export const selectMetricById = createSelector( // throw new Error('Metric not found'); // } return state.entities[props.id] || null; - } + }, ); // STATISTICS @@ -114,16 +142,14 @@ export const selectImprovementCountsPieChartData = createSelector( const counts: any = {}; metrics.forEach((metric: Metric) => { metric.improvements.forEach((improvementId: string) => { - counts[improvementId] = counts[improvementId] - ? counts[improvementId] + 1 - : 1; + counts[improvementId] = counts[improvementId] ? counts[improvementId] + 1 : 1; }); }); const chart: PieChartData = { labels: [], data: [], }; - Object.keys(counts).forEach(id => { + Object.keys(counts).forEach((id) => { const imp = improvementState.entities[id]; if (imp) { chart.labels.push(imp.title); @@ -133,7 +159,7 @@ export const selectImprovementCountsPieChartData = createSelector( } }); return chart; - } + }, ); export const selectObstructionCountsPieChartData = createSelector( @@ -147,16 +173,14 @@ export const selectObstructionCountsPieChartData = createSelector( const counts: any = {}; metrics.forEach((metric: Metric) => { metric.obstructions.forEach((obstructionId: string) => { - counts[obstructionId] = counts[obstructionId] - ? counts[obstructionId] + 1 - : 1; + counts[obstructionId] = counts[obstructionId] ? counts[obstructionId] + 1 : 1; }); }); const chart: PieChartData = { labels: [], data: [], }; - Object.keys(counts).forEach(id => { + Object.keys(counts).forEach((id) => { const obstr = obstructionState.entities[id]; if (obstr) { chart.labels.push(obstr.title); @@ -166,7 +190,7 @@ export const selectObstructionCountsPieChartData = createSelector( } }); return chart; - } + }, ); export const selectProductivityHappinessLineChartDataComplete = createSelector( @@ -178,18 +202,18 @@ export const selectProductivityHappinessLineChartDataComplete = createSelector( const v: any = { labels: [], data: [ - {data: [], label: 'Mood'}, - {data: [], label: 'Productivity'}, + { data: [], label: 'Mood' }, + { data: [], label: 'Productivity' }, ], }; - sorted.forEach(id => { + sorted.forEach((id) => { const metric = state.entities[id] as Metric; v.labels.push(metric.id); v.data[0].data.push(metric.mood); v.data[1].data.push(metric.productivity); }); return v; - } + }, ); export const selectProductivityHappinessLineChartData = createSelector( @@ -199,9 +223,9 @@ export const selectProductivityHappinessLineChartData = createSelector( return { labels: chart.labels.slice(f), data: [ - {data: (chart.data[0] as any).data.slice(f), label: chart.data[0].label}, - {data: (chart.data[1] as any).data.slice(f), label: chart.data[1].label}, + { data: (chart.data[0] as any).data.slice(f), label: chart.data[0].label }, + { data: (chart.data[1] as any).data.slice(f), label: chart.data[1].label }, ], }; - } + }, ); diff --git a/src/app/features/note/dialog-add-note/dialog-add-note.component.ts b/src/app/features/note/dialog-add-note/dialog-add-note.component.ts index 5218cc0b8..15724d6f8 100644 --- a/src/app/features/note/dialog-add-note/dialog-add-note.component.ts +++ b/src/app/features/note/dialog-add-note/dialog-add-note.component.ts @@ -12,14 +12,14 @@ import { map } from 'rxjs/operators'; selector: 'dialog-add-note', templateUrl: './dialog-add-note.component.html', styleUrls: ['./dialog-add-note.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogAddNoteComponent { T: typeof T = T; noteContent: string; isSubmitted: boolean = false; isInProjectContext$: Observable = this._workContextService.activeWorkContextTypeAndId$.pipe( - map(({activeType}) => activeType === WorkContextType.PROJECT) + map(({ activeType }) => activeType === WorkContextType.PROJECT), ); constructor( @@ -37,12 +37,8 @@ export class DialogAddNoteComponent { } submit() { - if (!this.isSubmitted - && (this.noteContent && this.noteContent.trim().length > 0)) { - this._noteService.add( - {content: this.noteContent}, - true, - ); + if (!this.isSubmitted && this.noteContent && this.noteContent.trim().length > 0) { + this._noteService.add({ content: this.noteContent }, true); this.isSubmitted = true; this._clearSessionStorage(); diff --git a/src/app/features/note/note.module.ts b/src/app/features/note/note.module.ts index dd6e2bb82..50c3e6b12 100644 --- a/src/app/features/note/note.module.ts +++ b/src/app/features/note/note.module.ts @@ -19,12 +19,7 @@ import { DialogAddNoteComponent } from './dialog-add-note/dialog-add-note.compon StoreModule.forFeature(NOTE_FEATURE_NAME, fromNote.noteReducer), EffectsModule.forFeature([NoteEffects]), ], - declarations: [ - NotesComponent, - NoteComponent, - DialogAddNoteComponent - ], + declarations: [NotesComponent, NoteComponent, DialogAddNoteComponent], exports: [NotesComponent], }) -export class NoteModule { -} +export class NoteModule {} diff --git a/src/app/features/note/note.service.ts b/src/app/features/note/note.service.ts index 640c8cc9b..6951395ec 100644 --- a/src/app/features/note/note.service.ts +++ b/src/app/features/note/note.service.ts @@ -2,9 +2,20 @@ import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { Note } from './note.model'; import { select, Store } from '@ngrx/store'; -import { addNote, deleteNote, loadNoteState, updateNote, updateNoteOrder } from './store/note.actions'; +import { + addNote, + deleteNote, + loadNoteState, + updateNote, + updateNoteOrder, +} from './store/note.actions'; import * as shortid from 'shortid'; -import { initialNoteState, NoteState, selectAllNotes, selectNoteById } from './store/note.reducer'; +import { + initialNoteState, + NoteState, + selectAllNotes, + selectNoteById, +} 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'; @@ -20,11 +31,10 @@ export class NoteService { constructor( private _store$: Store, private _persistenceService: PersistenceService, - ) { - } + ) {} getById$(id: string): Observable { - return this._store$.pipe(select(selectNoteById, {id}), take(1)); + return this._store$.pipe(select(selectNoteById, { id }), take(1)); } async getByIdFromEverywhere(id: string, projectId: string): Promise { @@ -32,43 +42,52 @@ export class NoteService { } public async loadStateForProject(projectId: string) { - const notes = await this._persistenceService.note.load(projectId) || initialNoteState; + const notes = + (await this._persistenceService.note.load(projectId)) || initialNoteState; this.loadState(notes); } public loadState(state: NoteState) { - this._store$.dispatch(loadNoteState({state})); + this._store$.dispatch(loadNoteState({ state })); } public add(note: Partial = {}, isPreventFocus: boolean = false) { const id = shortid(); - this._store$.dispatch(addNote({ - note: { - id, - content: '', - created: Date.now(), - modified: Date.now(), - ...note, - }, - isPreventFocus, - })); + this._store$.dispatch( + addNote({ + note: { + id, + content: '', + created: Date.now(), + modified: Date.now(), + ...note, + }, + isPreventFocus, + }), + ); } public remove(id: string) { - this._store$.dispatch(deleteNote({id})); + this._store$.dispatch(deleteNote({ id })); } public update(id: string, note: Partial) { - this._store$.dispatch(updateNote({ - note: { - id, - changes: note, - } - })); + this._store$.dispatch( + updateNote({ + note: { + id, + changes: note, + }, + }), + ); } - public async updateFromDifferentWorkContext(workContextId: string, id: string, updates: Partial) { + public async updateFromDifferentWorkContext( + workContextId: string, + id: string, + updates: Partial, + ) { const noteState = await this._persistenceService.note.load(workContextId); const noteToUpdate = noteState.entities[id]; if (noteToUpdate) { @@ -76,11 +95,13 @@ export class NoteService { } else { console.warn('Note not found while trying to update for different project'); } - return await this._persistenceService.note.save(workContextId, noteState, {isSyncModelChange: true}); + return await this._persistenceService.note.save(workContextId, noteState, { + isSyncModelChange: true, + }); } public updateOrder(ids: string[]) { - this._store$.dispatch(updateNoteOrder({ids})); + this._store$.dispatch(updateNoteOrder({ ids })); } // REMINDER @@ -105,7 +126,7 @@ export class NoteService { content: drop.path, }; - const isImg = isImageUrlSimple(drop.path) || await isImageUrl(drop.path); + const isImg = isImageUrlSimple(drop.path) || (await isImageUrl(drop.path)); if (isImg) { note.imgUrl = drop.path; } diff --git a/src/app/features/note/note/note.component.ts b/src/app/features/note/note/note.component.ts index 21d4a0aba..e04867858 100644 --- a/src/app/features/note/note/note.component.ts +++ b/src/app/features/note/note/note.component.ts @@ -9,7 +9,7 @@ import { DialogFullscreenMarkdownComponent } from '../../../ui/dialog-fullscreen selector: 'note', templateUrl: './note.component.html', styleUrls: ['./note.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class NoteComponent { @Input() note?: Note; @@ -22,21 +22,20 @@ export class NoteComponent { constructor( private readonly _matDialog: MatDialog, private readonly _noteService: NoteService, - ) { - } + ) {} toggleLock() { if (!this.note) { throw new Error('No note'); } - this._noteService.update(this.note.id, {isLock: !this.note.isLock}); + this._noteService.update(this.note.id, { isLock: !this.note.isLock }); } updateContent(newVal: any) { if (!this.note) { throw new Error('No note'); } - this._noteService.update(this.note.id, {content: newVal}); + this._noteService.update(this.note.id, { content: newVal }); } removeNote() { @@ -50,20 +49,23 @@ export class NoteComponent { if (!this.note) { throw new Error('No note'); } - this._matDialog.open(DialogFullscreenMarkdownComponent, { - minWidth: '100vw', - height: '100vh', - restoreFocus: true, - data: { - content: this.note.content, - } - }).afterClosed().subscribe((content) => { - if (!this.note) { - throw new Error('No note'); - } - if (typeof content === 'string') { - this._noteService.update(this.note.id, {content}); - } - }); + this._matDialog + .open(DialogFullscreenMarkdownComponent, { + minWidth: '100vw', + height: '100vh', + restoreFocus: true, + data: { + content: this.note.content, + }, + }) + .afterClosed() + .subscribe((content) => { + if (!this.note) { + throw new Error('No note'); + } + if (typeof content === 'string') { + this._noteService.update(this.note.id, { content }); + } + }); } } diff --git a/src/app/features/note/notes/notes.component.ts b/src/app/features/note/notes/notes.component.ts index 4158a7c1c..4b7821088 100644 --- a/src/app/features/note/notes/notes.component.ts +++ b/src/app/features/note/notes/notes.component.ts @@ -6,7 +6,7 @@ import { OnDestroy, OnInit, Output, - ViewChild + ViewChild, } from '@angular/core'; import { NoteService } from '../note.service'; import { DragulaService } from 'ng2-dragula'; @@ -26,7 +26,6 @@ import { Task } from '../../tasks/task.model'; styleUrls: ['./notes.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, animations: [standardListAnimation, fadeAnimation], - }) export class NotesComponent implements OnInit, OnDestroy { @Output() scrollToSidenav: EventEmitter = new EventEmitter(); @@ -36,15 +35,14 @@ export class NotesComponent implements OnInit, OnDestroy { isDragOver: boolean = false; dragEnterTarget?: HTMLElement; - @ViewChild('buttonEl', {static: true}) buttonEl?: MatButton; + @ViewChild('buttonEl', { static: true }) buttonEl?: MatButton; private _subs: Subscription = new Subscription(); constructor( public noteService: NoteService, private _dragulaService: DragulaService, private _matDialog: MatDialog, - ) { - } + ) {} @HostListener('dragenter', ['$event']) onDragEnter(ev: DragEvent) { this.dragEnterTarget = ev.target as HTMLElement; @@ -65,19 +63,23 @@ export class NotesComponent implements OnInit, OnDestroy { } ngOnInit() { - this._subs.add(this._dragulaService.dropModel('NOTES') - .subscribe(({targetModel}: any) => { + this._subs.add( + this._dragulaService.dropModel('NOTES').subscribe(({ targetModel }: any) => { // const {target, source, targetModel, item} = params; const targetNewIds = targetModel.map((task: Task) => task.id); this.noteService.updateOrder(targetNewIds); - }) + }), ); this._dragulaService.createGroup('NOTES', { direction: 'vertical', moves: (el, container, handle) => { - return !!handle && handle.className.indexOf && handle.className.indexOf('handle-drag') > -1; - } + return ( + !!handle && + handle.className.indexOf && + handle.className.indexOf('handle-drag') > -1 + ); + }, }); } diff --git a/src/app/features/note/store/note.actions.ts b/src/app/features/note/store/note.actions.ts index b33d35a90..8cc31c7b2 100644 --- a/src/app/features/note/store/note.actions.ts +++ b/src/app/features/note/store/note.actions.ts @@ -18,15 +18,9 @@ export const addNote = createAction( props<{ note: Note; isPreventFocus?: boolean }>(), ); -export const upsertNote = createAction( - '[Note] Upsert Note', - props<{ note: Note }>(), -); +export const upsertNote = createAction('[Note] Upsert Note', props<{ note: Note }>()); -export const addNotes = createAction( - '[Note] Add Notes', - props<{ notes: Note[] }>(), -); +export const addNotes = createAction('[Note] Add Notes', props<{ notes: Note[] }>()); export const upsertNotes = createAction( '[Note] Upsert Notes', @@ -40,19 +34,14 @@ export const updateNote = createAction( export const updateNotes = createAction( '[Note] Update Notes', - props<{ notes: Update [] }>(), + props<{ notes: Update[] }>(), ); -export const deleteNote = createAction( - '[Note] Delete Note', - props<{ id: string }>(), -); +export const deleteNote = createAction('[Note] Delete Note', props<{ id: string }>()); export const deleteNotes = createAction( '[Note] Delete Notes', props<{ ids: string[] }>(), ); -export const clearNotes = createAction( - '[Note] Clear Notes', -); +export const clearNotes = createAction('[Note] Clear Notes'); diff --git a/src/app/features/note/store/note.effects.ts b/src/app/features/note/store/note.effects.ts index f69fb5bd3..14c809d42 100644 --- a/src/app/features/note/store/note.effects.ts +++ b/src/app/features/note/store/note.effects.ts @@ -10,31 +10,33 @@ import { combineLatest, Observable } from 'rxjs'; @Injectable() export class NoteEffects { - updateNote$: Observable = createEffect(() => this._actions$.pipe( - ofType( - addNote, - deleteNote, - updateNote, - updateNoteOrder, - ), - switchMap(() => combineLatest([ - this._workContextService.activeWorkContextIdIfProject$, - this._store$.pipe(select(selectNoteFeatureState)), - ]).pipe(first())), - tap(([projectId, state]) => this._saveToLs(projectId, state)), - ), {dispatch: false}); + updateNote$: Observable = createEffect( + () => + this._actions$.pipe( + ofType(addNote, deleteNote, updateNote, updateNoteOrder), + switchMap(() => + combineLatest([ + this._workContextService.activeWorkContextIdIfProject$, + this._store$.pipe(select(selectNoteFeatureState)), + ]).pipe(first()), + ), + tap(([projectId, state]) => this._saveToLs(projectId, state)), + ), + { dispatch: false }, + ); constructor( private _actions$: Actions, private _store$: Store, private _persistenceService: PersistenceService, private _workContextService: WorkContextService, - ) { - } + ) {} private async _saveToLs(currentProjectId: string, noteState: NoteState) { if (currentProjectId) { - this._persistenceService.note.save(currentProjectId, noteState, {isSyncModelChange: true}); + this._persistenceService.note.save(currentProjectId, noteState, { + isSyncModelChange: true, + }); } else { throw new Error('No current project id'); } diff --git a/src/app/features/note/store/note.reducer.ts b/src/app/features/note/store/note.reducer.ts index 095d6a151..d1d60c456 100644 --- a/src/app/features/note/store/note.reducer.ts +++ b/src/app/features/note/store/note.reducer.ts @@ -11,9 +11,15 @@ import { updateNoteOrder, updateNotes, upsertNote, - upsertNotes + upsertNotes, } from './note.actions'; -import { Action, createFeatureSelector, createReducer, createSelector, on } from '@ngrx/store'; +import { + Action, + createFeatureSelector, + createReducer, + createSelector, + on, +} from '@ngrx/store'; export type NoteState = EntityState; @@ -39,7 +45,7 @@ export const selectNoteById = createSelector( throw new Error('No note'); } return n; - } + }, ); const _reducer = createReducer( @@ -52,41 +58,39 @@ const _reducer = createReducer( on(updateNoteOrder, (state, payload) => ({ ...state, - ids: payload.ids + ids: payload.ids, })), on(addNote, (state, payload) => ({ ...state, entities: { ...state.entities, - [payload.note.id]: payload.note + [payload.note.id]: payload.note, }, // add to top rather than bottom - ids: [payload.note.id, ...state.ids] as string[] | number[] + ids: [payload.note.id, ...state.ids] as string[] | number[], })), - on(upsertNote, (state, {note}) => adapter.upsertOne(note, state)), + on(upsertNote, (state, { note }) => adapter.upsertOne(note, state)), - on(addNotes, (state, {notes}) => adapter.addMany(notes, state)), + on(addNotes, (state, { notes }) => adapter.addMany(notes, state)), - on(upsertNotes, (state, {notes}) => adapter.upsertMany(notes, state)), + on(upsertNotes, (state, { notes }) => adapter.upsertMany(notes, state)), - on(updateNote, (state, {note}) => adapter.updateOne(note, state)), + on(updateNote, (state, { note }) => adapter.updateOne(note, state)), - on(updateNotes, (state, {notes}) => adapter.updateMany(notes, state)), + on(updateNotes, (state, { notes }) => adapter.updateMany(notes, state)), - on(deleteNote, (state, {id}) => adapter.removeOne(id, state)), + on(deleteNote, (state, { id }) => adapter.removeOne(id, state)), - on(deleteNotes, (state, {ids}) => adapter.removeMany(ids, state)), + on(deleteNotes, (state, { ids }) => adapter.removeMany(ids, state)), on(clearNotes, (state) => adapter.removeAll(state)), ); export function noteReducer( state: NoteState = initialNoteState, - action: Action + action: Action, ): NoteState { return _reducer(state, action); } - - diff --git a/src/app/features/planning-mode/planning-mode.service.ts b/src/app/features/planning-mode/planning-mode.service.ts index 90ab3281a..3a23dce65 100644 --- a/src/app/features/planning-mode/planning-mode.service.ts +++ b/src/app/features/planning-mode/planning-mode.service.ts @@ -4,19 +4,23 @@ import { delay, distinctUntilChanged, filter, map, withLatestFrom } from 'rxjs/o import { WorkContextService } from '../work-context/work-context.service'; import { TaskService } from '../tasks/task.service'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class PlanningModeService { - private _iPlanningModeEndedUser$: BehaviorSubject = new BehaviorSubject(false); - private _manualTriggerCheck$: BehaviorSubject = new BehaviorSubject(null); + private _iPlanningModeEndedUser$: BehaviorSubject = new BehaviorSubject( + false, + ); + private _manualTriggerCheck$: BehaviorSubject = new BehaviorSubject( + null, + ); private _isCurrentTask$: Observable = this._taskService.currentTaskId$.pipe( distinctUntilChanged(), - filter(id => !!id), + filter((id) => !!id), ); private _triggerCheck$: Observable = merge( this._manualTriggerCheck$, this._isCurrentTask$, // TODO fix hacky way of waiting for data to be loaded - this._workContextService.onWorkContextChange$.pipe(delay(100)) + this._workContextService.onWorkContextChange$.pipe(delay(100)), ); isPlanningMode$: Observable = this._triggerCheck$.pipe( @@ -24,7 +28,10 @@ export class PlanningModeService { this._workContextService.isHasTasksToWorkOn$, this._iPlanningModeEndedUser$, ), - map(([t, isHasTasksToWorkOn, isPlanningEndedByUser]) => !isHasTasksToWorkOn && !isPlanningEndedByUser), + map( + ([t, isHasTasksToWorkOn, isPlanningEndedByUser]) => + !isHasTasksToWorkOn && !isPlanningEndedByUser, + ), ); constructor( diff --git a/src/app/features/pomodoro/dialog-pomodoro-break/dialog-pomodoro-break.component.ts b/src/app/features/pomodoro/dialog-pomodoro-break/dialog-pomodoro-break.component.ts index 236fcb676..5030b3974 100644 --- a/src/app/features/pomodoro/dialog-pomodoro-break/dialog-pomodoro-break.component.ts +++ b/src/app/features/pomodoro/dialog-pomodoro-break/dialog-pomodoro-break.component.ts @@ -9,7 +9,7 @@ import { T } from '../../../t.const'; selector: 'dialog-pomodoro-break', templateUrl: './dialog-pomodoro-break.component.html', styleUrls: ['./dialog-pomodoro-break.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogPomodoroBreakComponent { T: typeof T = T; @@ -19,7 +19,7 @@ export class DialogPomodoroBreakComponent { ); isBreakDone$: Observable = this.pomodoroService.isManualPause$; currentCycle$: Observable = this.pomodoroService.currentCycle$.pipe( - map(cycle => cycle + 1) + map((cycle) => cycle + 1), ); private _subs: Subscription = new Subscription(); @@ -35,14 +35,18 @@ export class DialogPomodoroBreakComponent { ) { // _matDialogRef.disableClose = true; - this._subs.add(this.pomodoroService.isBreak$.subscribe((isBreak) => { - if (!isBreak) { - this.isStopCurrentTime$.next(true); - } - })); - this._subs.add(this._isCloseDialog$.subscribe(() => { - this.close(); - })); + this._subs.add( + this.pomodoroService.isBreak$.subscribe((isBreak) => { + if (!isBreak) { + this.isStopCurrentTime$.next(true); + } + }), + ); + this._subs.add( + this._isCloseDialog$.subscribe(() => { + this.close(); + }), + ); } close() { diff --git a/src/app/features/pomodoro/pomodoro.module.ts b/src/app/features/pomodoro/pomodoro.module.ts index 000dc7272..9ccb42508 100644 --- a/src/app/features/pomodoro/pomodoro.module.ts +++ b/src/app/features/pomodoro/pomodoro.module.ts @@ -16,5 +16,4 @@ import { UiModule } from '../../ui/ui.module'; EffectsModule.forFeature([PomodoroEffects]), ], }) -export class PomodoroModule { -} +export class PomodoroModule {} diff --git a/src/app/features/pomodoro/pomodoro.service.ts b/src/app/features/pomodoro/pomodoro.service.ts index 18277042f..f9ade497b 100644 --- a/src/app/features/pomodoro/pomodoro.service.ts +++ b/src/app/features/pomodoro/pomodoro.service.ts @@ -9,9 +9,9 @@ import { scan, shareReplay, switchMap, - withLatestFrom + withLatestFrom, } from 'rxjs/operators'; -import {PomodoroConfig, SoundConfig} from '../config/global-config.model'; +import { PomodoroConfig, SoundConfig } from '../config/global-config.model'; import { select, Store } from '@ngrx/store'; import { FinishPomodoroSession, @@ -19,9 +19,9 @@ import { PomodoroActionTypes, SkipPomodoroBreak, StartPomodoro, - StopPomodoro + StopPomodoro, } from './store/pomodoro.actions'; -import { selectCurrentCycle, selectIsBreak, selectIsManualPause } from './store/pomodoro.reducer'; +import { selectCurrentCycle, selectIsBreak, selectIsManualPause, } from './store/pomodoro.reducer'; import { DEFAULT_GLOBAL_CONFIG } from '../config/default-global-config.const'; import { Actions, ofType } from '@ngrx/effects'; import { distinctUntilChangedObject } from '../../util/distinct-until-changed-object'; @@ -34,12 +34,18 @@ const DEFAULT_TICK_SOUND = 'assets/snd/tick.mp3'; providedIn: 'root', }) export class PomodoroService { - onStop$: Observable = this._actions$.pipe(ofType(PomodoroActionTypes.StopPomodoro)); + onStop$: Observable = this._actions$.pipe( + ofType(PomodoroActionTypes.StopPomodoro), + ); - cfg$: Observable = this._configService.cfg$.pipe(map(cfg => cfg && cfg.pomodoro)); - soundConfig$: Observable = this._configService.cfg$.pipe(map (cfg => cfg && cfg.sound)); + cfg$: Observable = this._configService.cfg$.pipe( + map((cfg) => cfg && cfg.pomodoro), + ); + soundConfig$: Observable = this._configService.cfg$.pipe( + map((cfg) => cfg && cfg.sound), + ); isEnabled$: Observable = this.cfg$.pipe( - map(cfg => cfg && cfg.isEnabled), + map((cfg) => cfg && cfg.isEnabled), shareReplay(1), ); @@ -51,9 +57,15 @@ export class PomodoroService { this.isBreak$, this.currentCycle$, this.cfg$, - ]).pipe(map(([isBreak, currentCycle, cfg]) => { - return isBreak && !!cfg.cyclesBeforeLongerBreak && Number.isInteger(((currentCycle + 1) / cfg.cyclesBeforeLongerBreak)); - })); + ]).pipe( + map(([isBreak, currentCycle, cfg]) => { + return ( + isBreak && + !!cfg.cyclesBeforeLongerBreak && + Number.isInteger((currentCycle + 1) / cfg.cyclesBeforeLongerBreak) + ); + }), + ); isShortBreak$: Observable = combineLatest([ this.isBreak$, @@ -78,14 +90,9 @@ export class PomodoroService { this.cfg$.pipe(distinctUntilChanged(distinctUntilChangedObject)), this.onStop$, ).pipe( - withLatestFrom( - this.isLongBreak$, - this.isShortBreak$, - this.isBreak$, - this.cfg$, - ), + withLatestFrom(this.isLongBreak$, this.isShortBreak$, this.isBreak$, this.cfg$), map(([trigger, isLong, isShort, isBreak, cfg]) => { - cfg = {...cfg}; + cfg = { ...cfg }; // cfg.duration = 5000; // cfg.breakDuration = 15000; // cfg.longerBreakDuration = 20000; @@ -94,7 +101,9 @@ export class PomodoroService { } else if (isShort) { return cfg.breakDuration || DEFAULT_GLOBAL_CONFIG.pomodoro.breakDuration; } else if (isLong) { - return cfg.longerBreakDuration || DEFAULT_GLOBAL_CONFIG.pomodoro.longerBreakDuration; + return ( + cfg.longerBreakDuration || DEFAULT_GLOBAL_CONFIG.pomodoro.longerBreakDuration + ); } else { throw new Error('Pomodoro: nextSession$'); } @@ -102,14 +111,9 @@ export class PomodoroService { shareReplay(1), ); - currentSessionTime$: Observable = merge( - this.tick$, - this.nextSession$ - ).pipe( + currentSessionTime$: Observable = merge(this.tick$, this.nextSession$).pipe( scan((acc, value) => { - return (value < 0) - ? acc + value - : value; + return value < 0 ? acc + value : value; }), shareReplay(1), ); @@ -119,7 +123,7 @@ export class PomodoroService { withLatestFrom(this.nextSession$), map(([currentTime, initialTime]) => { return (initialTime - currentTime) / initialTime; - }) + }), ); constructor( @@ -127,11 +131,10 @@ export class PomodoroService { private _store$: Store, private _actions$: Actions, ) { - // NOTE: idle handling is not required, as unsetting the task auto triggers pause this.currentSessionTime$ .pipe( - filter(val => (val <= 0)), + filter((val) => val <= 0), withLatestFrom(this.cfg$, this.isBreak$), ) .subscribe(([val, cfg, isBreak]) => { @@ -142,15 +145,17 @@ export class PomodoroService { } }); - this.currentSessionTime$.pipe( - withLatestFrom(this.cfg$), - filter(([val, cfg]) => cfg.isEnabled && cfg.isPlayTick), - map(([val]) => val), - distinctUntilChanged(), - withLatestFrom(this.soundConfig$), - ).subscribe(([val, soundConfig]) => { - this._playTickSound(soundConfig.volume); - }); + this.currentSessionTime$ + .pipe( + withLatestFrom(this.cfg$), + filter(([val, cfg]) => cfg.isEnabled && cfg.isPlayTick), + map(([val]) => val), + distinctUntilChanged(), + withLatestFrom(this.soundConfig$), + ) + .subscribe(([val, soundConfig]) => { + this._playTickSound(soundConfig.volume); + }); } start() { @@ -158,7 +163,7 @@ export class PomodoroService { } pause(isBreakEndPause: boolean = false) { - this._store$.dispatch(new PausePomodoro({isBreakEndPause})); + this._store$.dispatch(new PausePomodoro({ isBreakEndPause })); } stop() { diff --git a/src/app/features/pomodoro/store/pomodoro.actions.ts b/src/app/features/pomodoro/store/pomodoro.actions.ts index a8bd3fef4..a6703fe8c 100644 --- a/src/app/features/pomodoro/store/pomodoro.actions.ts +++ b/src/app/features/pomodoro/store/pomodoro.actions.ts @@ -15,8 +15,7 @@ export class StartPomodoro implements Action { export class PausePomodoro implements Action { readonly type: string = PomodoroActionTypes.PausePomodoro; - constructor(public payload: { isBreakEndPause: boolean }) { - } + constructor(public payload: { isBreakEndPause: boolean }) {} } export class StopPomodoro implements Action { @@ -32,8 +31,8 @@ export class SkipPomodoroBreak implements Action { readonly type: string = PomodoroActionTypes.SkipPomodoroBreak; } -export type PomodoroActions - = StartPomodoro +export type PomodoroActions = + | StartPomodoro | PausePomodoro | StopPomodoro | SkipPomodoroBreak diff --git a/src/app/features/pomodoro/store/pomodoro.effects.spec.ts b/src/app/features/pomodoro/store/pomodoro.effects.spec.ts index 0b1ea9e32..32d04bd5f 100644 --- a/src/app/features/pomodoro/store/pomodoro.effects.spec.ts +++ b/src/app/features/pomodoro/store/pomodoro.effects.spec.ts @@ -38,7 +38,8 @@ describe('PomodoroEffects', () => { provideMockActions(() => actions$), provideMockStore(), { - provide: PomodoroService, useValue: { + provide: PomodoroService, + useValue: { sessionProgress$: EMPTY, cfg$, isBreak$, @@ -46,18 +47,18 @@ describe('PomodoroEffects', () => { isEnabled$, }, }, - {provide: MatDialog, useValue: {}}, - {provide: NotifyService, useValue: {}}, - {provide: ElectronService, useValue: {}}, - {provide: SnackService, useValue: {}}, - ] + { provide: MatDialog, useValue: {} }, + { provide: NotifyService, useValue: {} }, + { provide: ElectronService, useValue: {} }, + { provide: SnackService, useValue: {} }, + ], }); effects = TestBed.inject(PomodoroEffects); }); it('should start pomodoro when a task is set to current', (done) => { actions$ = of(new SetCurrentTask('something')); - effects.playPauseOnCurrentUpdate$.subscribe(effectAction => { + effects.playPauseOnCurrentUpdate$.subscribe((effectAction) => { expect(effectAction.type).toBe(PomodoroActionTypes.StartPomodoro); done(); }); @@ -65,7 +66,7 @@ describe('PomodoroEffects', () => { it('should pause pomodoro when a task is set none', (done) => { actions$ = of(new SetCurrentTask(null)); - effects.playPauseOnCurrentUpdate$.subscribe(effectAction => { + effects.playPauseOnCurrentUpdate$.subscribe((effectAction) => { expect(effectAction.type).toBe(PomodoroActionTypes.PausePomodoro); done(); }); @@ -77,10 +78,10 @@ describe('PomodoroEffects', () => { actions$ = of(new SetCurrentTask('something')); const as: Action[] = []; - effects.playPauseOnCurrentUpdate$.subscribe(effectAction => { + effects.playPauseOnCurrentUpdate$.subscribe((effectAction) => { as.push(effectAction); if (as.length === 2) { - expect(as.map(a => a.type)).toEqual([ + expect(as.map((a) => a.type)).toEqual([ PomodoroActionTypes.FinishPomodoroSession, PomodoroActionTypes.StartPomodoro, ]); diff --git a/src/app/features/pomodoro/store/pomodoro.effects.ts b/src/app/features/pomodoro/store/pomodoro.effects.ts index 0fc92cdb0..82f9823ac 100644 --- a/src/app/features/pomodoro/store/pomodoro.effects.ts +++ b/src/app/features/pomodoro/store/pomodoro.effects.ts @@ -1,6 +1,11 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; -import { SetCurrentTask, TaskActionTypes, ToggleStart, UnsetCurrentTask } from '../../tasks/store/task.actions'; +import { + SetCurrentTask, + TaskActionTypes, + ToggleStart, + UnsetCurrentTask, +} from '../../tasks/store/task.actions'; import { concatMap, filter, mapTo, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { PomodoroService } from '../pomodoro.service'; import { PomodoroConfig } from '../../config/global-config.model'; @@ -9,7 +14,7 @@ import { PausePomodoro, PomodoroActionTypes, SkipPomodoroBreak, - StartPomodoro + StartPomodoro, } from './pomodoro.actions'; import { MatDialog } from '@angular/material/dialog'; import { DialogPomodoroBreakComponent } from '../dialog-pomodoro-break/dialog-pomodoro-break.component'; @@ -26,191 +31,244 @@ import { IPC } from '../../../../../electron/ipc-events.const'; @Injectable() export class PomodoroEffects { - currentTaskId$: Observable = this._store$.pipe(select(selectCurrentTaskId)); + currentTaskId$: Observable = this._store$.pipe( + select(selectCurrentTaskId), + ); @Effect() playPauseOnCurrentUpdate$: Observable = this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._actions$.pipe( - ofType( - TaskActionTypes.SetCurrentTask, - TaskActionTypes.UnsetCurrentTask, - ), - withLatestFrom( - this._pomodoroService.cfg$, - this._pomodoroService.isBreak$, - this._pomodoroService.currentSessionTime$, - ), - // don't update when on break and stop time tracking is active - filter(([action, cfg, isBreak, currentSessionTime]: [SetCurrentTask | UnsetCurrentTask, PomodoroConfig, boolean, number]) => - !isBreak - || !cfg.isStopTrackingOnBreak - || (isBreak && currentSessionTime <= 0 && action.type === TaskActionTypes.SetCurrentTask)), - concatMap(([action, , isBreak, currentSessionTime]) => { - const payload = (action as any).payload; + switchMap((isEnabledI) => + !isEnabledI + ? EMPTY + : this._actions$.pipe( + ofType(TaskActionTypes.SetCurrentTask, TaskActionTypes.UnsetCurrentTask), + withLatestFrom( + this._pomodoroService.cfg$, + this._pomodoroService.isBreak$, + this._pomodoroService.currentSessionTime$, + ), + // don't update when on break and stop time tracking is active + filter( + ([action, cfg, isBreak, currentSessionTime]: [ + SetCurrentTask | UnsetCurrentTask, + PomodoroConfig, + boolean, + number, + ]) => + !isBreak || + !cfg.isStopTrackingOnBreak || + (isBreak && + currentSessionTime <= 0 && + action.type === TaskActionTypes.SetCurrentTask), + ), + concatMap(([action, , isBreak, currentSessionTime]) => { + const payload = (action as any).payload; - if (payload && action.type !== TaskActionTypes.UnsetCurrentTask) { - if (isBreak && currentSessionTime <= 0) { - return of( - new FinishPomodoroSession(), - new StartPomodoro() - ); - } - return of(new StartPomodoro()); - } else { - return of(new PausePomodoro({isBreakEndPause: false})); - } - }), - )), + if (payload && action.type !== TaskActionTypes.UnsetCurrentTask) { + if (isBreak && currentSessionTime <= 0) { + return of(new FinishPomodoroSession(), new StartPomodoro()); + } + return of(new StartPomodoro()); + } else { + return of(new PausePomodoro({ isBreakEndPause: false })); + } + }), + ), + ), ); @Effect() autoStartNextOnSessionStartIfNotAlready$: Observable = this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._actions$.pipe( - ofType( - PomodoroActionTypes.FinishPomodoroSession, - PomodoroActionTypes.SkipPomodoroBreak, - ), - withLatestFrom( - this._pomodoroService.isBreak$, - this.currentTaskId$, - ), - filter(([action, isBreak, currentTaskId]: [FinishPomodoroSession, boolean, string | null]) => - (!isBreak && !currentTaskId) - ), - mapTo(new ToggleStart()), - )), + switchMap((isEnabledI) => + !isEnabledI + ? EMPTY + : this._actions$.pipe( + ofType( + PomodoroActionTypes.FinishPomodoroSession, + PomodoroActionTypes.SkipPomodoroBreak, + ), + withLatestFrom(this._pomodoroService.isBreak$, this.currentTaskId$), + filter( + ([action, isBreak, currentTaskId]: [ + FinishPomodoroSession, + boolean, + string | null, + ]) => !isBreak && !currentTaskId, + ), + mapTo(new ToggleStart()), + ), + ), ); @Effect() stopPomodoro$: Observable = this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._actions$.pipe( - ofType(PomodoroActionTypes.StopPomodoro), - mapTo(new UnsetCurrentTask()), - )), + switchMap((isEnabledI) => + !isEnabledI + ? EMPTY + : this._actions$.pipe( + ofType(PomodoroActionTypes.StopPomodoro), + mapTo(new UnsetCurrentTask()), + ), + ), ); @Effect() pauseTimeTrackingIfOptionEnabled$: Observable = this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._actions$.pipe( - ofType(PomodoroActionTypes.FinishPomodoroSession), - withLatestFrom( - this._pomodoroService.cfg$, - this._pomodoroService.isBreak$, - ), - filter(([action, cfg, isBreak]: [FinishPomodoroSession, PomodoroConfig, boolean]) => - cfg.isStopTrackingOnBreak && isBreak), - mapTo(new UnsetCurrentTask()), - )), + switchMap((isEnabledI) => + !isEnabledI + ? EMPTY + : this._actions$.pipe( + ofType(PomodoroActionTypes.FinishPomodoroSession), + withLatestFrom(this._pomodoroService.cfg$, this._pomodoroService.isBreak$), + filter( + ([action, cfg, isBreak]: [ + FinishPomodoroSession, + PomodoroConfig, + boolean, + ]) => cfg.isStopTrackingOnBreak && isBreak, + ), + mapTo(new UnsetCurrentTask()), + ), + ), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) playSessionDoneSoundIfEnabled$: Observable = this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._actions$.pipe( - ofType( - PomodoroActionTypes.PausePomodoro, - PomodoroActionTypes.FinishPomodoroSession, - PomodoroActionTypes.SkipPomodoroBreak, - ), - withLatestFrom( - this._pomodoroService.cfg$, - this._pomodoroService.isBreak$, - ), - filter(([action, cfg, isBreak]: [FinishPomodoroSession | PausePomodoro | SkipPomodoroBreak, PomodoroConfig, boolean]) => { - return ((action.type === PomodoroActionTypes.FinishPomodoroSession || action.type === PomodoroActionTypes.SkipPomodoroBreak) - && (cfg.isPlaySound && isBreak) || (cfg.isPlaySoundAfterBreak && !cfg.isManualContinue && !isBreak)) - || (action.type === PomodoroActionTypes.PausePomodoro && (action as PausePomodoro).payload.isBreakEndPause); - }), - tap(() => this._pomodoroService.playSessionDoneSound()), - )), + switchMap((isEnabledI) => + !isEnabledI + ? EMPTY + : this._actions$.pipe( + ofType( + PomodoroActionTypes.PausePomodoro, + PomodoroActionTypes.FinishPomodoroSession, + PomodoroActionTypes.SkipPomodoroBreak, + ), + withLatestFrom(this._pomodoroService.cfg$, this._pomodoroService.isBreak$), + filter( + ([action, cfg, isBreak]: [ + FinishPomodoroSession | PausePomodoro | SkipPomodoroBreak, + PomodoroConfig, + boolean, + ]) => { + return ( + ((action.type === PomodoroActionTypes.FinishPomodoroSession || + action.type === PomodoroActionTypes.SkipPomodoroBreak) && + cfg.isPlaySound && + isBreak) || + (cfg.isPlaySoundAfterBreak && !cfg.isManualContinue && !isBreak) || + (action.type === PomodoroActionTypes.PausePomodoro && + (action as PausePomodoro).payload.isBreakEndPause) + ); + }, + ), + tap(() => this._pomodoroService.playSessionDoneSound()), + ), + ), ); @Effect() pauseTimeTrackingForPause$: Observable = this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._actions$.pipe( - ofType(PomodoroActionTypes.PausePomodoro), - withLatestFrom( - this.currentTaskId$, - ), - filter(([act, currentTaskId]) => !!currentTaskId), - mapTo(new UnsetCurrentTask()), - )), + switchMap((isEnabledI) => + !isEnabledI + ? EMPTY + : this._actions$.pipe( + ofType(PomodoroActionTypes.PausePomodoro), + withLatestFrom(this.currentTaskId$), + filter(([act, currentTaskId]) => !!currentTaskId), + mapTo(new UnsetCurrentTask()), + ), + ), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) openBreakDialog: Observable = this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._actions$.pipe( - ofType(PomodoroActionTypes.FinishPomodoroSession), - withLatestFrom( - this._pomodoroService.isBreak$, - ), - tap(([action, isBreak]: [FinishPomodoroSession, boolean]) => { - if (isBreak) { - this._matDialog.open(DialogPomodoroBreakComponent); - } - }), - )), + switchMap((isEnabledI) => + !isEnabledI + ? EMPTY + : this._actions$.pipe( + ofType(PomodoroActionTypes.FinishPomodoroSession), + withLatestFrom(this._pomodoroService.isBreak$), + tap(([action, isBreak]: [FinishPomodoroSession, boolean]) => { + if (isBreak) { + this._matDialog.open(DialogPomodoroBreakComponent); + } + }), + ), + ), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) sessionStartSnack$: Observable = this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._actions$.pipe( - ofType( - PomodoroActionTypes.FinishPomodoroSession, - PomodoroActionTypes.SkipPomodoroBreak, - ), - withLatestFrom( - this._pomodoroService.isBreak$, - this._pomodoroService.isManualPause$, - this._pomodoroService.currentCycle$, - ), - tap(([action, isBreak, isPause, currentCycle]: [FinishPomodoroSession, boolean, boolean, number]) => - // TODO only notify if window is not currently focused - this._notifyService.notifyDesktop({ - title: isBreak - ? T.F.POMODORO.NOTIFICATION.BREAK_X_START - : T.F.POMODORO.NOTIFICATION.SESSION_X_START, - translateParams: {nr: `${currentCycle + 1}`} - })), - filter(([action, isBreak, isPause, currentCycle]: [FinishPomodoroSession, boolean, boolean, number]) => - !isBreak && !isPause - ), - tap(([action, isBreak, isPause, currentCycle]: [FinishPomodoroSession, boolean, boolean, number]) => { - this._snackService.open({ - ico: 'timer', - msg: T.F.POMODORO.NOTIFICATION.SESSION_X_START, - translateParams: {nr: `${currentCycle + 1}`} - }); - }), - )), + switchMap((isEnabledI) => + !isEnabledI + ? EMPTY + : this._actions$.pipe( + ofType( + PomodoroActionTypes.FinishPomodoroSession, + PomodoroActionTypes.SkipPomodoroBreak, + ), + withLatestFrom( + this._pomodoroService.isBreak$, + this._pomodoroService.isManualPause$, + this._pomodoroService.currentCycle$, + ), + tap( + ([action, isBreak, isPause, currentCycle]: [ + FinishPomodoroSession, + boolean, + boolean, + number, + ]) => + // TODO only notify if window is not currently focused + this._notifyService.notifyDesktop({ + title: isBreak + ? T.F.POMODORO.NOTIFICATION.BREAK_X_START + : T.F.POMODORO.NOTIFICATION.SESSION_X_START, + translateParams: { nr: `${currentCycle + 1}` }, + }), + ), + filter( + ([action, isBreak, isPause, currentCycle]: [ + FinishPomodoroSession, + boolean, + boolean, + number, + ]) => !isBreak && !isPause, + ), + tap( + ([action, isBreak, isPause, currentCycle]: [ + FinishPomodoroSession, + boolean, + boolean, + number, + ]) => { + this._snackService.open({ + ico: 'timer', + msg: T.F.POMODORO.NOTIFICATION.SESSION_X_START, + translateParams: { nr: `${currentCycle + 1}` }, + }); + }, + ), + ), + ), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) setTaskBarIconProgress$: Observable = IS_ELECTRON ? this._pomodoroService.isEnabled$.pipe( - switchMap((isEnabledI) => !isEnabledI - ? EMPTY - : this._pomodoroService.sessionProgress$), - // we display pomodoro progress for pomodoro - tap((progress) => { - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.SET_PROGRESS_BAR, {progress}); - }), - ) + switchMap((isEnabledI) => + !isEnabledI ? EMPTY : this._pomodoroService.sessionProgress$, + ), + // we display pomodoro progress for pomodoro + tap((progress) => { + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.SET_PROGRESS_BAR, + { + progress, + }, + ); + }), + ) : EMPTY; constructor( @@ -221,6 +279,5 @@ export class PomodoroEffects { private _electronService: ElectronService, private _snackService: SnackService, private _store$: Store, - ) { - } + ) {} } diff --git a/src/app/features/pomodoro/store/pomodoro.reducer.ts b/src/app/features/pomodoro/store/pomodoro.reducer.ts index f2416f3f7..eb61d5079 100644 --- a/src/app/features/pomodoro/store/pomodoro.reducer.ts +++ b/src/app/features/pomodoro/store/pomodoro.reducer.ts @@ -16,29 +16,42 @@ export const initialPomodoroState: PomodoroState = { }; // SELECTORS -export const selectPomodoroFeatureState = createFeatureSelector(POMODORO_FEATURE_NAME); -export const selectIsManualPause = createSelector(selectPomodoroFeatureState, state => state.isManualPause); -export const selectIsBreak = createSelector(selectPomodoroFeatureState, state => state.isBreak); -export const selectCurrentCycle = createSelector(selectPomodoroFeatureState, state => state.currentCycle); +export const selectPomodoroFeatureState = createFeatureSelector( + POMODORO_FEATURE_NAME, +); +export const selectIsManualPause = createSelector( + selectPomodoroFeatureState, + (state) => state.isManualPause, +); +export const selectIsBreak = createSelector( + selectPomodoroFeatureState, + (state) => state.isBreak, +); +export const selectCurrentCycle = createSelector( + selectPomodoroFeatureState, + (state) => state.currentCycle, +); -export function pomodoroReducer(state: PomodoroState = initialPomodoroState, action: PomodoroActions): PomodoroState { +export function pomodoroReducer( + state: PomodoroState = initialPomodoroState, + action: PomodoroActions, +): PomodoroState { switch (action.type) { - - case PomodoroActionTypes.StartPomodoro: { + case PomodoroActionTypes.StartPomodoro: { return { ...state, isManualPause: false, }; } - case PomodoroActionTypes.PausePomodoro: { + case PomodoroActionTypes.PausePomodoro: { return { ...state, isManualPause: true, }; } - case PomodoroActionTypes.StopPomodoro: { + case PomodoroActionTypes.StopPomodoro: { return { isManualPause: true, isBreak: false, @@ -46,12 +59,12 @@ export function pomodoroReducer(state: PomodoroState = initialPomodoroState, act }; } - case PomodoroActionTypes.SkipPomodoroBreak: - case PomodoroActionTypes.FinishPomodoroSession: { + case PomodoroActionTypes.SkipPomodoroBreak: + case PomodoroActionTypes.FinishPomodoroSession: { return { ...state, isBreak: !state.isBreak, - currentCycle: (state.isBreak ? (state.currentCycle + 1) : state.currentCycle), + currentCycle: state.isBreak ? state.currentCycle + 1 : state.currentCycle, }; } diff --git a/src/app/features/procrastination/procrastination.component.ts b/src/app/features/procrastination/procrastination.component.ts index 3cbda2e45..fa784f0f5 100644 --- a/src/app/features/procrastination/procrastination.component.ts +++ b/src/app/features/procrastination/procrastination.component.ts @@ -6,17 +6,12 @@ import { T } from '../../t.const'; selector: 'procrastination', templateUrl: './procrastination.component.html', styleUrls: ['./procrastination.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProcrastinationComponent implements OnInit { T: typeof T = T; - constructor( - public taskService: TaskService, - ) { - } - - ngOnInit() { - } + constructor(public taskService: TaskService) {} + ngOnInit() {} } diff --git a/src/app/features/procrastination/procrastination.module.ts b/src/app/features/procrastination/procrastination.module.ts index a8e72b463..41d10f494 100644 --- a/src/app/features/procrastination/procrastination.module.ts +++ b/src/app/features/procrastination/procrastination.module.ts @@ -6,15 +6,7 @@ import { TasksModule } from '../tasks/tasks.module'; import { RouterModule } from '@angular/router'; @NgModule({ - imports: [ - CommonModule, - UiModule, - TasksModule, - RouterModule, - ], - declarations: [ - ProcrastinationComponent, - ], + imports: [CommonModule, UiModule, TasksModule, RouterModule], + declarations: [ProcrastinationComponent], }) -export class ProcrastinationModule { -} +export class ProcrastinationModule {} diff --git a/src/app/features/project/dialogs/create-project/dialog-create-project.component.html b/src/app/features/project/dialogs/create-project/dialog-create-project.component.html index 8036967f7..7ff0349b8 100644 --- a/src/app/features/project/dialogs/create-project/dialog-create-project.component.html +++ b/src/app/features/project/dialogs/create-project/dialog-create-project.component.html @@ -55,7 +55,8 @@ color="primary" mat-button type="button"> - + checkmark {{T.F.PROJECT.D_CREATE.SETUP_CALDAV|translate}} diff --git a/src/app/features/project/dialogs/create-project/dialog-create-project.component.ts b/src/app/features/project/dialogs/create-project/dialog-create-project.component.ts index dfe162af9..0a434a938 100644 --- a/src/app/features/project/dialogs/create-project/dialog-create-project.component.ts +++ b/src/app/features/project/dialogs/create-project/dialog-create-project.component.ts @@ -1,4 +1,11 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, + OnDestroy, + OnInit, +} from '@angular/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Project, ProjectCopy } from '../../project.model'; import { FormGroup } from '@angular/forms'; @@ -11,10 +18,13 @@ import { IssueIntegrationCfgs } from '../../../issue/issue.model'; import { DialogJiraInitialSetupComponent } from '../../../issue/providers/jira/jira-view-components/dialog-jira-initial-setup/dialog-jira-initial-setup.component'; import { SS_PROJECT_TMP } from '../../../../core/persistence/ls-keys.const'; import { Subscription } from 'rxjs'; -import { loadFromSessionStorage, saveToSessionStorage } from '../../../../core/persistence/local-storage'; +import { + loadFromSessionStorage, + saveToSessionStorage, +} from '../../../../core/persistence/local-storage'; import { GithubCfg } from '../../../issue/providers/github/github.model'; import { DialogGithubInitialSetupComponent } from '../../../issue/providers/github/github-view-components/dialog-github-initial-setup/dialog-github-initial-setup.component'; -import {CALDAV_TYPE, GITHUB_TYPE, GITLAB_TYPE} from '../../../issue/issue.const'; +import { CALDAV_TYPE, GITHUB_TYPE, GITLAB_TYPE } from '../../../issue/issue.const'; import { T } from '../../../../t.const'; import { DEFAULT_JIRA_CFG } from '../../../issue/providers/jira/jira.const'; import { DEFAULT_GITHUB_CFG } from '../../../issue/providers/github/github.const'; @@ -22,9 +32,9 @@ import { WORK_CONTEXT_THEME_CONFIG_FORM_CONFIG } from '../../../work-context/wor import { GitlabCfg } from 'src/app/features/issue/providers/gitlab/gitlab'; import { DEFAULT_GITLAB_CFG } from 'src/app/features/issue/providers/gitlab/gitlab.const'; import { DialogGitlabInitialSetupComponent } from 'src/app/features/issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.component'; -import {CaldavCfg} from 'src/app/features/issue/providers/caldav/caldav.model'; -import {DEFAULT_CALDAV_CFG} from 'src/app/features/issue/providers/caldav/caldav.const'; -import {DialogCaldavInitialSetupComponent} from 'src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.component'; +import { CaldavCfg } from 'src/app/features/issue/providers/caldav/caldav.model'; +import { DEFAULT_CALDAV_CFG } from 'src/app/features/issue/providers/caldav/caldav.const'; +import { DialogCaldavInitialSetupComponent } from 'src/app/features/issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.component'; @Component({ selector: 'dialog-create-project', @@ -73,8 +83,7 @@ export class DialogCreateProjectComponent implements OnInit, OnDestroy { ngOnInit() { if (this._project) { - this.projectData = {...this._project}; - + this.projectData = { ...this._project }; } else { const ssVal: any = loadFromSessionStorage(SS_PROJECT_TMP); if (ssVal) { @@ -155,59 +164,75 @@ export class DialogCreateProjectComponent implements OnInit, OnDestroy { } openJiraCfg() { - this._subs.add(this._matDialog.open(DialogJiraInitialSetupComponent, { - restoreFocus: true, - data: { - jiraCfg: this.jiraCfg, - } - }).afterClosed().subscribe((jiraCfg: JiraCfg) => { - - if (jiraCfg) { - this._saveJiraCfg(jiraCfg); - } - })); + this._subs.add( + this._matDialog + .open(DialogJiraInitialSetupComponent, { + restoreFocus: true, + data: { + jiraCfg: this.jiraCfg, + }, + }) + .afterClosed() + .subscribe((jiraCfg: JiraCfg) => { + if (jiraCfg) { + this._saveJiraCfg(jiraCfg); + } + }), + ); } openGithubCfg() { - this._subs.add(this._matDialog.open(DialogGithubInitialSetupComponent, { - restoreFocus: true, - data: { - githubCfg: this.githubCfg, - } - }).afterClosed().subscribe((gitCfg: GithubCfg) => { - - if (gitCfg) { - this._saveGithubCfg(gitCfg); - } - })); + this._subs.add( + this._matDialog + .open(DialogGithubInitialSetupComponent, { + restoreFocus: true, + data: { + githubCfg: this.githubCfg, + }, + }) + .afterClosed() + .subscribe((gitCfg: GithubCfg) => { + if (gitCfg) { + this._saveGithubCfg(gitCfg); + } + }), + ); } openGitlabCfg() { - this._subs.add(this._matDialog.open(DialogGitlabInitialSetupComponent, { - restoreFocus: true, - data: { - gitlabCfg: this.gitlabCfg, - } - }).afterClosed().subscribe((gitlabCfg: GitlabCfg) => { - - if (gitlabCfg) { - this._saveGitlabCfg(gitlabCfg); - } - })); + this._subs.add( + this._matDialog + .open(DialogGitlabInitialSetupComponent, { + restoreFocus: true, + data: { + gitlabCfg: this.gitlabCfg, + }, + }) + .afterClosed() + .subscribe((gitlabCfg: GitlabCfg) => { + if (gitlabCfg) { + this._saveGitlabCfg(gitlabCfg); + } + }), + ); } openCaldavCfg() { - this._subs.add(this._matDialog.open(DialogCaldavInitialSetupComponent, { - restoreFocus: true, - data: { - caldavCfg: this.caldavCfg, - } - }).afterClosed().subscribe((caldavCfg: CaldavCfg) => { - - if (caldavCfg) { - this._saveCaldavCfg(caldavCfg); - } - })); + this._subs.add( + this._matDialog + .open(DialogCaldavInitialSetupComponent, { + restoreFocus: true, + data: { + caldavCfg: this.caldavCfg, + }, + }) + .afterClosed() + .subscribe((caldavCfg: CaldavCfg) => { + if (caldavCfg) { + this._saveCaldavCfg(caldavCfg); + } + }), + ); } private _saveJiraCfg(jiraCfg: JiraCfg) { @@ -216,7 +241,11 @@ export class DialogCreateProjectComponent implements OnInit, OnDestroy { // if we're editing save right away if (this.projectData.id) { - this._projectService.updateIssueProviderConfig(this.projectData.id, 'JIRA', this.jiraCfg); + this._projectService.updateIssueProviderConfig( + this.projectData.id, + 'JIRA', + this.jiraCfg, + ); } } @@ -226,7 +255,11 @@ export class DialogCreateProjectComponent implements OnInit, OnDestroy { // if we're editing save right away if (this.projectData.id) { - this._projectService.updateIssueProviderConfig(this.projectData.id, GITHUB_TYPE, this.githubCfg); + this._projectService.updateIssueProviderConfig( + this.projectData.id, + GITHUB_TYPE, + this.githubCfg, + ); } } @@ -236,7 +269,11 @@ export class DialogCreateProjectComponent implements OnInit, OnDestroy { // if we're editing save right away if (this.projectData.id) { - this._projectService.updateIssueProviderConfig(this.projectData.id, GITLAB_TYPE, this.gitlabCfg); + this._projectService.updateIssueProviderConfig( + this.projectData.id, + GITLAB_TYPE, + this.gitlabCfg, + ); } } @@ -246,7 +283,11 @@ export class DialogCreateProjectComponent implements OnInit, OnDestroy { // if we're editing save right away if (this.projectData.id) { - this._projectService.updateIssueProviderConfig(this.projectData.id, CALDAV_TYPE, this.caldavCfg); + this._projectService.updateIssueProviderConfig( + this.projectData.id, + CALDAV_TYPE, + this.caldavCfg, + ); } } } diff --git a/src/app/features/project/migrate-projects-state.util.ts b/src/app/features/project/migrate-projects-state.util.ts index 2cc253d53..bb9a28c22 100644 --- a/src/app/features/project/migrate-projects-state.util.ts +++ b/src/app/features/project/migrate-projects-state.util.ts @@ -3,7 +3,11 @@ import { Dictionary } from '@ngrx/entity'; import { Project } from './project.model'; import { DEFAULT_PROJECT, PROJECT_MODEL_VERSION } from './project.const'; import { DEFAULT_ISSUE_PROVIDER_CFGS } from '../issue/issue.const'; -import { MODEL_VERSION_KEY, THEME_COLOR_MAP, WORKLOG_DATE_STR_FORMAT } from '../../app.constants'; +import { + MODEL_VERSION_KEY, + THEME_COLOR_MAP, + WORKLOG_DATE_STR_FORMAT, +} from '../../app.constants'; import { isMigrateModel } from '../../util/model-version'; import * as moment from 'moment'; import { convertToWesternArabic } from '../../util/numeric-converter'; @@ -17,10 +21,12 @@ export const migrateProjectState = (projectState: ProjectState): ProjectState => return projectState; } - const projectEntities: Dictionary = {...projectState.entities}; + const projectEntities: Dictionary = { ...projectState.entities }; Object.keys(projectEntities).forEach((key) => { projectEntities[key] = _updateThemeModel(projectEntities[key] as Project); - projectEntities[key] = _convertToWesternArabicDateKeys(projectEntities[key] as Project); + projectEntities[key] = _convertToWesternArabicDateKeys( + 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); @@ -43,7 +49,7 @@ const _extendProjectDefaults = (project: Project): Project => { issueIntegrationCfgs: { ...DEFAULT_ISSUE_PROVIDER_CFGS, ...project.issueIntegrationCfgs, - } + }, }; }; @@ -60,18 +66,20 @@ const __convertToWesternArabicDateKeys = (workStartEnd: { }): { [key: string]: any; } => { - return (workStartEnd) + return workStartEnd ? Object.keys(workStartEnd).reduce((acc, dateKey) => { - const date = moment(convertToWesternArabic(dateKey)); - if (!date.isValid()) { - throw new Error('Cannot migrate invalid non western arabic date string ' + dateKey); - } - const westernArabicKey = date.locale('en').format(WORKLOG_DATE_STR_FORMAT); - return { - ...acc, - [westernArabicKey]: workStartEnd[dateKey] - }; - }, {}) + const date = moment(convertToWesternArabic(dateKey)); + if (!date.isValid()) { + throw new Error( + 'Cannot migrate invalid non western arabic date string ' + dateKey, + ); + } + const westernArabicKey = date.locale('en').format(WORKLOG_DATE_STR_FORMAT); + return { + ...acc, + [westernArabicKey]: workStartEnd[dateKey], + }; + }, {}) : workStartEnd; }; @@ -86,25 +94,23 @@ const _convertToWesternArabicDateKeys = (project: Project) => { }; const _updateThemeModel = (project: Project): Project => { - return (project.hasOwnProperty('theme') && project.theme.primary) - ? project - : { + return project.hasOwnProperty('theme') && project.theme.primary + ? project + : { ...project, theme: { ...WORK_CONTEXT_DEFAULT_THEME, // eslint-disable-next-line - primary: (project.themeColor) - // eslint-disable-next-line - ? (THEME_COLOR_MAP as any)[project.themeColor] + primary: project.themeColor + ? // eslint-disable-next-line + (THEME_COLOR_MAP as any)[project.themeColor] : WORK_CONTEXT_DEFAULT_THEME.primary, // eslint-disable-next-line - } + }, }; - // TODO delete old theme properties later - } -; - + // TODO delete old theme properties later +}; const _fixIds = (projectState: ProjectState): ProjectState => { const currentIds = projectState.ids as string[]; const allIds = Object.keys(projectState.entities); @@ -120,14 +126,14 @@ const _fixIds = (projectState: ProjectState): ProjectState => { if (allIds.length !== currentIds.length) { let newIds; - const allP = allIds.map(id => projectState.entities[id]); + const allP = allIds.map((id) => projectState.entities[id]); const archivedIds = allP .filter((p) => (p as Project).isArchived) - .map(p => (p as Project).id); + .map((p) => (p as Project).id); const unarchivedIds = allP - .filter(p => !(p as Project).isArchived) - .map(p => (p as Project).id); + .filter((p) => !(p as Project).isArchived) + .map((p) => (p as Project).id); if (currentIds.length === unarchivedIds.length) { newIds = [...currentIds, ...archivedIds]; } else if (currentIds.length === unarchivedIds.length) { diff --git a/src/app/features/project/project-form-cfg.const.ts b/src/app/features/project/project-form-cfg.const.ts index c756c82e1..54779af8e 100644 --- a/src/app/features/project/project-form-cfg.const.ts +++ b/src/app/features/project/project-form-cfg.const.ts @@ -1,4 +1,7 @@ -import { ConfigFormSection, GenericConfigFormSection } from '../config/global-config.model'; +import { + ConfigFormSection, + GenericConfigFormSection, +} from '../config/global-config.model'; import { T } from '../../t.const'; import { Project } from './project.model'; @@ -14,7 +17,7 @@ export const BASIC_PROJECT_CONFIG_FORM_CONFIG: ConfigFormSection = { label: T.F.PROJECT.FORM_BASIC.L_TITLE, }, }, - ] + ], }; export const CREATE_PROJECT_BASIC_CONFIG_FORM_CONFIG: GenericConfigFormSection = { @@ -40,5 +43,5 @@ export const CREATE_PROJECT_BASIC_CONFIG_FORM_CONFIG: GenericConfigFormSection = type: 'color', }, }, - ] + ], }; diff --git a/src/app/features/project/project.const.ts b/src/app/features/project/project.const.ts index 8eaf6759e..d1d933548 100644 --- a/src/app/features/project/project.const.ts +++ b/src/app/features/project/project.const.ts @@ -3,7 +3,7 @@ import { DEFAULT_ISSUE_PROVIDER_CFGS } from '../issue/issue.const'; import { DEFAULT_PROJECT_COLOR, WORK_CONTEXT_DEFAULT_COMMON, - WORK_CONTEXT_DEFAULT_THEME + WORK_CONTEXT_DEFAULT_THEME, } from '../work-context/work-context.const'; export const DEFAULT_PROJECT: Project = { @@ -17,7 +17,7 @@ export const DEFAULT_PROJECT: Project = { theme: { ...WORK_CONTEXT_DEFAULT_THEME, primary: DEFAULT_PROJECT_COLOR, - } + }, }; export const DEFAULT_PROJECT_ID = 'DEFAULT'; diff --git a/src/app/features/project/project.model.ts b/src/app/features/project/project.model.ts index 91bce3e36..b37eb132e 100644 --- a/src/app/features/project/project.model.ts +++ b/src/app/features/project/project.model.ts @@ -1,5 +1,8 @@ import { IssueIntegrationCfgs, IssueProviderKey } from '../issue/issue.model'; -import { WorkContextAdvancedCfgKey, WorkContextCommon } from '../work-context/work-context.model'; +import { + WorkContextAdvancedCfgKey, + WorkContextCommon, +} from '../work-context/work-context.model'; export type RoundTimeOption = '5M' | 'QUARTER' | 'HALF' | 'HOUR' | null; @@ -24,6 +27,8 @@ export interface ProjectCopy extends ProjectBasicCfg, WorkContextCommon { export type Project = Readonly; -export type ProjectCfgFormKey = WorkContextAdvancedCfgKey | IssueProviderKey | 'basic' | 'theme'; - - +export type ProjectCfgFormKey = + | WorkContextAdvancedCfgKey + | IssueProviderKey + | 'basic' + | 'theme'; diff --git a/src/app/features/project/project.module.ts b/src/app/features/project/project.module.ts index 3178255f1..561236028 100644 --- a/src/app/features/project/project.module.ts +++ b/src/app/features/project/project.module.ts @@ -10,7 +10,7 @@ import { UiModule } from '../../ui/ui.module'; import { JiraViewComponentsModule } from '../issue/providers/jira/jira-view-components/jira-view-components.module'; import { GithubViewComponentsModule } from '../issue/providers/github/github-view-components/github-view-components.module'; import { DialogGitlabInitialSetupModule } from '../issue/providers/gitlab/dialog-gitlab-initial-setup/dialog-gitlab-initial-setup.module'; -import {DialogCaldavInitialSetupModule} from '../issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.module'; +import { DialogCaldavInitialSetupModule } from '../issue/providers/caldav/dialog-caldav-initial-setup/dialog-caldav-initial-setup.module'; @NgModule({ imports: [ @@ -23,12 +23,7 @@ import {DialogCaldavInitialSetupModule} from '../issue/providers/caldav/dialog-c DialogGitlabInitialSetupModule, DialogCaldavInitialSetupModule, ], - declarations: [ - DialogCreateProjectComponent, - ], - providers: [ - ProjectService - ], + declarations: [DialogCreateProjectComponent], + providers: [ProjectService], }) -export class ProjectModule { -} +export class ProjectModule {} diff --git a/src/app/features/project/project.service.ts b/src/app/features/project/project.service.ts index bb98b7495..5345bd5d1 100644 --- a/src/app/features/project/project.service.ts +++ b/src/app/features/project/project.service.ts @@ -6,7 +6,8 @@ import { select, Store } from '@ngrx/store'; import { ProjectActionTypes, UpdateProjectOrder } from './store/project.actions'; import * as shortid from 'shortid'; import { - selectArchivedProjects, selectCaldavCfgByProjectId, + selectArchivedProjects, + selectCaldavCfgByProjectId, selectGithubCfgByProjectId, selectGitlabCfgByProjectId, selectJiraCfgByProjectId, @@ -14,7 +15,7 @@ import { selectProjectBreakTimeForProject, selectProjectById, selectUnarchivedProjects, - selectUnarchivedProjectsWithoutCurrent + selectUnarchivedProjectsWithoutCurrent, } from './store/project.reducer'; import { IssueIntegrationCfg, IssueProviderKey } from '../issue/issue.model'; import { JiraCfg } from '../issue/providers/jira/jira.model'; @@ -29,7 +30,7 @@ import { WorkContextService } from '../work-context/work-context.service'; import { GITHUB_TYPE, GITLAB_TYPE, JIRA_TYPE } from '../issue/issue.const'; import { GitlabCfg } from '../issue/providers/gitlab/gitlab'; import { ExportedProject } from './project-archive.model'; -import {CaldavCfg} from '../issue/providers/caldav/caldav.model'; +import { CaldavCfg } from '../issue/providers/caldav/caldav.model'; @Injectable({ providedIn: 'root', @@ -40,24 +41,26 @@ export class ProjectService { archived$: Observable = this._store$.pipe(select(selectArchivedProjects)); currentProject$: Observable = this._workContextService.activeWorkContextTypeAndId$.pipe( - switchMap(({activeId, activeType}) => (activeType === WorkContextType.PROJECT) - ? this.getByIdLive$(activeId) - : of(null) + switchMap(({ activeId, activeType }) => + activeType === WorkContextType.PROJECT ? this.getByIdLive$(activeId) : of(null), ), shareReplay(1), ); /* @deprecated todo fix */ isRelatedDataLoadedForCurrentProject$: Observable = this._workContextService.isActiveWorkContextProject$.pipe( - switchMap(isProject => isProject - ? this._workContextService.activeWorkContextIdIfProject$.pipe( - switchMap((activeId) => this._actions$.pipe( - ofType(ProjectActionTypes.LoadProjectRelatedDataSuccess), - map(({payload: {projectId}}) => projectId === activeId), - )), - ) - : of(false) - ) + switchMap((isProject) => + isProject + ? this._workContextService.activeWorkContextIdIfProject$.pipe( + switchMap((activeId) => + this._actions$.pipe( + ofType(ProjectActionTypes.LoadProjectRelatedDataSuccess), + map(({ payload: { projectId } }) => projectId === activeId), + ), + ), + ) + : of(false), + ), ); // DYNAMIC @@ -69,39 +72,43 @@ export class ProjectService { // TODO correct type? private readonly _store$: Store, private readonly _actions$: Actions, - ) { - } + ) {} // ------- getJiraCfgForProject$(projectId: string): Observable { - return this._store$.pipe(select(selectJiraCfgByProjectId, {id: projectId})); + return this._store$.pipe(select(selectJiraCfgByProjectId, { id: projectId })); } getGithubCfgForProject$(projectId: string): Observable { - return this._store$.pipe(select(selectGithubCfgByProjectId, {id: projectId})); + return this._store$.pipe(select(selectGithubCfgByProjectId, { id: projectId })); } getGitlabCfgForProject$(projectId: string): Observable { - return this._store$.pipe(select(selectGitlabCfgByProjectId, {id: projectId})); + return this._store$.pipe(select(selectGitlabCfgByProjectId, { id: projectId })); } getCaldavCfgForProject$(projectId: string): Observable { - return this._store$.pipe(select(selectCaldavCfgByProjectId, {id: projectId})); + return this._store$.pipe(select(selectCaldavCfgByProjectId, { id: projectId })); } getProjectsWithoutId$(projectId: string | null): Observable { - return this._store$.pipe(select(selectUnarchivedProjectsWithoutCurrent, {currentId: projectId})); + return this._store$.pipe( + select(selectUnarchivedProjectsWithoutCurrent, { currentId: projectId }), + ); } getBreakNrForProject$(projectId: string): Observable { - return this._store$.pipe(select(selectProjectBreakNrForProject, {id: projectId})); + return this._store$.pipe(select(selectProjectBreakNrForProject, { id: projectId })); } getBreakTimeForProject$(projectId: string): Observable { - return this._store$.pipe(select(selectProjectBreakTimeForProject, {id: projectId})); + return this._store$.pipe(select(selectProjectBreakTimeForProject, { id: projectId })); } - getIssueProviderCfgForProject$(projectId: string, issueProviderKey: IssueProviderKey): Observable { + getIssueProviderCfgForProject$( + projectId: string, + issueProviderKey: IssueProviderKey, + ): Observable { if (issueProviderKey === GITHUB_TYPE) { return this.getGithubCfgForProject$(projectId); } else if (issueProviderKey === JIRA_TYPE) { @@ -116,14 +123,14 @@ export class ProjectService { archive(projectId: string) { this._store$.dispatch({ type: ProjectActionTypes.ArchiveProject, - payload: {id: projectId} + payload: { id: projectId }, }); } unarchive(projectId: string) { this._store$.dispatch({ type: ProjectActionTypes.UnarchiveProject, - payload: {id: projectId} + payload: { id: projectId }, }); } @@ -131,11 +138,11 @@ export class ProjectService { if (!id) { throw new Error('No id given'); } - return this._store$.pipe(select(selectProjectById, {id}), take(1)); + return this._store$.pipe(select(selectProjectById, { id }), take(1)); } getByIdLive$(id: string): Observable { - return this._store$.pipe(select(selectProjectById, {id})); + return this._store$.pipe(select(selectProjectById, { id })); } add(project: Partial) { @@ -144,8 +151,8 @@ export class ProjectService { payload: { project: Object.assign(project, { id: shortid(), - }) - } + }), + }, }); } @@ -155,16 +162,16 @@ export class ProjectService { payload: { project: { id: project.id || shortid(), - ...project - } - } + ...project, + }, + }, }); } remove(projectId: string) { this._store$.dispatch({ type: ProjectActionTypes.DeleteProject, - payload: {id: projectId} + payload: { id: projectId }, }); } @@ -174,9 +181,9 @@ export class ProjectService { payload: { project: { id: projectId, - changes: changedFields - } - } + changes: changedFields, + }, + }, }); } @@ -184,7 +191,7 @@ export class ProjectService { projectId: string, issueProviderKey: IssueProviderKey, providerCfg: Partial, - isOverwrite: boolean = false + isOverwrite: boolean = false, ) { this._store$.dispatch({ type: ProjectActionTypes.UpdateProjectIssueProviderCfg, @@ -192,33 +199,36 @@ export class ProjectService { projectId, issueProviderKey, providerCfg, - isOverwrite - } + isOverwrite, + }, }); } updateOrder(ids: string[]) { - this._store$.dispatch(new UpdateProjectOrder({ids})); + this._store$.dispatch(new UpdateProjectOrder({ ids })); } // DB INTERFACE async importCompleteProject(data: ExportedProject): Promise { console.log(data); - const {relatedModels, ...project} = data; + const { relatedModels, ...project } = data; if (isValidProjectExport(data)) { const state = await this._persistenceService.project.loadState(); if (state.entities[project.id]) { this._snackService.open({ type: 'ERROR', msg: T.F.PROJECT.S.E_EXISTS, - translateParams: {title: project.title} + translateParams: { title: project.title }, }); } else { - await this._persistenceService.restoreCompleteRelatedDataForProject(project.id, relatedModels); + await this._persistenceService.restoreCompleteRelatedDataForProject( + project.id, + relatedModels, + ); this.upsert(project); } } else { - this._snackService.open({type: 'ERROR', msg: T.F.PROJECT.S.E_INVALID_FILE}); + this._snackService.open({ type: 'ERROR', msg: T.F.PROJECT.S.E_INVALID_FILE }); } } } diff --git a/src/app/features/project/store/project.actions.ts b/src/app/features/project/store/project.actions.ts index 14e28b7f2..14d4c7d2c 100644 --- a/src/app/features/project/store/project.actions.ts +++ b/src/app/features/project/store/project.actions.ts @@ -29,129 +29,120 @@ export enum ProjectActionTypes { export class LoadProjectRelatedDataSuccess implements Action { readonly type: string = ProjectActionTypes.LoadProjectRelatedDataSuccess; - constructor(public payload: { projectId: string }) { - } + constructor(public payload: { projectId: string }) {} } export class SetCurrentProject implements Action { readonly type: string = ProjectActionTypes.SetCurrentProject; - constructor(public payload: any) { - } + constructor(public payload: any) {} } export class LoadProjects implements Action { readonly type: string = ProjectActionTypes.LoadProjects; - constructor(public payload: { projects: Project[] }) { - } + constructor(public payload: { projects: Project[] }) {} } export class AddProject implements Action { readonly type: string = ProjectActionTypes.AddProject; - constructor(public payload: { project: Project }) { - } + constructor(public payload: { project: Project }) {} } export class AddProjects implements Action { readonly type: string = ProjectActionTypes.AddProjects; - constructor(public payload: { projects: Project[] }) { - } + constructor(public payload: { projects: Project[] }) {} } export class UpsertProject implements Action { readonly type: string = ProjectActionTypes.UpsertProject; - constructor(public payload: { projects: Project[] }) { - } + constructor(public payload: { projects: Project[] }) {} } export class UpdateProject implements Action { readonly type: string = ProjectActionTypes.UpdateProject; - constructor(public payload: { project: Update }) { - } + constructor(public payload: { project: Update }) {} } export class UpdateProjectWorkStart implements Action { readonly type: string = ProjectActionTypes.UpdateProjectWorkStart; - constructor(public payload: { id: string; date: string; newVal: number }) { - } + constructor(public payload: { id: string; date: string; newVal: number }) {} } export class UpdateProjectWorkEnd implements Action { readonly type: string = ProjectActionTypes.UpdateProjectWorkEnd; - constructor(public payload: { id: string; date: string; newVal: number }) { - } + constructor(public payload: { id: string; date: string; newVal: number }) {} } export class AddToProjectBreakTime implements Action { readonly type: string = ProjectActionTypes.AddToProjectBreakTime; - constructor(public payload: { id: string; date: string; valToAdd: number }) { - } + constructor(public payload: { id: string; date: string; valToAdd: number }) {} } export class UpdateProjectAdvancedCfg implements Action { readonly type: string = ProjectActionTypes.UpdateProjectAdvancedCfg; - constructor(public payload: { projectId: string; sectionKey: WorkContextAdvancedCfgKey; data: any }) { - } + constructor( + public payload: { + projectId: string; + sectionKey: WorkContextAdvancedCfgKey; + data: any; + }, + ) {} } export class UpdateProjectIssueProviderCfg implements Action { readonly type: string = ProjectActionTypes.UpdateProjectIssueProviderCfg; - constructor(public payload: { - projectId: string; - issueProviderKey: IssueProviderKey; - providerCfg: Partial; - isOverwrite: boolean; - }) { - } + constructor( + public payload: { + projectId: string; + issueProviderKey: IssueProviderKey; + providerCfg: Partial; + isOverwrite: boolean; + }, + ) {} } export class DeleteProject implements Action { readonly type: string = ProjectActionTypes.DeleteProject; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class DeleteProjects implements Action { readonly type: string = ProjectActionTypes.DeleteProjects; - constructor(public payload: { ids: string[] }) { - } + constructor(public payload: { ids: string[] }) {} } export class UpdateProjectOrder implements Action { readonly type: string = ProjectActionTypes.UpdateProjectOrder; - constructor(public payload: { ids: string[] }) { - } + constructor(public payload: { ids: string[] }) {} } export class ArchiveProject implements Action { readonly type: string = ProjectActionTypes.ArchiveProject; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class UnarchiveProject implements Action { readonly type: string = ProjectActionTypes.UnarchiveProject; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } -export type ProjectActions - = LoadProjects +export type ProjectActions = + | LoadProjects | LoadProjectRelatedDataSuccess | SetCurrentProject | AddProject @@ -167,6 +158,4 @@ export type ProjectActions | DeleteProjects | UpdateProjectOrder | ArchiveProject - | UnarchiveProject - ; - + | UnarchiveProject; diff --git a/src/app/features/project/store/project.effects.ts b/src/app/features/project/store/project.effects.ts index d28dc0ff0..9dd5848ac 100644 --- a/src/app/features/project/store/project.effects.ts +++ b/src/app/features/project/store/project.effects.ts @@ -12,7 +12,7 @@ import { UpdateProject, UpdateProjectIssueProviderCfg, UpdateProjectWorkEnd, - UpdateProjectWorkStart + UpdateProjectWorkStart, } from './project.actions'; import { selectProjectFeatureState } from './project.reducer'; import { PersistenceService } from '../../../core/persistence/persistence.service'; @@ -30,7 +30,7 @@ import { MoveToOtherProject, RestoreTask, TaskActionTypes, - UpdateTaskTags + UpdateTaskTags, } from '../../tasks/store/task.actions'; import { ReminderService } from '../../reminder/reminder.service'; import { ProjectService } from '../project.service'; @@ -46,7 +46,7 @@ import { moveTaskToTodayList, moveTaskToTodayListAuto, moveTaskUpInBacklogList, - moveTaskUpInTodayList + moveTaskUpInTodayList, } from '../../work-context/store/work-context-meta.actions'; import { WorkContextType } from '../../work-context/work-context.model'; import { setActiveWorkContext } from '../../work-context/store/work-context.actions'; @@ -61,44 +61,44 @@ import { TaskRepeatCfg } from '../../task-repeat-cfg/task-repeat-cfg.model'; @Injectable() export class ProjectEffects { + @Effect({ dispatch: false }) + syncProjectToLs$: Observable = this._actions$.pipe( + ofType( + ProjectActionTypes.AddProject, + ProjectActionTypes.DeleteProject, + ProjectActionTypes.UpdateProject, + ProjectActionTypes.UpdateProjectAdvancedCfg, + ProjectActionTypes.UpdateProjectIssueProviderCfg, + ProjectActionTypes.UpdateProjectWorkStart, + ProjectActionTypes.UpdateProjectWorkEnd, + ProjectActionTypes.AddToProjectBreakTime, + ProjectActionTypes.UpdateProjectOrder, + ProjectActionTypes.ArchiveProject, + ProjectActionTypes.UnarchiveProject, - @Effect({dispatch: false}) - syncProjectToLs$: Observable = this._actions$ - .pipe( - ofType( - ProjectActionTypes.AddProject, - ProjectActionTypes.DeleteProject, - ProjectActionTypes.UpdateProject, - ProjectActionTypes.UpdateProjectAdvancedCfg, - ProjectActionTypes.UpdateProjectIssueProviderCfg, - ProjectActionTypes.UpdateProjectWorkStart, - ProjectActionTypes.UpdateProjectWorkEnd, - ProjectActionTypes.AddToProjectBreakTime, - ProjectActionTypes.UpdateProjectOrder, - ProjectActionTypes.ArchiveProject, - ProjectActionTypes.UnarchiveProject, - - moveTaskInBacklogList.type, - moveTaskToBacklogList.type, - moveTaskToTodayList.type, - moveTaskUpInBacklogList.type, - moveTaskDownInBacklogList.type, - moveTaskToBacklogListAuto.type, - moveTaskToTodayListAuto.type, - ), - switchMap((a) => { - // exclude ui only actions - if (([ + moveTaskInBacklogList.type, + moveTaskToBacklogList.type, + moveTaskToTodayList.type, + moveTaskUpInBacklogList.type, + moveTaskDownInBacklogList.type, + moveTaskToBacklogListAuto.type, + moveTaskToTodayListAuto.type, + ), + switchMap((a) => { + // exclude ui only actions + if ( + [ ProjectActionTypes.UpdateProjectWorkStart, ProjectActionTypes.UpdateProjectWorkEnd, - ].includes(a.type as any))) { - return this.saveToLs$(false); - } else { - return this.saveToLs$(true); - } - }), - ); - @Effect({dispatch: false}) + ].includes(a.type as any) + ) { + return this.saveToLs$(false); + } else { + return this.saveToLs$(true); + } + }), + ); + @Effect({ dispatch: false }) updateProjectStorageConditionalTask$: Observable = this._actions$.pipe( ofType( TaskActionTypes.AddTask, @@ -108,209 +108,196 @@ export class ProjectEffects { TaskActionTypes.MoveToArchive, TaskActionTypes.ConvertToMainTask, ), - switchMap((a: AddTask | DeleteTask | MoveToOtherProject | MoveToArchive | RestoreTask | ConvertToMainTask | Action) => { - let isChange = false; - switch (a.type) { - case TaskActionTypes.AddTask: - isChange = !!(a as AddTask).payload.task.projectId; - break; - case TaskActionTypes.DeleteTask: - isChange = !!(a as DeleteTask).payload.task.projectId; - break; - case TaskActionTypes.MoveToOtherProject: - isChange = !!(a as MoveToOtherProject).payload.task.projectId; - break; - case TaskActionTypes.MoveToArchive: - isChange = !!(a as MoveToArchive).payload.tasks.find(task => !!task.projectId); - break; - case TaskActionTypes.RestoreTask: - isChange = !!(a as RestoreTask).payload.task.projectId; - break; - case TaskActionTypes.ConvertToMainTask: - isChange = !!(a as ConvertToMainTask).payload.task.projectId; - break; - } - return isChange - ? of(a) - : EMPTY; - }), + switchMap( + ( + a: + | AddTask + | DeleteTask + | MoveToOtherProject + | MoveToArchive + | RestoreTask + | ConvertToMainTask + | Action, + ) => { + let isChange = false; + switch (a.type) { + case TaskActionTypes.AddTask: + isChange = !!(a as AddTask).payload.task.projectId; + break; + case TaskActionTypes.DeleteTask: + isChange = !!(a as DeleteTask).payload.task.projectId; + break; + case TaskActionTypes.MoveToOtherProject: + isChange = !!(a as MoveToOtherProject).payload.task.projectId; + break; + case TaskActionTypes.MoveToArchive: + isChange = !!(a as MoveToArchive).payload.tasks.find( + (task) => !!task.projectId, + ); + break; + case TaskActionTypes.RestoreTask: + isChange = !!(a as RestoreTask).payload.task.projectId; + break; + case TaskActionTypes.ConvertToMainTask: + isChange = !!(a as ConvertToMainTask).payload.task.projectId; + break; + } + return isChange ? of(a) : EMPTY; + }, + ), switchMap(() => this.saveToLs$(true)), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) updateProjectStorageConditional$: Observable = this._actions$.pipe( - ofType( - moveTaskInTodayList, - moveTaskUpInTodayList, - moveTaskDownInTodayList, - ), + ofType(moveTaskInTodayList, moveTaskUpInTodayList, moveTaskDownInTodayList), filter((p) => p.workContextType === WorkContextType.PROJECT), switchMap(() => this.saveToLs$(true)), ); @Effect() - updateWorkStart$: any = this._actions$ - .pipe( - ofType(TaskActionTypes.AddTimeSpent), - filter((action: AddTimeSpent) => !!action.payload.task.projectId), - concatMap((action: AddTimeSpent) => this._projectService.getByIdOnce$(action.payload.task.projectId as string).pipe(first())), - filter((project: Project) => !project.workStart[getWorklogStr()]), - map((project) => { - return new UpdateProjectWorkStart({ - id: project.id, - date: getWorklogStr(), - newVal: Date.now(), - }); - }) - ); + updateWorkStart$: any = this._actions$.pipe( + ofType(TaskActionTypes.AddTimeSpent), + filter((action: AddTimeSpent) => !!action.payload.task.projectId), + concatMap((action: AddTimeSpent) => + this._projectService + .getByIdOnce$(action.payload.task.projectId as string) + .pipe(first()), + ), + filter((project: Project) => !project.workStart[getWorklogStr()]), + map((project) => { + return new UpdateProjectWorkStart({ + id: project.id, + date: getWorklogStr(), + newVal: Date.now(), + }); + }), + ); @Effect() - updateWorkEnd$: Observable = this._actions$ - .pipe( - ofType(TaskActionTypes.AddTimeSpent), - filter((action: AddTimeSpent) => !!action.payload.task.projectId), - map((action: AddTimeSpent) => { - return new UpdateProjectWorkEnd({ - id: action.payload.task.projectId as string, - date: getWorklogStr(), - newVal: Date.now(), - }); - }) - ); + updateWorkEnd$: Observable = this._actions$.pipe( + ofType(TaskActionTypes.AddTimeSpent), + filter((action: AddTimeSpent) => !!action.payload.task.projectId), + map((action: AddTimeSpent) => { + return new UpdateProjectWorkEnd({ + id: action.payload.task.projectId as string, + date: getWorklogStr(), + newVal: Date.now(), + }); + }), + ); @Effect() - onProjectIdChange$: Observable = this._actions$ - .pipe( - ofType( - setActiveWorkContext - ), - filter(({activeType}) => activeType === WorkContextType.PROJECT), - switchMap((action) => { - const projectId = action.activeId; - return Promise.all([ - this._noteService.loadStateForProject(projectId), - this._bookmarkService.loadStateForProject(projectId), - ]).then(() => projectId); - }), - map(projectId => { - return new LoadProjectRelatedDataSuccess({projectId}); - }) - ); + onProjectIdChange$: Observable = this._actions$.pipe( + ofType(setActiveWorkContext), + filter(({ activeType }) => activeType === WorkContextType.PROJECT), + switchMap((action) => { + const projectId = action.activeId; + return Promise.all([ + this._noteService.loadStateForProject(projectId), + this._bookmarkService.loadStateForProject(projectId), + ]).then(() => projectId); + }), + map((projectId) => { + return new LoadProjectRelatedDataSuccess({ projectId }); + }), + ); // TODO a solution for orphaned tasks might be needed - @Effect({dispatch: false}) - deleteProjectRelatedData: Observable = this._actions$ - .pipe( - ofType( - ProjectActionTypes.DeleteProject, - ), - tap(async (action: DeleteProject) => { - await this._persistenceService.removeCompleteRelatedDataForProject(action.payload.id); - this._reminderService.removeRemindersByWorkContextId(action.payload.id); - this._removeAllTasksForProject(action.payload.id); - this._removeAllArchiveTasksForProject(action.payload.id); - this._removeAllRepeatingTasksForProject(action.payload.id); + @Effect({ dispatch: false }) + deleteProjectRelatedData: Observable = this._actions$.pipe( + ofType(ProjectActionTypes.DeleteProject), + tap(async (action: DeleteProject) => { + await this._persistenceService.removeCompleteRelatedDataForProject( + action.payload.id, + ); + this._reminderService.removeRemindersByWorkContextId(action.payload.id); + this._removeAllTasksForProject(action.payload.id); + this._removeAllArchiveTasksForProject(action.payload.id); + this._removeAllRepeatingTasksForProject(action.payload.id); - // we also might need to account for this unlikely but very nasty scenario - const misc = await this._globalConfigService.misc$.pipe(take(1)).toPromise(); - if (action.payload.id === misc.defaultProjectId) { - this._globalConfigService.updateSection('misc', {defaultProjectId: null}); - } - }), - ); + // we also might need to account for this unlikely but very nasty scenario + const misc = await this._globalConfigService.misc$.pipe(take(1)).toPromise(); + if (action.payload.id === misc.defaultProjectId) { + this._globalConfigService.updateSection('misc', { defaultProjectId: null }); + } + }), + ); - @Effect({dispatch: false}) - archiveProject: Observable = this._actions$ - .pipe( - ofType( - ProjectActionTypes.ArchiveProject, - ), - tap(async (action: ArchiveProject) => { - await this._persistenceService.archiveProject(action.payload.id); - this._reminderService.removeRemindersByWorkContextId(action.payload.id); - this._snackService.open({ - ico: 'archive', - msg: T.F.PROJECT.S.ARCHIVED, - }); - }), - ); + @Effect({ dispatch: false }) + archiveProject: Observable = this._actions$.pipe( + ofType(ProjectActionTypes.ArchiveProject), + tap(async (action: ArchiveProject) => { + await this._persistenceService.archiveProject(action.payload.id); + this._reminderService.removeRemindersByWorkContextId(action.payload.id); + this._snackService.open({ + ico: 'archive', + msg: T.F.PROJECT.S.ARCHIVED, + }); + }), + ); - @Effect({dispatch: false}) - unarchiveProject: Observable = this._actions$ - .pipe( - ofType( - ProjectActionTypes.UnarchiveProject, - ), - tap(async (action: UnarchiveProject) => { - await this._persistenceService.unarchiveProject(action.payload.id); + @Effect({ dispatch: false }) + unarchiveProject: Observable = this._actions$.pipe( + ofType(ProjectActionTypes.UnarchiveProject), + tap(async (action: UnarchiveProject) => { + await this._persistenceService.unarchiveProject(action.payload.id); - this._snackService.open({ - ico: 'unarchive', - msg: T.F.PROJECT.S.UNARCHIVED - }); - }), - ); + this._snackService.open({ + ico: 'unarchive', + msg: T.F.PROJECT.S.UNARCHIVED, + }); + }), + ); // PURE SNACKS // ----------- - @Effect({dispatch: false}) - snackUpdateIssueProvider$: Observable = this._actions$ - .pipe( - ofType( - ProjectActionTypes.UpdateProjectIssueProviderCfg, - ), - tap((action: UpdateProjectIssueProviderCfg) => { - this._snackService.open({ - type: 'SUCCESS', - msg: T.F.PROJECT.S.ISSUE_PROVIDER_UPDATED, - translateParams: { - issueProviderKey: action.payload.issueProviderKey - } - }); - }) - ); + @Effect({ dispatch: false }) + snackUpdateIssueProvider$: Observable = this._actions$.pipe( + ofType(ProjectActionTypes.UpdateProjectIssueProviderCfg), + tap((action: UpdateProjectIssueProviderCfg) => { + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.PROJECT.S.ISSUE_PROVIDER_UPDATED, + translateParams: { + issueProviderKey: action.payload.issueProviderKey, + }, + }); + }), + ); - @Effect({dispatch: false}) - snackUpdateBaseSettings$: Observable = this._actions$ - .pipe( - ofType( - ProjectActionTypes.UpdateProject, - ), - tap((action: UpdateProject) => { - this._snackService.open({ - type: 'SUCCESS', - msg: T.F.PROJECT.S.UPDATED, - }); - }) - ); + @Effect({ dispatch: false }) + snackUpdateBaseSettings$: Observable = this._actions$.pipe( + ofType(ProjectActionTypes.UpdateProject), + tap((action: UpdateProject) => { + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.PROJECT.S.UPDATED, + }); + }), + ); - @Effect({dispatch: false}) - onProjectCreatedSnack: Observable = this._actions$ - .pipe( - ofType( - ProjectActionTypes.AddProject, - ), - tap((action: AddProject) => { - this._snackService.open({ - ico: 'add', - type: 'SUCCESS', - msg: T.F.PROJECT.S.CREATED, - translateParams: {title: action.payload.project.title} - }); - }), - ); + @Effect({ dispatch: false }) + onProjectCreatedSnack: Observable = this._actions$.pipe( + ofType(ProjectActionTypes.AddProject), + tap((action: AddProject) => { + this._snackService.open({ + ico: 'add', + type: 'SUCCESS', + msg: T.F.PROJECT.S.CREATED, + translateParams: { title: action.payload.project.title }, + }); + }), + ); - @Effect({dispatch: false}) - showDeletionSnack: Observable = this._actions$ - .pipe( - ofType( - ProjectActionTypes.DeleteProject, - ), - tap((action: DeleteProject) => { - this._snackService.open({ - ico: 'delete_forever', - msg: T.F.PROJECT.S.DELETED - }); - }), - ); + @Effect({ dispatch: false }) + showDeletionSnack: Observable = this._actions$.pipe( + ofType(ProjectActionTypes.DeleteProject), + tap((action: DeleteProject) => { + this._snackService.open({ + ico: 'delete_forever', + msg: T.F.PROJECT.S.DELETED, + }); + }), + ); // DATA FIXING EFFECTS // ------------------- @@ -459,21 +446,27 @@ export class ProjectEffects { @Effect() moveToTodayListOnAddTodayTag: Observable = this._actions$.pipe( ofType(TaskActionTypes.UpdateTaskTags), - filter((action: UpdateTaskTags) => - !!action.payload.task.projectId && action.payload.newTagIds.includes(TODAY_TAG.id) + filter( + (action: UpdateTaskTags) => + !!action.payload.task.projectId && + action.payload.newTagIds.includes(TODAY_TAG.id), + ), + concatMap((action) => + this._projectService.getByIdOnce$(action.payload.task.projectId as string).pipe( + map((project) => ({ + project, + p: action.payload, + })), + ), + ), + filter(({ project }) => !project.taskIds.includes(TODAY_TAG.id)), + map(({ p, project }) => + moveTaskToTodayListAuto({ + workContextId: project.id, + taskId: p.task.id, + isMoveToTop: false, + }), ), - concatMap((action) => this._projectService.getByIdOnce$(action.payload.task.projectId as string).pipe( - map((project) => ({ - project, - p: action.payload, - })) - )), - filter(({project}) => !project.taskIds.includes(TODAY_TAG.id)), - map(({p, project}) => moveTaskToTodayListAuto({ - workContextId: project.id, - taskId: p.task.id, - isMoveToTop: false, - })), ); // @Effect() @@ -512,14 +505,15 @@ export class ProjectEffects { // private _workContextService: WorkContextService, private _taskService: TaskService, private _taskRepeatCfgService: TaskRepeatCfgService, - ) { - } + ) {} private async _removeAllTasksForProject(projectIdToDelete: string): Promise { - const taskState: TaskState = await this._taskService.taskFeatureState$.pipe( - filter(s => s.isDataLoaded), - first(), - ).toPromise(); + const taskState: TaskState = await this._taskService.taskFeatureState$ + .pipe( + filter((s) => s.isDataLoaded), + first(), + ) + .toPromise(); const nonArchiveTaskIdsToDelete = taskState.ids.filter((id) => { const t = taskState.entities[id] as Task; if (!t) { @@ -529,44 +523,62 @@ export class ProjectEffects { return t.projectId === projectIdToDelete && !t.parentId; }); - console.log('TaskIds to remove/unique', nonArchiveTaskIdsToDelete, unique(nonArchiveTaskIdsToDelete)); + console.log( + 'TaskIds to remove/unique', + nonArchiveTaskIdsToDelete, + unique(nonArchiveTaskIdsToDelete), + ); this._taskService.removeMultipleMainTasks(nonArchiveTaskIdsToDelete); } - private async _removeAllArchiveTasksForProject(projectIdToDelete: string): Promise { + 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) + const archiveTaskIdsToDelete = !!taskArchiveState ? (taskArchiveState.ids as string[]).filter((id) => { - const t = taskArchiveState.entities[id] as Task; - if (!t) { - throw new Error('No task'); - } - // NOTE sub tasks are accounted for in DeleteMainTasks action - return t.projectId === projectIdToDelete && !t.parentId; - }) + const t = taskArchiveState.entities[id] as Task; + if (!t) { + throw new Error('No task'); + } + // NOTE sub tasks are accounted for in DeleteMainTasks action + return t.projectId === projectIdToDelete && !t.parentId; + }) : []; - console.log('Archive TaskIds to remove/unique', archiveTaskIdsToDelete, unique(archiveTaskIdsToDelete)); + console.log( + 'Archive TaskIds to remove/unique', + archiveTaskIdsToDelete, + unique(archiveTaskIdsToDelete), + ); // remove archive - await this._persistenceService.taskArchive.execAction(new DeleteMainTasks({taskIds: archiveTaskIdsToDelete})); + await this._persistenceService.taskArchive.execAction( + new DeleteMainTasks({ taskIds: archiveTaskIdsToDelete }), + ); } - private async _removeAllRepeatingTasksForProject(projectIdToDelete: string): Promise { - const taskRepeatCfgs: TaskRepeatCfg[] = await this._taskRepeatCfgService.taskRepeatCfgs$.pipe(first()).toPromise(); - const allCfgIdsForProject = taskRepeatCfgs.filter(cfg => cfg.projectId === projectIdToDelete); + private async _removeAllRepeatingTasksForProject( + projectIdToDelete: string, + ): Promise { + const taskRepeatCfgs: TaskRepeatCfg[] = await this._taskRepeatCfgService.taskRepeatCfgs$ + .pipe(first()) + .toPromise(); + const allCfgIdsForProject = taskRepeatCfgs.filter( + (cfg) => cfg.projectId === projectIdToDelete, + ); const cfgsIdsToRemove: string[] = allCfgIdsForProject - .filter(cfg => (!cfg.tagIds || cfg.tagIds.length === 0)) - .map(cfg => cfg.id as string); + .filter((cfg) => !cfg.tagIds || cfg.tagIds.length === 0) + .map((cfg) => cfg.id as string); if (cfgsIdsToRemove.length > 0) { this._taskRepeatCfgService.deleteTaskRepeatCfgsNoTaskCleanup(cfgsIdsToRemove); } const cfgsToUpdate: string[] = allCfgIdsForProject - .filter(cfg => cfg.tagIds && cfg.tagIds.length > 0) - .map(taskRepeatCfg => taskRepeatCfg.id as string); + .filter((cfg) => cfg.tagIds && cfg.tagIds.length > 0) + .map((taskRepeatCfg) => taskRepeatCfg.id as string); if (cfgsToUpdate.length > 0) { - this._taskRepeatCfgService.updateTaskRepeatCfgs(cfgsToUpdate, {projectId: null}); + this._taskRepeatCfgService.updateTaskRepeatCfgs(cfgsToUpdate, { projectId: null }); } } @@ -575,9 +587,9 @@ export class ProjectEffects { // tap(() => console.log('SAVE')), select(selectProjectFeatureState), take(1), - switchMap((projectState) => this._persistenceService.project.saveState(projectState, {isSyncModelChange})), + switchMap((projectState) => + this._persistenceService.project.saveState(projectState, { isSyncModelChange }), + ), ); } } - - diff --git a/src/app/features/project/store/project.reducer.spec.ts b/src/app/features/project/store/project.reducer.spec.ts index eb7e474aa..22fa7a231 100644 --- a/src/app/features/project/store/project.reducer.spec.ts +++ b/src/app/features/project/store/project.reducer.spec.ts @@ -4,65 +4,64 @@ import { Project } from '../project.model'; import { UpdateProjectOrder } from './project.actions'; describe('projectReducer', () => { - describe('UpdateProjectOrder', () => { it('Should re-add archived projects if incomplete list is given as param', () => { const s = fakeEntityStateFromArray([ - {id: 'A', isArchived: false}, - {id: 'B', isArchived: false}, - {id: 'C', isArchived: true}, + { id: 'A', isArchived: false }, + { id: 'B', isArchived: false }, + { id: 'C', isArchived: true }, ] as Partial[]); const ids = ['B', 'A']; - const r = projectReducer(s as any, new UpdateProjectOrder({ids})); + const r = projectReducer(s as any, new UpdateProjectOrder({ ids })); expect(r.ids).toEqual(['B', 'A', 'C']); }); it('Should throw an error for inconsistent data', () => { const s = fakeEntityStateFromArray([ - {id: 'A', isArchived: false}, - {id: 'B', isArchived: false}, - {id: 'C', isArchived: false}, + { id: 'A', isArchived: false }, + { id: 'B', isArchived: false }, + { id: 'C', isArchived: false }, ] as Partial[]); const ids = ['B', 'A']; expect(() => { - projectReducer(s as any, new UpdateProjectOrder({ids})); + projectReducer(s as any, new UpdateProjectOrder({ ids })); }).toThrowError('Invalid param given to UpdateProjectOrder'); }); it('Should work with correct params', () => { const s = fakeEntityStateFromArray([ - {id: 'A', isArchived: false}, - {id: 'B', isArchived: false}, - {id: 'C', isArchived: true}, + { id: 'A', isArchived: false }, + { id: 'B', isArchived: false }, + { id: 'C', isArchived: true }, ] as Partial[]); const ids = ['B', 'A', 'C']; - const r = projectReducer(s as any, new UpdateProjectOrder({ids})); + const r = projectReducer(s as any, new UpdateProjectOrder({ ids })); expect(r.ids).toEqual(['B', 'A', 'C']); }); it('Should work with all unarchived projects', () => { const s = fakeEntityStateFromArray([ - {id: 'A', isArchived: false}, - {id: 'B', isArchived: false}, - {id: 'C', isArchived: false}, + { id: 'A', isArchived: false }, + { id: 'B', isArchived: false }, + { id: 'C', isArchived: false }, ] as Partial[]); const ids = ['B', 'A', 'C']; - const r = projectReducer(s as any, new UpdateProjectOrder({ids})); + const r = projectReducer(s as any, new UpdateProjectOrder({ ids })); expect(r.ids).toEqual(['B', 'A', 'C']); }); it('Should allow sorting of archived ids as well', () => { const s = fakeEntityStateFromArray([ - {id: 'A', isArchived: false}, - {id: 'B', isArchived: false}, - {id: 'C', isArchived: true}, - {id: 'D', isArchived: true}, + { id: 'A', isArchived: false }, + { id: 'B', isArchived: false }, + { id: 'C', isArchived: true }, + { id: 'D', isArchived: true }, ] as Partial[]); const ids = ['D', 'C']; - const r = projectReducer(s as any, new UpdateProjectOrder({ids})); + const r = projectReducer(s as any, new UpdateProjectOrder({ ids })); expect(r.ids).toEqual(['A', 'B', 'D', 'C']); }); }); diff --git a/src/app/features/project/store/project.reducer.ts b/src/app/features/project/store/project.reducer.ts index b3ca41c7d..04f2489da 100644 --- a/src/app/features/project/store/project.reducer.ts +++ b/src/app/features/project/store/project.reducer.ts @@ -8,7 +8,7 @@ import { GithubCfg } from '../../issue/providers/github/github.model'; import { WorkContextAdvancedCfg, WorkContextAdvancedCfgKey, - WorkContextType + WorkContextType, } from '../../work-context/work-context.model'; import { AddTask, @@ -17,7 +17,7 @@ import { MoveToArchive, MoveToOtherProject, RestoreTask, - TaskActionTypes + TaskActionTypes, } from '../../tasks/store/task.actions'; import { moveTaskDownInBacklogList, @@ -29,13 +29,21 @@ import { moveTaskToTodayList, moveTaskToTodayListAuto, moveTaskUpInBacklogList, - moveTaskUpInTodayList + moveTaskUpInTodayList, } from '../../work-context/store/work-context-meta.actions'; -import { moveItemInList, moveTaskForWorkContextLikeState } from '../../work-context/store/work-context-meta.helper'; +import { + moveItemInList, + moveTaskForWorkContextLikeState, +} from '../../work-context/store/work-context-meta.helper'; import { arrayMoveLeft, arrayMoveRight } from '../../../util/array-move'; import { filterOutId } from '../../../util/filter-out-id'; import { unique } from '../../../util/unique'; -import {CALDAV_TYPE, GITHUB_TYPE, GITLAB_TYPE, JIRA_TYPE} from '../../issue/issue.const'; +import { + CALDAV_TYPE, + GITHUB_TYPE, + GITLAB_TYPE, + JIRA_TYPE, +} from '../../issue/issue.const'; import { GitlabCfg } from '../../issue/providers/gitlab/gitlab'; import { loadAllData } from '../../../root-store/meta/load-all-data.action'; import { AppDataComplete } from '../../../imex/sync/sync.model'; @@ -45,7 +53,7 @@ import { exists } from '../../../util/exists'; import { Task } from '../../tasks/task.model'; import { IssueIntegrationCfg, IssueProviderKey } from '../../issue/issue.model'; import { devError } from '../../../util/dev-error'; -import {CaldavCfg} from '../../issue/providers/caldav/caldav.model'; +import { CaldavCfg } from '../../issue/providers/caldav/caldav.model'; export const PROJECT_FEATURE_NAME = 'projects'; const WORK_CONTEXT_TYPE: WorkContextType = WorkContextType.PROJECT; @@ -58,12 +66,18 @@ export const projectAdapter: EntityAdapter = createEntityAdapter(PROJECT_FEATURE_NAME); -const {selectAll} = projectAdapter.getSelectors(); +export const selectProjectFeatureState = createFeatureSelector( + PROJECT_FEATURE_NAME, +); +const { selectAll } = projectAdapter.getSelectors(); export const selectAllProjects = createSelector(selectProjectFeatureState, selectAll); -export const selectUnarchivedProjects = createSelector(selectAllProjects, (projects) => projects.filter(p => !p.isArchived)); +export const selectUnarchivedProjects = createSelector(selectAllProjects, (projects) => + projects.filter((p) => !p.isArchived), +); -export const selectArchivedProjects = createSelector(selectAllProjects, (projects) => projects.filter(p => p.isArchived)); +export const selectArchivedProjects = createSelector(selectAllProjects, (projects) => + projects.filter((p) => p.isArchived), +); // DYNAMIC SELECTORS // ----------------- @@ -78,27 +92,27 @@ export const selectProjectById = createSelector( throw new Error(`Project ${props.id} not found`); } return p; - } + }, ); export const selectJiraCfgByProjectId = createSelector( selectProjectById, - (p: Project): JiraCfg => p.issueIntegrationCfgs[JIRA_TYPE] as JiraCfg + (p: Project): JiraCfg => p.issueIntegrationCfgs[JIRA_TYPE] as JiraCfg, ); export const selectGithubCfgByProjectId = createSelector( selectProjectById, - (p: Project): GithubCfg => p.issueIntegrationCfgs[GITHUB_TYPE] as GithubCfg + (p: Project): GithubCfg => p.issueIntegrationCfgs[GITHUB_TYPE] as GithubCfg, ); export const selectGitlabCfgByProjectId = createSelector( selectProjectById, - (p: Project): GitlabCfg => p.issueIntegrationCfgs[GITLAB_TYPE] as GitlabCfg + (p: Project): GitlabCfg => p.issueIntegrationCfgs[GITLAB_TYPE] as GitlabCfg, ); export const selectCaldavCfgByProjectId = createSelector( selectProjectById, - (p: Project): CaldavCfg => p.issueIntegrationCfgs[CALDAV_TYPE] as CaldavCfg + (p: Project): CaldavCfg => p.issueIntegrationCfgs[CALDAV_TYPE] as CaldavCfg, ); export const selectUnarchivedProjectsWithoutCurrent = createSelector( @@ -106,23 +120,27 @@ export const selectUnarchivedProjectsWithoutCurrent = createSelector( (s: ProjectState, props: { currentId: string | null }) => { const ids = s.ids as string[]; return ids - .filter(id => id !== props.currentId) - .map(id => exists(s.entities[id]) as Project) - .filter(p => !p.isArchived && p.id); + .filter((id) => id !== props.currentId) + .map((id) => exists(s.entities[id]) as Project) + .filter((p) => !p.isArchived && p.id); }, ); -export const selectProjectBreakTimeForProject = createSelector(selectProjectById, (project) => project.breakTime); -export const selectProjectBreakNrForProject = createSelector(selectProjectById, (project) => project.breakNr); +export const selectProjectBreakTimeForProject = createSelector( + selectProjectById, + (project) => project.breakTime, +); +export const selectProjectBreakNrForProject = createSelector( + selectProjectById, + (project) => project.breakNr, +); // DEFAULT // ------- export const initialProjectState: ProjectState = projectAdapter.getInitialState({ - ids: [ - FIRST_PROJECT.id - ], + ids: [FIRST_PROJECT.id], entities: { - [FIRST_PROJECT.id]: FIRST_PROJECT + [FIRST_PROJECT.id]: FIRST_PROJECT, }, [MODEL_VERSION_KEY]: PROJECT_MODEL_VERSION, }); @@ -131,51 +149,79 @@ export const initialProjectState: ProjectState = projectAdapter.getInitialState( // ------- export function projectReducer( state: ProjectState = initialProjectState, - action: ProjectActions | AddTask | DeleteTask | MoveToOtherProject | MoveToArchive | RestoreTask + action: + | ProjectActions + | AddTask + | DeleteTask + | MoveToOtherProject + | MoveToArchive + | RestoreTask, ): ProjectState { // eslint-disable-next-line const payload = action['payload']; // TODO fix this hackyness once we use the new syntax everywhere if ((action.type as string) === loadAllData.type) { - const {appDataComplete}: { appDataComplete: AppDataComplete } = action as any; + const { appDataComplete }: { appDataComplete: AppDataComplete } = action as any; return appDataComplete.project - ? migrateProjectState({...appDataComplete.project}) + ? migrateProjectState({ ...appDataComplete.project }) : state; } if ((action.type as string) === moveTaskInTodayList.type) { - const {taskId, newOrderedIds, target, workContextType, workContextId} = action as any; + const { + taskId, + newOrderedIds, + target, + workContextType, + workContextId, + } = action as any; if (workContextType !== WORK_CONTEXT_TYPE) { return state; } const taskIdsBefore = (state.entities[workContextId] as Project).taskIds; - const taskIds = moveTaskForWorkContextLikeState(taskId, newOrderedIds, target, taskIdsBefore); - return projectAdapter.updateOne({ - id: workContextId, - changes: { - taskIds - } - }, state); + const taskIds = moveTaskForWorkContextLikeState( + taskId, + newOrderedIds, + target, + taskIdsBefore, + ); + return projectAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds, + }, + }, + state, + ); } if ((action.type as string) === moveTaskInBacklogList.type) { - const {taskId, newOrderedIds, workContextId} = action as any; + const { taskId, newOrderedIds, workContextId } = action as any; const taskIdsBefore = (state.entities[workContextId] as Project).backlogTaskIds; - const backlogTaskIds = moveTaskForWorkContextLikeState(taskId, newOrderedIds, null, taskIdsBefore); - return projectAdapter.updateOne({ - id: workContextId, - changes: { - backlogTaskIds - } - }, state); + const backlogTaskIds = moveTaskForWorkContextLikeState( + taskId, + newOrderedIds, + null, + taskIdsBefore, + ); + return projectAdapter.updateOne( + { + id: workContextId, + changes: { + backlogTaskIds, + }, + }, + state, + ); } if ((action.type as string) === moveTaskToBacklogList.type) { - const {taskId, newOrderedIds, workContextId} = action as any; + const { taskId, newOrderedIds, workContextId } = action as any; const todaysTaskIdsBefore = (state.entities[workContextId] as Project).taskIds; const backlogIdsBefore = (state.entities[workContextId] as Project).backlogTaskIds; @@ -183,17 +229,20 @@ export function projectReducer( const filteredToday = todaysTaskIdsBefore.filter(filterOutId(taskId)); const backlogTaskIds = moveItemInList(taskId, backlogIdsBefore, newOrderedIds); - return projectAdapter.updateOne({ - id: workContextId, - changes: { - taskIds: filteredToday, - backlogTaskIds, - } - }, state); + return projectAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds: filteredToday, + backlogTaskIds, + }, + }, + state, + ); } if ((action.type as string) === moveTaskToTodayList.type) { - const {taskId, newOrderedIds, workContextId} = action as any; + const { taskId, newOrderedIds, workContextId } = action as any; const backlogIdsBefore = (state.entities[workContextId] as Project).backlogTaskIds; const todaysTaskIdsBefore = (state.entities[workContextId] as Project).taskIds; @@ -201,187 +250,225 @@ export function projectReducer( const filteredBacklog = backlogIdsBefore.filter(filterOutId(taskId)); const newTodaysTaskIds = moveItemInList(taskId, todaysTaskIdsBefore, newOrderedIds); - return projectAdapter.updateOne({ - id: workContextId, - changes: { - taskIds: newTodaysTaskIds, - backlogTaskIds: filteredBacklog, - } - }, state); + return projectAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds: newTodaysTaskIds, + backlogTaskIds: filteredBacklog, + }, + }, + state, + ); } // up down today if ((action.type as string) === moveTaskUpInTodayList.type) { - const {taskId, workContextType, workContextId} = action as any; - return (workContextType === WORK_CONTEXT_TYPE) - ? projectAdapter.updateOne({ - id: workContextId, - changes: { - taskIds: arrayMoveLeft((state.entities[workContextId] as Project).taskIds, taskId) - } - }, state) + const { taskId, workContextType, workContextId } = action as any; + return workContextType === WORK_CONTEXT_TYPE + ? projectAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds: arrayMoveLeft( + (state.entities[workContextId] as Project).taskIds, + taskId, + ), + }, + }, + state, + ) : state; } if ((action.type as string) === moveTaskDownInTodayList.type) { - const {taskId, workContextType, workContextId} = action as any; - return (workContextType === WORK_CONTEXT_TYPE) - ? projectAdapter.updateOne({ - id: workContextId, - changes: { - taskIds: arrayMoveRight((state.entities[workContextId] as Project).taskIds, taskId) - } - }, state) + const { taskId, workContextType, workContextId } = action as any; + return workContextType === WORK_CONTEXT_TYPE + ? projectAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds: arrayMoveRight( + (state.entities[workContextId] as Project).taskIds, + taskId, + ), + }, + }, + state, + ) : state; } // up down backlog if ((action.type as string) === moveTaskUpInBacklogList.type) { - const {taskId, workContextId} = action as any; - return projectAdapter.updateOne({ - id: workContextId, - changes: { - backlogTaskIds: arrayMoveLeft((state.entities[workContextId] as Project).backlogTaskIds, taskId) - } - }, state); + const { taskId, workContextId } = action as any; + return projectAdapter.updateOne( + { + id: workContextId, + changes: { + backlogTaskIds: arrayMoveLeft( + (state.entities[workContextId] as Project).backlogTaskIds, + taskId, + ), + }, + }, + state, + ); } if ((action.type as string) === moveTaskDownInBacklogList.type) { - const {taskId, workContextId} = action as any; - return projectAdapter.updateOne({ - id: workContextId, - changes: { - backlogTaskIds: arrayMoveRight((state.entities[workContextId] as Project).backlogTaskIds, taskId) - } - }, state); + const { taskId, workContextId } = action as any; + return projectAdapter.updateOne( + { + id: workContextId, + changes: { + backlogTaskIds: arrayMoveRight( + (state.entities[workContextId] as Project).backlogTaskIds, + taskId, + ), + }, + }, + state, + ); } // AUTO move backlog/today if ((action.type as string) === moveTaskToBacklogListAuto.type) { - const {taskId, workContextId} = action as any; + const { taskId, workContextId } = action as any; const todaysTaskIdsBefore = (state.entities[workContextId] as Project).taskIds; const backlogIdsBefore = (state.entities[workContextId] as Project).backlogTaskIds; - return (backlogIdsBefore.includes(taskId)) + return backlogIdsBefore.includes(taskId) ? state - : projectAdapter.updateOne({ - id: workContextId, - changes: { - taskIds: todaysTaskIdsBefore.filter(filterOutId(taskId)), - backlogTaskIds: [taskId, ...backlogIdsBefore], - } - }, state); + : projectAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds: todaysTaskIdsBefore.filter(filterOutId(taskId)), + backlogTaskIds: [taskId, ...backlogIdsBefore], + }, + }, + state, + ); } if ((action.type as string) === moveTaskToTodayListAuto.type) { - const {taskId, workContextId, isMoveToTop} = action as any; + const { taskId, workContextId, isMoveToTop } = action as any; const todaysTaskIdsBefore = (state.entities[workContextId] as Project).taskIds; const backlogIdsBefore = (state.entities[workContextId] as Project).backlogTaskIds; - return (todaysTaskIdsBefore.includes(taskId)) + return todaysTaskIdsBefore.includes(taskId) ? state - : projectAdapter.updateOne({ - id: workContextId, - changes: { - backlogTaskIds: backlogIdsBefore.filter(filterOutId(taskId)), - taskIds: (isMoveToTop) - ? [taskId, ...todaysTaskIdsBefore] - : [...todaysTaskIdsBefore, taskId] - } - }, state); + : projectAdapter.updateOne( + { + id: workContextId, + changes: { + backlogTaskIds: backlogIdsBefore.filter(filterOutId(taskId)), + taskIds: isMoveToTop + ? [taskId, ...todaysTaskIdsBefore] + : [...todaysTaskIdsBefore, taskId], + }, + }, + state, + ); } switch (action.type) { // Meta Actions // ------------ case TaskActionTypes.AddTask: { - const {task, isAddToBottom, isAddToBacklog} = payload; + const { task, isAddToBottom, isAddToBacklog } = payload; const affectedEntity = task.projectId && state.entities[task.projectId]; - const prop: 'backlogTaskIds' | 'taskIds' = isAddToBacklog ? 'backlogTaskIds' : 'taskIds'; + const prop: 'backlogTaskIds' | 'taskIds' = isAddToBacklog + ? 'backlogTaskIds' + : 'taskIds'; - return (affectedEntity) - ? projectAdapter.updateOne({ - id: task.projectId, - changes: { - [prop]: isAddToBottom - ? [ - task.id, - ...affectedEntity[prop] - ] - : [ - ...affectedEntity[prop], - task.id, - ] - } - }, state) + return affectedEntity + ? projectAdapter.updateOne( + { + id: task.projectId, + changes: { + [prop]: isAddToBottom + ? [task.id, ...affectedEntity[prop]] + : [...affectedEntity[prop], task.id], + }, + }, + state, + ) : state; } case TaskActionTypes.ConvertToMainTask: { const a = action as ConvertToMainTask; - const {task} = a.payload; + const { task } = a.payload; const affectedEntity = task.projectId && state.entities[task.projectId]; - return (affectedEntity) - ? projectAdapter.updateOne({ - id: task.projectId as string, - changes: { - taskIds: [ - task.id, - ...affectedEntity.taskIds, - ] - } - }, state) + return affectedEntity + ? projectAdapter.updateOne( + { + id: task.projectId as string, + changes: { + taskIds: [task.id, ...affectedEntity.taskIds], + }, + }, + state, + ) : state; } case TaskActionTypes.DeleteTask: { - const {task} = action.payload; + const { task } = action.payload; const project = state.entities[task.projectId] as Project; - return (task.projectId) - ? projectAdapter.updateOne({ - id: task.projectId, - changes: { - taskIds: project.taskIds.filter(ptId => ptId !== task.id), - backlogTaskIds: project.backlogTaskIds.filter(ptId => ptId !== task.id) - } - }, state) + return task.projectId + ? projectAdapter.updateOne( + { + id: task.projectId, + changes: { + taskIds: project.taskIds.filter((ptId) => ptId !== task.id), + backlogTaskIds: project.backlogTaskIds.filter((ptId) => ptId !== task.id), + }, + }, + state, + ) : state; } case TaskActionTypes.MoveToArchive: { - const {tasks} = action.payload; + const { tasks } = action.payload; const taskIdsToMoveToArchive = tasks.map((t: Task) => t.id); const projectIds = unique( - tasks - .map((t: Task) => t.projectId) - .filter((pid: string) => !!pid) + tasks.map((t: Task) => t.projectId).filter((pid: string) => !!pid), ); const updates: Update[] = projectIds.map((pid: string) => ({ id: pid, changes: { - taskIds: (state.entities[pid] as Project).taskIds - .filter(taskId => !taskIdsToMoveToArchive.includes(taskId)), - backlogTaskIds: (state.entities[pid] as Project).backlogTaskIds - .filter(taskId => !taskIdsToMoveToArchive.includes(taskId)), - } + taskIds: (state.entities[pid] as Project).taskIds.filter( + (taskId) => !taskIdsToMoveToArchive.includes(taskId), + ), + backlogTaskIds: (state.entities[pid] as Project).backlogTaskIds.filter( + (taskId) => !taskIdsToMoveToArchive.includes(taskId), + ), + }, })); return projectAdapter.updateMany(updates, state); } case TaskActionTypes.RestoreTask: { - const {task} = action.payload; + const { task } = action.payload; if (!task.projectId) { return state; } - return projectAdapter.updateOne({ - id: task.projectId, - changes: { - taskIds: [...(state.entities[task.projectId] as Project).taskIds, task.id] - } - }, state); + return projectAdapter.updateOne( + { + id: task.projectId, + changes: { + taskIds: [...(state.entities[task.projectId] as Project).taskIds, task.id], + }, + }, + state, + ); } case TaskActionTypes.MoveToOtherProject: { - const {task, targetProjectId} = action.payload; + const { task, targetProjectId } = action.payload; const srcProjectId = task.projectId; const updates: Update[] = []; @@ -394,9 +481,13 @@ export function projectReducer( updates.push({ id: srcProjectId, changes: { - taskIds: (state.entities[srcProjectId] as Project).taskIds.filter(id => id !== task.id), - backlogTaskIds: (state.entities[srcProjectId] as Project).backlogTaskIds.filter(id => id !== task.id), - } + taskIds: (state.entities[srcProjectId] as Project).taskIds.filter( + (id) => id !== task.id, + ), + backlogTaskIds: (state.entities[ + srcProjectId + ] as Project).backlogTaskIds.filter((id) => id !== task.id), + }, }); } if (targetProjectId) { @@ -404,14 +495,13 @@ export function projectReducer( id: targetProjectId, changes: { taskIds: [...(state.entities[targetProjectId] as Project).taskIds, task.id], - } + }, }); } return projectAdapter.updateMany(updates, state); } - // Project Actions // ------------ case ProjectActionTypes.LoadProjectRelatedDataSuccess: { @@ -435,52 +525,61 @@ export function projectReducer( } case ProjectActionTypes.UpdateProjectWorkStart: { - const {id, date, newVal} = action.payload; + const { id, date, newVal } = action.payload; const oldP = state.entities[id] as Project; - return projectAdapter.updateOne({ - id, - changes: { - workStart: { - ...oldP.workStart, - [date]: newVal, - } - } - }, state); + return projectAdapter.updateOne( + { + id, + changes: { + workStart: { + ...oldP.workStart, + [date]: newVal, + }, + }, + }, + state, + ); } case ProjectActionTypes.UpdateProjectWorkEnd: { - const {id, date, newVal} = action.payload; + const { id, date, newVal } = action.payload; const oldP = state.entities[id] as Project; - return projectAdapter.updateOne({ - id, - changes: { - workEnd: { - ...oldP.workEnd, - [date]: newVal, - } - } - }, state); + return projectAdapter.updateOne( + { + id, + changes: { + workEnd: { + ...oldP.workEnd, + [date]: newVal, + }, + }, + }, + state, + ); } case ProjectActionTypes.AddToProjectBreakTime: { - const {id, date, valToAdd} = action.payload; + const { id, date, valToAdd } = action.payload; 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, + return projectAdapter.updateOne( + { + id, + changes: { + breakNr: { + ...oldP.breakNr, + [date]: oldBreakNr + 1, + }, + breakTime: { + ...oldP.breakTime, + [date]: oldBreakTime + valToAdd, + }, }, - breakTime: { - ...oldP.breakTime, - [date]: oldBreakTime + valToAdd, - } - } - }, state); + }, + state, + ); } case ProjectActionTypes.DeleteProject: { @@ -499,57 +598,85 @@ export function projectReducer( const { projectId, sectionKey, - data - }: { projectId: string; sectionKey: WorkContextAdvancedCfgKey; data: any } = payload; + data, + }: { + projectId: string; + sectionKey: WorkContextAdvancedCfgKey; + data: any; + } = payload; const currentProject = state.entities[projectId] as Project; - const advancedCfg: WorkContextAdvancedCfg = Object.assign({}, currentProject.advancedCfg); - return projectAdapter.updateOne({ - id: projectId, - changes: { - advancedCfg: { - ...advancedCfg, - [sectionKey]: { - ...advancedCfg[sectionKey], - ...data, - } - } - } - }, state); + const advancedCfg: WorkContextAdvancedCfg = Object.assign( + {}, + currentProject.advancedCfg, + ); + return projectAdapter.updateOne( + { + id: projectId, + changes: { + advancedCfg: { + ...advancedCfg, + [sectionKey]: { + ...advancedCfg[sectionKey], + ...data, + }, + }, + }, + }, + state, + ); } case ProjectActionTypes.UpdateProjectIssueProviderCfg: { - const {projectId, providerCfg, issueProviderKey, isOverwrite}: { + const { + projectId, + providerCfg, + issueProviderKey, + isOverwrite, + }: { projectId: string; issueProviderKey: IssueProviderKey; providerCfg: Partial; isOverwrite: boolean; } = action.payload; const currentProject = state.entities[projectId] as Project; - return projectAdapter.updateOne({ - id: projectId, - changes: { - issueIntegrationCfgs: { - ...currentProject.issueIntegrationCfgs, - [issueProviderKey]: { - ...(isOverwrite ? {} : currentProject.issueIntegrationCfgs[issueProviderKey]), - ...providerCfg, - } - } - } - }, state); + return projectAdapter.updateOne( + { + id: projectId, + changes: { + issueIntegrationCfgs: { + ...currentProject.issueIntegrationCfgs, + [issueProviderKey]: { + ...(isOverwrite + ? {} + : currentProject.issueIntegrationCfgs[issueProviderKey]), + ...providerCfg, + }, + }, + }, + }, + state, + ); } case ProjectActionTypes.UpdateProjectOrder: { - const {ids} = action.payload; + const { ids } = action.payload; const currentIds = state.ids as string[]; let newIds: string[] = ids; if (ids.length !== currentIds.length) { - const allP = currentIds.map(id => state.entities[id]) as Project[]; - const archivedIds = allP.filter(p => p.isArchived).map(p => p.id); - const unarchivedIds = allP.filter(p => !p.isArchived).map(p => p.id); - if (ids.length === unarchivedIds.length && ids.length > 0 && unarchivedIds.includes(ids[0])) { + const allP = currentIds.map((id) => state.entities[id]) as Project[]; + const archivedIds = allP.filter((p) => p.isArchived).map((p) => p.id); + const unarchivedIds = allP.filter((p) => !p.isArchived).map((p) => p.id); + if ( + ids.length === unarchivedIds.length && + ids.length > 0 && + unarchivedIds.includes(ids[0]) + ) { newIds = [...ids, ...archivedIds]; - } else if (ids.length === archivedIds.length && ids.length > 0 && archivedIds.includes(ids[0])) { + } else if ( + ids.length === archivedIds.length && + ids.length > 0 && + archivedIds.includes(ids[0]) + ) { newIds = [...unarchivedIds, ...ids]; } else { throw new Error('Invalid param given to UpdateProjectOrder'); @@ -560,25 +687,31 @@ export function projectReducer( throw new Error('Project ids are undefined'); } - return {...state, ids: newIds}; + return { ...state, ids: newIds }; } case ProjectActionTypes.ArchiveProject: { - return projectAdapter.updateOne({ - id: action.payload.id, - changes: { - isArchived: true, - } - }, state); + return projectAdapter.updateOne( + { + id: action.payload.id, + changes: { + isArchived: true, + }, + }, + state, + ); } case ProjectActionTypes.UnarchiveProject: { - return projectAdapter.updateOne({ - id: action.payload.id, - changes: { - isArchived: false, - } - }, state); + return projectAdapter.updateOne( + { + id: action.payload.id, + changes: { + isArchived: false, + }, + }, + state, + ); } default: { diff --git a/src/app/features/project/util/is-valid-project-export.ts b/src/app/features/project/util/is-valid-project-export.ts index c674b3280..f9a1c37b3 100644 --- a/src/app/features/project/util/is-valid-project-export.ts +++ b/src/app/features/project/util/is-valid-project-export.ts @@ -1,3 +1,10 @@ export const isValidProjectExport = (d: any): boolean => { - return !!(d && d.id && d.title && d.relatedModels && d.advancedCfg && d.relatedModels.task); + return !!( + d && + d.id && + d.title && + d.relatedModels && + d.advancedCfg && + d.relatedModels.task + ); }; diff --git a/src/app/features/reminder/migrate-reminder.util.ts b/src/app/features/reminder/migrate-reminder.util.ts index c412ccaa9..bce5277b1 100644 --- a/src/app/features/reminder/migrate-reminder.util.ts +++ b/src/app/features/reminder/migrate-reminder.util.ts @@ -2,10 +2,10 @@ import { Reminder } from './reminder.model'; import { WorkContextType } from '../work-context/work-context.model'; export const migrateReminders = (reminders: Reminder[]): Reminder[] => { - return reminders.map(reminder => { + return reminders.map((reminder) => { // eslint-disable-next-line if ((reminder as any)['projectId']) { - const {projectId, ...newReminder} = reminder as any; + const { projectId, ...newReminder } = reminder as any; return { ...newReminder, workContextId: projectId, diff --git a/src/app/features/reminder/reminder.module.ts b/src/app/features/reminder/reminder.module.ts index f47870493..20d10771f 100644 --- a/src/app/features/reminder/reminder.module.ts +++ b/src/app/features/reminder/reminder.module.ts @@ -16,11 +16,7 @@ import { SyncService } from '../../imex/sync/sync.service'; @NgModule({ declarations: [], - imports: [ - CommonModule, - NoteModule, - TasksModule, - ], + imports: [CommonModule, NoteModule, TasksModule], }) export class ReminderModule { constructor( @@ -31,56 +27,69 @@ export class ReminderModule { private readonly _dataInitService: DataInitService, private readonly _syncService: SyncService, ) { - - this._dataInitService.isAllDataLoadedInitially$.pipe(concatMap(() => - - // we do this to wait for syncing and the like - this._syncService.afterInitialSyncDoneAndDataLoadedInitially$.pipe( - first(), - delay(1000), - ))) + this._dataInitService.isAllDataLoadedInitially$ + .pipe( + concatMap(() => + // we do this to wait for syncing and the like + this._syncService.afterInitialSyncDoneAndDataLoadedInitially$.pipe( + first(), + delay(1000), + ), + ), + ) .subscribe(async () => { _reminderService.init(); }); - this._reminderService.onRemindersActive$.pipe( - // NOTE: we simply filter for open dialogs, as reminders are re-queried quite often - filter((reminder) => this._matDialog.openDialogs.length === 0 && !!reminder && reminder.length > 0), - ).subscribe((reminders: Reminder[]) => { + this._reminderService.onRemindersActive$ + .pipe( + // NOTE: we simply filter for open dialogs, as reminders are re-queried quite often + filter( + (reminder) => + this._matDialog.openDialogs.length === 0 && !!reminder && reminder.length > 0, + ), + ) + .subscribe((reminders: Reminder[]) => { + if (IS_ELECTRON) { + this._uiHelperService.focusApp(); + } - if (IS_ELECTRON) { - this._uiHelperService.focusApp(); - } + this._showNotification(reminders); - this._showNotification(reminders); - - const oldest = reminders[0]; - if (oldest.type === 'TASK') { - this._matDialog.open(DialogViewTaskRemindersComponent, { - autoFocus: false, - restoreFocus: true, - data: { - reminders, - } - }).afterClosed(); - } - }); + const oldest = reminders[0]; + if (oldest.type === 'TASK') { + this._matDialog + .open(DialogViewTaskRemindersComponent, { + autoFocus: false, + restoreFocus: true, + data: { + reminders, + }, + }) + .afterClosed(); + } + }); } @throttle(60000) private _showNotification(reminders: Reminder[]) { const isMultiple = reminders.length > 1; const title = isMultiple - ? '"' + reminders[0].title + '" and ' + (reminders.length - 1) + ' other tasks are due.' + ? '"' + + reminders[0].title + + '" and ' + + (reminders.length - 1) + + ' other tasks are due.' : reminders[0].title; const tag = reminders.reduce((acc, reminder) => acc + '_' + reminder.id, ''); - this._notifyService.notify({ - title, - // prevents multiple notifications on mobile - tag, - requireInteraction: true, - }).then(); + this._notifyService + .notify({ + title, + // prevents multiple notifications on mobile + tag, + requireInteraction: true, + }) + .then(); } - } diff --git a/src/app/features/reminder/reminder.service.ts b/src/app/features/reminder/reminder.service.ts index 431e53340..41c978701 100644 --- a/src/app/features/reminder/reminder.service.ts +++ b/src/app/features/reminder/reminder.service.ts @@ -23,9 +23,11 @@ import { WorkContextType } from '../work-context/work-context.model'; export class ReminderService { private _onRemindersActive$: Subject = new Subject(); onRemindersActive$: Observable = this._onRemindersActive$.pipe( - skipUntil(this._imexMetaService.isDataImportInProgress$.pipe( - filter(isInProgress => !isInProgress), - )), + skipUntil( + this._imexMetaService.isDataImportInProgress$.pipe( + filter((isInProgress) => !isInProgress), + ), + ), ); private _reminders$: ReplaySubject = new ReplaySubject(1); @@ -34,7 +36,9 @@ export class ReminderService { private _onReloadModel$: Subject = new Subject(); onReloadModel$: Observable = this._onReloadModel$.asObservable(); - private _isRemindersLoaded$: BehaviorSubject = new BehaviorSubject(false); + private _isRemindersLoaded$: BehaviorSubject = new BehaviorSubject( + false, + ); isRemindersLoaded$: Observable = this._isRemindersLoaded$.asObservable(); private _w: Worker; @@ -59,7 +63,7 @@ export class ReminderService { this._w = new Worker('./reminder.worker', { name: 'reminder', - type: 'module' + type: 'module', }); } @@ -89,26 +93,33 @@ export class ReminderService { // TODO maybe refactor to observable, because models can differ to sync value for yet unknown reasons getById(reminderId: string): ReminderCopy | null { - const _foundReminder = this._reminders && this._reminders.find(reminder => reminder.id === reminderId); - return !!_foundReminder - ? dirtyDeepCopy(_foundReminder) - : null; + const _foundReminder = + this._reminders && this._reminders.find((reminder) => reminder.id === reminderId); + return !!_foundReminder ? dirtyDeepCopy(_foundReminder) : null; } getById$(reminderId: string): Observable { return this.reminders$.pipe( - map(reminders => reminders.find(reminder => reminder.id === reminderId) || null), + map( + (reminders) => reminders.find((reminder) => reminder.id === reminderId) || null, + ), ); } getByRelatedId(relatedId: string): ReminderCopy | null { - const _foundReminder = this._reminders && this._reminders.find(reminder => reminder.relatedId === relatedId); - return !!_foundReminder - ? dirtyDeepCopy(_foundReminder) - : null; + const _foundReminder = + this._reminders && + this._reminders.find((reminder) => reminder.relatedId === relatedId); + return !!_foundReminder ? dirtyDeepCopy(_foundReminder) : null; } - addReminder(type: ReminderType, relatedId: string, title: string, remindAt: number, recurringConfig?: RecurringConfig): string { + addReminder( + type: ReminderType, + relatedId: string, + title: string, + remindAt: number, + recurringConfig?: RecurringConfig, + ): string { const id = shortid(); if (this.getByRelatedId(relatedId)) { throw new Error('A reminder for this ' + type + ' already exists'); @@ -124,7 +135,7 @@ export class ReminderService { title, remindAt, type, - recurringConfig + recurringConfig, }); this._saveModel(this._reminders); return id; @@ -132,11 +143,11 @@ export class ReminderService { snooze(reminderId: string, snoozeTime: number) { const remindAt = new Date().getTime() + snoozeTime; - this.updateReminder(reminderId, {remindAt}); + this.updateReminder(reminderId, { remindAt }); } updateReminder(reminderId: string, reminderChanges: Partial) { - const i = this._reminders.findIndex(reminder => reminder.id === reminderId); + const i = this._reminders.findIndex((reminder) => reminder.id === reminderId); if (i > -1) { // TODO find out why we need to do this this._reminders = dirtyDeepCopy(this._reminders); @@ -146,7 +157,7 @@ export class ReminderService { } removeReminder(reminderIdToRemove: string) { - const i = this._reminders.findIndex(reminder => reminder.id === reminderIdToRemove); + const i = this._reminders.findIndex((reminder) => reminder.id === reminderIdToRemove); if (i > -1) { // TODO find out why we need to do this @@ -159,16 +170,20 @@ export class ReminderService { } removeReminderByRelatedIdIfSet(relatedId: string) { - const reminder = this._reminders.find(reminderIN => reminderIN.relatedId === relatedId); + const reminder = this._reminders.find( + (reminderIN) => reminderIN.relatedId === relatedId, + ); if (reminder) { this.removeReminder(reminder.id); } } removeRemindersByWorkContextId(workContextId: string) { - const reminders = this._reminders.filter(reminderIN => reminderIN.workContextId === workContextId); + const reminders = this._reminders.filter( + (reminderIN) => reminderIN.workContextId === workContextId, + ); if (reminders && reminders.length) { - reminders.forEach(reminder => { + reminders.forEach((reminder) => { this.removeReminder(reminder.id); }); } @@ -176,19 +191,21 @@ export class ReminderService { private async _onReminderActivated(msg: MessageEvent) { const reminders = msg.data as Reminder[]; - const remindersWithData: Reminder[] = await Promise.all(reminders.map(async (reminder) => { - const relatedModel = await this._getRelatedDataForReminder(reminder); - // console.log('RelatedModel for Reminder', relatedModel); - // only show when not currently syncing and related model still exists - if (!relatedModel) { - devError('No Reminder Related Data found, removing reminder...'); - this.removeReminder(reminder.id); - return null; - } else { - return reminder; - } - })) as Reminder []; - const finalReminders = remindersWithData.filter(reminder => !!reminder); + const remindersWithData: Reminder[] = (await Promise.all( + reminders.map(async (reminder) => { + const relatedModel = await this._getRelatedDataForReminder(reminder); + // console.log('RelatedModel for Reminder', relatedModel); + // only show when not currently syncing and related model still exists + if (!relatedModel) { + devError('No Reminder Related Data found, removing reminder...'); + this.removeReminder(reminder.id); + return null; + } else { + return reminder; + } + }), + )) as Reminder[]; + const finalReminders = remindersWithData.filter((reminder) => !!reminder); if (finalReminders.length > 0) { this._onRemindersActive$.next(finalReminders); @@ -196,14 +213,12 @@ export class ReminderService { } private async _loadFromDatabase(): Promise { - return migrateReminders( - await this._persistenceService.reminders.loadState() || [] - ); + return migrateReminders((await this._persistenceService.reminders.loadState()) || []); } private _saveModel(reminders: Reminder[]) { this._updateRemindersInWorker(this._reminders); - this._persistenceService.reminders.saveState(reminders, {isSyncModelChange: true}); + this._persistenceService.reminders.saveState(reminders, { isSyncModelChange: true }); this._reminders$.next(this._reminders); } @@ -213,13 +228,16 @@ export class ReminderService { private _handleError(err: any) { console.error(err); - this._snackService.open({type: 'ERROR', msg: T.F.REMINDER.S_REMINDER_ERR}); + this._snackService.open({ type: 'ERROR', msg: T.F.REMINDER.S_REMINDER_ERR }); } private async _getRelatedDataForReminder(reminder: Reminder): Promise { switch (reminder.type) { case 'NOTE': - return await this._noteService.getByIdFromEverywhere(reminder.relatedId, reminder.workContextId); + return await this._noteService.getByIdFromEverywhere( + reminder.relatedId, + reminder.workContextId, + ); case 'TASK': // NOTE: remember we don't want archive tasks to pop up here return await this._taskService.getByIdOnce$(reminder.relatedId).toPromise(); diff --git a/src/app/features/reminder/reminder.worker.ts b/src/app/features/reminder/reminder.worker.ts index 26f4c8908..927fe7347 100644 --- a/src/app/features/reminder/reminder.worker.ts +++ b/src/app/features/reminder/reminder.worker.ts @@ -6,7 +6,7 @@ import { lazySetInterval } from '../../../../electron/lazy-set-interval'; const CHECK_INTERVAL_DURATION = 10000; let cancelCheckInterval: (() => void) | undefined; -addEventListener('message', ({data}) => { +addEventListener('message', ({ data }) => { // console.log('REMINDER WORKER', data); reInitCheckInterval(data); }); @@ -25,10 +25,11 @@ const reInitCheckInterval = (reminders: ReminderCopy[]) => { if (dueReminders.length) { const oldest = dueReminders[0]; - const remindersToSend = (oldest.type === 'TASK') - ? dueReminders.filter(r => r.type === 'TASK') - // NOTE: for notes we just send the oldest due reminder - : [oldest]; + const remindersToSend = + oldest.type === 'TASK' + ? dueReminders.filter((r) => r.type === 'TASK') + : // NOTE: for notes we just send the oldest due reminder + [oldest]; postMessage(remindersToSend); console.log('Worker postMessage', remindersToSend); @@ -39,6 +40,6 @@ const reInitCheckInterval = (reminders: ReminderCopy[]) => { const getDueReminders = (reminders: ReminderCopy[]): ReminderCopy[] => { const now = Date.now(); return reminders - .filter(reminder => (reminder.remindAt < now)) + .filter((reminder) => reminder.remindAt < now) .sort((a, b) => a.remindAt - b.remindAt); }; diff --git a/src/app/features/simple-counter/dialog-simple-counter-edit/dialog-simple-counter-edit.component.ts b/src/app/features/simple-counter/dialog-simple-counter-edit/dialog-simple-counter-edit.component.ts index 561ae0ea2..c416f6315 100644 --- a/src/app/features/simple-counter/dialog-simple-counter-edit/dialog-simple-counter-edit.component.ts +++ b/src/app/features/simple-counter/dialog-simple-counter-edit/dialog-simple-counter-edit.component.ts @@ -9,7 +9,7 @@ import { getWorklogStr } from '../../../util/get-work-log-str'; selector: 'dialog-simple-counter-edit', templateUrl: './dialog-simple-counter-edit.component.html', styleUrls: ['./dialog-simple-counter-edit.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogSimpleCounterEditComponent { T: typeof T = T; @@ -22,8 +22,7 @@ export class DialogSimpleCounterEditComponent { private _matDialogRef: MatDialogRef, private _simpleCounterService: SimpleCounterService, @Inject(MAT_DIALOG_DATA) public data: { simpleCounter: SimpleCounter }, - ) { - } + ) {} submit() { this._simpleCounterService.setCounterToday(this.data.simpleCounter.id, this.val); diff --git a/src/app/features/simple-counter/migrate-simple-counter-state.util.ts b/src/app/features/simple-counter/migrate-simple-counter-state.util.ts index 4d70a8b72..7571a9370 100644 --- a/src/app/features/simple-counter/migrate-simple-counter-state.util.ts +++ b/src/app/features/simple-counter/migrate-simple-counter-state.util.ts @@ -5,14 +5,20 @@ import { Dictionary } from '@ngrx/entity'; const MODEL_VERSION = 2; -export const migrateSimpleCounterState = (simpleCounterState: SimpleCounterState): SimpleCounterState => { +export const migrateSimpleCounterState = ( + simpleCounterState: SimpleCounterState, +): SimpleCounterState => { if (!isMigrateModel(simpleCounterState, MODEL_VERSION, 'SimpleCounter')) { return simpleCounterState; } - const simpleCounterEntities: Dictionary = {...simpleCounterState.entities}; + const simpleCounterEntities: Dictionary = { + ...simpleCounterState.entities, + }; Object.keys(simpleCounterEntities).forEach((key) => { - simpleCounterEntities[key] = _migrateSimpleCounterEntity(simpleCounterEntities[key] as SimpleCounter); + simpleCounterEntities[key] = _migrateSimpleCounterEntity( + simpleCounterEntities[key] as SimpleCounter, + ); }); // Update model version after all migrations ran successfully @@ -26,7 +32,7 @@ export const migrateSimpleCounterState = (simpleCounterState: SimpleCounterState const _migrateSimpleCounterEntity = (simpleCounter: SimpleCounter): SimpleCounter => { if (!simpleCounter.hasOwnProperty('countOnDay')) { - const cpy = {...simpleCounter}; + const cpy = { ...simpleCounter }; const countOnDay = (cpy as any).totalCountOnDay || {}; // delete unused diff --git a/src/app/features/simple-counter/simple-counter-button/simple-counter-button.component.ts b/src/app/features/simple-counter/simple-counter-button/simple-counter-button.component.ts index b120e6cc2..a61cf5fe5 100644 --- a/src/app/features/simple-counter/simple-counter-button/simple-counter-button.component.ts +++ b/src/app/features/simple-counter/simple-counter-button/simple-counter-button.component.ts @@ -10,7 +10,7 @@ import { getWorklogStr } from '../../../util/get-work-log-str'; selector: 'simple-counter-button', templateUrl: './simple-counter-button.component.html', styleUrls: ['./simple-counter-button.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SimpleCounterButtonComponent { T: typeof T = T; @@ -22,8 +22,7 @@ export class SimpleCounterButtonComponent { constructor( private _simpleCounterService: SimpleCounterService, private _matDialog: MatDialog, - ) { - } + ) {} toggleStopwatch() { if (!this.simpleCounter) { @@ -54,7 +53,7 @@ export class SimpleCounterButtonComponent { this._matDialog.open(DialogSimpleCounterEditComponent, { restoreFocus: true, data: { - simpleCounter: this.simpleCounter + simpleCounter: this.simpleCounter, }, }); } diff --git a/src/app/features/simple-counter/simple-counter-cfg/simple-counter-cfg.component.ts b/src/app/features/simple-counter/simple-counter-cfg/simple-counter-cfg.component.ts index cabb3c5fe..c852c413c 100644 --- a/src/app/features/simple-counter/simple-counter-cfg/simple-counter-cfg.component.ts +++ b/src/app/features/simple-counter/simple-counter-cfg/simple-counter-cfg.component.ts @@ -1,5 +1,15 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; -import { ConfigFormSection, GlobalConfigSectionKey } from '../../config/global-config.model'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnDestroy, + Output, +} from '@angular/core'; +import { + ConfigFormSection, + GlobalConfigSectionKey, +} from '../../config/global-config.model'; import { ProjectCfgFormKey } from '../../project/project.model'; import { SimpleCounterConfig } from '../simple-counter.model'; import { FormlyFormOptions } from '@ngx-formly/core'; @@ -15,19 +25,22 @@ import { DialogConfirmComponent } from '../../../ui/dialog-confirm/dialog-confir selector: 'simple-counter-cfg', templateUrl: './simple-counter-cfg.component.html', styleUrls: ['./simple-counter-cfg.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SimpleCounterCfgComponent implements OnDestroy { @Input() section?: ConfigFormSection; @Input() cfg?: SimpleCounterConfig; - @Output() save: EventEmitter<{ sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; config: any }> = new EventEmitter(); + @Output() save: EventEmitter<{ + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; + config: any; + }> = new EventEmitter(); T: typeof T = T; form: FormGroup = new FormGroup({}); options: FormlyFormOptions = {}; simpleCounterCfg$: Observable = this.simpleCounterService.simpleCountersUpdatedOnCfgChange$.pipe( - map(items => ({ + map((items) => ({ counters: items, })), ); @@ -39,13 +52,14 @@ export class SimpleCounterCfgComponent implements OnDestroy { constructor( public readonly simpleCounterService: SimpleCounterService, - private readonly _matDialog: MatDialog, - // private readonly _cd: ChangeDetectorRef, + private readonly _matDialog: MatDialog, // private readonly _cd: ChangeDetectorRef, ) { - this._subs.add(this.simpleCounterCfg$.subscribe(v => { - this.editModel = v; - this._inModelCopy = v; - })); + this._subs.add( + this.simpleCounterCfg$.subscribe((v) => { + this.editModel = v; + this._inModelCopy = v; + }), + ); } ngOnDestroy(): void { @@ -54,7 +68,7 @@ export class SimpleCounterCfgComponent implements OnDestroy { onModelChange(changes: SimpleCounterConfig) { // NOTE: it's important to create a new object, otherwise only 1 update happens - this.editModel = {...changes}; + this.editModel = { ...changes }; } submit() { @@ -62,11 +76,11 @@ export class SimpleCounterCfgComponent implements OnDestroy { throw new Error('Model not ready'); } - const oldIds = this._inModelCopy.counters.map(item => item.id); - const newItemIds = this.editModel.counters.map(item => item.id); + const oldIds = this._inModelCopy.counters.map((item) => item.id); + const newItemIds = this.editModel.counters.map((item) => item.id); - if (oldIds.find(id => !newItemIds.includes(id))) { - this._confirmDeletion$().subscribe(isConfirm => { + if (oldIds.find((id) => !newItemIds.includes(id))) { + this._confirmDeletion$().subscribe((isConfirm) => { if (!this._inModelCopy || !this.editModel) { throw new Error('Model not ready'); } @@ -80,12 +94,14 @@ export class SimpleCounterCfgComponent implements OnDestroy { } private _confirmDeletion$(): Observable { - return this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - message: T.F.SIMPLE_COUNTER.D_CONFIRM_REMOVE.MSG, - okTxt: T.F.SIMPLE_COUNTER.D_CONFIRM_REMOVE.OK, - } - }).afterClosed(); + return this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + message: T.F.SIMPLE_COUNTER.D_CONFIRM_REMOVE.MSG, + okTxt: T.F.SIMPLE_COUNTER.D_CONFIRM_REMOVE.OK, + }, + }) + .afterClosed(); } } diff --git a/src/app/features/simple-counter/simple-counter.const.ts b/src/app/features/simple-counter/simple-counter.const.ts index acebdec7c..e85101392 100644 --- a/src/app/features/simple-counter/simple-counter.const.ts +++ b/src/app/features/simple-counter/simple-counter.const.ts @@ -37,7 +37,7 @@ export const DEFAULT_SIMPLE_COUNTERS: SimpleCounter[] = [ title: 'Coffee Counter', type: SimpleCounterType.ClickCounter, icon: 'free_breakfast', - } + }, ]; export const SIMPLE_COUNTER_TRIGGER_ACTIONS: string[] = [ diff --git a/src/app/features/simple-counter/simple-counter.module.ts b/src/app/features/simple-counter/simple-counter.module.ts index 0f1786b7d..3f00a4d0a 100644 --- a/src/app/features/simple-counter/simple-counter.module.ts +++ b/src/app/features/simple-counter/simple-counter.module.ts @@ -3,7 +3,10 @@ import { CommonModule } from '@angular/common'; import { SimpleCounterButtonComponent } from './simple-counter-button/simple-counter-button.component'; import { UiModule } from '../../ui/ui.module'; import { StoreModule } from '@ngrx/store'; -import { SIMPLE_COUNTER_FEATURE_NAME, simpleCounterReducer } from './store/simple-counter.reducer'; +import { + SIMPLE_COUNTER_FEATURE_NAME, + simpleCounterReducer, +} from './store/simple-counter.reducer'; import { EffectsModule } from '@ngrx/effects'; import { SimpleCounterEffects } from './store/simple-counter.effects'; import { SimpleCounterCfgComponent } from './simple-counter-cfg/simple-counter-cfg.component'; @@ -24,10 +27,6 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms'; SimpleCounterCfgComponent, DialogSimpleCounterEditComponent, ], - exports: [ - SimpleCounterButtonComponent, - SimpleCounterCfgComponent, - ], + exports: [SimpleCounterButtonComponent, SimpleCounterCfgComponent], }) -export class SimpleCounterModule { -} +export class SimpleCounterModule {} diff --git a/src/app/features/simple-counter/simple-counter.service.ts b/src/app/features/simple-counter/simple-counter.service.ts index 2a11c505a..26e82fc63 100644 --- a/src/app/features/simple-counter/simple-counter.service.ts +++ b/src/app/features/simple-counter/simple-counter.service.ts @@ -14,16 +14,27 @@ import { upsertSimpleCounter, } from './store/simple-counter.actions'; import { Observable } from 'rxjs'; -import { SimpleCounter, SimpleCounterCfgFields, SimpleCounterState } from './simple-counter.model'; +import { + SimpleCounter, + SimpleCounterCfgFields, + SimpleCounterState, +} from './simple-counter.model'; import * as shortid from 'shortid'; import { distinctUntilChanged, map } from 'rxjs/operators'; const FIELDS_TO_COMPARE: (keyof SimpleCounterCfgFields)[] = [ - 'id', 'title', 'isEnabled', 'icon', 'iconOn', 'type', 'triggerOnActions', 'triggerOffActions' + 'id', + 'title', + 'isEnabled', + 'icon', + 'iconOn', + 'type', + 'triggerOnActions', + 'triggerOffActions', ]; const isEqualSimpleCounterCfg = (a: any, b: any): boolean => { - if ((Array.isArray(a) && Array.isArray(b))) { + if (Array.isArray(a) && Array.isArray(b)) { if (a.length !== b.length) { return false; } @@ -48,41 +59,40 @@ const isEqualSimpleCounterCfg = (a: any, b: any): boolean => { providedIn: 'root', }) export class SimpleCounterService { - simpleCounters$: Observable = this._store$.pipe(select(selectAllSimpleCounters)); - simpleCountersUpdatedOnCfgChange$: Observable = this.simpleCounters$.pipe( - distinctUntilChanged(isEqualSimpleCounterCfg), + simpleCounters$: Observable = this._store$.pipe( + select(selectAllSimpleCounters), ); + simpleCountersUpdatedOnCfgChange$: Observable< + SimpleCounter[] + > = this.simpleCounters$.pipe(distinctUntilChanged(isEqualSimpleCounterCfg)); - enabledSimpleCounters$: Observable = this._store$.pipe(select(selectAllSimpleCounters)).pipe( - map(items => items && items.filter(item => item.isEnabled)), - ); - enabledSimpleCountersUpdatedOnCfgChange$: Observable = this.enabledSimpleCounters$.pipe( - distinctUntilChanged(isEqualSimpleCounterCfg), - ); + enabledSimpleCounters$: Observable = this._store$ + .pipe(select(selectAllSimpleCounters)) + .pipe(map((items) => items && items.filter((item) => item.isEnabled))); + enabledSimpleCountersUpdatedOnCfgChange$: Observable< + SimpleCounter[] + > = this.enabledSimpleCounters$.pipe(distinctUntilChanged(isEqualSimpleCounterCfg)); - enabledAndToggledSimpleCounters$: Observable = this._store$.pipe(select(selectAllSimpleCounters)).pipe( - map(items => items && items.filter(item => item.isEnabled && item.isOn)), - ); + enabledAndToggledSimpleCounters$: Observable = this._store$ + .pipe(select(selectAllSimpleCounters)) + .pipe(map((items) => items && items.filter((item) => item.isEnabled && item.isOn))); - constructor( - private _store$: Store, - ) { - } + constructor(private _store$: Store) {} updateAll(items: SimpleCounter[]) { - this._store$.dispatch(updateAllSimpleCounters({items})); + this._store$.dispatch(updateAllSimpleCounters({ items })); } setCounterToday(id: string, newVal: number) { - this._store$.dispatch(setSimpleCounterCounterToday({id, newVal})); + this._store$.dispatch(setSimpleCounterCounterToday({ id, newVal })); } increaseCounterToday(id: string, increaseBy: number) { - this._store$.dispatch(increaseSimpleCounterCounterToday({id, increaseBy})); + this._store$.dispatch(increaseSimpleCounterCounterToday({ id, increaseBy })); } toggleCounter(id: string) { - this._store$.dispatch(toggleSimpleCounterCounter({id})); + this._store$.dispatch(toggleSimpleCounterCounter({ id })); } turnOffAll() { @@ -90,27 +100,29 @@ export class SimpleCounterService { } addSimpleCounter(simpleCounter: SimpleCounter) { - this._store$.dispatch(addSimpleCounter({ - simpleCounter: { - ...simpleCounter, - id: shortid() - } - })); + this._store$.dispatch( + addSimpleCounter({ + simpleCounter: { + ...simpleCounter, + id: shortid(), + }, + }), + ); } deleteSimpleCounter(id: string) { - this._store$.dispatch(deleteSimpleCounter({id})); + this._store$.dispatch(deleteSimpleCounter({ id })); } deleteSimpleCounters(ids: string[]) { - this._store$.dispatch(deleteSimpleCounters({ids})); + this._store$.dispatch(deleteSimpleCounters({ ids })); } updateSimpleCounter(id: string, changes: Partial) { - this._store$.dispatch(updateSimpleCounter({simpleCounter: {id, changes}})); + this._store$.dispatch(updateSimpleCounter({ simpleCounter: { id, changes } })); } upsertSimpleCounter(simpleCounter: SimpleCounter) { - this._store$.dispatch(upsertSimpleCounter({simpleCounter})); + this._store$.dispatch(upsertSimpleCounter({ simpleCounter })); } } diff --git a/src/app/features/simple-counter/store/simple-counter.actions.ts b/src/app/features/simple-counter/store/simple-counter.actions.ts index 3f668bc02..1a842f477 100644 --- a/src/app/features/simple-counter/store/simple-counter.actions.ts +++ b/src/app/features/simple-counter/store/simple-counter.actions.ts @@ -60,4 +60,3 @@ export const setSimpleCounterCounterOn = createAction( '[SimpleCounter] Set SimpleCounter Counter On', props<{ id: string }>(), ); - 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 4c1365e0f..1133345ef 100644 --- a/src/app/features/simple-counter/store/simple-counter.effects.ts +++ b/src/app/features/simple-counter/store/simple-counter.effects.ts @@ -12,9 +12,19 @@ import { setSimpleCounterCounterToday, updateAllSimpleCounters, updateSimpleCounter, - upsertSimpleCounter + upsertSimpleCounter, } from './simple-counter.actions'; -import { delay, filter, map, mapTo, mergeMap, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; +import { + delay, + filter, + map, + mapTo, + mergeMap, + switchMap, + take, + tap, + withLatestFrom, +} from 'rxjs/operators'; import { selectSimpleCounterFeatureState } from './simple-counter.reducer'; import { SimpleCounterState, SimpleCounterType } from '../simple-counter.model'; import { TimeTrackingService } from '../../time-tracking/time-tracking.service'; @@ -28,97 +38,119 @@ import { ImexMetaService } from '../../../imex/imex-meta/imex-meta.service'; @Injectable() export class SimpleCounterEffects { + updateSimpleCountersStorage$: Observable = createEffect( + () => + this._actions$.pipe( + ofType( + updateAllSimpleCounters, + setSimpleCounterCounterToday, + increaseSimpleCounterCounterToday, + setSimpleCounterCounterOn, + setSimpleCounterCounterOff, + // toggleSimpleCounterCounter, - updateSimpleCountersStorage$: Observable = createEffect(() => this._actions$.pipe( - ofType( - updateAllSimpleCounters, - setSimpleCounterCounterToday, - increaseSimpleCounterCounterToday, - setSimpleCounterCounterOn, - setSimpleCounterCounterOff, - // toggleSimpleCounterCounter, + // currently not used + addSimpleCounter, + updateSimpleCounter, + upsertSimpleCounter, + deleteSimpleCounter, + deleteSimpleCounters, + ), + withLatestFrom(this._store$.pipe(select(selectSimpleCounterFeatureState))), + tap(([, featureState]) => this._saveToLs(featureState)), + ), + { dispatch: false }, + ); - // currently not used - addSimpleCounter, - updateSimpleCounter, - upsertSimpleCounter, - deleteSimpleCounter, - deleteSimpleCounters, - ), - withLatestFrom( - this._store$.pipe(select(selectSimpleCounterFeatureState)), - ), - tap(([, featureState]) => this._saveToLs(featureState)), - ), {dispatch: false}); - - checkTimedCounters$: Observable = createEffect(() => this._simpleCounterService.enabledAndToggledSimpleCounters$.pipe( - switchMap((items) => (items && items.length) - ? this._timeTrackingService.tick$.pipe( - map(tick => ({tick, items})) - ) - : EMPTY - ), - mergeMap(({items, tick}) => { - return items.map( - (item) => increaseSimpleCounterCounterToday({id: item.id, increaseBy: tick.duration}) + checkTimedCounters$: Observable = createEffect(() => + this._simpleCounterService.enabledAndToggledSimpleCounters$.pipe( + switchMap((items) => + items && items.length + ? this._timeTrackingService.tick$.pipe(map((tick) => ({ tick, items }))) + : EMPTY, + ), + mergeMap(({ items, tick }) => { + return items.map((item) => + increaseSimpleCounterCounterToday({ id: item.id, increaseBy: tick.duration }), ); - } + }), ), - )); + ); - actionListeners$: Observable = createEffect(() => this._simpleCounterService.enabledSimpleCountersUpdatedOnCfgChange$.pipe( - map(items => items && items.filter(item => - (item.triggerOnActions && item.triggerOnActions.length) - || (item.triggerOffActions && item.triggerOffActions.length) - )), - switchMap((items) => (items && items.length) - ? this._actions$.pipe( - ofType(...SIMPLE_COUNTER_TRIGGER_ACTIONS), - map(action => ({action, items})) - ) - : EMPTY - ), - switchMap(({items, action}) => action.type === loadAllData.type - // NOTE: we delay because otherwise we might write into db while importing data - ? this._imexMetaService.isDataImportInProgress$.pipe( - filter(isInProgress => !isInProgress), - take(1), - delay(3000), - mapTo(({items, action})), - ) - : of({items, action}) - ), - mergeMap(({items, action}) => { - const clickCounter = items.filter(item => item.type === SimpleCounterType.ClickCounter); - const stopWatch = items.filter(item => item.type === SimpleCounterType.StopWatch); + actionListeners$: Observable = createEffect(() => + this._simpleCounterService.enabledSimpleCountersUpdatedOnCfgChange$.pipe( + map( + (items) => + items && + items.filter( + (item) => + (item.triggerOnActions && item.triggerOnActions.length) || + (item.triggerOffActions && item.triggerOffActions.length), + ), + ), + switchMap((items) => + items && items.length + ? this._actions$.pipe( + ofType(...SIMPLE_COUNTER_TRIGGER_ACTIONS), + map((action) => ({ action, items })), + ) + : EMPTY, + ), + switchMap(({ items, action }) => + action.type === loadAllData.type + ? // NOTE: we delay because otherwise we might write into db while importing data + this._imexMetaService.isDataImportInProgress$.pipe( + filter((isInProgress) => !isInProgress), + take(1), + delay(3000), + mapTo({ items, action }), + ) + : of({ items, action }), + ), + mergeMap(({ items, action }) => { + const clickCounter = items.filter( + (item) => item.type === SimpleCounterType.ClickCounter, + ); + const stopWatch = items.filter( + (item) => item.type === SimpleCounterType.StopWatch, + ); const startItems = stopWatch.filter( - item => item.triggerOnActions && item.triggerOnActions.includes(action.type) + (item) => item.triggerOnActions && item.triggerOnActions.includes(action.type), ); const counterUpItems = clickCounter.filter( - item => item.triggerOnActions && item.triggerOnActions.includes(action.type) + (item) => item.triggerOnActions && item.triggerOnActions.includes(action.type), ); const stopItems = stopWatch.filter( - item => item.triggerOffActions && item.triggerOffActions.includes(action.type) + (item) => + item.triggerOffActions && item.triggerOffActions.includes(action.type), ); return [ - ...startItems.map(item => setSimpleCounterCounterOn({id: item.id})), - ...stopItems.map(item => setSimpleCounterCounterOff({id: item.id})), - ...counterUpItems.map(item => increaseSimpleCounterCounterToday({id: item.id, increaseBy: 1})) + ...startItems.map((item) => setSimpleCounterCounterOn({ id: item.id })), + ...stopItems.map((item) => setSimpleCounterCounterOff({ id: item.id })), + ...counterUpItems.map((item) => + increaseSimpleCounterCounterToday({ id: item.id, increaseBy: 1 }), + ), ]; - } + }), ), - )); + ); - successSnack$: Observable = createEffect(() => this._actions$.pipe( - ofType(updateAllSimpleCounters), - tap(() => this._snackService.open({ - type: 'SUCCESS', - msg: T.F.CONFIG.S.UPDATE_SECTION, - translateParams: {sectionKey: 'Simple Counters'} - })) - ), {dispatch: false}); + successSnack$: Observable = createEffect( + () => + this._actions$.pipe( + ofType(updateAllSimpleCounters), + tap(() => + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.CONFIG.S.UPDATE_SECTION, + translateParams: { sectionKey: 'Simple Counters' }, + }), + ), + ), + { dispatch: false }, + ); constructor( private _actions$: Actions, @@ -128,10 +160,11 @@ export class SimpleCounterEffects { private _simpleCounterService: SimpleCounterService, private _imexMetaService: ImexMetaService, private _snackService: SnackService, - ) { - } + ) {} private _saveToLs(simpleCounterState: SimpleCounterState) { - this._persistenceService.simpleCounter.saveState(simpleCounterState, {isSyncModelChange: true}); + this._persistenceService.simpleCounter.saveState(simpleCounterState, { + isSyncModelChange: true, + }); } } diff --git a/src/app/features/simple-counter/store/simple-counter.reducer.ts b/src/app/features/simple-counter/store/simple-counter.reducer.ts index 177b78a0e..10a9bbcfb 100644 --- a/src/app/features/simple-counter/store/simple-counter.reducer.ts +++ b/src/app/features/simple-counter/store/simple-counter.reducer.ts @@ -1,7 +1,17 @@ import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; import * as simpleCounterActions from './simple-counter.actions'; -import { Action, createFeatureSelector, createReducer, createSelector, on } from '@ngrx/store'; -import { SimpleCounter, SimpleCounterState, SimpleCounterType } from '../simple-counter.model'; +import { + Action, + createFeatureSelector, + createReducer, + createSelector, + on, +} from '@ngrx/store'; +import { + SimpleCounter, + SimpleCounterState, + SimpleCounterType, +} from '../simple-counter.model'; import { DEFAULT_SIMPLE_COUNTERS } from '../simple-counter.const'; import { arrayToDictionary } from '../../../util/array-to-dictionary'; import { loadAllData } from '../../../root-store/meta/load-all-data.action'; @@ -13,45 +23,53 @@ import { Update } from '@ngrx/entity/src/models'; export const SIMPLE_COUNTER_FEATURE_NAME = 'simpleCounter'; export const adapter: EntityAdapter = createEntityAdapter(); -export const selectSimpleCounterFeatureState = createFeatureSelector(SIMPLE_COUNTER_FEATURE_NAME); -export const {selectIds, selectEntities, selectAll, selectTotal} = adapter.getSelectors(); -export const selectAllSimpleCounters = createSelector(selectSimpleCounterFeatureState, selectAll); +export const selectSimpleCounterFeatureState = createFeatureSelector( + SIMPLE_COUNTER_FEATURE_NAME, +); +export const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = adapter.getSelectors(); +export const selectAllSimpleCounters = createSelector( + selectSimpleCounterFeatureState, + selectAll, +); export const selectSimpleCounterById = createSelector( selectSimpleCounterFeatureState, - (state: SimpleCounterState, props: { id: string }) => state.entities[props.id] + (state: SimpleCounterState, props: { id: string }) => state.entities[props.id], ); -export const initialSimpleCounterState: SimpleCounterState = adapter.getInitialState({ - ids: DEFAULT_SIMPLE_COUNTERS.map(value => value.id), - entities: arrayToDictionary(DEFAULT_SIMPLE_COUNTERS), -}); +export const initialSimpleCounterState: SimpleCounterState = adapter.getInitialState( + { + ids: DEFAULT_SIMPLE_COUNTERS.map((value) => value.id), + entities: arrayToDictionary(DEFAULT_SIMPLE_COUNTERS), + }, +); const disableIsOnForAll = (state: SimpleCounterState): SimpleCounterState => { return { ...state, - entities: updateAllInDictionary(state.entities, {isOn: false}) + entities: updateAllInDictionary(state.entities, { isOn: false }), }; }; const _reducer = createReducer( initialSimpleCounterState, - on(loadAllData, (oldState, {appDataComplete}) => + on(loadAllData, (oldState, { appDataComplete }) => appDataComplete.simpleCounter ? { - // ...appDataComplete.simpleCounter, - ...migrateSimpleCounterState( - disableIsOnForAll( - appDataComplete.simpleCounter - ) - ), - } - : oldState + // ...appDataComplete.simpleCounter, + ...migrateSimpleCounterState(disableIsOnForAll(appDataComplete.simpleCounter)), + } + : oldState, ), - on(simpleCounterActions.updateAllSimpleCounters, (state, {items}) => { - const allNewItemIds = items.map(item => item.id); - const itemIdsToRemove = state.ids.filter(id => !allNewItemIds.includes(id)); + on(simpleCounterActions.updateAllSimpleCounters, (state, { items }) => { + const allNewItemIds = items.map((item) => item.id); + const itemIdsToRemove = state.ids.filter((id) => !allNewItemIds.includes(id)); let newState = state; newState = adapter.removeMany(itemIdsToRemove, newState); @@ -59,66 +77,106 @@ const _reducer = createReducer( return newState; }), - on(simpleCounterActions.setSimpleCounterCounterToday, (state, {id, newVal}) => adapter.updateOne({ - id, - changes: { - countOnDay: { - ...(state.entities[id] as SimpleCounter).countOnDay, - [getWorklogStr()]: newVal, - } - } - }, state)), + on(simpleCounterActions.setSimpleCounterCounterToday, (state, { id, newVal }) => + adapter.updateOne( + { + id, + changes: { + countOnDay: { + ...(state.entities[id] as SimpleCounter).countOnDay, + [getWorklogStr()]: newVal, + }, + }, + }, + state, + ), + ), - on(simpleCounterActions.increaseSimpleCounterCounterToday, (state, {id, increaseBy}) => { - const todayStr = getWorklogStr(); - const oldEntity = state.entities[id] as SimpleCounter; - const currentTotalCount = oldEntity.countOnDay || {}; - const currentVal = currentTotalCount[todayStr] || 0; - const newValForToday = currentVal + increaseBy; - return adapter.updateOne({ - id, - changes: { - countOnDay: { - ...currentTotalCount, - [todayStr]: newValForToday, - } - } - }, state); - }), + on( + simpleCounterActions.increaseSimpleCounterCounterToday, + (state, { id, increaseBy }) => { + const todayStr = getWorklogStr(); + const oldEntity = state.entities[id] as SimpleCounter; + const currentTotalCount = oldEntity.countOnDay || {}; + const currentVal = currentTotalCount[todayStr] || 0; + const newValForToday = currentVal + increaseBy; + return adapter.updateOne( + { + id, + changes: { + countOnDay: { + ...currentTotalCount, + [todayStr]: newValForToday, + }, + }, + }, + state, + ); + }, + ), - on(simpleCounterActions.toggleSimpleCounterCounter, (state, {id}) => adapter.updateOne({ - id, - changes: {isOn: !(state.entities[id] as SimpleCounter).isOn} - }, state)), + on(simpleCounterActions.toggleSimpleCounterCounter, (state, { id }) => + adapter.updateOne( + { + id, + changes: { isOn: !(state.entities[id] as SimpleCounter).isOn }, + }, + state, + ), + ), - on(simpleCounterActions.setSimpleCounterCounterOn, (state, {id}) => adapter.updateOne({ - id, - changes: {isOn: true} - }, state)), + on(simpleCounterActions.setSimpleCounterCounterOn, (state, { id }) => + adapter.updateOne( + { + id, + changes: { isOn: true }, + }, + state, + ), + ), - on(simpleCounterActions.setSimpleCounterCounterOff, (state, {id}) => adapter.updateOne({ - id, - changes: {isOn: false} - }, state)), + on(simpleCounterActions.setSimpleCounterCounterOff, (state, { id }) => + adapter.updateOne( + { + id, + changes: { isOn: false }, + }, + state, + ), + ), - on(simpleCounterActions.addSimpleCounter, (state, {simpleCounter}) => adapter.addOne(simpleCounter, state)), + on(simpleCounterActions.addSimpleCounter, (state, { simpleCounter }) => + adapter.addOne(simpleCounter, state), + ), - on(simpleCounterActions.updateSimpleCounter, (state, {simpleCounter}) => adapter.updateOne(simpleCounter, state)), + on(simpleCounterActions.updateSimpleCounter, (state, { simpleCounter }) => + adapter.updateOne(simpleCounter, state), + ), - on(simpleCounterActions.upsertSimpleCounter, (state, {simpleCounter}) => adapter.upsertOne(simpleCounter, state)), + on(simpleCounterActions.upsertSimpleCounter, (state, { simpleCounter }) => + adapter.upsertOne(simpleCounter, state), + ), - on(simpleCounterActions.deleteSimpleCounter, (state, {id}) => adapter.removeOne(id, state)), + on(simpleCounterActions.deleteSimpleCounter, (state, { id }) => + adapter.removeOne(id, state), + ), - on(simpleCounterActions.deleteSimpleCounters, (state, {ids}) => adapter.removeMany(ids, state)), + on(simpleCounterActions.deleteSimpleCounters, (state, { ids }) => + adapter.removeMany(ids, state), + ), on(simpleCounterActions.turnOffAllSimpleCounterCounters, (state) => { const updates: Update[] = state.ids - .filter(id => state.entities[id]?.type === SimpleCounterType.StopWatch && state.entities[id]?.isOn) + .filter( + (id) => + state.entities[id]?.type === SimpleCounterType.StopWatch && + state.entities[id]?.isOn, + ) .map((id: string) => ({ id, changes: { isOn: false, - } + }, })); return adapter.updateMany(updates, state); }), @@ -130,5 +188,3 @@ export function simpleCounterReducer( ): SimpleCounterState { return _reducer(state, action); } - - diff --git a/src/app/features/tag/dialog-edit-tags/dialog-edit-tags-for-task.component.ts b/src/app/features/tag/dialog-edit-tags/dialog-edit-tags-for-task.component.ts index 0c33eae3c..5489f9f8c 100644 --- a/src/app/features/tag/dialog-edit-tags/dialog-edit-tags-for-task.component.ts +++ b/src/app/features/tag/dialog-edit-tags/dialog-edit-tags-for-task.component.ts @@ -14,7 +14,7 @@ import { Tag } from '../tag.model'; selector: 'dialog-edit-tags', templateUrl: './dialog-edit-tags-for-task.component.html', styleUrls: ['./dialog-edit-tags-for-task.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogEditTagsForTaskComponent implements OnDestroy { T: typeof T = T; @@ -33,10 +33,12 @@ export class DialogEditTagsForTaskComponent implements OnDestroy { private _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: DialogEditTagsForTaskPayload, ) { - this._subs.add(this.task$.subscribe(task => { - this.tagIds = task.tagIds; - this.task = task; - })); + this._subs.add( + this.task$.subscribe((task) => { + this.tagIds = task.tagIds; + this.task = task; + }), + ); } ngOnDestroy(): void { @@ -52,12 +54,12 @@ export class DialogEditTagsForTaskComponent implements OnDestroy { } addNewTag(title: string) { - const id = this._tagService.addTag({title}); + const id = this._tagService.addTag({ title }); this._updateTags(unique([...this.tagIds, id])); } removeTag(id: string) { - const updatedTagIds = this.tagIds.filter(tagId => tagId !== id); + const updatedTagIds = this.tagIds.filter((tagId) => tagId !== id); this._updateTags(updatedTagIds); } diff --git a/src/app/features/tag/migrate-tag-state.util.ts b/src/app/features/tag/migrate-tag-state.util.ts index db163f762..5733112b5 100644 --- a/src/app/features/tag/migrate-tag-state.util.ts +++ b/src/app/features/tag/migrate-tag-state.util.ts @@ -11,7 +11,7 @@ export const migrateTagState = (tagState: TagState): TagState => { return tagState; } - const tagEntities: Dictionary = ({...tagState.entities}); + const tagEntities: Dictionary = { ...tagState.entities }; Object.keys(tagEntities).forEach((key) => { if (key === TODAY_TAG.id) { tagEntities[key] = _addBackgroundImageForDarkTheme(tagEntities[key] as TagCopy); @@ -19,7 +19,7 @@ export const migrateTagState = (tagState: TagState): TagState => { // tagEntities[key] = _addNewIssueFields(tagEntities[key] as TagCopy); }); - return {...tagState, entities: tagEntities, [MODEL_VERSION_KEY]: MODEL_VERSION}; + return { ...tagState, entities: tagEntities, [MODEL_VERSION_KEY]: MODEL_VERSION }; }; const _addBackgroundImageForDarkTheme = (tag: Tag): Tag => { @@ -31,7 +31,7 @@ const _addBackgroundImageForDarkTheme = (tag: Tag): Tag => { theme: { ...tag.theme, backgroundImageDark: TODAY_TAG.theme.backgroundImageDark, - } + }, }; } }; diff --git a/src/app/features/tag/store/tag.actions.ts b/src/app/features/tag/store/tag.actions.ts index 4b2f23270..07b6e5e94 100644 --- a/src/app/features/tag/store/tag.actions.ts +++ b/src/app/features/tag/store/tag.actions.ts @@ -3,25 +3,13 @@ import { Update } from '@ngrx/entity'; import { Tag } from '../tag.model'; import { WorkContextAdvancedCfgKey } from '../../work-context/work-context.model'; -export const addTag = createAction( - '[Tag] Add Tag', - props<{ tag: Tag }>(), -); +export const addTag = createAction('[Tag] Add Tag', props<{ tag: Tag }>()); -export const updateTag = createAction( - '[Tag] Update Tag', - props<{ tag: Update }>(), -); +export const updateTag = createAction('[Tag] Update Tag', props<{ tag: Update }>()); -export const upsertTag = createAction( - '[Tag] Upsert Tag', - props<{ tag: Tag }>(), -); +export const upsertTag = createAction('[Tag] Upsert Tag', props<{ tag: Tag }>()); -export const deleteTag = createAction( - '[Tag] Delete Tag', - props<{ id: string }>(), -); +export const deleteTag = createAction('[Tag] Delete Tag', props<{ id: string }>()); export const deleteTags = createAction( '[Tag] Delete multiple Tags', diff --git a/src/app/features/tag/store/tag.effects.ts b/src/app/features/tag/store/tag.effects.ts index e0052227a..bb262bc33 100644 --- a/src/app/features/tag/store/tag.effects.ts +++ b/src/app/features/tag/store/tag.effects.ts @@ -15,18 +15,19 @@ import { updateTag, updateWorkEndForTag, updateWorkStartForTag, - upsertTag + upsertTag, } from './tag.actions'; import { AddTask, - AddTimeSpent, ConvertToMainTask, + AddTimeSpent, + ConvertToMainTask, DeleteMainTasks, DeleteTask, MoveToArchive, MoveToOtherProject, RemoveTagsForAllTasks, RestoreTask, - TaskActionTypes + TaskActionTypes, } from '../../tasks/store/task.actions'; import { TagService } from '../tag.service'; import { TaskService } from '../../tasks/task.service'; @@ -42,7 +43,7 @@ import { createEmptyEntity } from '../../../util/create-empty-entity'; import { moveTaskDownInTodayList, moveTaskInTodayList, - moveTaskUpInTodayList + moveTaskUpInTodayList, } from '../../work-context/store/work-context-meta.actions'; import { TaskRepeatCfgService } from '../../task-repeat-cfg/task-repeat-cfg.service'; @@ -51,155 +52,185 @@ export class TagEffects { saveToLs$: Observable = this._store$.pipe( select(selectTagFeatureState), take(1), - switchMap((tagState) => this._persistenceService.tag.saveState(tagState, {isSyncModelChange: true})), + switchMap((tagState) => + this._persistenceService.tag.saveState(tagState, { isSyncModelChange: true }), + ), ); - updateTagsStorage$: Observable = createEffect(() => this._actions$.pipe( - ofType( - addTag, - updateTag, - upsertTag, - deleteTag, - deleteTags, + updateTagsStorage$: Observable = createEffect( + () => + this._actions$.pipe( + ofType( + addTag, + updateTag, + upsertTag, + deleteTag, + deleteTags, - updateAdvancedConfigForTag, - updateWorkStartForTag, - updateWorkEndForTag, - addToBreakTimeForTag, + updateAdvancedConfigForTag, + updateWorkStartForTag, + updateWorkEndForTag, + addToBreakTimeForTag, - TaskActionTypes.DeleteMainTasks, - TaskActionTypes.UpdateTaskTags, - ), - switchMap(() => this.saveToLs$), - ), {dispatch: false}); - updateProjectStorageConditionalTask$: Observable = createEffect(() => this._actions$.pipe( - ofType( - TaskActionTypes.AddTask, - TaskActionTypes.ConvertToMainTask, - TaskActionTypes.DeleteTask, - TaskActionTypes.RestoreTask, - TaskActionTypes.MoveToArchive, - ), - switchMap((a: AddTask | DeleteTask | MoveToOtherProject | MoveToArchive | RestoreTask) => { - let isChange = false; - switch (a.type) { - case TaskActionTypes.AddTask: - isChange = !!(a as AddTask).payload.task.tagIds.length; - break; - case TaskActionTypes.DeleteTask: - isChange = !!(a as DeleteTask).payload.task.tagIds.length; - break; - case TaskActionTypes.MoveToArchive: - isChange = !!(a as MoveToArchive).payload.tasks.find(task => task.tagIds.length); - break; - case TaskActionTypes.RestoreTask: - isChange = !!(a as RestoreTask).payload.task.tagIds.length; - break; - case TaskActionTypes.ConvertToMainTask: - isChange = !!(a as ConvertToMainTask).payload.parentTagIds.length; - break; - } - return isChange - ? of(a) - : EMPTY; - }), - switchMap(() => this.saveToLs$), - ), {dispatch: false}); - updateTagsStorageConditional$: Observable = createEffect(() => this._actions$.pipe( - ofType( - moveTaskInTodayList, - moveTaskUpInTodayList, - moveTaskDownInTodayList, - ), - filter((p) => p.workContextType === WorkContextType.TAG), - switchMap(() => this.saveToLs$), - ), {dispatch: false}); - @Effect({dispatch: false}) + TaskActionTypes.DeleteMainTasks, + TaskActionTypes.UpdateTaskTags, + ), + switchMap(() => this.saveToLs$), + ), + { dispatch: false }, + ); + updateProjectStorageConditionalTask$: Observable = createEffect( + () => + this._actions$.pipe( + ofType( + TaskActionTypes.AddTask, + TaskActionTypes.ConvertToMainTask, + TaskActionTypes.DeleteTask, + TaskActionTypes.RestoreTask, + TaskActionTypes.MoveToArchive, + ), + switchMap( + ( + a: AddTask | DeleteTask | MoveToOtherProject | MoveToArchive | RestoreTask, + ) => { + let isChange = false; + switch (a.type) { + case TaskActionTypes.AddTask: + isChange = !!(a as AddTask).payload.task.tagIds.length; + break; + case TaskActionTypes.DeleteTask: + isChange = !!(a as DeleteTask).payload.task.tagIds.length; + break; + case TaskActionTypes.MoveToArchive: + isChange = !!(a as MoveToArchive).payload.tasks.find( + (task) => task.tagIds.length, + ); + break; + case TaskActionTypes.RestoreTask: + isChange = !!(a as RestoreTask).payload.task.tagIds.length; + break; + case TaskActionTypes.ConvertToMainTask: + isChange = !!(a as ConvertToMainTask).payload.parentTagIds.length; + break; + } + return isChange ? of(a) : EMPTY; + }, + ), + switchMap(() => this.saveToLs$), + ), + { dispatch: false }, + ); + updateTagsStorageConditional$: Observable = createEffect( + () => + this._actions$.pipe( + ofType(moveTaskInTodayList, moveTaskUpInTodayList, moveTaskDownInTodayList), + filter((p) => p.workContextType === WorkContextType.TAG), + switchMap(() => this.saveToLs$), + ), + { dispatch: false }, + ); + @Effect({ dispatch: false }) snackUpdateBaseSettings$: any = this._actions$.pipe( ofType(updateTag), - tap(() => this._snackService.open({ - type: 'SUCCESS', - msg: T.F.TAG.S.UPDATED, - })) + tap(() => + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.TAG.S.UPDATED, + }), + ), ); @Effect() updateWorkStart$: any = this._actions$.pipe( ofType(TaskActionTypes.AddTimeSpent), - concatMap(({payload}: AddTimeSpent) => payload.task.parentId - ? this._taskService.getByIdOnce$(payload.task.parentId).pipe(first()) - : of(payload.task) + concatMap(({ payload }: AddTimeSpent) => + payload.task.parentId + ? this._taskService.getByIdOnce$(payload.task.parentId).pipe(first()) + : of(payload.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[getWorklogStr()]) - .map((tag) => updateWorkStartForTag({ - id: tag.id, - date: getWorklogStr(), - newVal: Date.now(), - }) - ) + concatMap((tags: Tag[]) => + tags + // only if not assigned for day already + .filter((tag) => !tag.workStart[getWorklogStr()]) + .map((tag) => + updateWorkStartForTag({ + id: tag.id, + date: getWorklogStr(), + newVal: Date.now(), + }), + ), ), ); @Effect() updateWorkEnd$: Observable = this._actions$.pipe( ofType(TaskActionTypes.AddTimeSpent), - concatMap(({payload}: AddTimeSpent) => payload.task.parentId - ? this._taskService.getByIdOnce$(payload.task.parentId).pipe(first()) - : of(payload.task) + concatMap(({ payload }: AddTimeSpent) => + payload.task.parentId + ? this._taskService.getByIdOnce$(payload.task.parentId).pipe(first()) + : of(payload.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({ + concatMap((tags: Tag[]) => + tags.map((tag) => + updateWorkEndForTag({ id: tag.id, date: getWorklogStr(), newVal: Date.now(), - }) - ) + }), + ), ), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) deleteTagRelatedData: Observable = this._actions$.pipe( - ofType( - deleteTag, - deleteTags, - ), - map((a: any) => a.ids ? a.ids : [a.id]), + ofType(deleteTag, deleteTags), + map((a: any) => (a.ids ? a.ids : [a.id])), tap(async (tagIdsToRemove: string[]) => { // remove from all tasks this._taskService.removeTagsForAllTask(tagIdsToRemove); // remove from archive - await this._persistenceService.taskArchive.execAction(new RemoveTagsForAllTasks({tagIdsToRemove})); + await this._persistenceService.taskArchive.execAction( + new RemoveTagsForAllTasks({ tagIdsToRemove }), + ); - const isOrphanedParentTask = (t: Task) => !t.projectId && !t.tagIds.length && !t.parentId; + const isOrphanedParentTask = (t: Task) => + !t.projectId && !t.tagIds.length && !t.parentId; // remove orphaned const tasks = await this._taskService.allTasks$.pipe(first()).toPromise(); - const taskIdsToRemove: string[] = tasks.filter(isOrphanedParentTask).map(t => t.id); + const taskIdsToRemove: string[] = tasks + .filter(isOrphanedParentTask) + .map((t) => t.id); this._taskService.removeMultipleMainTasks(taskIdsToRemove); // remove orphaned for archive - const taskArchiveState: TaskArchive = await this._persistenceService.taskArchive.loadState() || createEmptyEntity(); + const taskArchiveState: TaskArchive = + (await this._persistenceService.taskArchive.loadState()) || createEmptyEntity(); const archiveTaskIdsToDelete = (taskArchiveState.ids as string[]).filter((id) => { const t = taskArchiveState.entities[id] as Task; return isOrphanedParentTask(t); }); - await this._persistenceService.taskArchive.execAction(new DeleteMainTasks({taskIds: archiveTaskIdsToDelete})); + await this._persistenceService.taskArchive.execAction( + new DeleteMainTasks({ taskIds: archiveTaskIdsToDelete }), + ); // remove from task repeat - const taskRepeatCfgs = await this._taskRepeatCfgService.taskRepeatCfgs$.pipe(take(1)).toPromise(); - taskRepeatCfgs.forEach(taskRepeatCfg => { - if (taskRepeatCfg.tagIds.some(r => tagIdsToRemove.indexOf(r) >= 0)) { - const tagIds = taskRepeatCfg.tagIds.filter(tagId => !tagIdsToRemove.includes(tagId)); + const taskRepeatCfgs = await this._taskRepeatCfgService.taskRepeatCfgs$ + .pipe(take(1)) + .toPromise(); + taskRepeatCfgs.forEach((taskRepeatCfg) => { + if (taskRepeatCfg.tagIds.some((r) => tagIdsToRemove.indexOf(r) >= 0)) { + const tagIds = taskRepeatCfg.tagIds.filter( + (tagId) => !tagIdsToRemove.includes(tagId), + ); if (tagIds.length === 0 && !taskRepeatCfg.projectId) { this._taskRepeatCfgService.deleteTaskRepeatCfg(taskRepeatCfg.id as string); } else { this._taskRepeatCfgService.updateTaskRepeatCfg(taskRepeatCfg.id as string, { - tagIds + tagIds, }); } } @@ -207,40 +238,43 @@ export class TagEffects { }), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) redirectIfCurrentTagIsDeleted: Observable = this._actions$.pipe( - ofType( - deleteTag, - deleteTags, - ), - map((a: any) => a.ids ? a.ids : [a.id]), + ofType(deleteTag, deleteTags), + map((a: any) => (a.ids ? a.ids : [a.id])), tap(async (tagIdsToRemove: string[]) => { - if (tagIdsToRemove.includes(this._workContextService.activeWorkContextId as string)) { + if ( + tagIdsToRemove.includes(this._workContextService.activeWorkContextId as string) + ) { this._router.navigate([`tag/${TODAY_TAG.id}/tasks`]); } }), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) cleanupNullTasksForTaskList: Observable = this._workContextService.activeWorkContextTypeAndId$.pipe( - filter(({activeType}) => activeType === WorkContextType.TAG), - switchMap(({activeType, activeId}) => this._workContextService.todaysTasks$.pipe( - take(1), - map((tasks) => ({ - allTasks: tasks, - nullTasks: tasks.filter(task => !task), - activeType, - activeId, - })), - )), - filter(({nullTasks}) => nullTasks.length > 0), + filter(({ activeType }) => activeType === WorkContextType.TAG), + switchMap(({ activeType, activeId }) => + this._workContextService.todaysTasks$.pipe( + take(1), + map((tasks) => ({ + allTasks: tasks, + nullTasks: tasks.filter((task) => !task), + activeType, + activeId, + })), + ), + ), + filter(({ nullTasks }) => nullTasks.length > 0), tap((arg) => console.log('Error INFO Today:', arg)), - tap(({activeId, allTasks}) => { - const allIds = allTasks.map(t => t && t.id); - const r = confirm('Nooo! We found some tasks with no data. It is strongly recommended to delete them to avoid further data corruption. Delete them now?'); + tap(({ activeId, allTasks }) => { + const allIds = allTasks.map((t) => t && t.id); + const r = confirm( + 'Nooo! We found some tasks with no data. It is strongly recommended to delete them to avoid further data corruption. Delete them now?', + ); if (r) { this._tagService.updateTag(activeId, { - taskIds: allIds.filter((id => !!id)), + taskIds: allIds.filter((id) => !!id), }); alert('Done!'); } @@ -257,6 +291,5 @@ export class TagEffects { private _taskService: TaskService, private _taskRepeatCfgService: TaskRepeatCfgService, private _router: Router, - ) { - } + ) {} } diff --git a/src/app/features/tag/store/tag.reducer.ts b/src/app/features/tag/store/tag.reducer.ts index 13585ce83..00dbbbe14 100644 --- a/src/app/features/tag/store/tag.reducer.ts +++ b/src/app/features/tag/store/tag.reducer.ts @@ -1,7 +1,13 @@ import { createEntityAdapter, EntityAdapter } from '@ngrx/entity'; import * as tagActions from './tag.actions'; import { Tag, TagState } from '../tag.model'; -import { Action, createFeatureSelector, createReducer, createSelector, on } from '@ngrx/store'; +import { + Action, + createFeatureSelector, + createReducer, + createSelector, + on, +} from '@ngrx/store'; import { AddTask, ConvertToMainTask, @@ -10,14 +16,14 @@ import { MoveToArchive, RestoreTask, TaskActionTypes, - UpdateTaskTags + UpdateTaskTags, } from '../../tasks/store/task.actions'; import { TODAY_TAG } from '../tag.const'; import { WorkContextType } from '../../work-context/work-context.model'; import { moveTaskDownInTodayList, moveTaskInTodayList, - moveTaskUpInTodayList + moveTaskUpInTodayList, } from '../../work-context/store/work-context-meta.actions'; import { moveTaskForWorkContextLikeState } from '../../work-context/store/work-context-meta.helper'; import { arrayMoveLeft, arrayMoveRight } from '../../../util/array-move'; @@ -32,11 +38,16 @@ const WORK_CONTEXT_TYPE: WorkContextType = WorkContextType.TAG; export const tagAdapter: EntityAdapter = createEntityAdapter(); export const selectTagFeatureState = createFeatureSelector(TAG_FEATURE_NAME); -export const {selectIds, selectEntities, selectAll, selectTotal} = tagAdapter.getSelectors(); +export const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = tagAdapter.getSelectors(); export const selectAllTags = createSelector(selectTagFeatureState, selectAll); export const selectAllTagsWithoutMyDay = createSelector( selectAllTags, - (tags: Tag[]): Tag[] => tags.filter(tag => tag.id !== TODAY_TAG.id) + (tags: Tag[]): Tag[] => tags.filter((tag) => tag.id !== TODAY_TAG.id), ); export const selectTagById = createSelector( @@ -47,22 +58,20 @@ export const selectTagById = createSelector( throw new Error('No tag ' + props.id); } return tag; - } + }, ); export const selectTagsByIds = createSelector( selectTagFeatureState, - (state: TagState, props: { ids: string[]; isAllowNull: boolean }): Tag[] => (props.isAllowNull - ? props.ids - .map(id => state.entities[id]) - .filter(tag => !!tag) as Tag[] - - : props.ids.map(id => { - const tag = state.entities[id]; - if (!tag) { - throw new Error('No tag ' + id); - } - return tag; - })) + (state: TagState, props: { ids: string[]; isAllowNull: boolean }): Tag[] => + props.isAllowNull + ? (props.ids.map((id) => state.entities[id]).filter((tag) => !!tag) as Tag[]) + : props.ids.map((id) => { + const tag = state.entities[id]; + if (!tag) { + throw new Error('No tag ' + id); + } + return tag; + }), ); const _addMyDayTagIfNecessary = (state: TagState): TagState => { @@ -70,119 +79,157 @@ const _addMyDayTagIfNecessary = (state: TagState): TagState => { if (ids && !ids.includes(TODAY_TAG.id)) { return { ...state, - ids: ([TODAY_TAG.id, ...ids] as string[]), + ids: [TODAY_TAG.id, ...ids] as string[], entities: { ...state.entities, [TODAY_TAG.id]: TODAY_TAG, - } + }, }; } return state; }; -export const initialTagState: TagState = _addMyDayTagIfNecessary(tagAdapter.getInitialState({ - // additional entity state properties -})); +export const initialTagState: TagState = _addMyDayTagIfNecessary( + tagAdapter.getInitialState({ + // additional entity state properties + }), +); const _reducer = createReducer( initialTagState, // META ACTIONS // ------------ - on(loadAllData, (oldState, {appDataComplete}) => _addMyDayTagIfNecessary( - appDataComplete.tag - ? migrateTagState({...appDataComplete.tag}) - : oldState) + on(loadAllData, (oldState, { appDataComplete }) => + _addMyDayTagIfNecessary( + appDataComplete.tag ? migrateTagState({ ...appDataComplete.tag }) : oldState, + ), ), - on(moveTaskInTodayList, (state: TagState, { - taskId, - newOrderedIds, - src, - target, - workContextType, - workContextId, - }) => { - if (workContextType !== WORK_CONTEXT_TYPE) { - return state; - } - const tag = state.entities[workContextId]; - if (!tag) { - throw new Error('No tag'); - } - const taskIdsBefore = tag.taskIds; - const taskIds = moveTaskForWorkContextLikeState(taskId, newOrderedIds, target, taskIdsBefore); - return tagAdapter.updateOne({ - id: workContextId, - changes: { - taskIds + on( + moveTaskInTodayList, + ( + state: TagState, + { taskId, newOrderedIds, src, target, workContextType, workContextId }, + ) => { + if (workContextType !== WORK_CONTEXT_TYPE) { + return state; } - }, state); - }), - - on(moveTaskUpInTodayList, (state: TagState, { - taskId, - workContextId, - workContextType - }) => (workContextType === WORK_CONTEXT_TYPE) - ? tagAdapter.updateOne({ - id: workContextId, - changes: { - taskIds: arrayMoveLeft((state.entities[workContextId] as Tag).taskIds, taskId) + const tag = state.entities[workContextId]; + if (!tag) { + throw new Error('No tag'); } - }, state) - : state + const taskIdsBefore = tag.taskIds; + const taskIds = moveTaskForWorkContextLikeState( + taskId, + newOrderedIds, + target, + taskIdsBefore, + ); + return tagAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds, + }, + }, + state, + ); + }, ), - on(moveTaskDownInTodayList, (state: TagState, { - taskId, - workContextId, - workContextType - }) => (workContextType === WORK_CONTEXT_TYPE) - ? tagAdapter.updateOne({ - id: workContextId, - changes: { - taskIds: arrayMoveRight((state.entities[workContextId] as Tag).taskIds, taskId) - } - }, state) - : state + on( + moveTaskUpInTodayList, + (state: TagState, { taskId, workContextId, workContextType }) => + workContextType === WORK_CONTEXT_TYPE + ? tagAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds: arrayMoveLeft( + (state.entities[workContextId] as Tag).taskIds, + taskId, + ), + }, + }, + state, + ) + : state, + ), + + on( + moveTaskDownInTodayList, + (state: TagState, { taskId, workContextId, workContextType }) => + workContextType === WORK_CONTEXT_TYPE + ? tagAdapter.updateOne( + { + id: workContextId, + changes: { + taskIds: arrayMoveRight( + (state.entities[workContextId] as Tag).taskIds, + taskId, + ), + }, + }, + state, + ) + : state, ), // INTERNAL // -------- - on(tagActions.addTag, (state: TagState, {tag}) => tagAdapter.addOne(tag, state)), + on(tagActions.addTag, (state: TagState, { tag }) => tagAdapter.addOne(tag, state)), - on(tagActions.updateTag, (state: TagState, {tag}) => tagAdapter.updateOne(tag, state)), + on(tagActions.updateTag, (state: TagState, { tag }) => + tagAdapter.updateOne(tag, state), + ), - on(tagActions.upsertTag, (state: TagState, {tag}) => tagAdapter.upsertOne(tag, state)), + on(tagActions.upsertTag, (state: TagState, { tag }) => + tagAdapter.upsertOne(tag, state), + ), - on(tagActions.deleteTag, (state: TagState, {id}) => tagAdapter.removeOne(id, state)), + on(tagActions.deleteTag, (state: TagState, { id }) => tagAdapter.removeOne(id, state)), - on(tagActions.deleteTags, (state: TagState, {ids}) => tagAdapter.removeMany(ids, state)), + on(tagActions.deleteTags, (state: TagState, { ids }) => + tagAdapter.removeMany(ids, state), + ), - on(tagActions.updateWorkStartForTag, (state: TagState, {id, newVal, date}) => tagAdapter.updateOne({ - id, changes: { - workStart: { - ...(state.entities[id] as Tag).workStart, - [date]: newVal, - } - } - }, state)), + on(tagActions.updateWorkStartForTag, (state: TagState, { id, newVal, date }) => + tagAdapter.updateOne( + { + id, + changes: { + workStart: { + ...(state.entities[id] as Tag).workStart, + [date]: newVal, + }, + }, + }, + state, + ), + ), - on(tagActions.updateWorkEndForTag, (state: TagState, {id, newVal, date}) => tagAdapter.updateOne({ - id, changes: { - workEnd: { - ...(state.entities[id] as Tag).workEnd, - [date]: newVal, - } - } - }, state)), + on(tagActions.updateWorkEndForTag, (state: TagState, { id, newVal, date }) => + tagAdapter.updateOne( + { + id, + changes: { + workEnd: { + ...(state.entities[id] as Tag).workEnd, + [date]: newVal, + }, + }, + }, + state, + ), + ), - on(tagActions.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({ + on(tagActions.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: { @@ -192,150 +239,152 @@ const _reducer = createReducer( breakTime: { ...oldTag.breakTime, [date]: oldBreakTime + valToAdd, - } - } - }, state); - } - ), - - on(tagActions.updateAdvancedConfigForTag, (state: TagState, {tagId, sectionKey, data}) => { - const tagToUpdate = state.entities[tagId] as Tag; - return tagAdapter.updateOne({ - id: tagId, - changes: { - advancedCfg: { - ...tagToUpdate.advancedCfg, - [sectionKey]: { - ...tagToUpdate.advancedCfg[sectionKey], - ...data, - } - } - } - }, state); + }, + }, + }, + state, + ); }), + + on( + tagActions.updateAdvancedConfigForTag, + (state: TagState, { tagId, sectionKey, data }) => { + const tagToUpdate = state.entities[tagId] as Tag; + return tagAdapter.updateOne( + { + id: tagId, + changes: { + advancedCfg: { + ...tagToUpdate.advancedCfg, + [sectionKey]: { + ...tagToUpdate.advancedCfg[sectionKey], + ...data, + }, + }, + }, + }, + state, + ); + }, + ), ); -export function tagReducer( - state: TagState = initialTagState, - action: Action, -): TagState { +export function tagReducer(state: TagState = initialTagState, action: Action): TagState { switch (action.type) { // TASK STUFF // --------- case TaskActionTypes.AddTask: { - const {payload} = action as AddTask; - const {task, isAddToBottom} = payload; + const { payload } = action as AddTask; + const { task, isAddToBottom } = payload; const updates: Update[] = task.tagIds.map((tagId) => ({ id: tagId, changes: { taskIds: isAddToBottom - ? [ - ...(state.entities[tagId] as Tag).taskIds, - task.id, - ] - : [ - task.id, - ...(state.entities[tagId] as Tag).taskIds - ] - } + ? [...(state.entities[tagId] as Tag).taskIds, task.id] + : [task.id, ...(state.entities[tagId] as Tag).taskIds], + }, })); return tagAdapter.updateMany(updates, state); } case TaskActionTypes.ConvertToMainTask: { - const {payload} = action as ConvertToMainTask; - const {task, parentTagIds} = payload; + const { payload } = action as ConvertToMainTask; + const { task, parentTagIds } = payload; console.log(task); const updates: Update[] = parentTagIds.map((tagId) => ({ id: tagId, changes: { - taskIds: [ - task.id, - ...(state.entities[tagId] as Tag).taskIds - ] - } + taskIds: [task.id, ...(state.entities[tagId] as Tag).taskIds], + }, })); return tagAdapter.updateMany(updates, state); } case TaskActionTypes.DeleteTask: { - const {payload} = action as DeleteTask; - const {task} = payload; - const updates: Update[] = task.tagIds.map(tagId => ({ + const { payload } = action as DeleteTask; + const { task } = payload; + const updates: Update[] = task.tagIds.map((tagId) => ({ id: tagId, changes: { - taskIds: (state.entities[tagId] as Tag).taskIds.filter(taskIdForTag => taskIdForTag !== task.id) - } + taskIds: (state.entities[tagId] as Tag).taskIds.filter( + (taskIdForTag) => taskIdForTag !== task.id, + ), + }, })); return tagAdapter.updateMany(updates, state); } case TaskActionTypes.MoveToArchive: { - const {payload} = action as MoveToArchive; - const {tasks} = payload; - const taskIdsToMoveToArchive = tasks.map(t => t.id); + const { payload } = action as MoveToArchive; + const { tasks } = payload; + const taskIdsToMoveToArchive = tasks.map((t) => t.id); const tagIds = unique( - tasks.reduce((acc: string[], t: TaskWithSubTasks) => ([ - ...acc, - ...t.tagIds - ]), []) + tasks.reduce((acc: string[], t: TaskWithSubTasks) => [...acc, ...t.tagIds], []), ); const updates: Update[] = tagIds.map((pid: string) => ({ id: pid, changes: { - taskIds: (state.entities[pid] as Tag).taskIds.filter(taskId => !taskIdsToMoveToArchive.includes(taskId)), - } + taskIds: (state.entities[pid] as Tag).taskIds.filter( + (taskId) => !taskIdsToMoveToArchive.includes(taskId), + ), + }, })); return tagAdapter.updateMany(updates, state); } // cleans up all occurrences case TaskActionTypes.DeleteMainTasks: { - const {payload} = action as DeleteMainTasks; - const {taskIds} = payload; - const updates: Update[] = (state.ids as string[]).map(tagId => ({ + const { payload } = action as DeleteMainTasks; + const { taskIds } = payload; + const updates: Update[] = (state.ids as string[]).map((tagId) => ({ id: tagId, changes: { - taskIds: (state.entities[tagId] as Tag).taskIds.filter(taskId => !taskIds.includes(taskId)), - } + taskIds: (state.entities[tagId] as Tag).taskIds.filter( + (taskId) => !taskIds.includes(taskId), + ), + }, })); return tagAdapter.updateMany(updates, state); } case TaskActionTypes.RestoreTask: { - const {payload} = action as RestoreTask; - const {task} = payload; + const { payload } = action as RestoreTask; + const { task } = payload; - return tagAdapter.updateMany(task.tagIds - // NOTE: if the tag model is gone we don't update - .filter(tagId => !!(state.entities[tagId] as Tag)) - .map(tagId => ({ + return tagAdapter.updateMany( + task.tagIds + // NOTE: if the tag model is gone we don't update + .filter((tagId) => !!(state.entities[tagId] as Tag)) + .map((tagId) => ({ id: tagId, changes: { - taskIds: [...(state.entities[tagId] as Tag).taskIds, task.id] - } - }) - ), state); + taskIds: [...(state.entities[tagId] as Tag).taskIds, task.id], + }, + })), + state, + ); } case TaskActionTypes.UpdateTaskTags: { - const {payload} = action as UpdateTaskTags; - const {newTagIds = [], oldTagIds = [], task} = payload; + const { payload } = action as UpdateTaskTags; + const { newTagIds = [], oldTagIds = [], task } = payload; const taskId = task.id; - const removedFrom: string[] = oldTagIds.filter(oldId => !newTagIds.includes(oldId)); - const addedTo: string[] = newTagIds.filter(newId => !oldTagIds.includes(newId)); - const removeFrom: Update[] = removedFrom.map(tagId => ({ + const removedFrom: string[] = oldTagIds.filter( + (oldId) => !newTagIds.includes(oldId), + ); + const addedTo: string[] = newTagIds.filter((newId) => !oldTagIds.includes(newId)); + const removeFrom: Update[] = removedFrom.map((tagId) => ({ id: tagId, changes: { - taskIds: (state.entities[tagId] as Tag).taskIds.filter(id => id !== taskId), - } + taskIds: (state.entities[tagId] as Tag).taskIds.filter((id) => id !== taskId), + }, })); - const addTo: Update[] = addedTo.map(tagId => ({ + const addTo: Update[] = addedTo.map((tagId) => ({ id: tagId, changes: { taskIds: [taskId, ...(state.entities[tagId] as Tag).taskIds], - } + }, })); return tagAdapter.updateMany([...removeFrom, ...addTo], state); } @@ -343,5 +392,3 @@ export function tagReducer( return _reducer(state, action); } } - - diff --git a/src/app/features/tag/tag-form-cfg.const.ts b/src/app/features/tag/tag-form-cfg.const.ts index 1e75fd274..c6d44f137 100644 --- a/src/app/features/tag/tag-form-cfg.const.ts +++ b/src/app/features/tag/tag-form-cfg.const.ts @@ -1,4 +1,7 @@ -import { ConfigFormSection, GenericConfigFormSection } from '../config/global-config.model'; +import { + ConfigFormSection, + GenericConfigFormSection, +} from '../config/global-config.model'; import { T } from '../../t.const'; import { Tag } from './tag.model'; @@ -29,7 +32,7 @@ export const BASIC_TAG_CONFIG_FORM_CONFIG: ConfigFormSection = { type: 'color', }, }, - ] + ], }; export const CREATE_TAG_BASIC_CONFIG_FORM_CONFIG: GenericConfigFormSection = { @@ -55,5 +58,5 @@ export const CREATE_TAG_BASIC_CONFIG_FORM_CONFIG: GenericConfigFormSection = { type: 'color', }, }, - ] + ], }; 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 adc72b6fc..a73db25f7 100644 --- a/src/app/features/tag/tag-list/tag-list.component.ts +++ b/src/app/features/tag/tag-list/tag-list.component.ts @@ -1,4 +1,11 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnDestroy, + Output, +} from '@angular/core'; import { standardListAnimation } from '../../../ui/animations/standard-list.ani'; import { Tag } from '../tag.model'; import { TagService } from '../tag.service'; @@ -18,7 +25,7 @@ import { expandFadeAnimation } from '../../../ui/animations/expand.ani'; templateUrl: './tag-list.component.html', styleUrls: ['./tag-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [standardListAnimation, expandFadeAnimation] + animations: [standardListAnimation, expandFadeAnimation], }) export class TagListComponent implements OnDestroy { @Input() isDisableEdit: boolean = false; @@ -27,24 +34,29 @@ export class TagListComponent implements OnDestroy { @Output() replacedTagForTask: EventEmitter = new EventEmitter(); projectTag?: TagComponentTag | null; tags: Tag[] = []; - private _isShowProjectTagAlways$: BehaviorSubject = new BehaviorSubject(false); - private _projectId$: BehaviorSubject = new BehaviorSubject(null); + private _isShowProjectTagAlways$: BehaviorSubject = new BehaviorSubject( + false, + ); + private _projectId$: BehaviorSubject = new BehaviorSubject< + string | null + >(null); projectTag$: Observable = combineLatest([ this._workContextService.activeWorkContextTypeAndId$, - this._isShowProjectTagAlways$ + this._isShowProjectTagAlways$, ]).pipe( - switchMap(([{activeType}, isShowAlways]) => isShowAlways || (activeType === WorkContextType.TAG) - ? this._projectId$.pipe( - switchMap(id => id - ? this._projectService.getByIdOnce$(id) - : of(null) - ), - map(project => (project && { - ...project, - icon: 'list' - })) - ) - : of(null) + switchMap(([{ activeType }, isShowAlways]) => + isShowAlways || activeType === WorkContextType.TAG + ? this._projectId$.pipe( + switchMap((id) => (id ? this._projectService.getByIdOnce$(id) : of(null))), + map( + (project) => + project && { + ...project, + icon: 'list', + }, + ), + ) + : of(null), ), ); private _tagIds$: BehaviorSubject = new BehaviorSubject([]); @@ -54,7 +66,10 @@ export class TagListComponent implements OnDestroy { ]).pipe( // TODO there should be a better way... switchMap(([ids, activeId]) => - this._tagService.getTagsByIds$(ids.filter(id => id !== activeId), true) + this._tagService.getTagsByIds$( + ids.filter((id) => id !== activeId), + true, + ), ), ); // private _hideId: string = this._workContextService.activeWorkContextId; @@ -66,8 +81,8 @@ export class TagListComponent implements OnDestroy { private readonly _workContextService: WorkContextService, private readonly _matDialog: MatDialog, ) { - this._subs.add(this.projectTag$.subscribe(v => this.projectTag = v)); - this._subs.add(this.tags$.subscribe(v => this.tags = v)); + this._subs.add(this.projectTag$.subscribe((v) => (this.projectTag = v))); + this._subs.add(this.tags$.subscribe((v) => (this.tags = v))); } @Input() set isShowProjectTagAlways(v: boolean) { @@ -98,8 +113,6 @@ export class TagListComponent implements OnDestroy { } trackByFn(i: number, tag: Tag) { - return tag - ? tag.id - : i; + return tag ? tag.id : i; } } diff --git a/src/app/features/tag/tag.const.ts b/src/app/features/tag/tag.const.ts index a466c9313..8ed58c41a 100644 --- a/src/app/features/tag/tag.const.ts +++ b/src/app/features/tag/tag.const.ts @@ -3,7 +3,7 @@ import { DEFAULT_TAG_COLOR, DEFAULT_TODAY_TAG_COLOR, WORK_CONTEXT_DEFAULT_COMMON, - WORK_CONTEXT_DEFAULT_THEME + WORK_CONTEXT_DEFAULT_THEME, } from '../work-context/work-context.const'; import { WorkContextThemeCfg } from '../work-context/work-context.model'; import { IS_USE_DARK_THEME_AS_DEFAULT } from '../config/default-global-config.const'; @@ -22,12 +22,12 @@ export const TODAY_TAG: Tag = { primary: DEFAULT_TODAY_TAG_COLOR, backgroundImageDark: 'assets/bg/NIGHT_manuel-will.jpg', - ...(IS_USE_DARK_THEME_AS_DEFAULT + ...((IS_USE_DARK_THEME_AS_DEFAULT ? { - isDisableBackgroundGradient: false, - } - : {}) as Partial, - } + isDisableBackgroundGradient: false, + } + : {}) as Partial), + }, }; export const DEFAULT_TAG: Tag = { @@ -42,5 +42,5 @@ export const DEFAULT_TAG: Tag = { theme: { ...WORK_CONTEXT_DEFAULT_THEME, primary: DEFAULT_TAG_COLOR, - } + }, }; diff --git a/src/app/features/tag/tag.model.ts b/src/app/features/tag/tag.model.ts index 4de96048d..613f3c926 100644 --- a/src/app/features/tag/tag.model.ts +++ b/src/app/features/tag/tag.model.ts @@ -1,5 +1,8 @@ import { EntityState } from '@ngrx/entity'; -import { WorkContextAdvancedCfgKey, WorkContextCommon } from '../work-context/work-context.model'; +import { + WorkContextAdvancedCfgKey, + WorkContextCommon, +} from '../work-context/work-context.model'; import { MODEL_VERSION_KEY } from '../../app.constants'; export interface TagCopy extends WorkContextCommon { diff --git a/src/app/features/tag/tag.module.ts b/src/app/features/tag/tag.module.ts index 0bebfab9e..1247bab2e 100644 --- a/src/app/features/tag/tag.module.ts +++ b/src/app/features/tag/tag.module.ts @@ -16,7 +16,7 @@ import { TagComponent } from './tag/tag.component'; UiModule, FormsModule, StoreModule.forFeature(TAG_FEATURE_NAME, tagReducer), - EffectsModule.forFeature([TagEffects]) + EffectsModule.forFeature([TagEffects]), ], declarations: [ TagListComponent, @@ -24,11 +24,6 @@ import { TagComponent } from './tag/tag.component'; TagComponent, // FindContrastColorPipe ], - exports: [ - DialogEditTagsForTaskComponent, - TagListComponent, - TagComponent - ], + exports: [DialogEditTagsForTaskComponent, TagListComponent, TagComponent], }) -export class TagModule { -} +export class TagModule {} diff --git a/src/app/features/tag/tag.service.ts b/src/app/features/tag/tag.service.ts index 970ef372b..7290628f8 100644 --- a/src/app/features/tag/tag.service.ts +++ b/src/app/features/tag/tag.service.ts @@ -1,6 +1,11 @@ import { Injectable } from '@angular/core'; import { select, Store } from '@ngrx/store'; -import { selectAllTags, selectAllTagsWithoutMyDay, selectTagById, selectTagsByIds } from './store/tag.reducer'; +import { + selectAllTags, + selectAllTagsWithoutMyDay, + selectTagById, + selectTagsByIds, +} from './store/tag.reducer'; import { addTag, deleteTag, deleteTags, updateTag, upsertTag } from './store/tag.actions'; import { Observable } from 'rxjs'; import { Tag, TagState } from './tag.model'; @@ -15,53 +20,51 @@ export class TagService { tags$: Observable = this._store$.pipe(select(selectAllTags)); tagsNoMyDay$: Observable = this._store$.pipe(select(selectAllTagsWithoutMyDay)); - constructor( - private _store$: Store, - ) { - } + constructor(private _store$: Store) {} getTagById$(id: string): Observable { - return this._store$.pipe(select(selectTagById, {id})); + return this._store$.pipe(select(selectTagById, { id })); } getTagsByIds$(ids: string[], isAllowNull: boolean = false): Observable { - return this._store$.pipe(select(selectTagsByIds, {ids, isAllowNull})); + return this._store$.pipe(select(selectTagsByIds, { ids, isAllowNull })); } addTag(tag: Partial): string { - const {id, action} = this.getAddTagActionAndId(tag); + const { id, action } = this.getAddTagActionAndId(tag); this._store$.dispatch(action); return id; } deleteTag(id: string) { - this._store$.dispatch(deleteTag({id})); + this._store$.dispatch(deleteTag({ id })); } removeTag(id: string) { - this._store$.dispatch(deleteTag({id})); + this._store$.dispatch(deleteTag({ id })); } updateColor(id: string, color: string) { - this._store$.dispatch(updateTag({tag: {id, changes: {color}}})); + this._store$.dispatch(updateTag({ tag: { id, changes: { color } } })); } deleteTags(ids: string[]) { - this._store$.dispatch(deleteTags({ids})); + this._store$.dispatch(deleteTags({ ids })); } updateTag(id: string, changes: Partial) { - this._store$.dispatch(updateTag({tag: {id, changes}})); + this._store$.dispatch(updateTag({ tag: { id, changes } })); } upsertTag(tag: Tag) { - this._store$.dispatch(upsertTag({tag})); + this._store$.dispatch(upsertTag({ tag })); } getAddTagActionAndId(tag: Partial): { action: TypedAction; id: string } { const id = shortid(); return { - id, action: addTag({ + id, + action: addTag({ tag: { ...DEFAULT_TAG, id, @@ -72,8 +75,8 @@ export class TagService { color: tag.color || null, taskIds: [], ...tag, - } - }) + }, + }), }; } } diff --git a/src/app/features/tag/tag/tag.component.ts b/src/app/features/tag/tag/tag.component.ts index e0705a413..dd9dc2781 100644 --- a/src/app/features/tag/tag/tag.component.ts +++ b/src/app/features/tag/tag/tag.component.ts @@ -11,19 +11,17 @@ export interface TagComponentTag { selector: 'tag', templateUrl: './tag.component.html', styleUrls: ['./tag.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TagComponent { tag?: TagComponentTag; // @HostBinding('style.background') color?: string; - constructor() { - } + constructor() {} @Input('tag') set tagIn(v: TagComponentTag) { this.tag = v; - this.color = v.color || v.theme && v.theme.primary; + this.color = v.color || (v.theme && v.theme.primary); } - } diff --git a/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts b/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts index 3ab341ddd..7d55cb224 100644 --- a/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts +++ b/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject, OnDestroy, OnInit, } from '@angular/core'; import { Task } from '../../tasks/task.model'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { TaskRepeatCfgService } from '../task-repeat-cfg.service'; @@ -18,7 +18,7 @@ import { exists } from '../../../util/exists'; selector: 'dialog-edit-task-repeat-cfg', templateUrl: './dialog-edit-task-repeat-cfg.component.html', styleUrls: ['./dialog-edit-task-repeat-cfg.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogEditTaskRepeatCfgComponent implements OnInit, OnDestroy { T: typeof T = T; @@ -46,15 +46,18 @@ export class DialogEditTaskRepeatCfgComponent implements OnInit, OnDestroy { private _taskRepeatCfgService: TaskRepeatCfgService, private _matDialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: { task: Task }, - ) { - } + ) {} ngOnInit(): void { if (this.isEdit && this.task.repeatCfgId) { - this._subs.add(this._taskRepeatCfgService.getTaskRepeatCfgById$(this.task.repeatCfgId).subscribe((cfg) => { - this.taskRepeatCfg = cfg; - this._cd.detectChanges(); - })); + this._subs.add( + this._taskRepeatCfgService + .getTaskRepeatCfgById$(this.task.repeatCfgId) + .subscribe((cfg) => { + this.taskRepeatCfg = cfg; + this._cd.detectChanges(); + }), + ); } } @@ -64,16 +67,25 @@ export class DialogEditTaskRepeatCfgComponent implements OnInit, OnDestroy { save() { if (this.isEdit) { - this._taskRepeatCfgService.updateTaskRepeatCfg(exists(this.taskRepeatCfgId), this.taskRepeatCfg); + this._taskRepeatCfgService.updateTaskRepeatCfg( + exists(this.taskRepeatCfgId), + this.taskRepeatCfg, + ); this.close(); } else { - this._taskRepeatCfgService.addTaskRepeatCfgToTask(this.task.id, this.task.projectId, this.taskRepeatCfg); + this._taskRepeatCfgService.addTaskRepeatCfgToTask( + this.task.id, + this.task.projectId, + this.taskRepeatCfg, + ); this.close(); } } remove() { - this._taskRepeatCfgService.deleteTaskRepeatCfgWithDialog(exists(this.task.repeatCfgId)); + this._taskRepeatCfgService.deleteTaskRepeatCfgWithDialog( + exists(this.task.repeatCfgId), + ); this.close(); } @@ -86,12 +98,12 @@ export class DialogEditTaskRepeatCfgComponent implements OnInit, OnDestroy { } addNewTag(title: string) { - const id = this._tagService.addTag({title}); + const id = this._tagService.addTag({ title }); this._updateTags(unique([...this.taskRepeatCfg.tagIds, id])); } removeTag(id: string) { - const updatedTagIds = this.taskRepeatCfg.tagIds.filter(tagId => tagId !== id); + const updatedTagIds = this.taskRepeatCfg.tagIds.filter((tagId) => tagId !== id); this._updateTags(updatedTagIds); } diff --git a/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/task-repeat-cfg-form.const.ts b/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/task-repeat-cfg-form.const.ts index 7c96976eb..5a6076739 100644 --- a/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/task-repeat-cfg-form.const.ts +++ b/src/app/features/task-repeat-cfg/dialog-edit-task-repeat-cfg/task-repeat-cfg-form.const.ts @@ -6,70 +6,70 @@ export const TASK_REPEAT_CFG_FORM_CFG: FormlyFieldConfig[] = [ key: 'title', type: 'input', templateOptions: { - label: T.F.TASK_REPEAT.F.TITLE + label: T.F.TASK_REPEAT.F.TITLE, }, }, { key: 'defaultEstimate', type: 'duration', templateOptions: { - label: T.F.TASK_REPEAT.F.DEFAULT_ESTIMATE + label: T.F.TASK_REPEAT.F.DEFAULT_ESTIMATE, }, }, { key: 'monday', type: 'checkbox', templateOptions: { - label: T.F.TASK_REPEAT.F.MONDAY + label: T.F.TASK_REPEAT.F.MONDAY, }, }, { key: 'tuesday', type: 'checkbox', templateOptions: { - label: T.F.TASK_REPEAT.F.TUESDAY + label: T.F.TASK_REPEAT.F.TUESDAY, }, }, { key: 'wednesday', type: 'checkbox', templateOptions: { - label: T.F.TASK_REPEAT.F.WEDNESDAY + label: T.F.TASK_REPEAT.F.WEDNESDAY, }, }, { key: 'thursday', type: 'checkbox', templateOptions: { - label: T.F.TASK_REPEAT.F.THURSDAY + label: T.F.TASK_REPEAT.F.THURSDAY, }, }, { key: 'friday', type: 'checkbox', templateOptions: { - label: T.F.TASK_REPEAT.F.FRIDAY + label: T.F.TASK_REPEAT.F.FRIDAY, }, }, { key: 'saturday', type: 'checkbox', templateOptions: { - label: T.F.TASK_REPEAT.F.SATURDAY + label: T.F.TASK_REPEAT.F.SATURDAY, }, }, { key: 'sunday', type: 'checkbox', templateOptions: { - label: T.F.TASK_REPEAT.F.SUNDAY + label: T.F.TASK_REPEAT.F.SUNDAY, }, }, { key: 'isAddToBottom', type: 'checkbox', templateOptions: { - label: T.F.TASK_REPEAT.F.IS_ADD_TO_BOTTOM + label: T.F.TASK_REPEAT.F.IS_ADD_TO_BOTTOM, }, }, ]; diff --git a/src/app/features/task-repeat-cfg/migrate-task-repeat-cfg-state.util.ts b/src/app/features/task-repeat-cfg/migrate-task-repeat-cfg-state.util.ts index ff5e3131c..9994c2bc0 100644 --- a/src/app/features/task-repeat-cfg/migrate-task-repeat-cfg-state.util.ts +++ b/src/app/features/task-repeat-cfg/migrate-task-repeat-cfg-state.util.ts @@ -5,15 +5,19 @@ import { TaskRepeatCfg, TaskRepeatCfgState } from './task-repeat-cfg.model'; const MODEL_VERSION = 1; -export const migrateTaskRepeatCfgState = (taskRepeatState: TaskRepeatCfgState): TaskRepeatCfgState => { +export const migrateTaskRepeatCfgState = ( + taskRepeatState: TaskRepeatCfgState, +): TaskRepeatCfgState => { if (!isMigrateModel(taskRepeatState, MODEL_VERSION, 'TaskRepeat')) { return taskRepeatState; } - const taskRepeatEntities: Dictionary = {...taskRepeatState.entities}; + const taskRepeatEntities: Dictionary = { ...taskRepeatState.entities }; Object.keys(taskRepeatEntities).forEach((key) => { // NOTE: absolutely needs to come last as otherwise the previous defaults won't work - taskRepeatEntities[key] = _addNewFieldsToTaskRepeatCfgs(taskRepeatEntities[key] as TaskRepeatCfg); + taskRepeatEntities[key] = _addNewFieldsToTaskRepeatCfgs( + taskRepeatEntities[key] as TaskRepeatCfg, + ); }); return { @@ -27,6 +31,6 @@ export const migrateTaskRepeatCfgState = (taskRepeatState: TaskRepeatCfgState): const _addNewFieldsToTaskRepeatCfgs = (taskRepeat: TaskRepeatCfg): TaskRepeatCfg => { return { ...taskRepeat, - tagIds: taskRepeat.tagIds || [] + tagIds: taskRepeat.tagIds || [], }; }; diff --git a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.actions.ts b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.actions.ts index 17e635b83..099185eac 100644 --- a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.actions.ts +++ b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.actions.ts @@ -14,50 +14,43 @@ export enum TaskRepeatCfgActionTypes { export class AddTaskRepeatCfgToTask implements Action { readonly type: string = TaskRepeatCfgActionTypes.AddTaskRepeatCfgToTask; - constructor(public payload: { taskId: string; taskRepeatCfg: TaskRepeatCfg }) { - } + constructor(public payload: { taskId: string; taskRepeatCfg: TaskRepeatCfg }) {} } export class UpdateTaskRepeatCfg implements Action { readonly type: string = TaskRepeatCfgActionTypes.UpdateTaskRepeatCfg; - constructor(public payload: { taskRepeatCfg: Update }) { - } + constructor(public payload: { taskRepeatCfg: Update }) {} } export class UpdateTaskRepeatCfgs implements Action { readonly type: string = TaskRepeatCfgActionTypes.UpdateTaskRepeatCfgs; - constructor(public payload: { ids: string[]; changes: Partial }) { - } + constructor(public payload: { ids: string[]; changes: Partial }) {} } export class UpsertTaskRepeatCfg implements Action { readonly type: string = TaskRepeatCfgActionTypes.UpsertTaskRepeatCfg; - constructor(public payload: { taskRepeatCfg: TaskRepeatCfg }) { - } + constructor(public payload: { taskRepeatCfg: TaskRepeatCfg }) {} } export class DeleteTaskRepeatCfg implements Action { readonly type: string = TaskRepeatCfgActionTypes.DeleteTaskRepeatCfg; - constructor(public payload: { id: string }) { - } + constructor(public payload: { id: string }) {} } export class DeleteTaskRepeatCfgs implements Action { readonly type: string = TaskRepeatCfgActionTypes.DeleteTaskRepeatCfgs; - constructor(public payload: { ids: string[] }) { - } + constructor(public payload: { ids: string[] }) {} } -export type TaskRepeatCfgActions - = AddTaskRepeatCfgToTask +export type TaskRepeatCfgActions = + | AddTaskRepeatCfgToTask | UpdateTaskRepeatCfg | UpdateTaskRepeatCfgs | UpsertTaskRepeatCfg | DeleteTaskRepeatCfg - | DeleteTaskRepeatCfgs - ; + | DeleteTaskRepeatCfgs; 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 277b7df1b..e4a79a234 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 @@ -1,20 +1,38 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; -import { concatMap, delay, filter, map, mergeMap, take, tap, withLatestFrom } from 'rxjs/operators'; +import { + concatMap, + delay, + filter, + map, + mergeMap, + take, + tap, + withLatestFrom, +} from 'rxjs/operators'; import { Action, select, Store } from '@ngrx/store'; import { AddTaskRepeatCfgToTask, DeleteTaskRepeatCfg, TaskRepeatCfgActionTypes, - UpdateTaskRepeatCfg + UpdateTaskRepeatCfg, } from './task-repeat-cfg.actions'; import { selectTaskRepeatCfgFeatureState } from './task-repeat-cfg.reducer'; import { PersistenceService } from '../../../core/persistence/persistence.service'; import { Task, TaskArchive, TaskWithSubTasks } from '../../tasks/task.model'; -import { AddTask, MoveToArchive, UnScheduleTask, UpdateTask } from '../../tasks/store/task.actions'; +import { + AddTask, + MoveToArchive, + UnScheduleTask, + UpdateTask, +} from '../../tasks/store/task.actions'; import { TaskService } from '../../tasks/task.service'; import { TaskRepeatCfgService } from '../task-repeat-cfg.service'; -import { TASK_REPEAT_WEEKDAY_MAP, TaskRepeatCfg, TaskRepeatCfgState } from '../task-repeat-cfg.model'; +import { + TASK_REPEAT_WEEKDAY_MAP, + TaskRepeatCfg, + TaskRepeatCfgState, +} from '../task-repeat-cfg.model'; import { from } from 'rxjs'; import { isToday } from '../../../util/is-today.util'; import { WorkContextService } from '../../work-context/work-context.service'; @@ -25,8 +43,7 @@ import { TODAY_TAG } from '../../tag/tag.const'; @Injectable() export class TaskRepeatCfgEffects { - - @Effect({dispatch: false}) updateTaskRepeatCfgs$: any = this._actions$.pipe( + @Effect({ dispatch: false }) updateTaskRepeatCfgs$: any = this._actions$.pipe( ofType( TaskRepeatCfgActionTypes.AddTaskRepeatCfgToTask, TaskRepeatCfgActionTypes.UpdateTaskRepeatCfg, @@ -34,110 +51,123 @@ export class TaskRepeatCfgEffects { TaskRepeatCfgActionTypes.DeleteTaskRepeatCfg, TaskRepeatCfgActionTypes.DeleteTaskRepeatCfgs, ), - withLatestFrom( - this._store$.pipe(select(selectTaskRepeatCfgFeatureState)), - ), - tap(this._saveToLs.bind(this)) + withLatestFrom(this._store$.pipe(select(selectTaskRepeatCfgFeatureState))), + tap(this._saveToLs.bind(this)), ); @Effect() createRepeatableTasks: any = this._actions$.pipe( ofType(setActiveWorkContext), - concatMap((() => this._syncService.afterInitialSyncDoneAndDataLoadedInitially$)), + concatMap(() => this._syncService.afterInitialSyncDoneAndDataLoadedInitially$), delay(1000), - concatMap(() => this._taskRepeatCfgService.taskRepeatCfgs$.pipe( - take(1), - )), + concatMap(() => this._taskRepeatCfgService.taskRepeatCfgs$.pipe(take(1))), // filter out the configs which have been created today already // and those which are not scheduled for the current week day map((taskRepeatCfgs: TaskRepeatCfg[]): TaskRepeatCfg[] => { const day = new Date().getDay(); const dayStr: keyof TaskRepeatCfg = TASK_REPEAT_WEEKDAY_MAP[day]; - return taskRepeatCfgs && taskRepeatCfgs.filter( - (taskRepeatCfg: TaskRepeatCfg) => - (taskRepeatCfg[dayStr] && !isToday(taskRepeatCfg.lastTaskCreation)) + return ( + taskRepeatCfgs && + taskRepeatCfgs.filter( + (taskRepeatCfg: TaskRepeatCfg) => + taskRepeatCfg[dayStr] && !isToday(taskRepeatCfg.lastTaskCreation), + ) ); }), filter((taskRepeatCfgs) => taskRepeatCfgs && !!taskRepeatCfgs.length), // existing tasks with sub tasks are loaded, because need to move them to the archive - mergeMap(taskRepeatCfgs => from(taskRepeatCfgs).pipe( - mergeMap((taskRepeatCfg: TaskRepeatCfg) => - // NOTE: there might be multiple configs in case something went wrong - // we want to move all of them to the archive - this._taskService.getTasksWithSubTasksByRepeatCfgId$(taskRepeatCfg.id as string).pipe( - take(1), - concatMap((tasks: Task[]) => { - const isCreateNew = (tasks.filter(task => isToday(task.created)).length === 0); - const moveToArchiveActions: (MoveToArchive | AddTask | UpdateTaskRepeatCfg)[] = isCreateNew - ? tasks.filter(task => isToday(task.created)) - .map(task => new MoveToArchive({tasks: [task]})) - : []; + mergeMap((taskRepeatCfgs) => + from(taskRepeatCfgs).pipe( + mergeMap((taskRepeatCfg: TaskRepeatCfg) => + // NOTE: there might be multiple configs in case something went wrong + // we want to move all of them to the archive + this._taskService + .getTasksWithSubTasksByRepeatCfgId$(taskRepeatCfg.id as string) + .pipe( + take(1), + concatMap((tasks: Task[]) => { + const isCreateNew = + tasks.filter((task) => isToday(task.created)).length === 0; + const moveToArchiveActions: ( + | MoveToArchive + | AddTask + | UpdateTaskRepeatCfg + )[] = isCreateNew + ? tasks + .filter((task) => isToday(task.created)) + .map((task) => new MoveToArchive({ tasks: [task] })) + : []; - if (!taskRepeatCfg.id) { - throw new Error('No taskRepeatCfg.id'); - } + if (!taskRepeatCfg.id) { + throw new Error('No taskRepeatCfg.id'); + } - const isAddToTodayAsFallback = !taskRepeatCfg.projectId && !taskRepeatCfg.tagIds.length; + const isAddToTodayAsFallback = + !taskRepeatCfg.projectId && !taskRepeatCfg.tagIds.length; - return from([ - ...moveToArchiveActions, - ...(isCreateNew - ? [ - new AddTask({ - task: this._taskService.createNewTaskWithDefaults({ - title: taskRepeatCfg.title, - additional: { - repeatCfgId: taskRepeatCfg.id, - timeEstimate: taskRepeatCfg.defaultEstimate, - projectId: taskRepeatCfg.projectId, - tagIds: isAddToTodayAsFallback - ? [TODAY_TAG.id] - : taskRepeatCfg.tagIds || [], - } - }), - workContextType: this._workContextService.activeWorkContextType as WorkContextType, - workContextId: this._workContextService.activeWorkContextId as string, - isAddToBacklog: false, - isAddToBottom: taskRepeatCfg.isAddToBottom || false, - }), - new UpdateTaskRepeatCfg({ - taskRepeatCfg: { - id: taskRepeatCfg.id, - changes: { - lastTaskCreation: Date.now(), - } - } - }) - ] - : [] - ) - ]); - }), - ) - ) - )), + return from([ + ...moveToArchiveActions, + ...(isCreateNew + ? [ + new AddTask({ + task: this._taskService.createNewTaskWithDefaults({ + title: taskRepeatCfg.title, + additional: { + repeatCfgId: taskRepeatCfg.id, + timeEstimate: taskRepeatCfg.defaultEstimate, + projectId: taskRepeatCfg.projectId, + tagIds: isAddToTodayAsFallback + ? [TODAY_TAG.id] + : taskRepeatCfg.tagIds || [], + }, + }), + workContextType: this._workContextService + .activeWorkContextType as WorkContextType, + workContextId: this._workContextService + .activeWorkContextId as string, + isAddToBacklog: false, + isAddToBottom: taskRepeatCfg.isAddToBottom || false, + }), + new UpdateTaskRepeatCfg({ + taskRepeatCfg: { + id: taskRepeatCfg.id, + changes: { + lastTaskCreation: Date.now(), + }, + }, + }), + ] + : []), + ]); + }), + ), + ), + ), + ), tap((v) => console.log('IMP Create Repeatable Tasks', v)), ); @Effect() removeConfigIdFromTaskStateTasks$: any = this._actions$.pipe( - ofType( - TaskRepeatCfgActionTypes.DeleteTaskRepeatCfg, - ), + ofType(TaskRepeatCfgActionTypes.DeleteTaskRepeatCfg), concatMap((action: DeleteTaskRepeatCfg) => - this._taskService.getTasksByRepeatCfgId$(action.payload.id).pipe( - take(1) + this._taskService.getTasksByRepeatCfgId$(action.payload.id).pipe(take(1)), + ), + filter((tasks) => tasks && !!tasks.length), + mergeMap((tasks: Task[]) => + tasks.map( + (task) => + new UpdateTask({ + task: { + id: task.id, + changes: { repeatCfgId: null }, + }, + }), ), ), - filter(tasks => tasks && !!tasks.length), - mergeMap((tasks: Task[]) => tasks.map(task => new UpdateTask({ - task: { - id: task.id, - changes: {repeatCfgId: null} - } - }))), ); - @Effect({dispatch: false}) removeConfigIdFromTaskArchiveTasks$: any = this._actions$.pipe( + @Effect({ dispatch: false }) + removeConfigIdFromTaskArchiveTasks$: any = this._actions$.pipe( ofType(TaskRepeatCfgActionTypes.DeleteTaskRepeatCfg), tap((a: DeleteTaskRepeatCfg) => { this._removeRepeatCfgFromArchiveTasks(a.payload.id); @@ -145,15 +175,18 @@ export class TaskRepeatCfgEffects { ); @Effect() removeRemindersOnCreation$: any = this._actions$.pipe( - ofType( - TaskRepeatCfgActionTypes.AddTaskRepeatCfgToTask, + ofType(TaskRepeatCfgActionTypes.AddTaskRepeatCfgToTask), + concatMap((a: AddTaskRepeatCfgToTask) => + this._taskService.getByIdOnce$(a.payload.taskId).pipe(take(1)), ), - concatMap((a: AddTaskRepeatCfgToTask) => this._taskService.getByIdOnce$(a.payload.taskId).pipe(take(1))), filter((task: TaskWithSubTasks) => typeof task.reminderId === 'string'), - map((task: TaskWithSubTasks) => new UnScheduleTask({ - id: task.id, - reminderId: task.reminderId as string - })), + map( + (task: TaskWithSubTasks) => + new UnScheduleTask({ + id: task.id, + reminderId: task.reminderId as string, + }), + ), ); constructor( @@ -164,11 +197,12 @@ export class TaskRepeatCfgEffects { private _workContextService: WorkContextService, private _taskRepeatCfgService: TaskRepeatCfgService, private _syncService: SyncService, - ) { - } + ) {} private _saveToLs([action, taskRepeatCfgState]: [Action, TaskRepeatCfgState]) { - this._persistenceService.taskRepeatCfg.saveState(taskRepeatCfgState, {isSyncModelChange: true}); + this._persistenceService.taskRepeatCfg.saveState(taskRepeatCfgState, { + isSyncModelChange: true, + }); } private _removeRepeatCfgFromArchiveTasks(repeatConfigId: string) { @@ -178,18 +212,19 @@ export class TaskRepeatCfgEffects { return; } - const newState = {...taskArchive}; + const newState = { ...taskArchive }; const ids = newState.ids as string[]; const tasksWithRepeatCfgId = ids - .map(id => newState.entities[id] as Task) + .map((id) => newState.entities[id] as Task) .filter((task: TaskWithSubTasks) => task.repeatCfgId === repeatConfigId); if (tasksWithRepeatCfgId && tasksWithRepeatCfgId.length) { - tasksWithRepeatCfgId.forEach((task: any) => task.repeatCfgId = null); - this._persistenceService.taskArchive.saveState(newState, {isSyncModelChange: true}); + tasksWithRepeatCfgId.forEach((task: any) => (task.repeatCfgId = null)); + this._persistenceService.taskArchive.saveState(newState, { + isSyncModelChange: true, + }); } }); } - } diff --git a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.reducer.ts b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.reducer.ts index f0fce820e..ee5bcaf93 100644 --- a/src/app/features/task-repeat-cfg/store/task-repeat-cfg.reducer.ts +++ b/src/app/features/task-repeat-cfg/store/task-repeat-cfg.reducer.ts @@ -7,7 +7,7 @@ import { TaskRepeatCfgActionTypes, UpdateTaskRepeatCfg, UpdateTaskRepeatCfgs, - UpsertTaskRepeatCfg + UpsertTaskRepeatCfg, } from './task-repeat-cfg.actions'; import { TaskRepeatCfg, TaskRepeatCfgState } from '../task-repeat-cfg.model'; import { createFeatureSelector, createSelector } from '@ngrx/store'; @@ -18,9 +18,19 @@ import { migrateTaskRepeatCfgState } from '../migrate-task-repeat-cfg-state.util export const TASK_REPEAT_CFG_FEATURE_NAME = 'taskRepeatCfg'; export const adapter: EntityAdapter = createEntityAdapter(); -export const selectTaskRepeatCfgFeatureState = createFeatureSelector(TASK_REPEAT_CFG_FEATURE_NAME); -export const {selectIds, selectEntities, selectAll, selectTotal} = adapter.getSelectors(); -export const selectAllTaskRepeatCfgs = createSelector(selectTaskRepeatCfgFeatureState, selectAll); +export const selectTaskRepeatCfgFeatureState = createFeatureSelector( + TASK_REPEAT_CFG_FEATURE_NAME, +); +export const { + selectIds, + selectEntities, + selectAll, + selectTotal, +} = adapter.getSelectors(); +export const selectAllTaskRepeatCfgs = createSelector( + selectTaskRepeatCfgFeatureState, + selectAll, +); export const selectTaskRepeatCfgById = createSelector( selectTaskRepeatCfgFeatureState, (state: TaskRepeatCfgState, props: { id: string }): TaskRepeatCfg => { @@ -29,11 +39,13 @@ export const selectTaskRepeatCfgById = createSelector( throw new Error('Missing taskRepeatCfg'); } return cfg; - } + }, ); export const selectTaskRepeatCfgByIdAllowUndefined = createSelector( selectTaskRepeatCfgFeatureState, - (state: TaskRepeatCfgState, props: { id: string }): TaskRepeatCfg | undefined => state.entities[props.id]); + (state: TaskRepeatCfgState, props: { id: string }): TaskRepeatCfg | undefined => + state.entities[props.id], +); export const initialTaskRepeatCfgState: TaskRepeatCfgState = adapter.getInitialState({ // additional entity state properties @@ -41,36 +53,47 @@ export const initialTaskRepeatCfgState: TaskRepeatCfgState = adapter.getInitialS export function taskRepeatCfgReducer( state: TaskRepeatCfgState = initialTaskRepeatCfgState, - action: TaskRepeatCfgActions + action: TaskRepeatCfgActions, ): TaskRepeatCfgState { - // TODO fix this hackyness once we use the new syntax everywhere if ((action.type as string) === loadAllData.type) { - const {appDataComplete}: { appDataComplete: AppDataComplete } = action as any; + const { appDataComplete }: { appDataComplete: AppDataComplete } = action as any; return appDataComplete.taskRepeatCfg - ? migrateTaskRepeatCfgState({...appDataComplete.taskRepeatCfg}) + ? migrateTaskRepeatCfgState({ ...appDataComplete.taskRepeatCfg }) : state; } switch (action.type) { case TaskRepeatCfgActionTypes.AddTaskRepeatCfgToTask: { - return adapter.addOne((action as AddTaskRepeatCfgToTask).payload.taskRepeatCfg, state); + return adapter.addOne( + (action as AddTaskRepeatCfgToTask).payload.taskRepeatCfg, + state, + ); } case TaskRepeatCfgActionTypes.UpdateTaskRepeatCfg: { - return adapter.updateOne((action as UpdateTaskRepeatCfg).payload.taskRepeatCfg, state); + return adapter.updateOne( + (action as UpdateTaskRepeatCfg).payload.taskRepeatCfg, + state, + ); } case TaskRepeatCfgActionTypes.UpdateTaskRepeatCfgs: { - const {ids, changes} = (action as UpdateTaskRepeatCfgs).payload; - return adapter.updateMany(ids.map(id => ({ - id, - changes, - })), state); + const { ids, changes } = (action as UpdateTaskRepeatCfgs).payload; + return adapter.updateMany( + ids.map((id) => ({ + id, + changes, + })), + state, + ); } case TaskRepeatCfgActionTypes.UpsertTaskRepeatCfg: { - return adapter.upsertOne((action as UpsertTaskRepeatCfg).payload.taskRepeatCfg, state); + return adapter.upsertOne( + (action as UpsertTaskRepeatCfg).payload.taskRepeatCfg, + state, + ); } case TaskRepeatCfgActionTypes.DeleteTaskRepeatCfg: { @@ -86,5 +109,3 @@ export function taskRepeatCfgReducer( } } } - - diff --git a/src/app/features/task-repeat-cfg/task-repeat-cfg.module.ts b/src/app/features/task-repeat-cfg/task-repeat-cfg.module.ts index e8ddb7db2..a12627f29 100644 --- a/src/app/features/task-repeat-cfg/task-repeat-cfg.module.ts +++ b/src/app/features/task-repeat-cfg/task-repeat-cfg.module.ts @@ -3,7 +3,10 @@ import { CommonModule } from '@angular/common'; import { StoreModule } from '@ngrx/store'; import { EffectsModule } from '@ngrx/effects'; import { TaskRepeatCfgEffects } from './store/task-repeat-cfg.effects'; -import { TASK_REPEAT_CFG_FEATURE_NAME, taskRepeatCfgReducer } from './store/task-repeat-cfg.reducer'; +import { + TASK_REPEAT_CFG_FEATURE_NAME, + taskRepeatCfgReducer, +} from './store/task-repeat-cfg.reducer'; import { DialogEditTaskRepeatCfgComponent } from './dialog-edit-task-repeat-cfg/dialog-edit-task-repeat-cfg.component'; import { UiModule } from '../../ui/ui.module'; import { FormsModule } from '@angular/forms'; @@ -14,10 +17,9 @@ import { FormsModule } from '@angular/forms'; UiModule, FormsModule, StoreModule.forFeature(TASK_REPEAT_CFG_FEATURE_NAME, taskRepeatCfgReducer), - EffectsModule.forFeature([TaskRepeatCfgEffects]) + EffectsModule.forFeature([TaskRepeatCfgEffects]), ], declarations: [DialogEditTaskRepeatCfgComponent], exports: [], }) -export class TaskRepeatCfgModule { -} +export class TaskRepeatCfgModule {} diff --git a/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts b/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts index 6543d01a0..3ea8e3d95 100644 --- a/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts +++ b/src/app/features/task-repeat-cfg/task-repeat-cfg.service.ts @@ -3,7 +3,7 @@ import { select, Store } from '@ngrx/store'; import { selectAllTaskRepeatCfgs, selectTaskRepeatCfgById, - selectTaskRepeatCfgByIdAllowUndefined + selectTaskRepeatCfgByIdAllowUndefined, } from './store/task-repeat-cfg.reducer'; import { AddTaskRepeatCfgToTask, @@ -14,7 +14,11 @@ import { UpsertTaskRepeatCfg, } from './store/task-repeat-cfg.actions'; import { Observable } from 'rxjs'; -import { TaskRepeatCfg, TaskRepeatCfgCopy, TaskRepeatCfgState } from './task-repeat-cfg.model'; +import { + TaskRepeatCfg, + TaskRepeatCfgCopy, + TaskRepeatCfgState, +} from './task-repeat-cfg.model'; import * as shortid from 'shortid'; import { DialogConfirmComponent } from '../../ui/dialog-confirm/dialog-confirm.component'; import { MatDialog } from '@angular/material/dialog'; @@ -24,61 +28,70 @@ import { T } from '../../t.const'; providedIn: 'root', }) export class TaskRepeatCfgService { - taskRepeatCfgs$: Observable = this._store$.pipe(select(selectAllTaskRepeatCfgs)); + taskRepeatCfgs$: Observable = this._store$.pipe( + select(selectAllTaskRepeatCfgs), + ); constructor( private _store$: Store, private _matDialog: MatDialog, - ) { - } + ) {} getTaskRepeatCfgById$(id: string): Observable { - return this._store$.pipe(select(selectTaskRepeatCfgById, {id})); + return this._store$.pipe(select(selectTaskRepeatCfgById, { id })); } getTaskRepeatCfgByIdAllowUndefined$(id: string): Observable { - return this._store$.pipe(select(selectTaskRepeatCfgByIdAllowUndefined, {id})); + return this._store$.pipe(select(selectTaskRepeatCfgByIdAllowUndefined, { id })); } - addTaskRepeatCfgToTask(taskId: string, projectId: string | null, taskRepeatCfg: Omit) { - this._store$.dispatch(new AddTaskRepeatCfgToTask({ - taskRepeatCfg: { - ...taskRepeatCfg, - projectId, - id: shortid() - }, - taskId, - })); + addTaskRepeatCfgToTask( + taskId: string, + projectId: string | null, + taskRepeatCfg: Omit, + ) { + this._store$.dispatch( + new AddTaskRepeatCfgToTask({ + taskRepeatCfg: { + ...taskRepeatCfg, + projectId, + id: shortid(), + }, + taskId, + }), + ); } deleteTaskRepeatCfg(id: string) { - this._store$.dispatch(new DeleteTaskRepeatCfg({id})); + this._store$.dispatch(new DeleteTaskRepeatCfg({ id })); } deleteTaskRepeatCfgsNoTaskCleanup(ids: string[]) { - this._store$.dispatch(new DeleteTaskRepeatCfgs({ids})); + this._store$.dispatch(new DeleteTaskRepeatCfgs({ ids })); } updateTaskRepeatCfg(id: string, changes: Partial) { - this._store$.dispatch(new UpdateTaskRepeatCfg({taskRepeatCfg: {id, changes}})); + this._store$.dispatch(new UpdateTaskRepeatCfg({ taskRepeatCfg: { id, changes } })); } updateTaskRepeatCfgs(ids: string[], changes: Partial) { - this._store$.dispatch(new UpdateTaskRepeatCfgs({ids, changes})); + this._store$.dispatch(new UpdateTaskRepeatCfgs({ ids, changes })); } upsertTaskRepeatCfg(taskRepeatCfg: TaskRepeatCfg) { - this._store$.dispatch(new UpsertTaskRepeatCfg({taskRepeatCfg})); + this._store$.dispatch(new UpsertTaskRepeatCfg({ taskRepeatCfg })); } deleteTaskRepeatCfgWithDialog(id: string) { - this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - message: T.F.TASK_REPEAT.D_CONFIRM_REMOVE.MSG, - okTxt: T.F.TASK_REPEAT.D_CONFIRM_REMOVE.OK, - } - }).afterClosed() + this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + message: T.F.TASK_REPEAT.D_CONFIRM_REMOVE.MSG, + okTxt: T.F.TASK_REPEAT.D_CONFIRM_REMOVE.OK, + }, + }) + .afterClosed() .subscribe((isConfirm: boolean) => { if (isConfirm) { this.deleteTaskRepeatCfg(id); diff --git a/src/app/features/tasks/add-task-bar/add-task-bar.component.ts b/src/app/features/tasks/add-task-bar/add-task-bar.component.ts index 568bf5f79..2525cdc64 100644 --- a/src/app/features/tasks/add-task-bar/add-task-bar.component.ts +++ b/src/app/features/tasks/add-task-bar/add-task-bar.component.ts @@ -8,11 +8,21 @@ import { Input, OnDestroy, Output, - ViewChild + ViewChild, } from '@angular/core'; import { FormControl } from '@angular/forms'; import { TaskService } from '../task.service'; -import { debounceTime, filter, first, map, startWith, switchMap, take, tap, withLatestFrom } from 'rxjs/operators'; +import { + debounceTime, + filter, + first, + map, + startWith, + switchMap, + take, + tap, + withLatestFrom, +} from 'rxjs/operators'; import { JiraIssue } from '../../issue/providers/jira/jira-issue/jira-issue.model'; import { BehaviorSubject, forkJoin, from, Observable, of, Subscription, zip } from 'rxjs'; import { IssueService } from '../../issue/issue.service'; @@ -38,7 +48,7 @@ import { SS_TODO_TMP } from '../../../core/persistence/ls-keys.const'; templateUrl: './add-task-bar.component.html', styleUrls: ['./add-task-bar.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [slideAnimation, fadeAnimation] + animations: [slideAnimation, fadeAnimation], }) export class AddTaskBarComponent implements AfterViewInit, OnDestroy { @Input() isAddToBacklog: boolean = false; @@ -50,7 +60,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { @Output() blurred: EventEmitter = new EventEmitter(); @Output() done: EventEmitter = new EventEmitter(); - @ViewChild('inputEl', {static: true}) inputEl?: ElementRef; + @ViewChild('inputEl', { static: true }) inputEl?: ElementRef; T: typeof T = T; isLoading$: BehaviorSubject = new BehaviorSubject(false); @@ -58,33 +68,40 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { taskSuggestionsCtrl: FormControl = new FormControl(); - filteredIssueSuggestions$: Observable = this.taskSuggestionsCtrl.valueChanges.pipe( + filteredIssueSuggestions$: Observable< + AddTaskSuggestion[] + > = this.taskSuggestionsCtrl.valueChanges.pipe( debounceTime(300), tap(() => this.isLoading$.next(true)), withLatestFrom(this._workContextService.activeWorkContextTypeAndId$), - switchMap(([searchTerm, {activeType, activeId}]) => (activeType === WorkContextType.PROJECT) - ? this._searchForProject$(searchTerm) - : this._searchForTag$(searchTerm, activeId) + switchMap(([searchTerm, { activeType, activeId }]) => + activeType === WorkContextType.PROJECT + ? this._searchForProject$(searchTerm) + : this._searchForTag$(searchTerm, activeId), ) as any, // don't show issues twice // NOTE: this only works because backlog items come first - map((items: AddTaskSuggestion[]) => items.reduce( - (unique: AddTaskSuggestion[], item: AddTaskSuggestion) => { - return (item.issueData && unique.find( - // NOTE: we check defined because we don't want to run into - // false == false or similar - u => !!u.taskIssueId && !!item.issueData && u.taskIssueId === item.issueData.id - )) + map((items: AddTaskSuggestion[]) => + items.reduce((unique: AddTaskSuggestion[], item: AddTaskSuggestion) => { + return item.issueData && + unique.find( + // NOTE: we check defined because we don't want to run into + // false == false or similar + (u) => + !!u.taskIssueId && !!item.issueData && u.taskIssueId === item.issueData.id, + ) ? unique : [...unique, item]; - }, []) + }, []), ), tap(() => { this.isLoading$.next(false); }), ); - activatedIssueTask$: BehaviorSubject = new BehaviorSubject(null); + activatedIssueTask$: BehaviorSubject = new BehaviorSubject( + null, + ); activatedIssueTask: AddTaskSuggestion | null = null; shortSyntaxTags: { @@ -92,19 +109,27 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { color: string; icon: string; }[] = []; - shortSyntaxTags$: Observable<{ - title: string; - color: string; - icon: string; - }[]> = this.taskSuggestionsCtrl.valueChanges.pipe( - filter(val => typeof val === 'string'), - withLatestFrom(this._tagService.tags$, this._projectService.list$, this._workContextService.activeWorkContext$), - map(([val, tags, projects, activeWorkContext]) => shortSyntaxToTags({ - val, - tags, - projects, - defaultColor: activeWorkContext.theme.primary - })), + shortSyntaxTags$: Observable< + { + title: string; + color: string; + icon: string; + }[] + > = this.taskSuggestionsCtrl.valueChanges.pipe( + filter((val) => typeof val === 'string'), + withLatestFrom( + this._tagService.tags$, + this._projectService.list$, + this._workContextService.activeWorkContext$, + ), + map(([val, tags, projects, activeWorkContext]) => + shortSyntaxToTags({ + val, + tags, + projects, + defaultColor: activeWorkContext.theme.primary, + }), + ), startWith([]), ); @@ -128,9 +153,11 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { private _tagService: TagService, private _cd: ChangeDetectorRef, ) { - this._subs.add(this.activatedIssueTask$.subscribe((v) => this.activatedIssueTask = v)); - this._subs.add(this.shortSyntaxTags$.subscribe((v) => this.shortSyntaxTags = v)); - this._subs.add(this.inputVal$.subscribe((v) => this.inputVal = v)); + this._subs.add( + this.activatedIssueTask$.subscribe((v) => (this.activatedIssueTask = v)), + ); + this._subs.add(this.shortSyntaxTags$.subscribe((v) => (this.shortSyntaxTags = v))); + this._subs.add(this.inputVal$.subscribe((v) => (this.inputVal = v))); } ngAfterViewInit(): void { @@ -141,15 +168,18 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { }); this._attachKeyDownHandlerTimeout = window.setTimeout(() => { - (this.inputEl as ElementRef).nativeElement.addEventListener('keydown', (ev: KeyboardEvent) => { - if (ev.key === 'Escape') { - this.blurred.emit(); - } else if (ev.key === '1' && ev.ctrlKey) { - this.isAddToBacklog = !this.isAddToBacklog; - this._cd.detectChanges(); - ev.preventDefault(); - } - }); + (this.inputEl as ElementRef).nativeElement.addEventListener( + 'keydown', + (ev: KeyboardEvent) => { + if (ev.key === 'Escape') { + this.blurred.emit(); + } else if (ev.key === '1' && ev.ctrlKey) { + this.isAddToBacklog = !this.isAddToBacklog; + this._cd.detectChanges(); + ev.preventDefault(); + } + }, + ); }); const savedTodo = sessionStorage.getItem(SS_TODO_TMP) || ''; @@ -192,10 +222,16 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { onBlur(ev: FocusEvent) { const relatedTarget: HTMLElement = ev.relatedTarget as HTMLElement; - if (!relatedTarget || (relatedTarget && - !relatedTarget.className.includes('close-btn') && - !relatedTarget.className.includes('switch-add-to-btn'))) { - sessionStorage.setItem(SS_TODO_TMP, (this.inputEl as ElementRef).nativeElement.value); + if ( + !relatedTarget || + (relatedTarget && + !relatedTarget.className.includes('close-btn') && + !relatedTarget.className.includes('switch-add-to-btn')) + ) { + sessionStorage.setItem( + SS_TODO_TMP, + (this.inputEl as ElementRef).nativeElement.value, + ); } if (relatedTarget && relatedTarget.className.includes('switch-add-to-btn')) { (this.inputEl as ElementRef).nativeElement.focus(); @@ -236,7 +272,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { newTaskStr, this.isAddToBacklog, {}, - this.isAddToBottom + this.isAddToBottom, ); } else if (this.doubleEnterCount > 0) { this.blurred.emit(); @@ -244,20 +280,22 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { } else if (this.isDoubleEnterMode) { this.doubleEnterCount++; } - } else if (item.taskId && item.isFromOtherContextAndTagOnlySearch) { this._lastAddedTaskId = item.taskId; const task = await this._taskService.getByIdOnce$(item.taskId).toPromise(); - this._taskService.updateTags(task, [...task.tagIds, this._workContextService.activeWorkContextId as string], task.tagIds); + this._taskService.updateTags( + task, + [...task.tagIds, this._workContextService.activeWorkContextId as string], + task.tagIds, + ); this._snackService.open({ ico: 'playlist_add', msg: T.F.TASK.S.FOUND_MOVE_FROM_OTHER_LIST, translateParams: { title: truncate(item.title), - contextTitle: (item.ctx && item.ctx.title) - ? truncate(item.ctx.title) - : '~the void~' + contextTitle: + item.ctx && item.ctx.title ? truncate(item.ctx.title) : '~the void~', }, }); // NOTE: it's important that this comes before the issue check @@ -268,15 +306,17 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { this._snackService.open({ ico: 'arrow_upward', msg: T.F.TASK.S.FOUND_MOVE_FROM_BACKLOG, - translateParams: {title: item.title}, + translateParams: { title: item.title }, }); } else { if (!item.issueType || !item.issueData) { throw new Error('No issueData'); } - const res = await this._taskService.checkForTaskWithIssueInProject(item.issueData.id, + const res = await this._taskService.checkForTaskWithIssueInProject( + item.issueData.id, item.issueType, - this._workContextService.activeWorkContextId as string); + this._workContextService.activeWorkContextId as string, + ); if (!res) { this._lastAddedTaskId = await this._issueService.addTaskWithIssue( item.issueType, @@ -290,7 +330,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { this._snackService.open({ ico: 'info', msg: T.F.TASK.S.FOUND_RESTORE_FROM_ARCHIVE, - translateParams: {title: res.task.title}, + translateParams: { title: res.task.title }, }); } else { this._lastAddedTaskId = res.task.id; @@ -298,7 +338,7 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { this._snackService.open({ ico: 'arrow_upward', msg: T.F.TASK.S.FOUND_MOVE_FROM_BACKLOG, - translateParams: {title: res.task.title}, + translateParams: { title: res.task.title }, }); } } @@ -307,7 +347,10 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { this._isAddInProgress = false; } - private async _getCtxForTaskSuggestion({projectId, tagIds}: AddTaskSuggestion): Promise { + private async _getCtxForTaskSuggestion({ + projectId, + tagIds, + }: AddTaskSuggestion): Promise { if (projectId) { return await this._projectService.getByIdOnce$(projectId).toPromise(); } else { @@ -329,62 +372,79 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { } // TODO improve typing - private _searchForProject$(searchTerm: string): Observable<(AddTaskSuggestion | SearchResultItem)[]> { + private _searchForProject$( + searchTerm: string, + ): Observable<(AddTaskSuggestion | SearchResultItem)[]> { if (searchTerm && searchTerm.length > 0) { const backlog$ = this._workContextService.backlogTasks$.pipe( - map(tasks => tasks - .filter(task => this._filterBacklog(searchTerm, task)) - .map((task): AddTaskSuggestion => ({ - title: task.title, - taskId: task.id, - taskIssueId: task.issueId || undefined, - issueType: task.issueType || undefined, - })) - ) + map((tasks) => + tasks + .filter((task) => this._filterBacklog(searchTerm, task)) + .map( + (task): AddTaskSuggestion => ({ + title: task.title, + taskId: task.id, + taskIssueId: task.issueId || undefined, + issueType: task.issueType || undefined, + }), + ), + ), + ); + const issues$ = this._issueService.searchIssues$( + searchTerm, + this._workContextService.activeWorkContextId as string, ); - const issues$ = this._issueService.searchIssues$(searchTerm, this._workContextService.activeWorkContextId as string); return zip(backlog$, issues$).pipe( - map(([backlog, issues]) => ([...backlog, ...issues])), + map(([backlog, issues]) => [...backlog, ...issues]), ); } else { return of([]); } } - private _searchForTag$(searchTerm: string, currentTagId: string): Observable<(AddTaskSuggestion | SearchResultItem)[]> { + private _searchForTag$( + searchTerm: string, + currentTagId: string, + ): Observable<(AddTaskSuggestion | SearchResultItem)[]> { if (searchTerm && searchTerm.length > 0) { return this._taskService.getAllParentWithoutTag$(currentTagId).pipe( take(1), - map(tasks => tasks - .filter(task => this._filterBacklog(searchTerm, task)) - .map((task): AddTaskSuggestion => { - return { - title: task.title, - taskId: task.id, - taskIssueId: task.issueId || undefined, - issueType: task.issueType || undefined, - projectId: task.projectId || undefined, + map((tasks) => + tasks + .filter((task) => this._filterBacklog(searchTerm, task)) + .map( + (task): AddTaskSuggestion => { + return { + title: task.title, + taskId: task.id, + taskIssueId: task.issueId || undefined, + issueType: task.issueType || undefined, + projectId: task.projectId || undefined, - isFromOtherContextAndTagOnlySearch: true, - tagIds: task.tagIds, - }; - }) + isFromOtherContextAndTagOnlySearch: true, + tagIds: task.tagIds, + }; + }, + ), ), - switchMap(tasks => !!(tasks.length) - ? forkJoin(tasks.map(task => { - const isFromProject = !!task.projectId; - return from(this._getCtxForTaskSuggestion(task)).pipe( - first(), - map(ctx => ({ - ...task, - ctx: { - ...ctx, - icon: (ctx && (ctx as Tag).icon) || (isFromProject && 'list') - }, - })), - ); - })) - : of([]) + switchMap((tasks) => + !!tasks.length + ? forkJoin( + tasks.map((task) => { + const isFromProject = !!task.projectId; + return from(this._getCtxForTaskSuggestion(task)).pipe( + first(), + map((ctx) => ({ + ...task, + ctx: { + ...ctx, + icon: (ctx && (ctx as Tag).icon) || (isFromProject && 'list'), + }, + })), + ); + }), + ) + : of([]), ), // TODO revisit typing here ) as any; diff --git a/src/app/features/tasks/add-task-bar/short-syntax-to-tags.util.ts b/src/app/features/tasks/add-task-bar/short-syntax-to-tags.util.ts index de02af474..6cbacb685 100644 --- a/src/app/features/tasks/add-task-bar/short-syntax-to-tags.util.ts +++ b/src/app/features/tasks/add-task-bar/short-syntax-to-tags.util.ts @@ -5,7 +5,12 @@ import { Tag } from '../../tag/tag.model'; import { Project } from '../../project/project.model'; import { getWorklogStr } from '../../../util/get-work-log-str'; -export const shortSyntaxToTags = ({val, tags, projects, defaultColor}: { +export const shortSyntaxToTags = ({ + val, + tags, + projects, + defaultColor, +}: { val: string; tags: Tag[]; projects: Project[]; @@ -15,11 +20,15 @@ export const shortSyntaxToTags = ({val, tags, projects, defaultColor}: { color: string; icon: string; }[] => { - const r = shortSyntax({ - title: val, - tagIds: [], - projectId: undefined, - }, tags, projects); + const r = shortSyntax( + { + title: val, + tagIds: [], + projectId: undefined, + }, + tags, + projects, + ); const shortSyntaxTags: { title: string; color: string; @@ -31,14 +40,14 @@ export const shortSyntaxToTags = ({val, tags, projects, defaultColor}: { } if (r.projectId) { - const project = projects.find(p => p.id === r.projectId); + const project = projects.find((p) => p.id === r.projectId); if (!project) { throw new Error('Project not found'); } shortSyntaxTags.push({ title: project.title, color: project.theme.primary, - icon: 'list' + icon: 'list', }); } @@ -53,7 +62,7 @@ export const shortSyntaxToTags = ({val, tags, projects, defaultColor}: { shortSyntaxTags.push({ title: time, color: defaultColor, - icon: 'timer' + icon: 'timer', }); } @@ -67,25 +76,25 @@ export const shortSyntaxToTags = ({val, tags, projects, defaultColor}: { // } if (r.taskChanges.tagIds) { - r.taskChanges.tagIds.forEach(tagId => { - const tag = tags.find(p => p.id === tagId); + r.taskChanges.tagIds.forEach((tagId) => { + const tag = tags.find((p) => p.id === tagId); if (!tag) { throw new Error('Tag not found'); } shortSyntaxTags.push({ title: tag.title, color: tag.color || tag.theme.primary, - icon: tag.icon || 'style' + icon: tag.icon || 'style', }); }); } if (r.newTagTitles) { - r.newTagTitles.forEach(tagTitle => { + r.newTagTitles.forEach((tagTitle) => { shortSyntaxTags.push({ title: tagTitle, color: DEFAULT_TODAY_TAG_COLOR, - icon: 'style' + icon: 'style', }); }); } diff --git a/src/app/features/tasks/dialog-add-task-reminder/dialog-add-task-reminder.component.ts b/src/app/features/tasks/dialog-add-task-reminder/dialog-add-task-reminder.component.ts index 2364939a0..988b3975b 100644 --- a/src/app/features/tasks/dialog-add-task-reminder/dialog-add-task-reminder.component.ts +++ b/src/app/features/tasks/dialog-add-task-reminder/dialog-add-task-reminder.component.ts @@ -13,7 +13,7 @@ import { millisecondsDiffToRemindOption } from '../util/remind-option-to-millise selector: 'dialog-add-task-reminder', templateUrl: './dialog-add-task-reminder.component.html', styleUrls: ['./dialog-add-task-reminder.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogAddTaskReminderComponent { T: typeof T = T; @@ -24,31 +24,39 @@ export class DialogAddTaskReminderComponent { isEdit: boolean = !!(this.reminder && this.reminder.id); dateTime?: number = this.task.plannedAt || undefined; - isShowMoveToBacklog: boolean = (!this.isEdit && !!this.task.projectId && this.task.parentId === null); - isMoveToBacklog: boolean = (this.isShowMoveToBacklog); + isShowMoveToBacklog: boolean = + !this.isEdit && !!this.task.projectId && this.task.parentId === null; + isMoveToBacklog: boolean = this.isShowMoveToBacklog; // TODO make translatable - remindAvailableOptions: TaskReminderOption[] = [{ - // id: TaskReminderOptionId.DoNotRemind, - // title: 'Dont show reminder', - // }, { - id: TaskReminderOptionId.AtStart, - title: 'when it starts', - }, { - id: TaskReminderOptionId.m5, - title: '5 minutes before it starts', - }, { - id: TaskReminderOptionId.m10, - title: '10 minutes before it starts', - }, { - id: TaskReminderOptionId.m15, - title: '15 minutes before it starts', - }, { - id: TaskReminderOptionId.m30, - title: '30 minutes before it starts', - }, { - id: TaskReminderOptionId.h1, - title: '1 hour before it starts', - }]; + remindAvailableOptions: TaskReminderOption[] = [ + { + // id: TaskReminderOptionId.DoNotRemind, + // title: 'Dont show reminder', + // }, { + id: TaskReminderOptionId.AtStart, + title: 'when it starts', + }, + { + id: TaskReminderOptionId.m5, + title: '5 minutes before it starts', + }, + { + id: TaskReminderOptionId.m10, + title: '10 minutes before it starts', + }, + { + id: TaskReminderOptionId.m15, + title: '15 minutes before it starts', + }, + { + id: TaskReminderOptionId.m30, + title: '30 minutes before it starts', + }, + { + id: TaskReminderOptionId.h1, + title: '1 hour before it starts', + }, + ]; reminderCfgId: TaskReminderOptionId; constructor( @@ -58,14 +66,17 @@ export class DialogAddTaskReminderComponent { @Inject(MAT_DIALOG_DATA) public data: AddTaskReminderInterface, ) { if (this.isEdit) { - this.reminderCfgId = millisecondsDiffToRemindOption(this.task.plannedAt as number, this.reminder?.remindAt); + this.reminderCfgId = millisecondsDiffToRemindOption( + this.task.plannedAt as number, + this.reminder?.remindAt, + ); } else { this.reminderCfgId = TaskReminderOptionId.AtStart; } } // NOTE: throttle is used as quick way to prevent multiple submits - @throttle(2000, {leading: true, trailing: false}) + @throttle(2000, { leading: true, trailing: false }) save() { const timestamp = this.dateTime; @@ -94,7 +105,7 @@ export class DialogAddTaskReminderComponent { } // NOTE: throttle is used as quick way to prevent multiple submits - @throttle(2000, {leading: true, trailing: false}) + @throttle(2000, { leading: true, trailing: false }) remove() { if (!this.reminder || !this.reminder.id) { console.log(this.reminder, this.task); @@ -112,4 +123,3 @@ export class DialogAddTaskReminderComponent { return i; } } - diff --git a/src/app/features/tasks/dialog-add-time-estimate-for-other-day/dialog-add-time-estimate-for-other-day.component.ts b/src/app/features/tasks/dialog-add-time-estimate-for-other-day/dialog-add-time-estimate-for-other-day.component.ts index bc485edc3..53085e6f6 100644 --- a/src/app/features/tasks/dialog-add-time-estimate-for-other-day/dialog-add-time-estimate-for-other-day.component.ts +++ b/src/app/features/tasks/dialog-add-time-estimate-for-other-day/dialog-add-time-estimate-for-other-day.component.ts @@ -17,10 +17,12 @@ export class DialogAddTimeEstimateForOtherDayComponent { T: typeof T = T; newEntry: NewTimeEntry; - constructor(private _matDialogRef: MatDialogRef) { + constructor( + private _matDialogRef: MatDialogRef, + ) { this.newEntry = { date: '', - timeSpent: 0 + timeSpent: 0, }; } diff --git a/src/app/features/tasks/dialog-time-estimate/dialog-time-estimate.component.ts b/src/app/features/tasks/dialog-time-estimate/dialog-time-estimate.component.ts index 4110cf0c6..f8f8c0c45 100644 --- a/src/app/features/tasks/dialog-time-estimate/dialog-time-estimate.component.ts +++ b/src/app/features/tasks/dialog-time-estimate/dialog-time-estimate.component.ts @@ -1,4 +1,9 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, Inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, +} from '@angular/core'; import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from '@angular/material/dialog'; import { Task, TaskCopy, TimeSpentOnDayCopy } from '../task.model'; import { TaskService } from '../task.service'; @@ -21,18 +26,19 @@ export class DialogTimeEstimateComponent { taskCopy: TaskCopy; timeSpentOnDayCopy: TimeSpentOnDayCopy; - constructor(private _matDialogRef: MatDialogRef, + constructor( + private _matDialogRef: MatDialogRef, private _matDialog: MatDialog, private _taskService: TaskService, private _cd: ChangeDetectorRef, - @Inject(MAT_DIALOG_DATA) public data: any) { + @Inject(MAT_DIALOG_DATA) public data: any, + ) { this.task = this.data.task; this.todayStr = getTodayStr(); this._taskService = _taskService; this.taskCopy = createTaskCopy(this.task); this.timeSpentOnDayCopy = this.taskCopy.timeSpentOnDay || {}; console.log(this.timeSpentOnDayCopy); - } submit() { @@ -47,16 +53,19 @@ export class DialogTimeEstimateComponent { } showAddForAnotherDayForm() { - this._matDialog.open(DialogAddTimeEstimateForOtherDayComponent).afterClosed().subscribe((result) => { - if (result && result.timeSpent > 0 && result.date) { - this.timeSpentOnDayCopy = { - ...this.timeSpentOnDayCopy, - [getWorklogStr(result.date)]: result.timeSpent, - }; - this.taskCopy.timeSpentOnDay = this.timeSpentOnDayCopy; - this._cd.detectChanges(); - } - }); + this._matDialog + .open(DialogAddTimeEstimateForOtherDayComponent) + .afterClosed() + .subscribe((result) => { + if (result && result.timeSpent > 0 && result.date) { + this.timeSpentOnDayCopy = { + ...this.timeSpentOnDayCopy, + [getWorklogStr(result.date)]: result.timeSpent, + }; + this.taskCopy.timeSpentOnDay = this.timeSpentOnDayCopy; + this._cd.detectChanges(); + } + }); } deleteValue(strDate: string) { diff --git a/src/app/features/tasks/dialog-view-task-reminders/dialog-view-task-reminders.component.ts b/src/app/features/tasks/dialog-view-task-reminders/dialog-view-task-reminders.component.ts index ba9527bcb..751086190 100644 --- a/src/app/features/tasks/dialog-view-task-reminders/dialog-view-task-reminders.component.ts +++ b/src/app/features/tasks/dialog-view-task-reminders/dialog-view-task-reminders.component.ts @@ -28,23 +28,30 @@ export class DialogViewTaskRemindersComponent implements OnDestroy { isDisableControls: boolean = false; reminders$: BehaviorSubject = new BehaviorSubject(this.data.reminders); tasks$: Observable = this.reminders$.pipe( - switchMap((reminders) => this._taskService.getByIdsLive$(reminders.map(r => r.relatedId)).pipe( - first(), - map((tasks: Task[]) => tasks - .filter(task => !!task) - .map((task): TaskWithReminderData => ({ - ...task, - reminderData: reminders.find(r => r.relatedId === task.id) as Reminder - })) - ) - )), + switchMap((reminders) => + this._taskService.getByIdsLive$(reminders.map((r) => r.relatedId)).pipe( + first(), + map((tasks: Task[]) => + tasks + .filter((task) => !!task) + .map( + (task): TaskWithReminderData => ({ + ...task, + reminderData: reminders.find((r) => r.relatedId === task.id) as Reminder, + }), + ), + ), + ), + ), ); isSingleOnToday$: Observable = this.tasks$.pipe( - map((tasks) => (tasks.length === 1) && tasks[0] && tasks[0].tagIds.includes(TODAY_TAG.id)), + map( + (tasks) => tasks.length === 1 && tasks[0] && tasks[0].tagIds.includes(TODAY_TAG.id), + ), ); isMultiple$: Observable = this.tasks$.pipe( - map(tasks => tasks.length > 1), - takeWhile(isMultiple => !isMultiple, true) + map((tasks) => tasks.length > 1), + takeWhile((isMultiple) => !isMultiple, true), ); isMultiple: boolean = false; @@ -58,13 +65,19 @@ export class DialogViewTaskRemindersComponent implements OnDestroy { @Inject(MAT_DIALOG_DATA) public data: { reminders: Reminder[] }, ) { // this._matDialogRef.disableClose = true; - this._subs.add(this._reminderService.onReloadModel$.subscribe(() => { - this._close(); - })); - this._subs.add(this._reminderService.onRemindersActive$.subscribe(reminders => { - this.reminders$.next(reminders); - })); - this._subs.add(this.isMultiple$.subscribe(isMultiple => this.isMultiple = isMultiple)); + this._subs.add( + this._reminderService.onReloadModel$.subscribe(() => { + this._close(); + }), + ); + this._subs.add( + this._reminderService.onRemindersActive$.subscribe((reminders) => { + this.reminders$.next(reminders); + }), + ); + this._subs.add( + this.isMultiple$.subscribe((isMultiple) => (this.isMultiple = isMultiple)), + ); } ngOnDestroy(): void { @@ -74,8 +87,15 @@ export class DialogViewTaskRemindersComponent implements OnDestroy { async addToToday(task: TaskWithReminderData) { // NOTE: we need to account for the parent task as well if (task.parentId) { - const parent = await this._taskService.getByIdOnce$(task.parentId).pipe(first()).toPromise(); - this._taskService.updateTags(parent, [TODAY_TAG.id, ...parent.tagIds], parent.tagIds); + const parent = await this._taskService + .getByIdOnce$(task.parentId) + .pipe(first()) + .toPromise(); + this._taskService.updateTags( + parent, + [TODAY_TAG.id, ...parent.tagIds], + parent.tagIds, + ); this.dismiss(task); } else { this._taskService.updateTags(task, [TODAY_TAG.id, ...task.tagIds], task.tagIds); @@ -93,28 +113,33 @@ export class DialogViewTaskRemindersComponent implements OnDestroy { snooze(task: TaskWithReminderData, snoozeInMinutes: number) { this._reminderService.updateReminder(task.reminderData.id, { - remindAt: Date.now() + (snoozeInMinutes * M) + remindAt: Date.now() + snoozeInMinutes * M, }); this._removeFromList(task.reminderId as string); } snoozeUntilTomorrow(task: TaskWithReminderData) { this._reminderService.updateReminder(task.reminderData.id, { - remindAt: getTomorrow().getTime() + remindAt: getTomorrow().getTime(), }); this._removeFromList(task.reminderId as string); } editReminder(task: TaskWithReminderData, isCloseAfter: boolean = false) { - this._subs.add(this._matDialog.open(DialogAddTaskReminderComponent, { - restoreFocus: true, - data: {task} as AddTaskReminderInterface - }).afterClosed().subscribe(() => { - this._removeFromList(task.reminderId as string); - if (isCloseAfter) { - this._close(); - } - })); + this._subs.add( + this._matDialog + .open(DialogAddTaskReminderComponent, { + restoreFocus: true, + data: { task } as AddTaskReminderInterface, + }) + .afterClosed() + .subscribe(() => { + this._removeFromList(task.reminderId as string); + if (isCloseAfter) { + this._close(); + } + }), + ); } trackById(i: number, task: Task) { @@ -127,7 +152,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy { this.isDisableControls = true; this.reminders$.getValue().forEach((reminder) => { this._reminderService.updateReminder(reminder.id, { - remindAt: Date.now() + (snoozeInMinutes * M) + remindAt: Date.now() + snoozeInMinutes * M, }); }); this._close(); @@ -138,7 +163,7 @@ export class DialogViewTaskRemindersComponent implements OnDestroy { const tomorrow = getTomorrow().getTime(); this.reminders$.getValue().forEach((reminder) => { this._reminderService.updateReminder(reminder.id, { - remindAt: tomorrow + remindAt: tomorrow, }); }); this._close(); @@ -146,17 +171,21 @@ export class DialogViewTaskRemindersComponent implements OnDestroy { async addAllToToday() { this.isDisableControls = true; - const tasksToDismiss = await this.tasks$.pipe(first()).toPromise() as TaskWithReminderData[]; - const mainTasks = tasksToDismiss.filter(t => !t.parentId); + const tasksToDismiss = (await this.tasks$ + .pipe(first()) + .toPromise()) as TaskWithReminderData[]; + const mainTasks = tasksToDismiss.filter((t) => !t.parentId); const parentIds: string[] = unique( - tasksToDismiss - .map(t => t.parentId as string) - .filter(pid => !!pid) + tasksToDismiss.map((t) => t.parentId as string).filter((pid) => !!pid), + ); + const parents = await Promise.all( + parentIds.map((parentId) => + this._taskService.getByIdOnce$(parentId).pipe(first()).toPromise(), + ), ); - const parents = await Promise.all(parentIds.map(parentId => this._taskService.getByIdOnce$(parentId).pipe(first()).toPromise())); const updateTagTasks = [...parents, ...mainTasks]; - updateTagTasks.forEach(task => { + updateTagTasks.forEach((task) => { this._taskService.updateTags(task, [TODAY_TAG.id, ...task.tagIds], task.tagIds); }); tasksToDismiss.forEach((task: TaskWithReminderData) => { @@ -197,7 +226,9 @@ export class DialogViewTaskRemindersComponent implements OnDestroy { } private _removeFromList(reminderId: string) { - const newReminders = this.reminders$.getValue().filter(reminder => reminder.id !== reminderId); + const newReminders = this.reminders$ + .getValue() + .filter((reminder) => reminder.id !== reminderId); if (newReminders.length <= 0) { this._close(); } else { diff --git a/src/app/features/tasks/filter-done-tasks.pipe.spec.ts b/src/app/features/tasks/filter-done-tasks.pipe.spec.ts index fd18c0136..1d0b1e23a 100644 --- a/src/app/features/tasks/filter-done-tasks.pipe.spec.ts +++ b/src/app/features/tasks/filter-done-tasks.pipe.spec.ts @@ -14,17 +14,32 @@ describe('filterDoneTasks()', () => { }); it('should filter done', () => { - const r = filterDoneTasks([{isDone: true}, {isDone: false}, {isDone: true}] as any, null, true, false); - expect(r).toEqual([{isDone: false}]); + const r = filterDoneTasks( + [{ isDone: true }, { isDone: false }, { isDone: true }] as any, + null, + true, + false, + ); + expect(r).toEqual([{ isDone: false }]); }); it('should filter all but current', () => { - const r = filterDoneTasks([{id: 'CURRENT'}, {id: '1'}, {id: '2'}, {id: '3'}] as any, 'CURRENT', false, true); - expect(r).toEqual([{id: 'CURRENT'}]); + const r = filterDoneTasks( + [{ id: 'CURRENT' }, { id: '1' }, { id: '2' }, { id: '3' }] as any, + 'CURRENT', + false, + true, + ); + expect(r).toEqual([{ id: 'CURRENT' }]); }); it('should not filter', () => { - const r = filterDoneTasks([{isDone: true}, {isDone: false}, {isDone: true}] as any, null, false, false); - expect(r).toEqual([{isDone: true}, {isDone: false}, {isDone: true}]); + const r = filterDoneTasks( + [{ isDone: true }, { isDone: false }, { isDone: true }] as any, + null, + false, + false, + ); + expect(r).toEqual([{ isDone: true }, { isDone: false }, { isDone: true }]); }); }); diff --git a/src/app/features/tasks/filter-done-tasks.pipe.ts b/src/app/features/tasks/filter-done-tasks.pipe.ts index d7fabf5e1..b191194d8 100644 --- a/src/app/features/tasks/filter-done-tasks.pipe.ts +++ b/src/app/features/tasks/filter-done-tasks.pipe.ts @@ -1,18 +1,23 @@ import { Pipe, PipeTransform } from '@angular/core'; import { TaskWithSubTasks } from './task.model'; -export const filterDoneTasks = (tasks: TaskWithSubTasks[], currentTaskId: string | null, isFilterDone: boolean, isFilterAll: boolean): any => { +export const filterDoneTasks = ( + tasks: TaskWithSubTasks[], + currentTaskId: string | null, + isFilterDone: boolean, + isFilterAll: boolean, +): any => { return isFilterDone - ? tasks.filter(task => !task.isDone) - : (isFilterAll) - ? !!currentTaskId - ? tasks.filter(task => task.id === currentTaskId) - : [] - : tasks; + ? tasks.filter((task) => !task.isDone) + : isFilterAll + ? !!currentTaskId + ? tasks.filter((task) => task.id === currentTaskId) + : [] + : tasks; }; @Pipe({ - name: 'filterDoneTasks' + name: 'filterDoneTasks', }) export class FilterDoneTasksPipe implements PipeTransform { transform: (value: any, ...args: any[]) => any = filterDoneTasks; diff --git a/src/app/features/tasks/migrate-task-state.util.ts b/src/app/features/tasks/migrate-task-state.util.ts index 60fa44701..406a72446 100644 --- a/src/app/features/tasks/migrate-task-state.util.ts +++ b/src/app/features/tasks/migrate-task-state.util.ts @@ -14,7 +14,9 @@ export const migrateTaskState = (taskState: TaskState): TaskState => { return taskState; } - const taskEntities: Dictionary = _addProjectIdForSubTasksAndRemoveTags({...taskState.entities}); + const taskEntities: Dictionary = _addProjectIdForSubTasksAndRemoveTags({ + ...taskState.entities, + }); Object.keys(taskEntities).forEach((key) => { taskEntities[key] = _addNewIssueFields(taskEntities[key] as TaskCopy); @@ -25,17 +27,18 @@ export const migrateTaskState = (taskState: TaskState): TaskState => { taskEntities[key] = _convertToWesternArabicDateKeys(taskEntities[key] as TaskCopy); }); - return {...taskState, entities: taskEntities, [MODEL_VERSION_KEY]: MODEL_VERSION}; + return { ...taskState, entities: taskEntities, [MODEL_VERSION_KEY]: MODEL_VERSION }; }; -export const migrateTaskArchiveState = ( - taskArchiveState: TaskArchive, -): TaskArchive => { - if (!taskArchiveState || (taskArchiveState && taskArchiveState[MODEL_VERSION_KEY] === MODEL_VERSION)) { +export const migrateTaskArchiveState = (taskArchiveState: TaskArchive): TaskArchive => { + if ( + !taskArchiveState || + (taskArchiveState && taskArchiveState[MODEL_VERSION_KEY] === MODEL_VERSION) + ) { return taskArchiveState; } - const taskEntities: Dictionary = {...taskArchiveState.entities}; + const taskEntities: Dictionary = { ...taskArchiveState.entities }; Object.keys(taskEntities).forEach((key) => { taskEntities[key] = _addNewIssueFields(taskEntities[key] as ArchiveTask); taskEntities[key] = _replaceLegacyGitType(taskEntities[key] as ArchiveTask); @@ -44,34 +47,34 @@ export const migrateTaskArchiveState = ( }); taskArchiveState[MODEL_VERSION_KEY] = MODEL_VERSION; - return {...taskArchiveState, entities: taskEntities as Dictionary}; + return { ...taskArchiveState, entities: taskEntities as Dictionary }; }; const _addTagIds = (task: Task): Task => { - return (task.hasOwnProperty('tagIds')) + return task.hasOwnProperty('tagIds') ? task : { - ...task, - tagIds: [] - }; + ...task, + tagIds: [], + }; }; const _addNewIssueFields = (task: Task): Task => { if (!task.hasOwnProperty('issueLastUpdated')) { - return (task.issueId !== null) + return task.issueId !== null ? { - // NOTE: we intentionally leave it as is, to allow for an update - // issueAttachmentNr: null, - // issueLastUpdated: Date.now(), - // issueWasUpdated: false, - ...task, - } + // NOTE: we intentionally leave it as is, to allow for an update + // issueAttachmentNr: null, + // issueLastUpdated: Date.now(), + // issueWasUpdated: false, + ...task, + } : { - issueAttachmentNr: null, - issueLastUpdated: null, - issueWasUpdated: null, - ...task - }; + issueAttachmentNr: null, + issueLastUpdated: null, + issueWasUpdated: null, + ...task, + }; } else { return task; } @@ -79,34 +82,36 @@ const _addNewIssueFields = (task: Task): Task => { const _replaceLegacyGitType = (task: Task) => { const issueType = task.issueType as string; - return (issueType === LEGACY_GITHUB_TYPE) - ? {...task, issueType: GITHUB_TYPE} - : task; + return issueType === LEGACY_GITHUB_TYPE ? { ...task, issueType: GITHUB_TYPE } : task; }; const _convertToWesternArabicDateKeys = (task: Task) => { - return (task.timeSpentOnDay) + return task.timeSpentOnDay ? { - ...task, - timeSpentOnDay: Object.keys(task.timeSpentOnDay).reduce((acc, dateKey) => { - const date = moment(convertToWesternArabic(dateKey)); - if (!date.isValid()) { - throw new Error('Cannot migrate invalid non western arabic date string ' + dateKey); - } - const westernArabicKey = date.locale('en').format(WORKLOG_DATE_STR_FORMAT); + ...task, + timeSpentOnDay: Object.keys(task.timeSpentOnDay).reduce((acc, dateKey) => { + const date = moment(convertToWesternArabic(dateKey)); + if (!date.isValid()) { + throw new Error( + 'Cannot migrate invalid non western arabic date string ' + dateKey, + ); + } + const westernArabicKey = date.locale('en').format(WORKLOG_DATE_STR_FORMAT); - const totalTimeSpentOnDay = Object.keys(task.timeSpentOnDay).filter((key) => { - return key === westernArabicKey && westernArabicKey !== dateKey; - }).reduce((tot, val) => { - return tot + task.timeSpentOnDay[val]; - }, task.timeSpentOnDay[dateKey]); + const totalTimeSpentOnDay = Object.keys(task.timeSpentOnDay) + .filter((key) => { + return key === westernArabicKey && westernArabicKey !== dateKey; + }) + .reduce((tot, val) => { + return tot + task.timeSpentOnDay[val]; + }, task.timeSpentOnDay[dateKey]); - return { - ...acc, - [westernArabicKey]: totalTimeSpentOnDay - }; - }, {}) - } + return { + ...acc, + [westernArabicKey]: totalTimeSpentOnDay, + }; + }, {}), + } : task; }; @@ -148,9 +153,11 @@ const _makeNullAndArraysConsistent = (task: Task): Task => { }; }; -const _addProjectIdForSubTasksAndRemoveTags = (entities: Dictionary): Dictionary => { - const entitiesCopy: any = {...entities}; - Object.keys(entitiesCopy).forEach(id => { +const _addProjectIdForSubTasksAndRemoveTags = ( + entities: Dictionary, +): Dictionary => { + const entitiesCopy: any = { ...entities }; + Object.keys(entitiesCopy).forEach((id) => { const task = entitiesCopy[id]; if (!task) { throw new Error('No task'); @@ -167,6 +174,3 @@ const _addProjectIdForSubTasksAndRemoveTags = (entities: Dictionary): Dict return entitiesCopy; }; - - - diff --git a/src/app/features/tasks/pipes/sub-task-total-time-estimate.pipe.ts b/src/app/features/tasks/pipes/sub-task-total-time-estimate.pipe.ts index 481294431..7c1e3b32a 100644 --- a/src/app/features/tasks/pipes/sub-task-total-time-estimate.pipe.ts +++ b/src/app/features/tasks/pipes/sub-task-total-time-estimate.pipe.ts @@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { Task } from '../task.model'; @Pipe({ - name: 'subTaskTotalTimeEstimate' + name: 'subTaskTotalTimeEstimate', }) export class SubTaskTotalTimeEstimatePipe implements PipeTransform { transform: (value: any, ...args: any[]) => any = getSubTasksTotalTimeEstimate; diff --git a/src/app/features/tasks/pipes/sub-task-total-time-spent.pipe.ts b/src/app/features/tasks/pipes/sub-task-total-time-spent.pipe.ts index df5768a18..ba7246722 100644 --- a/src/app/features/tasks/pipes/sub-task-total-time-spent.pipe.ts +++ b/src/app/features/tasks/pipes/sub-task-total-time-spent.pipe.ts @@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import { Task } from '../task.model'; @Pipe({ - name: 'subTaskTotalTimeSpent' + name: 'subTaskTotalTimeSpent', }) export class SubTaskTotalTimeSpentPipe implements PipeTransform { transform: (value: any, ...args: any[]) => any = getSubTasksTotalTimeSpent; diff --git a/src/app/features/tasks/scheduled-task.service.ts b/src/app/features/tasks/scheduled-task.service.ts index b5d8dbd22..9d5116f54 100644 --- a/src/app/features/tasks/scheduled-task.service.ts +++ b/src/app/features/tasks/scheduled-task.service.ts @@ -6,49 +6,61 @@ import { ReminderService } from '../reminder/reminder.service'; import { TaskWithReminderData } from './task.model'; import { devError } from '../../util/dev-error'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class ScheduledTaskService { - allScheduledTasks$: Observable = this._reminderService.reminders$.pipe( - map((reminders) => reminders.filter( - reminder => reminder.type === 'TASK' - )), + allScheduledTasks$: Observable< + TaskWithReminderData[] + > = this._reminderService.reminders$.pipe( + map((reminders) => reminders.filter((reminder) => reminder.type === 'TASK')), switchMap((reminders) => { - const ids = reminders.map(r => r.relatedId); + const ids = reminders.map((r) => r.relatedId); return this._taskService.getByIdsLive$(ids).pipe( - map((tasks) => tasks - .filter((task) => { - if (!task) { - console.log({reminders, tasks}); - devError('Reminder without task data'); - } - return !!task; - }) - .map( - task => ({ - ...task, - reminderData: reminders.find(reminder => reminder.relatedId === task.id) - } as TaskWithReminderData)), + map((tasks) => + tasks + .filter((task) => { + if (!task) { + console.log({ reminders, tasks }); + devError('Reminder without task data'); + } + return !!task; + }) + .map( + (task) => + ({ + ...task, + reminderData: reminders.find( + (reminder) => reminder.relatedId === task.id, + ), + } as TaskWithReminderData), + ), ), // NOTE: task length check is required, because otherwise the observable won't trigger for empty array - switchMap((tasks: TaskWithReminderData[]) => tasks.length - ? forkJoin(tasks.map(task => !!task.parentId - ? this._taskService.getByIdOnce$(task.parentId).pipe(map(parentData => ({ - ...task, - parentData - }))) - : of(task))) - : of([]) + switchMap((tasks: TaskWithReminderData[]) => + tasks.length + ? forkJoin( + tasks.map((task) => + !!task.parentId + ? this._taskService.getByIdOnce$(task.parentId).pipe( + map((parentData) => ({ + ...task, + parentData, + })), + ) + : of(task), + ), + ) + : of([]), ), ); }), - map((tasks: TaskWithReminderData[]) => tasks - .sort((a, b) => a.reminderData.remindAt - b.reminderData.remindAt)), + map((tasks: TaskWithReminderData[]) => + tasks.sort((a, b) => a.reminderData.remindAt - b.reminderData.remindAt), + ), shareReplay(1), ); constructor( private _taskService: TaskService, private _reminderService: ReminderService, - ) { - } + ) {} } diff --git a/src/app/features/tasks/select-task/select-task.component.ts b/src/app/features/tasks/select-task/select-task.component.ts index a6dce0455..5a93bfd1f 100644 --- a/src/app/features/tasks/select-task/select-task.component.ts +++ b/src/app/features/tasks/select-task/select-task.component.ts @@ -1,4 +1,12 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + OnDestroy, + OnInit, + Output, +} from '@angular/core'; import { FormControl } from '@angular/forms'; import { Task } from '../task.model'; import { map, startWith, takeUntil, withLatestFrom } from 'rxjs/operators'; @@ -11,7 +19,7 @@ import { TaskService } from '../task.service'; selector: 'select-task', templateUrl: './select-task.component.html', styleUrls: ['./select-task.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SelectTaskComponent implements OnInit, OnDestroy { T: typeof T = T; @@ -25,11 +33,10 @@ export class SelectTaskComponent implements OnInit, OnDestroy { constructor( private _workContextService: WorkContextService, private _taskService: TaskService, - ) { - } + ) {} @Input() set initialTask(task: Task) { - if (task && !this.taskSelectCtrl.value || this.taskSelectCtrl.value === '') { + if ((task && !this.taskSelectCtrl.value) || this.taskSelectCtrl.value === '') { this.isCreate = false; this.taskSelectCtrl.setValue(task); } @@ -40,19 +47,20 @@ export class SelectTaskComponent implements OnInit, OnDestroy { ? this._workContextService.startableTasksForActiveContext$ : this._taskService.allStartableTasks$; - this.taskSelectCtrl.valueChanges.pipe( - startWith(''), - withLatestFrom(tasks$), - map(([str, tasks]) => - typeof str === 'string' - ? tasks.filter(task => task.title.toLowerCase().includes(str.toLowerCase())) - : tasks - ), - takeUntil(this._destroy$) - ) + this.taskSelectCtrl.valueChanges + .pipe( + startWith(''), + withLatestFrom(tasks$), + map(([str, tasks]) => + typeof str === 'string' + ? tasks.filter((task) => task.title.toLowerCase().includes(str.toLowerCase())) + : tasks, + ), + takeUntil(this._destroy$), + ) .subscribe((filteredTasks) => { const taskOrTitle = this.taskSelectCtrl.value; - this.isCreate = (typeof taskOrTitle === 'string'); + this.isCreate = typeof taskOrTitle === 'string'; this.filteredTasks = this.isCreate ? filteredTasks : []; this.taskChange.emit(taskOrTitle); }); diff --git a/src/app/features/tasks/short-syntax.spec.ts b/src/app/features/tasks/short-syntax.spec.ts index 2f41ac002..d302bcb86 100644 --- a/src/app/features/tasks/short-syntax.spec.ts +++ b/src/app/features/tasks/short-syntax.spec.ts @@ -35,12 +35,12 @@ const TASK: TaskCopy = { issueWasUpdated: null, }; const ALL_TAGS: Tag[] = [ - {...DEFAULT_TAG, id: 'blu_id', title: 'blu'}, - {...DEFAULT_TAG, id: 'bla_id', title: 'bla'}, - {...DEFAULT_TAG, id: 'hihi_id', title: 'hihi'}, - {...DEFAULT_TAG, id: '1_id', title: '1'}, - {...DEFAULT_TAG, id: 'A_id', title: 'A'}, - {...DEFAULT_TAG, id: 'multi_word_id', title: 'Multi Word Tag'}, + { ...DEFAULT_TAG, id: 'blu_id', title: 'blu' }, + { ...DEFAULT_TAG, id: 'bla_id', title: 'bla' }, + { ...DEFAULT_TAG, id: 'hihi_id', title: 'hihi' }, + { ...DEFAULT_TAG, id: '1_id', title: '1' }, + { ...DEFAULT_TAG, id: 'A_id', title: 'A' }, + { ...DEFAULT_TAG, id: 'multi_word_id', title: 'Multi Word Tag' }, ]; describe('shortSyntax', () => { @@ -52,7 +52,7 @@ describe('shortSyntax', () => { it('should ignore if the changes cause no further changes', () => { const r = shortSyntax({ ...TASK, - title: 'So what shall I do' + title: 'So what shall I do', }); expect(r).toEqual(undefined); }); @@ -61,7 +61,7 @@ describe('shortSyntax', () => { it('', () => { const t = { ...TASK, - title: 'Fun title 10m/1h' + title: 'Fun title 10m/1h', }; const r = shortSyntax(t); expect(r).toEqual({ @@ -72,9 +72,9 @@ describe('shortSyntax', () => { title: 'Fun title', // timeSpent: 7200000, timeSpentOnDay: { - [getWorklogStr()]: 600000 + [getWorklogStr()]: 600000, }, - timeEstimate: 3600000 + timeEstimate: 3600000, }, }); }); @@ -82,7 +82,7 @@ describe('shortSyntax', () => { it('', () => { const t = { ...TASK, - title: 'Fun title whatever 1h/120m' + title: 'Fun title whatever 1h/120m', }; const r = shortSyntax(t); expect(r).toEqual({ @@ -93,10 +93,10 @@ describe('shortSyntax', () => { title: 'Fun title whatever', // timeSpent: 7200000, timeSpentOnDay: { - [getWorklogStr()]: 3600000 + [getWorklogStr()]: 3600000, }, - timeEstimate: 7200000 - } + timeEstimate: 7200000, + }, }); }); }); @@ -105,7 +105,7 @@ describe('shortSyntax', () => { it('should not trigger for tasks with starting # (e.g. github issues)', () => { const t = { ...TASK, - title: '#134 Fun title' + title: '#134 Fun title', }; const r = shortSyntax(t, ALL_TAGS); @@ -115,7 +115,7 @@ describe('shortSyntax', () => { it('should not trigger for tasks with starting # (e.g. github issues) when adding tags', () => { const t = { ...TASK, - title: '#134 Fun title #blu' + title: '#134 Fun title #blu', }; const r = shortSyntax(t, ALL_TAGS); @@ -125,15 +125,15 @@ describe('shortSyntax', () => { projectId: undefined, taskChanges: { title: '#134 Fun title', - tagIds: ['blu_id'] - } + tagIds: ['blu_id'], + }, }); }); it('should work with tags', () => { const t = { ...TASK, - title: 'Fun title #blu #A' + title: 'Fun title #blu #A', }; const r = shortSyntax(t, ALL_TAGS); @@ -143,8 +143,8 @@ describe('shortSyntax', () => { projectId: undefined, taskChanges: { title: 'Fun title', - tagIds: ['blu_id', 'A_id'] - } + tagIds: ['blu_id', 'A_id'], + }, }); }); @@ -152,7 +152,7 @@ describe('shortSyntax', () => { const t = { ...TASK, title: 'Fun title #blu #hihi', - tagIds: ['blu_id', 'A', 'multi_word_id'] + tagIds: ['blu_id', 'A', 'multi_word_id'], }; const r = shortSyntax(t, ALL_TAGS); @@ -162,8 +162,8 @@ describe('shortSyntax', () => { projectId: undefined, taskChanges: { title: 'Fun title', - tagIds: ['blu_id', 'A', 'multi_word_id', 'hihi_id'] - } + tagIds: ['blu_id', 'A', 'multi_word_id', 'hihi_id'], + }, }); }); @@ -171,7 +171,7 @@ describe('shortSyntax', () => { const t = { ...TASK, title: 'Fun title #blu #idontexist', - tagIds: [] + tagIds: [], }; const r = shortSyntax(t, ALL_TAGS); @@ -181,8 +181,8 @@ describe('shortSyntax', () => { projectId: undefined, taskChanges: { title: 'Fun title', - tagIds: ['blu_id'] - } + tagIds: ['blu_id'], + }, }); }); @@ -190,7 +190,7 @@ describe('shortSyntax', () => { const t = { ...TASK, title: 'asd #asd', - tagIds: [] + tagIds: [], }; const r = shortSyntax(t, ALL_TAGS); @@ -200,7 +200,7 @@ describe('shortSyntax', () => { projectId: undefined, taskChanges: { title: 'asd', - } + }, }); }); @@ -209,7 +209,7 @@ describe('shortSyntax', () => { ...TASK, parentId: 'SOMEPARENT', title: 'Fun title #blu #idontexist', - tagIds: [] + tagIds: [], }; const r = shortSyntax(t, ALL_TAGS); @@ -221,7 +221,7 @@ describe('shortSyntax', () => { it('', () => { const t = { ...TASK, - title: 'Fun title #blu 10m/1h' + title: 'Fun title #blu 10m/1h', }; const r = shortSyntax(t, ALL_TAGS); expect(r).toEqual({ @@ -232,11 +232,11 @@ describe('shortSyntax', () => { title: 'Fun title', // timeSpent: 7200000, timeSpentOnDay: { - [getWorklogStr()]: 600000 + [getWorklogStr()]: 600000, }, timeEstimate: 3600000, - tagIds: ['blu_id'] - } + tagIds: ['blu_id'], + }, }); }); @@ -265,19 +265,19 @@ describe('shortSyntax', () => { projects = [ { title: 'ProjectEasyShort', - id: 'ProjectEasyShortID' + id: 'ProjectEasyShortID', }, { title: 'Some Project Title', - id: 'SomeProjectID' - } + id: 'SomeProjectID', + }, ] as any; }); it('should work', () => { const t = { ...TASK, - title: 'Fun title +ProjectEasyShort' + title: 'Fun title +ProjectEasyShort', }; const r = shortSyntax(t, [], projects); expect(r).toEqual({ @@ -293,7 +293,7 @@ describe('shortSyntax', () => { it('should work together with time estimates', () => { const t = { ...TASK, - title: 'Fun title +ProjectEasyShort 10m/1h' + title: 'Fun title +ProjectEasyShort 10m/1h', }; const r = shortSyntax(t, [], projects); expect(r).toEqual({ @@ -314,7 +314,7 @@ describe('shortSyntax', () => { it('should work with only the beginning of a project title if it is at least 3 chars long', () => { const t = { ...TASK, - title: 'Fun title +Project' + title: 'Fun title +Project', }; const r = shortSyntax(t, [], projects); expect(r).toEqual({ @@ -330,7 +330,7 @@ describe('shortSyntax', () => { it('should work with multi word project titles', () => { const t = { ...TASK, - title: 'Fun title +Some Project Title' + title: 'Fun title +Some Project Title', }; const r = shortSyntax(t, [], projects); expect(r).toEqual({ @@ -346,7 +346,7 @@ describe('shortSyntax', () => { it('should work with multi word project titles partial', () => { const t = { ...TASK, - title: 'Fun title +Some Pro' + title: 'Fun title +Some Pro', }; const r = shortSyntax(t, [], projects); expect(r).toEqual({ @@ -362,7 +362,7 @@ describe('shortSyntax', () => { it('should work with multi word project titles partial written without white space', () => { const t = { ...TASK, - title: 'Other fun title +SomePro' + title: 'Other fun title +SomePro', }; const r = shortSyntax(t, [], projects); expect(r).toEqual({ @@ -378,27 +378,26 @@ describe('shortSyntax', () => { it('should ignore non existing', () => { const t = { ...TASK, - title: 'Other fun title +Some non existing project' + title: 'Other fun title +Some non existing project', }; const r = shortSyntax(t, [], projects); expect(r).toEqual(undefined); }); }); - describe('due:', () => { - }); + describe('due:', () => {}); describe('combined', () => { it('should work when time comes first', () => { const projects = [ { title: 'ProjectEasyShort', - id: 'ProjectEasyShortID' + id: 'ProjectEasyShortID', }, ] as any; const t = { ...TASK, - title: 'Fun title 10m/1h +ProjectEasyShort' + title: 'Fun title 10m/1h +ProjectEasyShort', }; const r = shortSyntax(t, [], projects); expect(r).toEqual({ @@ -409,9 +408,9 @@ describe('shortSyntax', () => { title: 'Fun title', // timeSpent: 7200000, timeSpentOnDay: { - [getWorklogStr()]: 600000 + [getWorklogStr()]: 600000, }, - timeEstimate: 3600000 + timeEstimate: 3600000, }, }); }); diff --git a/src/app/features/tasks/short-syntax.util.ts b/src/app/features/tasks/short-syntax.util.ts index 3dcf286a7..91fdb0274 100644 --- a/src/app/features/tasks/short-syntax.util.ts +++ b/src/app/features/tasks/short-syntax.util.ts @@ -12,16 +12,25 @@ const CH_TAG = '#'; const CH_DUE = '@'; const ALL_SPECIAL = `(\\${CH_PRO}|\\${CH_TAG}|\\${CH_DUE})`; -export const SHORT_SYNTAX_PROJECT_REG_EX = new RegExp(`\\${CH_PRO}[^${ALL_SPECIAL}]+`, 'gi'); +export const SHORT_SYNTAX_PROJECT_REG_EX = new RegExp( + `\\${CH_PRO}[^${ALL_SPECIAL}]+`, + 'gi', +); export const SHORT_SYNTAX_TAGS_REG_EX = new RegExp(`\\${CH_TAG}[^${ALL_SPECIAL}]+`, 'gi'); // export const SHORT_SYNTAX_DUE_REG_EX = new RegExp(`\\${CH_DUE}[^${ALL_SPECIAL}]+`, 'gi'); -export const shortSyntax = (task: Task | Partial, allTags?: Tag[], allProjects?: Project[]): { - taskChanges: Partial; - newTagTitles: string[]; - remindAt: number | null; - projectId: string | undefined; -} | undefined => { +export const shortSyntax = ( + task: Task | Partial, + allTags?: Tag[], + allProjects?: Project[], +): + | { + taskChanges: Partial; + newTagTitles: string[]; + remindAt: number | null; + projectId: string | undefined; + } + | undefined => { if (!task.title) { return; } @@ -33,7 +42,10 @@ export const shortSyntax = (task: Task | Partial, allTags?: Tag[], allProj let taskChanges: Partial; taskChanges = parseTimeSpentChanges(task); - const changesForProject = parseProjectChanges({...task, title: taskChanges.title || task.title}, allProjects); + const changesForProject = parseProjectChanges( + { ...task, title: taskChanges.title || task.title }, + allProjects, + ); if (changesForProject.projectId) { taskChanges = { ...taskChanges, @@ -41,14 +53,17 @@ export const shortSyntax = (task: Task | Partial, allTags?: Tag[], allProj }; } - const changesForTag = parseTagChanges({...task, title: taskChanges.title || task.title}, allTags); + const changesForTag = parseTagChanges( + { ...task, title: taskChanges.title || task.title }, + allTags, + ); taskChanges = { ...taskChanges, - ...changesForTag.taskChanges + ...changesForTag.taskChanges, }; taskChanges = { ...taskChanges, - ...parseTimeSpentChanges(taskChanges) + ...parseTimeSpentChanges(taskChanges), }; // const changesForDue = parseDueChanges({...task, title: taskChanges.title || task.title}); @@ -72,7 +87,10 @@ export const shortSyntax = (task: Task | Partial, allTags?: Tag[], allProj }; }; -const parseProjectChanges = (task: Partial, allProjects?: Project[]): { +const parseProjectChanges = ( + task: Partial, + allProjects?: Project[], +): { title?: string; projectId?: string; } => { @@ -92,14 +110,11 @@ const parseProjectChanges = (task: Partial, allProjects?: Project[]): if (rr && rr[0]) { const projectTitle: string = rr[0].trim().replace(CH_PRO, ''); const existingProject = allProjects.find( - project => project.title - .replace(' ', '') - .toLowerCase() - .indexOf( - projectTitle - .replace(' ', '') - .toLowerCase() - ) === 0 + (project) => + project.title + .replace(' ', '') + .toLowerCase() + .indexOf(projectTitle.replace(' ', '').toLowerCase()) === 0, ); if (existingProject) { @@ -113,11 +128,14 @@ const parseProjectChanges = (task: Partial, allProjects?: Project[]): return {}; }; -const parseTagChanges = (task: Partial, allTags?: Tag[]): { taskChanges: Partial; newTagTitlesToCreate: string[] } => { +const parseTagChanges = ( + task: Partial, + allTags?: Tag[], +): { taskChanges: Partial; newTagTitlesToCreate: string[] } => { const taskChanges: Partial = {}; if (task.parentId) { - return {taskChanges, newTagTitlesToCreate: []}; + return { taskChanges, newTagTitlesToCreate: [] }; } const newTagTitlesToCreate: string[] = []; @@ -127,16 +145,19 @@ const parseTagChanges = (task: Partial, allTags?: Tag[]): { taskChange const regexTagTitles = initialTitle.match(SHORT_SYNTAX_TAGS_REG_EX); if (regexTagTitles && regexTagTitles.length) { const regexTagTitlesTrimmedAndFiltered: string[] = regexTagTitles - .map(title => title.trim().replace(CH_TAG, '')) - .filter(newTagTitle => - newTagTitle.length >= 1 - // NOTE: we check this to not trigger for "#123 blasfs dfasdf" - && initialTitle.trim().lastIndexOf(newTagTitle) > 4 + .map((title) => title.trim().replace(CH_TAG, '')) + .filter( + (newTagTitle) => + newTagTitle.length >= 1 && + // NOTE: we check this to not trigger for "#123 blasfs dfasdf" + initialTitle.trim().lastIndexOf(newTagTitle) > 4, ); const tagIdsToAdd: string[] = []; - regexTagTitlesTrimmedAndFiltered.forEach(newTagTitle => { - const existingTag = allTags.find(tag => newTagTitle.toLowerCase() === tag.title.toLowerCase()); + regexTagTitlesTrimmedAndFiltered.forEach((newTagTitle) => { + const existingTag = allTags.find( + (tag) => newTagTitle.toLowerCase() === tag.title.toLowerCase(), + ); if (existingTag) { if (!task.tagIds?.includes(existingTag.id)) { tagIdsToAdd.push(existingTag.id); @@ -147,7 +168,7 @@ const parseTagChanges = (task: Partial, allTags?: Tag[]): { taskChange }); if (tagIdsToAdd.length) { - taskChanges.tagIds = [...task.tagIds as string[], ...tagIdsToAdd]; + taskChanges.tagIds = [...(task.tagIds as string[]), ...tagIdsToAdd]; } if (newTagTitlesToCreate.length || tagIdsToAdd.length) { @@ -170,7 +191,7 @@ const parseTagChanges = (task: Partial, allTags?: Tag[]): { taskChange return { taskChanges, - newTagTitlesToCreate + newTagTitlesToCreate, }; }; @@ -187,18 +208,16 @@ const parseTimeSpentChanges = (task: Partial): Partial => { const timeEstimate = matches[4]; return { - ...( - timeSpent - ? { + ...(timeSpent + ? { timeSpentOnDay: { ...(task.timeSpentOnDay || {}), - [getWorklogStr()]: stringToMs(timeSpent) - } + [getWorklogStr()]: stringToMs(timeSpent), + }, } - : {} - ), + : {}), timeEstimate: stringToMs(timeEstimate), - title: task.title.replace(full, '') + title: task.title.replace(full, ''), }; } diff --git a/src/app/features/tasks/store/task-db.effects.ts b/src/app/features/tasks/store/task-db.effects.ts index b5d8590fa..11499d0be 100644 --- a/src/app/features/tasks/store/task-db.effects.ts +++ b/src/app/features/tasks/store/task-db.effects.ts @@ -12,79 +12,70 @@ import { environment } from '../../../../environments/environment'; @Injectable() export class TaskDbEffects { - @Effect({dispatch: false}) updateTask$: any = this._actions$ - .pipe( - ofType( - TaskActionTypes.AddTask, - TaskActionTypes.RestoreTask, - TaskActionTypes.AddTimeSpent, - TaskActionTypes.UnScheduleTask, - TaskActionTypes.DeleteTask, - TaskActionTypes.DeleteMainTasks, - TaskActionTypes.UndoDeleteTask, - TaskActionTypes.AddSubTask, - TaskActionTypes.ConvertToMainTask, - // TaskActionTypes.SetCurrentTask, - // TaskActionTypes.UnsetCurrentTask, - TaskActionTypes.UpdateTask, - TaskActionTypes.UpdateTaskTags, - TaskActionTypes.RemoveTagsForAllTasks, - TaskActionTypes.MoveSubTask, - TaskActionTypes.MoveSubTaskUp, - TaskActionTypes.MoveSubTaskDown, - TaskActionTypes.MoveToArchive, - TaskActionTypes.MoveToOtherProject, - TaskActionTypes.ToggleStart, - TaskActionTypes.RoundTimeSpentForDay, + @Effect({ dispatch: false }) updateTask$: any = this._actions$.pipe( + ofType( + TaskActionTypes.AddTask, + TaskActionTypes.RestoreTask, + TaskActionTypes.AddTimeSpent, + TaskActionTypes.UnScheduleTask, + TaskActionTypes.DeleteTask, + TaskActionTypes.DeleteMainTasks, + TaskActionTypes.UndoDeleteTask, + TaskActionTypes.AddSubTask, + TaskActionTypes.ConvertToMainTask, + // TaskActionTypes.SetCurrentTask, + // TaskActionTypes.UnsetCurrentTask, + TaskActionTypes.UpdateTask, + TaskActionTypes.UpdateTaskTags, + TaskActionTypes.RemoveTagsForAllTasks, + TaskActionTypes.MoveSubTask, + TaskActionTypes.MoveSubTaskUp, + TaskActionTypes.MoveSubTaskDown, + TaskActionTypes.MoveToArchive, + TaskActionTypes.MoveToOtherProject, + TaskActionTypes.ToggleStart, + TaskActionTypes.RoundTimeSpentForDay, - // REMINDER - TaskActionTypes.ScheduleTask, - TaskActionTypes.ReScheduleTask, - TaskActionTypes.UnScheduleTask, + // REMINDER + TaskActionTypes.ScheduleTask, + TaskActionTypes.ReScheduleTask, + TaskActionTypes.UnScheduleTask, - // SUB ACTIONS - TaskAttachmentActionTypes.AddTaskAttachment, - TaskAttachmentActionTypes.DeleteTaskAttachment, - TaskAttachmentActionTypes.UpdateTaskAttachment, + // SUB ACTIONS + TaskAttachmentActionTypes.AddTaskAttachment, + TaskAttachmentActionTypes.DeleteTaskAttachment, + TaskAttachmentActionTypes.UpdateTaskAttachment, - // RELATED ACTIONS - TaskRepeatCfgActionTypes.AddTaskRepeatCfgToTask, - ), - withLatestFrom( - this._store$.pipe(select(selectTaskFeatureState)), - ), - tap(([, taskState]) => this._saveToLs(taskState, true)), - ); + // RELATED ACTIONS + TaskRepeatCfgActionTypes.AddTaskRepeatCfgToTask, + ), + withLatestFrom(this._store$.pipe(select(selectTaskFeatureState))), + tap(([, taskState]) => this._saveToLs(taskState, true)), + ); - @Effect({dispatch: false}) updateTaskUi$: any = this._actions$ - .pipe( - ofType( - TaskActionTypes.UpdateTaskUi, - TaskActionTypes.ToggleTaskShowSubTasks, - ), - withLatestFrom( - this._store$.pipe(select(selectTaskFeatureState)), - ), - tap(([, taskState]) => this._saveToLs(taskState)), - ); + @Effect({ dispatch: false }) updateTaskUi$: any = this._actions$.pipe( + ofType(TaskActionTypes.UpdateTaskUi, TaskActionTypes.ToggleTaskShowSubTasks), + withLatestFrom(this._store$.pipe(select(selectTaskFeatureState))), + tap(([, taskState]) => this._saveToLs(taskState)), + ); - constructor(private _actions$: Actions, + constructor( + private _actions$: Actions, private _store$: Store, - private _persistenceService: PersistenceService) { - } + private _persistenceService: PersistenceService, + ) {} // @debounce(50) private _saveToLs(taskState: TaskState, isSyncModelChange: boolean = false) { - this._persistenceService.task.saveState({ - ...taskState, + this._persistenceService.task.saveState( + { + ...taskState, - // make sure those are never set to something - selectedTaskId: environment.production - ? null - : taskState.selectedTaskId, - currentTaskId: null, - }, {isSyncModelChange}); + // make sure those are never set to something + selectedTaskId: environment.production ? null : taskState.selectedTaskId, + currentTaskId: null, + }, + { isSyncModelChange }, + ); } } - - diff --git a/src/app/features/tasks/store/task-electron.effects.ts b/src/app/features/tasks/store/task-electron.effects.ts index 95a89c8bb..673a0852d 100644 --- a/src/app/features/tasks/store/task-electron.effects.ts +++ b/src/app/features/tasks/store/task-electron.effects.ts @@ -17,7 +17,7 @@ import { ipcRenderer } from 'electron'; @Injectable() export class TaskElectronEffects { - @Effect({dispatch: false}) + @Effect({ dispatch: false }) taskChangeElectron$: any = this._actions$.pipe( ofType( TaskActionTypes.SetCurrentTask, @@ -27,37 +27,50 @@ export class TaskElectronEffects { withLatestFrom(this._store$.pipe(select(selectCurrentTask))), tap(([action, current]) => { if (IS_ELECTRON) { - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.CURRENT_TASK_UPDATED, {current}); - } - }) - ); - - @Effect({dispatch: false}) - setTaskBarNoProgress$: Observable = this._actions$.pipe( - ofType( - TaskActionTypes.SetCurrentTask, - ), - filter(() => IS_ELECTRON), - tap((act: SetCurrentTask) => { - if (!act.payload) { - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.SET_PROGRESS_BAR, {progress: 0}); + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.CURRENT_TASK_UPDATED, + { + current, + }, + ); } }), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) + setTaskBarNoProgress$: Observable = this._actions$.pipe( + ofType(TaskActionTypes.SetCurrentTask), + filter(() => IS_ELECTRON), + tap((act: SetCurrentTask) => { + if (!act.payload) { + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.SET_PROGRESS_BAR, + { + progress: 0, + }, + ); + } + }), + ); + + @Effect({ dispatch: false }) setTaskBarProgress$: Observable = this._actions$.pipe( - ofType( - TaskActionTypes.AddTimeSpent, - ), + ofType(TaskActionTypes.AddTimeSpent), filter(() => IS_ELECTRON), withLatestFrom(this._configService.cfg$), // we display pomodoro progress for pomodoro - filter(([a, cfg]: [AddTimeSpent, GlobalConfigState]) => !cfg || !cfg.pomodoro.isEnabled), + filter( + ([a, cfg]: [AddTimeSpent, GlobalConfigState]) => !cfg || !cfg.pomodoro.isEnabled, + ), map(([act]) => act.payload.task), tap((task: Task) => { const progress = task.timeSpent / task.timeEstimate; - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.SET_PROGRESS_BAR, {progress}); + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.SET_PROGRESS_BAR, + { + progress, + }, + ); }), ); @@ -66,9 +79,5 @@ export class TaskElectronEffects { private _store$: Store, private _configService: GlobalConfigService, private _electronService: ElectronService, - ) { - } - + ) {} } - - diff --git a/src/app/features/tasks/store/task-internal.effects.ts b/src/app/features/tasks/store/task-internal.effects.ts index d9d4b4c6d..379c75090 100644 --- a/src/app/features/tasks/store/task-internal.effects.ts +++ b/src/app/features/tasks/store/task-internal.effects.ts @@ -1,6 +1,11 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; -import { SetCurrentTask, TaskActionTypes, UnsetCurrentTask, UpdateTask } from './task.actions'; +import { + SetCurrentTask, + TaskActionTypes, + UnsetCurrentTask, + UpdateTask, +} from './task.actions'; import { select, Store } from '@ngrx/store'; import { filter, map, mergeMap, withLatestFrom } from 'rxjs/operators'; import { selectTaskFeatureState } from './task.selectors'; @@ -8,24 +13,28 @@ import { selectMiscConfig } from '../../config/store/global-config.reducer'; import { Task, TaskState } from '../task.model'; import { EMPTY, of } from 'rxjs'; import { MiscConfig } from '../../config/global-config.model'; -import { moveTaskToBacklogList, moveTaskToBacklogListAuto } from '../../work-context/store/work-context-meta.actions'; +import { + moveTaskToBacklogList, + moveTaskToBacklogListAuto, +} from '../../work-context/store/work-context-meta.actions'; import { WorkContextService } from '../../work-context/work-context.service'; @Injectable() export class TaskInternalEffects { @Effect() onAllSubTasksDone$: any = this._actions$.pipe( - ofType( - TaskActionTypes.UpdateTask, - ), + ofType(TaskActionTypes.UpdateTask), withLatestFrom( this._store$.pipe(select(selectMiscConfig)), - this._store$.pipe(select(selectTaskFeatureState)) + this._store$.pipe(select(selectTaskFeatureState)), ), - filter(([action, miscCfg, state]: [UpdateTask, MiscConfig, TaskState]) => - !!miscCfg && miscCfg.isAutMarkParentAsDone && !!action.payload.task.changes.isDone && - // @ts-ignore - !!(state.entities[action.payload.task.id].parentId) + filter( + ([action, miscCfg, state]: [UpdateTask, MiscConfig, TaskState]) => + !!miscCfg && + miscCfg.isAutMarkParentAsDone && + !!action.payload.task.changes.isDone && + // @ts-ignore + !!state.entities[action.payload.task.id].parentId, ), filter(([action, miscCfg, state]) => { const task = state.entities[action.payload.task.id]; @@ -33,15 +42,20 @@ export class TaskInternalEffects { throw new Error('!task || !task.parentId'); } const parent = state.entities[task.parentId] as Task; - const undoneSubTasks = parent.subTaskIds.filter(id => !(state.entities[id] as Task).isDone); + const undoneSubTasks = parent.subTaskIds.filter( + (id) => !(state.entities[id] as Task).isDone, + ); return undoneSubTasks.length === 0; }), - map(([action, miscCfg, state]) => new UpdateTask({ - task: { - id: (state.entities[action.payload.task.id] as Task).parentId as string, - changes: {isDone: true}, - } - })), + map( + ([action, miscCfg, state]) => + new UpdateTask({ + task: { + id: (state.entities[action.payload.task.id] as Task).parentId as string, + changes: { isDone: true }, + }, + }), + ), ); @Effect() @@ -53,7 +67,7 @@ export class TaskInternalEffects { TaskActionTypes.MoveToArchive, moveTaskToBacklogList.type, - moveTaskToBacklogListAuto.type + moveTaskToBacklogListAuto.type, ), withLatestFrom( this._store$.pipe(select(selectMiscConfig)), @@ -64,9 +78,9 @@ export class TaskInternalEffects { state, isAutoStartNextTask: miscCfg.isAutoStartNextTask, todaysTaskIds, - }) + }), ), - mergeMap(({action, state, isAutoStartNextTask, todaysTaskIds}) => { + mergeMap(({ action, state, isAutoStartNextTask, todaysTaskIds }) => { const currentId = state.currentTaskId; let nextId: 'NO_UPDATE' | string | null; @@ -77,23 +91,22 @@ export class TaskInternalEffects { } case TaskActionTypes.UpdateTask: { - const {isDone} = (action as UpdateTask).payload.task.changes; + const { isDone } = (action as UpdateTask).payload.task.changes; const oldId = (action as UpdateTask).payload.task.id; - const isCurrent = (oldId === currentId); - nextId = (isDone && isCurrent) - - ? ((isAutoStartNextTask) - ? this._findNextTask(state, todaysTaskIds, oldId as string) - : null) - - : 'NO_UPDATE'; + const isCurrent = oldId === currentId; + nextId = + isDone && isCurrent + ? isAutoStartNextTask + ? this._findNextTask(state, todaysTaskIds, oldId as string) + : null + : 'NO_UPDATE'; break; } case moveTaskToBacklogList.type: case moveTaskToBacklogListAuto.type: { - const isCurrent = (currentId === (action as any).taskId); - nextId = (isCurrent) ? null : 'NO_UPDATE'; + const isCurrent = currentId === (action as any).taskId; + nextId = isCurrent ? null : 'NO_UPDATE'; break; } @@ -120,32 +133,37 @@ export class TaskInternalEffects { return of(new UnsetCurrentTask()); } } - }) + }), ); constructor( private _actions$: Actions, private _store$: Store, private _workContextSession: WorkContextService, - ) { - } + ) {} - private _findNextTask(state: TaskState, todaysTaskIds: string[], oldCurrentId?: string): string | null { + private _findNextTask( + state: TaskState, + todaysTaskIds: string[], + oldCurrentId?: string, + ): string | null { let nextId: string | null = null; - const {entities} = state; + const { entities } = state; - const filterUndoneNotCurrent = (id: string) => !(entities[id] as Task).isDone && id !== oldCurrentId; - const flattenToSelectable = (arr: string[]) => arr.reduce((acc: string[], next: string) => { - return (entities[next] as Task).subTaskIds.length > 0 - ? acc.concat((entities[next] as Task).subTaskIds) - : acc.concat(next); - }, []); + const filterUndoneNotCurrent = (id: string) => + !(entities[id] as Task).isDone && id !== oldCurrentId; + const flattenToSelectable = (arr: string[]) => + arr.reduce((acc: string[], next: string) => { + return (entities[next] as Task).subTaskIds.length > 0 + ? acc.concat((entities[next] as Task).subTaskIds) + : acc.concat(next); + }, []); if (oldCurrentId) { const oldCurTask = entities[oldCurrentId]; if (oldCurTask && oldCurTask.parentId) { (entities[oldCurTask.parentId] as Task).subTaskIds.some((id) => { - return (id !== oldCurrentId && !(entities[id] as Task).isDone) + return id !== oldCurrentId && !(entities[id] as Task).isDone ? (nextId = id) && true // assign !!! : false; }); @@ -157,27 +175,29 @@ export class TaskInternalEffects { const mainTasksAfter = todaysTaskIds.slice(oldCurIndex + 1); const selectableBefore = flattenToSelectable(mainTasksBefore); const selectableAfter = flattenToSelectable(mainTasksAfter); - nextId = selectableAfter.find(filterUndoneNotCurrent) - || selectableBefore.reverse().find(filterUndoneNotCurrent) - || null; - nextId = (Array.isArray(nextId)) - ? nextId[0] - : nextId; - + nextId = + selectableAfter.find(filterUndoneNotCurrent) || + selectableBefore.reverse().find(filterUndoneNotCurrent) || + null; + nextId = Array.isArray(nextId) ? nextId[0] : nextId; } } else { const lastTask = state.lastCurrentTaskId && entities[state.lastCurrentTaskId]; - const isLastSelectable = state.lastCurrentTaskId && lastTask && !lastTask.isDone && !lastTask.subTaskIds.length; + const isLastSelectable = + state.lastCurrentTaskId && + lastTask && + !lastTask.isDone && + !lastTask.subTaskIds.length; if (isLastSelectable) { nextId = state.lastCurrentTaskId; } else { - const selectable = flattenToSelectable(todaysTaskIds).find(filterUndoneNotCurrent); - nextId = (Array.isArray(selectable)) ? selectable[0] : selectable; + const selectable = flattenToSelectable(todaysTaskIds).find( + filterUndoneNotCurrent, + ); + nextId = Array.isArray(selectable) ? selectable[0] : selectable; } } return nextId; } } - - 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 f511d89e6..e0eb855dd 100644 --- a/src/app/features/tasks/store/task-related-model.effects.ts +++ b/src/app/features/tasks/store/task-related-model.effects.ts @@ -8,13 +8,27 @@ import { RestoreTask, TaskActionTypes, UpdateTask, - UpdateTaskTags + UpdateTaskTags, } from './task.actions'; -import { concatMap, delay, filter, first, map, mapTo, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { + concatMap, + delay, + filter, + first, + map, + mapTo, + mergeMap, + switchMap, + tap, + withLatestFrom, +} from 'rxjs/operators'; import { PersistenceService } from '../../../core/persistence/persistence.service'; import { Task, TaskArchive, TaskWithSubTasks } from '../task.model'; import { ReminderService } from '../../reminder/reminder.service'; -import { moveTaskInTodayList, moveTaskToTodayList } from '../../work-context/store/work-context-meta.actions'; +import { + moveTaskInTodayList, + moveTaskToTodayList, +} 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'; @@ -32,138 +46,152 @@ import { environment } from '../../../../environments/environment'; export class TaskRelatedModelEffects { // EFFECTS ===> EXTERNAL // --------------------- - @Effect({dispatch: false}) + @Effect({ dispatch: false }) moveToArchive$: any = this._actions$.pipe( ofType(TaskActionTypes.MoveToArchive), tap(this._moveToArchive.bind(this)), ); // TODO remove once reminder is changed - @Effect({dispatch: false}) + @Effect({ dispatch: false }) moveToOtherProject: any = this._actions$.pipe( ofType(TaskActionTypes.MoveToOtherProject), tap(this._moveToOtherProject.bind(this)), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) restoreTask$: any = this._actions$.pipe( ofType(TaskActionTypes.RestoreTask), - tap(this._removeFromArchive.bind(this)) + tap(this._removeFromArchive.bind(this)), ); @Effect() autoAddTodayTag: any = this._actions$.pipe( ofType(TaskActionTypes.AddTimeSpent), - switchMap((a: AddTimeSpent) => a.payload.task.parentId - ? this._taskService.getByIdOnce$(a.payload.task.parentId) - : of(a.payload.task), + switchMap((a: AddTimeSpent) => + a.payload.task.parentId + ? this._taskService.getByIdOnce$(a.payload.task.parentId) + : of(a.payload.task), ), filter((task: Task) => !task.tagIds.includes(TODAY_TAG.id)), - concatMap((task: Task) => this._globalConfigService.misc$.pipe( - first(), - map(miscCfg => ({ - miscCfg, - task, - })) - )), - filter(({miscCfg, task}) => miscCfg.isAutoAddWorkedOnToToday), - map(({miscCfg, task}) => new UpdateTaskTags({ - task, - newTagIds: unique([...task.tagIds, TODAY_TAG.id]), - oldTagIds: task.tagIds, - })) + concatMap((task: Task) => + this._globalConfigService.misc$.pipe( + first(), + map((miscCfg) => ({ + miscCfg, + task, + })), + ), + ), + filter(({ miscCfg, task }) => miscCfg.isAutoAddWorkedOnToToday), + map( + ({ miscCfg, task }) => + new UpdateTaskTags({ + task, + newTagIds: unique([...task.tagIds, TODAY_TAG.id]), + oldTagIds: task.tagIds, + }), + ), ); // EXTERNAL ===> TASKS // ------------------- @Effect() moveTaskToUnDone$: any = this._actions$.pipe( - ofType( - moveTaskInTodayList, - moveTaskToTodayList, + ofType(moveTaskInTodayList, moveTaskToTodayList), + filter( + ({ src, target }) => (src === 'DONE' || src === 'BACKLOG') && target === 'UNDONE', + ), + map( + ({ taskId }) => + new UpdateTask({ + task: { + id: taskId, + changes: { + isDone: false, + }, + }, + }), ), - filter(({src, target}) => (src === 'DONE' || src === 'BACKLOG') && target === 'UNDONE'), - map(({taskId}) => new UpdateTask({ - task: { - id: taskId, - changes: { - isDone: false, - } - } - })) ); @Effect() moveTaskToDone$: any = this._actions$.pipe( - ofType( - moveTaskInTodayList, - moveTaskToTodayList, + ofType(moveTaskInTodayList, moveTaskToTodayList), + filter( + ({ src, target }) => (src === 'UNDONE' || src === 'BACKLOG') && target === 'DONE', + ), + map( + ({ taskId }) => + new UpdateTask({ + task: { + id: taskId, + changes: { + isDone: true, + }, + }, + }), ), - filter(({src, target}) => (src === 'UNDONE' || src === 'BACKLOG') && target === 'DONE'), - map(({taskId}) => new UpdateTask({ - task: { - id: taskId, - changes: { - isDone: true, - } - } - })) ); @Effect() setDefaultProjectId$: any = this._actions$.pipe( - ofType( - TaskActionTypes.AddTask, + ofType(TaskActionTypes.AddTask), + concatMap((act: AddTask) => + this._globalConfigService.misc$.pipe( + first(), + // error handling + switchMap((miscConfig) => + !!miscConfig.defaultProjectId + ? this._projectService.getByIdOnce$(miscConfig.defaultProjectId).pipe( + tap((project) => { + if (!project) { + throw new Error('Default Project not found'); + } + }), + mapTo(miscConfig), + ) + : of(miscConfig), + ), + // error handling end + map((miscCfg) => ({ + defaultProjectId: miscCfg.defaultProjectId, + task: act.payload.task, + })), + ), + ), + filter( + ({ defaultProjectId, task }) => + !!defaultProjectId && !task.projectId && !task.parentId, + ), + map( + ({ task, defaultProjectId }) => + new MoveToOtherProject({ + task: task as TaskWithSubTasks, + targetProjectId: defaultProjectId as string, + }), ), - concatMap((act: AddTask) => this._globalConfigService.misc$.pipe( - first(), - // error handling - switchMap((miscConfig) => (!!(miscConfig.defaultProjectId) - ? this._projectService.getByIdOnce$(miscConfig.defaultProjectId).pipe( - tap((project) => { - if (!project) { - throw new Error('Default Project not found'); - } - }), - mapTo(miscConfig), - ) - : of(miscConfig))), - // error handling end - map(miscCfg => ({ - defaultProjectId: miscCfg.defaultProjectId, - task: act.payload.task, - })) - )), - filter(({defaultProjectId, task}) => !!defaultProjectId && !task.projectId && !task.parentId), - map(({task, defaultProjectId}) => new MoveToOtherProject({ - task: task as TaskWithSubTasks, - targetProjectId: defaultProjectId as string, - })), ); @Effect() shortSyntax$: any = this._actions$.pipe( - ofType( - TaskActionTypes.AddTask, - TaskActionTypes.UpdateTask, - ), + ofType(TaskActionTypes.AddTask, TaskActionTypes.UpdateTask), filter((action: AddTask | UpdateTask): boolean => { if (action.type !== TaskActionTypes.UpdateTask) { return true; } const changeProps = Object.keys((action as UpdateTask).payload.task.changes); // we only want to execute this for task title updates - return (changeProps.length === 1 && changeProps[0] === 'title'); + return changeProps.length === 1 && changeProps[0] === 'title'; }), // dirty fix to execute this after setDefaultProjectId$ effect delay(20), - concatMap((action: AddTask | UpdateTask): Observable => { - return this._taskService.getByIdOnce$(action.payload.task.id as string); - }), - withLatestFrom( - this._tagService.tags$, - this._projectService.list$, + concatMap( + (action: AddTask | UpdateTask): Observable => { + return this._taskService.getByIdOnce$(action.payload.task.id as string); + }, ), + withLatestFrom(this._tagService.tags$, this._projectService.list$), mergeMap(([task, tags, projects]) => { const r = shortSyntax(task, tags, projects); if (environment.production) { @@ -178,39 +206,44 @@ export class TaskRelatedModelEffects { actions.push( new UpdateTask({ - task: { - id: task.id, - changes: r.taskChanges, - } - } - ) + task: { + id: task.id, + changes: r.taskChanges, + }, + }), ); if (r.projectId && r.projectId !== task.projectId) { - actions.push(new MoveToOtherProject({ - task, - targetProjectId: r.projectId, - })); + actions.push( + new MoveToOtherProject({ + task, + targetProjectId: r.projectId, + }), + ); } if (r.newTagTitles.length) { - r.newTagTitles.forEach(newTagTitle => { - const {action, id} = this._tagService.getAddTagActionAndId({title: newTagTitle}); + r.newTagTitles.forEach((newTagTitle) => { + const { action, id } = this._tagService.getAddTagActionAndId({ + title: newTagTitle, + }); tagIds.push(id); actions.push(action); }); } if (tagIds && tagIds.length) { - const isEqualTags = (JSON.stringify(tagIds) === JSON.stringify(task.tagIds)); + const isEqualTags = JSON.stringify(tagIds) === JSON.stringify(task.tagIds); if (!task.tagIds) { throw new Error('Task Old TagIds need to be passed'); } if (!isEqualTags) { - actions.push(new UpdateTaskTags({ - task, - newTagIds: unique(tagIds), - oldTagIds: task.tagIds, - })); + actions.push( + new UpdateTaskTags({ + task, + newTagIds: unique(tagIds), + oldTagIds: task.tagIds, + }), + ); } } @@ -225,15 +258,15 @@ export class TaskRelatedModelEffects { private _tagService: TagService, private _projectService: ProjectService, private _globalConfigService: GlobalConfigService, - private _persistenceService: PersistenceService - ) { - } + private _persistenceService: PersistenceService, + ) {} private async _removeFromArchive(action: RestoreTask) { const task = action.payload.task; const taskIds = [task.id, ...task.subTaskIds]; - const currentArchive: TaskArchive = await this._persistenceService.taskArchive.loadState() || createEmptyEntity(); - const allIds = currentArchive.ids as string[] || []; + const currentArchive: TaskArchive = + (await this._persistenceService.taskArchive.loadState()) || createEmptyEntity(); + const allIds = (currentArchive.ids as string[]) || []; const idsToRemove: string[] = []; taskIds.forEach((taskId) => { @@ -243,10 +276,13 @@ export class TaskRelatedModelEffects { } }); - return this._persistenceService.taskArchive.saveState({ - ...currentArchive, - ids: allIds.filter((id) => !idsToRemove.includes(id)), - }, {isSyncModelChange: true}); + return this._persistenceService.taskArchive.saveState( + { + ...currentArchive, + ids: allIds.filter((id) => !idsToRemove.includes(id)), + }, + { isSyncModelChange: true }, + ); } private async _moveToArchive(action: MoveToArchive) { @@ -255,24 +291,30 @@ export class TaskRelatedModelEffects { return; } - const currentArchive: TaskArchive = await this._persistenceService.taskArchive.loadState() || createEmptyEntity(); + const currentArchive: TaskArchive = + (await this._persistenceService.taskArchive.loadState()) || createEmptyEntity(); - const newArchive = taskAdapter.addMany(flatTasks.map(({subTasks, ...task}) => ({ - ...task, - reminderId: null, - isDone: true, - })), currentArchive); + const newArchive = taskAdapter.addMany( + flatTasks.map(({ subTasks, ...task }) => ({ + ...task, + reminderId: null, + isDone: true, + })), + currentArchive, + ); flatTasks - .filter(t => !!t.reminderId) - .forEach(t => { + .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._persistenceService.taskArchive.saveState(newArchive, { + isSyncModelChange: true, + }); } private _moveToOtherProject(action: MoveToOtherProject) { @@ -280,17 +322,15 @@ export class TaskRelatedModelEffects { const workContextId = action.payload.targetProjectId; if (mainTasks.reminderId) { - this._reminderService.updateReminder(mainTasks.reminderId, {workContextId}); + this._reminderService.updateReminder(mainTasks.reminderId, { workContextId }); } if (mainTasks.subTasks) { mainTasks.subTasks.forEach((subTask) => { if (subTask.reminderId) { - this._reminderService.updateReminder(subTask.reminderId, {workContextId}); + this._reminderService.updateReminder(subTask.reminderId, { workContextId }); } }); } } } - - diff --git a/src/app/features/tasks/store/task-reminder.effects.ts b/src/app/features/tasks/store/task-reminder.effects.ts index f0adf0537..90a68ee4e 100644 --- a/src/app/features/tasks/store/task-reminder.effects.ts +++ b/src/app/features/tasks/store/task-reminder.effects.ts @@ -7,7 +7,7 @@ import { TaskActionTypes, UnScheduleTask, UpdateTask, - UpdateTaskTags + UpdateTaskTags, } from './task.actions'; import { filter, map, mergeMap, tap } from 'rxjs/operators'; import { ReminderService } from '../../reminder/reminder.service'; @@ -20,24 +20,23 @@ import { EMPTY } from 'rxjs'; @Injectable() export class TaskReminderEffects { - @Effect() addTaskReminder$: any = this._actions$.pipe( - ofType( - TaskActionTypes.ScheduleTask, + ofType(TaskActionTypes.ScheduleTask), + tap((a: ScheduleTask) => + this._snackService.open({ + type: 'SUCCESS', + translateParams: { + title: truncate(a.payload.task.title), + }, + msg: T.F.TASK.S.REMINDER_ADDED, + ico: 'schedule', + }), ), - tap((a: ScheduleTask) => this._snackService.open({ - type: 'SUCCESS', - translateParams: { - title: truncate(a.payload.task.title) - }, - msg: T.F.TASK.S.REMINDER_ADDED, - ico: 'schedule', - })), mergeMap((a: ScheduleTask) => { console.log(a); - const {task, remindAt, isMoveToBacklog} = a.payload; + const { task, remindAt, isMoveToBacklog } = a.payload; if (isMoveToBacklog && !task.projectId) { throw new Error('Move to backlog not possible for non project tasks'); } @@ -45,99 +44,104 @@ export class TaskReminderEffects { return EMPTY; } - const reminderId = this._reminderService.addReminder('TASK', task.id, truncate(task.title), remindAt); + const reminderId = this._reminderService.addReminder( + 'TASK', + task.id, + truncate(task.title), + remindAt, + ); const isRemoveFromToday = isMoveToBacklog && task.tagIds.includes(TODAY_TAG.id); return [ new UpdateTask({ - task: {id: task.id, changes: {reminderId}} + task: { id: task.id, changes: { reminderId } }, }), ...(isMoveToBacklog - ? [moveTaskToBacklogListAuto({ - taskId: task.id, - workContextId: task.projectId as string - })] - : [] - ), + ? [ + moveTaskToBacklogListAuto({ + taskId: task.id, + workContextId: task.projectId as string, + }), + ] + : []), ...(isRemoveFromToday - ? [new UpdateTaskTags({ - task, - newTagIds: task.tagIds.filter(tagId => tagId !== TODAY_TAG.id), - oldTagIds: task.tagIds, - })] - : [] - ), + ? [ + new UpdateTaskTags({ + task, + newTagIds: task.tagIds.filter((tagId) => tagId !== TODAY_TAG.id), + oldTagIds: task.tagIds, + }), + ] + : []), ]; - }) + }), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) updateTaskReminder$: any = this._actions$.pipe( - ofType( - TaskActionTypes.ReScheduleTask, + ofType(TaskActionTypes.ReScheduleTask), + filter( + ({ payload }: ReScheduleTask) => + typeof payload.remindAt === 'number' && !!payload.reminderId, ), - filter(({payload}: ReScheduleTask) => typeof payload.remindAt === 'number' && !!payload.reminderId), tap((a: ReScheduleTask) => { console.log(a); - const {title, remindAt, reminderId} = a.payload; + const { title, remindAt, reminderId } = a.payload; this._reminderService.updateReminder(reminderId as string, { remindAt, title, }); }), - tap((a: ReScheduleTask) => this._snackService.open({ - type: 'SUCCESS', - translateParams: { - title: truncate(a.payload.title) - }, - msg: T.F.TASK.S.REMINDER_UPDATED, - ico: 'schedule', - })), + tap((a: ReScheduleTask) => + this._snackService.open({ + type: 'SUCCESS', + translateParams: { + title: truncate(a.payload.title), + }, + msg: T.F.TASK.S.REMINDER_UPDATED, + ico: 'schedule', + }), + ), ); @Effect() removeTaskReminder$: any = this._actions$.pipe( - ofType( - TaskActionTypes.UnScheduleTask, - ), - filter(({payload}: UnScheduleTask) => !!payload.reminderId), + ofType(TaskActionTypes.UnScheduleTask), + filter(({ payload }: UnScheduleTask) => !!payload.reminderId), map((a: UnScheduleTask) => { - const {id, reminderId} = a.payload; + const { id, reminderId } = a.payload; this._reminderService.removeReminder(reminderId as string); return new UpdateTask({ task: { id, - changes: {reminderId: null} - } + changes: { reminderId: null }, + }, }); }), - tap(() => this._snackService.open({ - type: 'SUCCESS', - msg: T.F.TASK.S.REMINDER_DELETED, - ico: 'schedule', - })), + tap(() => + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.TASK.S.REMINDER_DELETED, + ico: 'schedule', + }), + ), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) clearReminders: any = this._actions$.pipe( - ofType( - TaskActionTypes.DeleteTask, - ), + ofType(TaskActionTypes.DeleteTask), tap((a: DeleteTask) => { const deletedTaskIds = [a.payload.task.id, ...a.payload.task.subTaskIds]; deletedTaskIds.forEach((id) => { this._reminderService.removeReminderByRelatedIdIfSet(id); }); - }) + }), ); constructor( private _actions$: Actions, private _reminderService: ReminderService, private _snackService: SnackService, - ) { - } + ) {} } - - diff --git a/src/app/features/tasks/store/task-ui.effects.ts b/src/app/features/tasks/store/task-ui.effects.ts index e270c89d7..5a05f88cb 100644 --- a/src/app/features/tasks/store/task-ui.effects.ts +++ b/src/app/features/tasks/store/task-ui.effects.ts @@ -19,61 +19,64 @@ import { playDoneSound } from '../util/play-done-sound'; @Injectable() export class TaskUiEffects { - @Effect({dispatch: false}) + @Effect({ dispatch: false }) taskCreatedSnack$: any = this._actions$.pipe( - ofType( - TaskActionTypes.AddTask, + ofType(TaskActionTypes.AddTask), + tap((a: AddTask) => + this._snackService.open({ + type: 'SUCCESS', + translateParams: { + title: truncate(a.payload.task.title), + }, + msg: T.F.TASK.S.TASK_CREATED, + ico: 'add', + }), ), - tap((a: AddTask) => this._snackService.open({ - type: 'SUCCESS', - translateParams: { - title: truncate(a.payload.task.title) - }, - msg: T.F.TASK.S.TASK_CREATED, - ico: 'add', - })), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) snackDelete$: any = this._actions$.pipe( - ofType( - TaskActionTypes.DeleteTask, - ), + ofType(TaskActionTypes.DeleteTask), tap((actionIN: DeleteTask) => { const action = actionIN as DeleteTask; this._snackService.open({ translateParams: { - title: truncate(action.payload.task.title) + title: truncate(action.payload.task.title), }, msg: T.F.TASK.S.DELETED, - config: {duration: 5000}, + config: { duration: 5000 }, actionStr: T.G.UNDO, - actionId: TaskActionTypes.UndoDeleteTask + actionId: TaskActionTypes.UndoDeleteTask, }); - }) + }), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) timeEstimateExceeded$: any = this._actions$.pipe( - ofType( - TaskActionTypes.AddTimeSpent, - ), + ofType(TaskActionTypes.AddTimeSpent), // refresh every 10 minute max throttleTime(10 * 60 * 1000), withLatestFrom( this._store$.pipe(select(selectCurrentTask)), this._store$.pipe(select(selectConfigFeatureState)), ), - tap((args) => this._notifyAboutTimeEstimateExceeded(args)) + tap((args) => this._notifyAboutTimeEstimateExceeded(args)), ); - @Effect({dispatch: false}) + @Effect({ dispatch: false }) taskDoneSound$: any = this._actions$.pipe( - ofType( - TaskActionTypes.UpdateTask, + ofType(TaskActionTypes.UpdateTask), + filter( + ({ + payload: { + task: { changes }, + }, + }: UpdateTask) => !!changes.isDone, + ), + withLatestFrom( + this._workContextService.flatDoneTodayNr$, + this._globalConfigService.sound$, ), - filter(({payload: {task: {changes}}}: UpdateTask) => !!changes.isDone), - withLatestFrom(this._workContextService.flatDoneTodayNr$, this._globalConfigService.sound$), filter(([, , soundCfg]) => soundCfg.isPlayDoneSound), tap(([, doneToday, soundCfg]) => playDoneSound(soundCfg, doneToday)), ); @@ -87,35 +90,41 @@ export class TaskUiEffects { private _snackService: SnackService, private _globalConfigService: GlobalConfigService, private _workContextService: WorkContextService, - ) { - } + ) {} - private _notifyAboutTimeEstimateExceeded([action, ct, globalCfg]: [Action, any, GlobalConfigState]) { - if (globalCfg && globalCfg.misc.isNotifyWhenTimeEstimateExceeded - && ct && ct.timeEstimate > 0 - && ct.timeSpent > ct.timeEstimate) { + private _notifyAboutTimeEstimateExceeded([action, ct, globalCfg]: [ + Action, + any, + GlobalConfigState, + ]) { + if ( + globalCfg && + globalCfg.misc.isNotifyWhenTimeEstimateExceeded && + ct && + ct.timeEstimate > 0 && + ct.timeSpent > ct.timeEstimate + ) { const title = truncate(ct.title); this._notifyService.notify({ title: T.F.TASK.N.ESTIMATE_EXCEEDED, body: T.F.TASK.N.ESTIMATE_EXCEEDED_BODY, - translateParams: {title}, + translateParams: { title }, }); this._bannerService.open({ msg: T.F.TASK.B.ESTIMATE_EXCEEDED, id: BannerId.TimeEstimateExceeded, ico: 'timer', - translateParams: {title}, + translateParams: { title }, action: { label: T.F.TASK.B.ADD_HALF_HOUR, - fn: () => this._taskService.update(ct.id, { - timeEstimate: (ct.timeSpent + 30 * 60000) - }) - } + fn: () => + this._taskService.update(ct.id, { + timeEstimate: ct.timeSpent + 30 * 60000, + }), + }, }); } } } - - diff --git a/src/app/features/tasks/store/task.actions.ts b/src/app/features/tasks/store/task.actions.ts index c1f88ed45..24a13d90d 100644 --- a/src/app/features/tasks/store/task.actions.ts +++ b/src/app/features/tasks/store/task.actions.ts @@ -42,21 +42,23 @@ export enum TaskActionTypes { 'MoveToOtherProject' = '[Task] Move tasks to other project', 'ToggleStart' = '[Task] Toggle start', 'RoundTimeSpentForDay' = '[Task] RoundTimeSpentForDay', - } export class SetCurrentTask implements Action { readonly type: string = TaskActionTypes.SetCurrentTask; - constructor(public payload: string | null) { - } + constructor(public payload: string | null) {} } export class SetSelectedTask implements Action { readonly type: string = TaskActionTypes.SetSelectedTask; - constructor(public payload: { id: string | null; taskAdditionalInfoTargetPanel?: TaskAdditionalInfoTargetPanel }) { - } + constructor( + public payload: { + id: string | null; + taskAdditionalInfoTargetPanel?: TaskAdditionalInfoTargetPanel; + }, + ) {} } export class UnsetCurrentTask implements Action { @@ -66,71 +68,66 @@ export class UnsetCurrentTask implements Action { export class AddTask implements Action { readonly type: string = TaskActionTypes.AddTask; - constructor(public payload: { - task: Task; - issue?: IssueDataReduced; - workContextId: string; - workContextType: WorkContextType; - isAddToBacklog: boolean; - isAddToBottom: boolean; - }) { - } + constructor( + public payload: { + task: Task; + issue?: IssueDataReduced; + workContextId: string; + workContextType: WorkContextType; + isAddToBacklog: boolean; + isAddToBottom: boolean; + }, + ) {} } export class UpdateTask implements Action { readonly type: string = TaskActionTypes.UpdateTask; - constructor(public payload: { task: Update }) { - } + constructor(public payload: { task: Update }) {} } export class UpdateTaskUi implements Action { readonly type: string = TaskActionTypes.UpdateTaskUi; - constructor(public payload: { task: Update }) { - } + constructor(public payload: { task: Update }) {} } export class UpdateTaskTags implements Action { readonly type: string = TaskActionTypes.UpdateTaskTags; - constructor(public payload: { task: Task; newTagIds: string[]; oldTagIds: string[] }) { - } + constructor(public payload: { task: Task; newTagIds: string[]; oldTagIds: string[] }) {} } export class RemoveTagsForAllTasks implements Action { readonly type: string = TaskActionTypes.RemoveTagsForAllTasks; - constructor(public payload: { tagIdsToRemove: string[] }) { - } + constructor(public payload: { tagIdsToRemove: string[] }) {} } export class ToggleTaskShowSubTasks implements Action { readonly type: string = TaskActionTypes.ToggleTaskShowSubTasks; - constructor(public payload: { taskId: string; isShowLess: boolean; isEndless: boolean }) { - } + constructor( + public payload: { taskId: string; isShowLess: boolean; isEndless: boolean }, + ) {} } export class UpdateTasks implements Action { readonly type: string = TaskActionTypes.UpdateTasks; - constructor(public payload: { tasks: Update[] }) { - } + constructor(public payload: { tasks: Update[] }) {} } export class DeleteTask implements Action { readonly type: string = TaskActionTypes.DeleteTask; - constructor(public payload: { task: TaskWithSubTasks }) { - } + constructor(public payload: { task: TaskWithSubTasks }) {} } export class DeleteMainTasks implements Action { readonly type: string = TaskActionTypes.DeleteMainTasks; - constructor(public payload: { taskIds: string[] }) { - } + constructor(public payload: { taskIds: string[] }) {} } export class UndoDeleteTask implements Action { @@ -140,98 +137,102 @@ export class UndoDeleteTask implements Action { export class MoveSubTask implements Action { readonly type: string = TaskActionTypes.MoveSubTask; - constructor(public payload: { - taskId: string; - srcTaskId: string; - targetTaskId: string; - newOrderedIds: string[]; - }) { - } + constructor( + public payload: { + taskId: string; + srcTaskId: string; + targetTaskId: string; + newOrderedIds: string[]; + }, + ) {} } export class MoveSubTaskUp implements Action { readonly type: string = TaskActionTypes.MoveSubTaskUp; - constructor(public payload: { id: string; parentId: string }) { - } + constructor(public payload: { id: string; parentId: string }) {} } export class MoveSubTaskDown implements Action { readonly type: string = TaskActionTypes.MoveSubTaskDown; - constructor(public payload: { id: string; parentId: string }) { - } + constructor(public payload: { id: string; parentId: string }) {} } export class AddTimeSpent implements Action { readonly type: string = TaskActionTypes.AddTimeSpent; - constructor(public payload: { task: Task; date: string; duration: number }) { - } + constructor(public payload: { task: Task; date: string; duration: number }) {} } export class RemoveTimeSpent implements Action { readonly type: string = TaskActionTypes.RemoveTimeSpent; - constructor(public payload: { id: string; date: string; duration: number }) { - } + constructor(public payload: { id: string; date: string; duration: number }) {} } // Reminder Actions export class ScheduleTask implements Action { readonly type: string = TaskActionTypes.ScheduleTask; - constructor(public payload: { task: Task; plannedAt: number; remindAt?: number; isMoveToBacklog: boolean }) { - } + constructor( + public payload: { + task: Task; + plannedAt: number; + remindAt?: number; + isMoveToBacklog: boolean; + }, + ) {} } export class ReScheduleTask implements Action { readonly type: string = TaskActionTypes.ReScheduleTask; - constructor(public payload: { id: string; title: string; plannedAt: number; reminderId?: string; remindAt?: number }) { - } + constructor( + public payload: { + id: string; + title: string; + plannedAt: number; + reminderId?: string; + remindAt?: number; + }, + ) {} } export class UnScheduleTask implements Action { readonly type: string = TaskActionTypes.UnScheduleTask; - constructor(public payload: { id: string; reminderId?: string }) { - } + constructor(public payload: { id: string; reminderId?: string }) {} } export class RestoreTask implements Action { readonly type: string = TaskActionTypes.RestoreTask; - constructor(public payload: { task: TaskWithSubTasks; subTasks: Task[] }) { - } + constructor(public payload: { task: TaskWithSubTasks; subTasks: Task[] }) {} } export class AddSubTask implements Action { readonly type: string = TaskActionTypes.AddSubTask; - constructor(public payload: { task: Task; parentId: string }) { - } + constructor(public payload: { task: Task; parentId: string }) {} } export class ConvertToMainTask implements Action { readonly type: string = TaskActionTypes.ConvertToMainTask; - constructor(public payload: { task: Task; parentTagIds: string[] }) { - } + constructor(public payload: { task: Task; parentTagIds: string[] }) {} } export class MoveToArchive implements Action { readonly type: string = TaskActionTypes.MoveToArchive; - constructor(public payload: { tasks: TaskWithSubTasks[] }) { - } + constructor(public payload: { tasks: TaskWithSubTasks[] }) {} } export class MoveToOtherProject implements Action { readonly type: string = TaskActionTypes.MoveToOtherProject; - constructor(public payload: { task: TaskWithSubTasks; targetProjectId: string }) { - } + constructor(public payload: { task: TaskWithSubTasks; targetProjectId: string }) {} } export class ToggleStart implements Action { @@ -241,12 +242,19 @@ export class ToggleStart implements Action { export class RoundTimeSpentForDay implements Action { readonly type: string = TaskActionTypes.RoundTimeSpentForDay; - constructor(public payload: { day: string; taskIds: string[]; roundTo: RoundTimeOption; isRoundUp: boolean; projectId?: string | null }) { - } + constructor( + public payload: { + day: string; + taskIds: string[]; + roundTo: RoundTimeOption; + isRoundUp: boolean; + projectId?: string | null; + }, + ) {} } -export type TaskActions - = SetCurrentTask +export type TaskActions = + | SetCurrentTask | SetSelectedTask | UnsetCurrentTask | AddTask @@ -274,4 +282,3 @@ export type TaskActions | RoundTimeSpentForDay | MoveToOtherProject | MoveToArchive; - diff --git a/src/app/features/tasks/store/task.reducer.ts b/src/app/features/tasks/store/task.reducer.ts index 79128e24c..01e8ceaf8 100644 --- a/src/app/features/tasks/store/task.reducer.ts +++ b/src/app/features/tasks/store/task.reducer.ts @@ -1,7 +1,6 @@ import { AddSubTask, AddTask, - ScheduleTask, AddTimeSpent, ConvertToMainTask, DeleteMainTasks, @@ -12,23 +11,32 @@ import { MoveToArchive, MoveToOtherProject, RemoveTagsForAllTasks, - UnScheduleTask, RemoveTimeSpent, + ReScheduleTask, RestoreTask, RoundTimeSpentForDay, - ReScheduleTask, + ScheduleTask, SetCurrentTask, SetSelectedTask, TaskActions, TaskActionTypes, ToggleTaskShowSubTasks, + UnScheduleTask, UpdateTask, UpdateTaskTags, - UpdateTaskUi + UpdateTaskUi, } from './task.actions'; -import { ShowSubTasksMode, Task, TaskAdditionalInfoTargetPanel, TaskState } from '../task.model'; +import { + ShowSubTasksMode, + Task, + TaskAdditionalInfoTargetPanel, + TaskState, +} from '../task.model'; import { calcTotalTimeSpent } from '../util/calc-total-time-spent'; -import { AddTaskRepeatCfgToTask, TaskRepeatCfgActionTypes } from '../../task-repeat-cfg/store/task-repeat-cfg.actions'; +import { + AddTaskRepeatCfgToTask, + TaskRepeatCfgActionTypes, +} from '../../task-repeat-cfg/store/task-repeat-cfg.actions'; import { deleteTask, getTaskById, @@ -37,7 +45,7 @@ import { removeTaskFromParentSideEffects, updateDoneOnForTask, updateTimeEstimateForTask, - updateTimeSpentForTask + updateTimeSpentForTask, } from './task.reducer.util'; import { taskAdapter } from './task.adapter'; import { moveItemInList } from '../../work-context/store/work-context-meta.helper'; @@ -48,7 +56,7 @@ import { DeleteTaskAttachment, TaskAttachmentActions, TaskAttachmentActionTypes, - UpdateTaskAttachment + UpdateTaskAttachment, } from '../task-attachment/task-attachment.actions'; import { Update } from '@ngrx/entity'; import { unique } from '../../../util/unique'; @@ -77,7 +85,7 @@ export const initialTaskState: TaskState = taskAdapter.getInitialState({ // TODO unit test the shit out of this once the model is settled export function taskReducer( state: TaskState = initialTaskState, - action: TaskActions | AddTaskRepeatCfgToTask | TaskAttachmentActions + action: TaskActions | AddTaskRepeatCfgToTask | TaskAttachmentActions, ): TaskState { if (environment.production) { console.log(action.type, (action as any)?.payload || action, state); @@ -85,15 +93,15 @@ export function taskReducer( // TODO fix this hackyness once we use the new syntax everywhere if ((action.type as string) === loadAllData.type) { - const {appDataComplete}: { appDataComplete: AppDataComplete } = action as any; + const { appDataComplete }: { appDataComplete: AppDataComplete } = action as any; return appDataComplete.task ? migrateTaskState({ - ...appDataComplete.task, - currentTaskId: null, - lastCurrentTaskId: appDataComplete.task.currentTaskId, - isDataLoaded: true, - }) : - state; + ...appDataComplete.task, + currentTaskId: null, + lastCurrentTaskId: appDataComplete.task.currentTaskId, + isDataLoaded: true, + }) + : state; } switch (action.type) { @@ -105,17 +113,18 @@ export function taskReducer( let taskToStartId = a.payload; if (subTaskIds && subTaskIds.length) { const undoneTasks = subTaskIds - .map(id => state.entities[id] as Task) + .map((id) => state.entities[id] as Task) .filter((ta: Task) => !ta.isDone); - taskToStartId = undoneTasks.length - ? undoneTasks[0].id - : subTaskIds[0]; + taskToStartId = undoneTasks.length ? undoneTasks[0].id : subTaskIds[0]; } return { - ...(taskAdapter.updateOne({ - id: taskToStartId, - changes: {isDone: false, doneOn: null} - }, state)), + ...taskAdapter.updateOne( + { + id: taskToStartId, + changes: { isDone: false, doneOn: null }, + }, + state, + ), currentTaskId: taskToStartId, selectedTaskId: state.selectedTaskId && taskToStartId, }; @@ -128,19 +137,18 @@ export function taskReducer( } case TaskActionTypes.UnsetCurrentTask: { - return {...state, currentTaskId: null, lastCurrentTaskId: state.currentTaskId}; + return { ...state, currentTaskId: null, lastCurrentTaskId: state.currentTaskId }; } case TaskActionTypes.SetSelectedTask: { - const {id, taskAdditionalInfoTargetPanel} = (action as SetSelectedTask).payload; + const { id, taskAdditionalInfoTargetPanel } = (action as SetSelectedTask).payload; return { ...state, - taskAdditionalInfoTargetPanel: (!id || (id === state.selectedTaskId)) - ? null - : taskAdditionalInfoTargetPanel || null, - selectedTaskId: (id === state.selectedTaskId) - ? null - : id, + taskAdditionalInfoTargetPanel: + !id || id === state.selectedTaskId + ? null + : taskAdditionalInfoTargetPanel || null, + selectedTaskId: id === state.selectedTaskId ? null : id, }; } @@ -158,7 +166,7 @@ export function taskReducer( let stateCopy = state; const a: UpdateTask = action as UpdateTask; const id = a.payload.task.id as string; - const {timeSpentOnDay, timeEstimate} = a.payload.task.changes; + const { timeSpentOnDay, timeEstimate } = a.payload.task.changes; stateCopy = timeSpentOnDay ? updateTimeSpentForTask(id, timeSpentOnDay, stateCopy) : stateCopy; @@ -172,13 +180,16 @@ export function taskReducer( } case TaskActionTypes.UpdateTaskTags: { - const {task, newTagIds} = (action as UpdateTaskTags).payload; - return taskAdapter.updateOne({ - id: task.id, - changes: { - tagIds: newTagIds, - } - }, state); + const { task, newTagIds } = (action as UpdateTaskTags).payload; + return taskAdapter.updateOne( + { + id: task.id, + changes: { + tagIds: newTagIds, + }, + }, + state, + ); } case TaskActionTypes.RemoveTagsForAllTasks: { @@ -186,20 +197,25 @@ export function taskReducer( id: taskId, changes: { tagIds: (state.entities[taskId] as Task).tagIds.filter( - tagId => !(action as RemoveTagsForAllTasks).payload.tagIdsToRemove.includes(tagId) + (tagId) => + !(action as RemoveTagsForAllTasks).payload.tagIdsToRemove.includes(tagId), ), - } + }, })); return taskAdapter.updateMany(updates, state); } // TODO simplify case TaskActionTypes.ToggleTaskShowSubTasks: { - const {taskId, isShowLess, isEndless} = (action as ToggleTaskShowSubTasks).payload; - const task = (state.entities[taskId] as Task); - const subTasks = task.subTaskIds.map(id => state.entities[id] as Task); + const { + taskId, + isShowLess, + isEndless, + } = (action as ToggleTaskShowSubTasks).payload; + const task = state.entities[taskId] as Task; + const subTasks = task.subTaskIds.map((id) => state.entities[id] as Task); const doneTasksLength = subTasks.filter((t) => t.isDone).length; - const isDoneTaskCaseNeeded = doneTasksLength && (doneTasksLength < subTasks.length); + const isDoneTaskCaseNeeded = doneTasksLength && doneTasksLength < subTasks.length; const oldVal = +task._showSubTasksMode; let newVal; @@ -219,7 +235,6 @@ export function taskReducer( newVal = ShowSubTasksMode.HideAll; } } - } else { if (isEndless) { if (oldVal === ShowSubTasksMode.Show) { @@ -229,23 +244,22 @@ export function taskReducer( newVal = ShowSubTasksMode.Show; } } else { - newVal = (isShowLess) - ? ShowSubTasksMode.HideAll - : ShowSubTasksMode.Show; + newVal = isShowLess ? ShowSubTasksMode.HideAll : ShowSubTasksMode.Show; } } // failsafe - newVal = (isNaN(newVal as any)) - ? ShowSubTasksMode.HideAll - : newVal; + newVal = isNaN(newVal as any) ? ShowSubTasksMode.HideAll : newVal; - return taskAdapter.updateOne({ - id: taskId, - changes: { - _showSubTasksMode: newVal - } - }, state); + return taskAdapter.updateOne( + { + id: taskId, + changes: { + _showSubTasksMode: newVal, + }, + }, + state, + ); } case TaskActionTypes.DeleteTask: { @@ -253,164 +267,186 @@ export function taskReducer( } case TaskActionTypes.DeleteMainTasks: { - const allIds = (action as DeleteMainTasks).payload.taskIds.reduce((acc: string[], id: string) => { - return [ - ...acc, - id, - ...(state.entities[id] as Task).subTaskIds - ]; - }, []); + const allIds = (action as DeleteMainTasks).payload.taskIds.reduce( + (acc: string[], id: string) => { + return [...acc, id, ...(state.entities[id] as Task).subTaskIds]; + }, + [], + ); return taskAdapter.removeMany(allIds, state); } case TaskActionTypes.MoveSubTask: { let newState = state; - const {taskId, srcTaskId, targetTaskId, newOrderedIds} = (action as MoveSubTask).payload; + const { + taskId, + srcTaskId, + targetTaskId, + newOrderedIds, + } = (action as MoveSubTask).payload; const oldPar = state.entities[srcTaskId] as Task; const newPar = state.entities[targetTaskId] as Task; // for old parent remove - newState = taskAdapter.updateOne({ - id: oldPar.id, - changes: { - subTaskIds: oldPar.subTaskIds.filter(filterOutId(taskId)) - } - }, newState); + newState = taskAdapter.updateOne( + { + id: oldPar.id, + changes: { + subTaskIds: oldPar.subTaskIds.filter(filterOutId(taskId)), + }, + }, + newState, + ); newState = reCalcTimesForParentIfParent(oldPar.id, newState); // for new parent add and move - newState = taskAdapter.updateOne({ - id: newPar.id, - changes: { - subTaskIds: moveItemInList(taskId, newPar.subTaskIds, newOrderedIds), - } - }, newState); + newState = taskAdapter.updateOne( + { + id: newPar.id, + changes: { + subTaskIds: moveItemInList(taskId, newPar.subTaskIds, newOrderedIds), + }, + }, + newState, + ); newState = reCalcTimesForParentIfParent(newPar.id, newState); // change parent id for moving task - newState = taskAdapter.updateOne({ - id: taskId, - changes: { - parentId: newPar.id, - projectId: newPar.projectId, - } - }, newState); + newState = taskAdapter.updateOne( + { + id: taskId, + changes: { + parentId: newPar.id, + projectId: newPar.projectId, + }, + }, + newState, + ); return newState; } case TaskActionTypes.MoveSubTaskUp: { - const {id, parentId} = (action as MoveSubTaskUp).payload; + const { id, parentId } = (action as MoveSubTaskUp).payload; const parentSubTaskIds = (state.entities[parentId] as Task).subTaskIds; - return taskAdapter.updateOne({ - id: parentId, - changes: { - subTaskIds: arrayMoveLeft(parentSubTaskIds, id) - } - }, state); + return taskAdapter.updateOne( + { + id: parentId, + changes: { + subTaskIds: arrayMoveLeft(parentSubTaskIds, id), + }, + }, + state, + ); } case TaskActionTypes.MoveSubTaskDown: { - const {id, parentId} = (action as MoveSubTaskDown).payload; + const { id, parentId } = (action as MoveSubTaskDown).payload; const parentSubTaskIds = (state.entities[parentId] as Task).subTaskIds; - return taskAdapter.updateOne({ - id: parentId, - changes: { - subTaskIds: arrayMoveRight(parentSubTaskIds, id) - } - }, state); + return taskAdapter.updateOne( + { + id: parentId, + changes: { + subTaskIds: arrayMoveRight(parentSubTaskIds, id), + }, + }, + state, + ); } case TaskActionTypes.AddTimeSpent: { - const {task, date, duration} = (action as AddTimeSpent).payload; - const currentTimeSpentForTickDay = task.timeSpentOnDay && +task.timeSpentOnDay[date] || 0; + const { task, date, duration } = (action as AddTimeSpent).payload; + const currentTimeSpentForTickDay = + (task.timeSpentOnDay && +task.timeSpentOnDay[date]) || 0; return updateTimeSpentForTask( - task.id, { + task.id, + { ...task.timeSpentOnDay, - [date]: (currentTimeSpentForTickDay + duration) + [date]: currentTimeSpentForTickDay + duration, }, - state + state, ); } case TaskActionTypes.RemoveTimeSpent: { - const {id, date, duration} = (action as RemoveTimeSpent).payload; + const { id, date, duration } = (action as RemoveTimeSpent).payload; const task = getTaskById(id, state) as Task; - const currentTimeSpentForTickDay = task.timeSpentOnDay && +task.timeSpentOnDay[date] || 0; + const currentTimeSpentForTickDay = + (task.timeSpentOnDay && +task.timeSpentOnDay[date]) || 0; return updateTimeSpentForTask( - id, { + id, + { ...task.timeSpentOnDay, - [date]: Math.max((currentTimeSpentForTickDay - duration), 0) + [date]: Math.max(currentTimeSpentForTickDay - duration, 0), }, - state + state, ); } case TaskActionTypes.AddSubTask: { - const {task, parentId} = (action as AddSubTask).payload; + const { task, parentId } = (action as AddSubTask).payload; const parentTask = state.entities[parentId] as Task; // add item1 - const stateCopy = taskAdapter.addOne({ - ...task, - parentId, - // update timeSpent if first sub task and non present - ...( - (parentTask.subTaskIds.length === 0 && Object.keys(task.timeSpentOnDay).length === 0) + const stateCopy = taskAdapter.addOne( + { + ...task, + parentId, + // update timeSpent if first sub task and non present + ...(parentTask.subTaskIds.length === 0 && + Object.keys(task.timeSpentOnDay).length === 0 ? { - timeSpentOnDay: parentTask.timeSpentOnDay, - timeSpent: calcTotalTimeSpent(parentTask.timeSpentOnDay) - } - : {} - ), - // update timeEstimate if first sub task and non present - ...( - (parentTask.subTaskIds.length === 0 && !task.timeEstimate) - ? {timeEstimate: parentTask.timeEstimate} - : {} - ), - // should always be empty - tagIds: [], - // should always be the one of the parent - projectId: parentTask.projectId - }, state); + timeSpentOnDay: parentTask.timeSpentOnDay, + timeSpent: calcTotalTimeSpent(parentTask.timeSpentOnDay), + } + : {}), + // update timeEstimate if first sub task and non present + ...(parentTask.subTaskIds.length === 0 && !task.timeEstimate + ? { timeEstimate: parentTask.timeEstimate } + : {}), + // should always be empty + tagIds: [], + // should always be the one of the parent + projectId: parentTask.projectId, + }, + state, + ); return { ...stateCopy, // update current task to new sub task if parent was current before - ...( - (state.currentTaskId === parentId) - ? {currentTaskId: task.id} - : {} - ), + ...(state.currentTaskId === parentId ? { currentTaskId: task.id } : {}), // also add to parent task entities: { ...stateCopy.entities, [parentId]: { ...parentTask, - subTaskIds: [...parentTask.subTaskIds, task.id] - } - } + subTaskIds: [...parentTask.subTaskIds, task.id], + }, + }, }; } case TaskActionTypes.ConvertToMainTask: { - const {task} = (action as ConvertToMainTask).payload; + const { task } = (action as ConvertToMainTask).payload; const par = state.entities[task.parentId as string]; if (!par) { throw new Error('No parent for sub task'); } const stateCopy = removeTaskFromParentSideEffects(state, task); - return taskAdapter.updateOne({ - id: task.id, - changes: { - parentId: null, - tagIds: [...par.tagIds], - } - }, stateCopy); + return taskAdapter.updateOne( + { + id: task.id, + changes: { + parentId: null, + tagIds: [...par.tagIds], + }, + }, + stateCopy, + ); } case TaskActionTypes.ToggleStart: { @@ -424,54 +460,66 @@ export function taskReducer( } case TaskActionTypes.MoveToOtherProject: { - const {targetProjectId, task} = (action as MoveToOtherProject).payload; - const updates: Update[] = [task.id, ...task.subTaskIds].map(id => ({ + const { targetProjectId, task } = (action as MoveToOtherProject).payload; + const updates: Update[] = [task.id, ...task.subTaskIds].map((id) => ({ id, changes: { - projectId: targetProjectId - } + projectId: targetProjectId, + }, })); return taskAdapter.updateMany(updates, state); } case TaskActionTypes.RoundTimeSpentForDay: { - const {day, taskIds, isRoundUp, roundTo, projectId} = (action as RoundTimeSpentForDay).payload; + const { + day, + taskIds, + isRoundUp, + roundTo, + projectId, + } = (action as RoundTimeSpentForDay).payload; const isLimitToProject: boolean = !!projectId || projectId === null; - const idsToUpdateDirectly: string[] = taskIds.filter(id => { - const task: Task = state.entities[id] as Task; - return (task.subTaskIds.length === 0 || !!task.parentId) - && (!isLimitToProject || task.projectId === projectId); - } + const idsToUpdateDirectly: string[] = taskIds.filter((id) => { + const task: Task = state.entities[id] as Task; + return ( + (task.subTaskIds.length === 0 || !!task.parentId) && + (!isLimitToProject || task.projectId === projectId) + ); + }); + const subTaskIds: string[] = idsToUpdateDirectly.filter( + (id) => !!(state.entities[id] as Task).parentId, ); - const subTaskIds: string[] = idsToUpdateDirectly.filter(id => !!(state.entities[id] as Task).parentId); - const parentTaskToReCalcIds: string[] = unique(subTaskIds - .map(id => (state.entities[id] as Task).parentId as string) + const parentTaskToReCalcIds: string[] = unique( + subTaskIds.map((id) => (state.entities[id] as Task).parentId as string), ); - const updateSubsAndMainWithoutSubs: Update[] = idsToUpdateDirectly.map(id => { - const spentOnDayBefore = (state.entities[id] as Task).timeSpentOnDay; - const timeSpentOnDayUpdated = { - ...spentOnDayBefore, - [day]: roundDurationVanilla(spentOnDayBefore[day], roundTo, isRoundUp) - }; - return { - id, - changes: { - timeSpentOnDay: timeSpentOnDayUpdated, - timeSpent: calcTotalTimeSpent(timeSpentOnDayUpdated), - } - }; - }); + const updateSubsAndMainWithoutSubs: Update[] = idsToUpdateDirectly.map( + (id) => { + const spentOnDayBefore = (state.entities[id] as Task).timeSpentOnDay; + const timeSpentOnDayUpdated = { + ...spentOnDayBefore, + [day]: roundDurationVanilla(spentOnDayBefore[day], roundTo, isRoundUp), + }; + return { + id, + changes: { + timeSpentOnDay: timeSpentOnDayUpdated, + timeSpent: calcTotalTimeSpent(timeSpentOnDayUpdated), + }, + }; + }, + ); // // update subs const newState = taskAdapter.updateMany(updateSubsAndMainWithoutSubs, state); // reCalc parents - return parentTaskToReCalcIds.reduce((acc, parentId) => - reCalcTimeSpentForParentIfParent(parentId, acc), newState); + return parentTaskToReCalcIds.reduce( + (acc, parentId) => reCalcTimeSpentForParentIfParent(parentId, acc), + newState, + ); } - // TASK ARCHIVE STUFF // ------------------ // TODO fix @@ -481,12 +529,16 @@ export function taskReducer( copyState = deleteTask(copyState, task); }); return { - ...copyState + ...copyState, }; } case TaskActionTypes.RestoreTask: { - const task = {...(action as RestoreTask).payload.task, isDone: false, doneOn: null}; + const task = { + ...(action as RestoreTask).payload.task, + isDone: false, + doneOn: null, + }; const subTasks = (action as RestoreTask).payload.subTasks || []; return taskAdapter.addMany([task, ...subTasks], state); } @@ -494,89 +546,112 @@ export function taskReducer( // REPEAT STUFF // ------------ case TaskRepeatCfgActionTypes.AddTaskRepeatCfgToTask: { - return taskAdapter.updateOne({ - id: (action as AddTaskRepeatCfgToTask).payload.taskId, - changes: { - repeatCfgId: (action as AddTaskRepeatCfgToTask).payload.taskRepeatCfg.id - } - }, state); + return taskAdapter.updateOne( + { + id: (action as AddTaskRepeatCfgToTask).payload.taskId, + changes: { + repeatCfgId: (action as AddTaskRepeatCfgToTask).payload.taskRepeatCfg.id, + }, + }, + state, + ); } - // TASK ATTACHMENTS // ---------------- case TaskAttachmentActionTypes.AddTaskAttachment: { - const {taskId, taskAttachment} = (action as AddTaskAttachment).payload; - return taskAdapter.updateOne({ - id: taskId, - changes: { - attachments: [ - ...(state.entities[taskId] as Task).attachments, taskAttachment - ] - } - }, state); + const { taskId, taskAttachment } = (action as AddTaskAttachment).payload; + return taskAdapter.updateOne( + { + id: taskId, + changes: { + attachments: [ + ...(state.entities[taskId] as Task).attachments, + taskAttachment, + ], + }, + }, + state, + ); } case TaskAttachmentActionTypes.UpdateTaskAttachment: { - const {taskId, taskAttachment} = (action as UpdateTaskAttachment).payload; + const { taskId, taskAttachment } = (action as UpdateTaskAttachment).payload; const attachments = (state.entities[taskId] as Task).attachments; - const updatedAttachments = attachments.map( - attachment => attachment.id === taskAttachment.id - ? ({ - ...attachment, - ...taskAttachment.changes - }) - : attachment + const updatedAttachments = attachments.map((attachment) => + attachment.id === taskAttachment.id + ? { + ...attachment, + ...taskAttachment.changes, + } + : attachment, ); - return taskAdapter.updateOne({ - id: taskId, - changes: { - attachments: updatedAttachments, - } - }, state); + return taskAdapter.updateOne( + { + id: taskId, + changes: { + attachments: updatedAttachments, + }, + }, + state, + ); } case TaskAttachmentActionTypes.DeleteTaskAttachment: { - const {taskId, id} = (action as DeleteTaskAttachment).payload; - return taskAdapter.updateOne({ - id: taskId, - changes: { - attachments: (state.entities[taskId] as Task).attachments.filter(at => at.id !== id) - } - }, state); + const { taskId, id } = (action as DeleteTaskAttachment).payload; + return taskAdapter.updateOne( + { + id: taskId, + changes: { + attachments: (state.entities[taskId] as Task).attachments.filter( + (at) => at.id !== id, + ), + }, + }, + state, + ); } // REMINDER STUFF // -------------- case TaskActionTypes.ScheduleTask: { - const {task, remindAt} = (action as ScheduleTask).payload; - return taskAdapter.updateOne({ - id: task.id, - changes: { - plannedAt: remindAt, - } - }, state); + const { task, remindAt } = (action as ScheduleTask).payload; + return taskAdapter.updateOne( + { + id: task.id, + changes: { + plannedAt: remindAt, + }, + }, + state, + ); } case TaskActionTypes.ReScheduleTask: { - const {id, plannedAt} = (action as ReScheduleTask).payload; - return taskAdapter.updateOne({ - id, - changes: { - plannedAt, - } - }, state); + const { id, plannedAt } = (action as ReScheduleTask).payload; + return taskAdapter.updateOne( + { + id, + changes: { + plannedAt, + }, + }, + state, + ); } case TaskActionTypes.UnScheduleTask: { - const {id} = (action as UnScheduleTask).payload; - return taskAdapter.updateOne({ - id, - changes: { - plannedAt: null, - } - }, state); + const { id } = (action as UnScheduleTask).payload; + return taskAdapter.updateOne( + { + id, + changes: { + plannedAt: null, + }, + }, + state, + ); } default: { diff --git a/src/app/features/tasks/store/task.reducer.util.ts b/src/app/features/tasks/store/task.reducer.util.ts index b03d3b5c5..c9a07f8d4 100644 --- a/src/app/features/tasks/store/task.reducer.util.ts +++ b/src/app/features/tasks/store/task.reducer.util.ts @@ -16,19 +16,25 @@ export const getTaskById = (taskId: string, state: TaskState): Task => { // SHARED REDUCER ACTIONS // ---------------------- -export const reCalcTimesForParentIfParent = (parentId: string, state: TaskState): TaskState => { +export const reCalcTimesForParentIfParent = ( + parentId: string, + state: TaskState, +): TaskState => { const stateWithTimeEstimate = reCalcTimeEstimateForParentIfParent(parentId, state); return reCalcTimeSpentForParentIfParent(parentId, stateWithTimeEstimate); }; -export const reCalcTimeSpentForParentIfParent = (parentId: string, state: TaskState): TaskState => { +export const reCalcTimeSpentForParentIfParent = ( + parentId: string, + state: TaskState, +): TaskState => { if (parentId) { const parentTask: Task = getTaskById(parentId, state); const subTasks = parentTask.subTaskIds.map((id) => state.entities[id] as Task); const timeSpentOnDayParent: { [key: string]: number } = {}; subTasks.forEach((subTask: Task) => { - Object.keys(subTask.timeSpentOnDay).forEach(strDate => { + Object.keys(subTask.timeSpentOnDay).forEach((strDate) => { if (subTask.timeSpentOnDay[strDate]) { if (!timeSpentOnDayParent[strDate]) { timeSpentOnDayParent[strDate] = 0; @@ -37,50 +43,62 @@ export const reCalcTimeSpentForParentIfParent = (parentId: string, state: TaskSt } }); }); - return taskAdapter.updateOne({ - id: parentId, - changes: { - timeSpentOnDay: timeSpentOnDayParent, - timeSpent: calcTotalTimeSpent(timeSpentOnDayParent), - } - }, state); + return taskAdapter.updateOne( + { + id: parentId, + changes: { + timeSpentOnDay: timeSpentOnDayParent, + timeSpent: calcTotalTimeSpent(timeSpentOnDayParent), + }, + }, + state, + ); } else { return state; } }; -export const reCalcTimeEstimateForParentIfParent = (parentId: string, state: TaskState): TaskState => { +export const reCalcTimeEstimateForParentIfParent = ( + parentId: string, + state: TaskState, +): TaskState => { if (parentId) { const parentTask: Task = state.entities[parentId] as Task; const subTasks = parentTask.subTaskIds.map((id) => state.entities[id] as Task); - return taskAdapter.updateOne({ - id: parentId, - changes: { - timeEstimate: subTasks.reduce((acc: number, task: Task) => acc + task.timeEstimate, 0), - } - }, state); + return taskAdapter.updateOne( + { + id: parentId, + changes: { + timeEstimate: subTasks.reduce( + (acc: number, task: Task) => acc + task.timeEstimate, + 0, + ), + }, + }, + state, + ); } else { return state; } }; -export const updateDoneOnForTask = ( - upd: Update, - state: TaskState, -): TaskState => { +export const updateDoneOnForTask = (upd: Update, state: TaskState): TaskState => { const task = state.entities[upd.id] as Task; - const isToDone = (upd.changes.isDone === true); - const isToUnDone = (upd.changes.isDone === false); + const isToDone = upd.changes.isDone === true; + const isToUnDone = upd.changes.isDone === false; if (isToDone || isToUnDone) { const changes = { - ...(isToDone ? {doneOn: Date.now()} : {}), - ...(isToUnDone ? {doneOn: null} : {}), + ...(isToDone ? { doneOn: Date.now() } : {}), + ...(isToUnDone ? { doneOn: null } : {}), }; - return taskAdapter.updateOne({ - id: task.id, - changes - }, state); + return taskAdapter.updateOne( + { + id: task.id, + changes, + }, + state, + ); } else { return state; } @@ -98,13 +116,16 @@ export const updateTimeSpentForTask = ( const task = getTaskById(id, state); const timeSpent = calcTotalTimeSpent(newTimeSpentOnDay); - const stateAfterUpdate = taskAdapter.updateOne({ - id, - changes: { - timeSpentOnDay: newTimeSpentOnDay, - timeSpent, - } - }, state); + const stateAfterUpdate = taskAdapter.updateOne( + { + id, + changes: { + timeSpentOnDay: newTimeSpentOnDay, + timeSpent, + }, + }, + state, + ); return task.parentId ? reCalcTimeSpentForParentIfParent(task.parentId, stateAfterUpdate) @@ -116,31 +137,34 @@ export const updateTimeEstimateForTask = ( newEstimate: number | null = null, state: TaskState, ): TaskState => { - if (typeof newEstimate !== 'number') { return state; } const task = getTaskById(taskId, state); - const stateAfterUpdate = taskAdapter.updateOne({ - id: taskId, - changes: { - timeEstimate: newEstimate, - } - }, state); + const stateAfterUpdate = taskAdapter.updateOne( + { + id: taskId, + changes: { + timeEstimate: newEstimate, + }, + }, + state, + ); return task.parentId ? reCalcTimeEstimateForParentIfParent(task.parentId, stateAfterUpdate) : stateAfterUpdate; }; -export const deleteTask = (state: TaskState, - taskToDelete: TaskWithSubTasks | Task): TaskState => { +export const deleteTask = ( + state: TaskState, + taskToDelete: TaskWithSubTasks | Task, +): TaskState => { let stateCopy: TaskState = taskAdapter.removeOne(taskToDelete.id, state); - let currentTaskId = (state.currentTaskId === taskToDelete.id) - ? null - : state.currentTaskId; + let currentTaskId = + state.currentTaskId === taskToDelete.id ? null : state.currentTaskId; // PARENT TASK side effects // also delete from parent task if any @@ -153,9 +177,10 @@ export const deleteTask = (state: TaskState, if (taskToDelete.subTaskIds) { stateCopy = taskAdapter.removeMany(taskToDelete.subTaskIds, stateCopy); // unset current if one of them is the current task - currentTaskId = !!currentTaskId && taskToDelete.subTaskIds.includes(currentTaskId) - ? null - : currentTaskId; + currentTaskId = + !!currentTaskId && taskToDelete.subTaskIds.includes(currentTaskId) + ? null + : currentTaskId; } return { @@ -164,27 +189,32 @@ export const deleteTask = (state: TaskState, }; }; -export const removeTaskFromParentSideEffects = (state: TaskState, taskToRemove: Task, isCopyTimesAfterLast: boolean = false): TaskState => { +export const removeTaskFromParentSideEffects = ( + state: TaskState, + taskToRemove: Task, + isCopyTimesAfterLast: boolean = false, +): TaskState => { const parentId: string = taskToRemove.parentId as string; const parentTask = state.entities[parentId] as Task; - const isWasLastSubTask = (parentTask.subTaskIds.length === 1); + const isWasLastSubTask = parentTask.subTaskIds.length === 1; - let newState = taskAdapter.updateOne({ - id: parentId, - changes: { - subTaskIds: parentTask.subTaskIds.filter(filterOutId(taskToRemove.id)), + let newState = taskAdapter.updateOne( + { + id: parentId, + changes: { + subTaskIds: parentTask.subTaskIds.filter(filterOutId(taskToRemove.id)), - // copy over sub task time stuff if it was the last sub task - ...( - (isWasLastSubTask && isCopyTimesAfterLast) + // copy over sub task time stuff if it was the last sub task + ...(isWasLastSubTask && isCopyTimesAfterLast ? { - timeSpentOnDay: taskToRemove.timeSpentOnDay, - timeEstimate: taskToRemove.timeEstimate, - } - : {} - ) - } - }, state); + timeSpentOnDay: taskToRemove.timeSpentOnDay, + timeEstimate: taskToRemove.timeEstimate, + } + : {}), + }, + }, + state, + ); // also update time spent for parent if it was not copied over from sub task if (!isWasLastSubTask || !isCopyTimesAfterLast) { newState = reCalcTimeSpentForParentIfParent(parentId, newState); diff --git a/src/app/features/tasks/store/task.selectors.ts b/src/app/features/tasks/store/task.selectors.ts index 001c0b428..1961655d9 100644 --- a/src/app/features/tasks/store/task.selectors.ts +++ b/src/app/features/tasks/store/task.selectors.ts @@ -8,13 +8,15 @@ import { TODAY_TAG } from '../../tag/tag.const'; // TODO fix null stuff here const mapSubTasksToTasks = (tasksIN: any[]): TaskWithSubTasks[] => { - return tasksIN.filter((task) => !task.parentId) + return tasksIN + .filter((task) => !task.parentId) .map((task) => { if (task.subTaskIds && task.subTaskIds.length > 0) { return { ...task, - subTasks: task.subTaskIds - .map((subTaskId: string) => tasksIN.find((taskIN) => taskIN.id === subTaskId)) + subTasks: task.subTaskIds.map((subTaskId: string) => + tasksIN.find((taskIN) => taskIN.id === subTaskId), + ), }; } else { return task; @@ -27,19 +29,19 @@ const mapSubTasksToTask = (task: Task | null, s: TaskState): TaskWithSubTasks | } return { ...task, - subTasks: task.subTaskIds.map(id => { + subTasks: task.subTaskIds.map((id) => { const subTask = s.entities[id] as Task; if (!subTask) { devError('Task data not found for ' + id); } return subTask; - }) + }), }; }; export const flattenTasks = (tasksIN: TaskWithSubTasks[]): TaskWithSubTasks[] => { let flatTasks: TaskWithSubTasks[] = []; - tasksIN.forEach(task => { + tasksIN.forEach((task) => { flatTasks.push(task); if (task.subTasks) { flatTasks = flatTasks.concat(task.subTasks); @@ -50,32 +52,47 @@ export const flattenTasks = (tasksIN: TaskWithSubTasks[]): TaskWithSubTasks[] => // SELECTORS // --------- -const {selectEntities, selectAll} = taskAdapter.getSelectors(); +const { selectEntities, selectAll } = taskAdapter.getSelectors(); export const selectTaskFeatureState = createFeatureSelector(TASK_FEATURE_NAME); export const selectTaskEntities = createSelector(selectTaskFeatureState, selectEntities); -export const selectCurrentTaskId = createSelector(selectTaskFeatureState, state => state.currentTaskId); -export const selectIsTaskDataLoaded = createSelector(selectTaskFeatureState, state => state.isDataLoaded); -export const selectCurrentTask = createSelector(selectTaskFeatureState, - s => s.currentTaskId ? s.entities[s.currentTaskId] as Task : null); +export const selectCurrentTaskId = createSelector( + selectTaskFeatureState, + (state) => state.currentTaskId, +); +export const selectIsTaskDataLoaded = createSelector( + selectTaskFeatureState, + (state) => state.isDataLoaded, +); +export const selectCurrentTask = createSelector(selectTaskFeatureState, (s) => + s.currentTaskId ? (s.entities[s.currentTaskId] as Task) : null, +); export const selectCurrentTaskOrParentWithData = createSelector( selectTaskFeatureState, (s): TaskWithSubTasks | null => { - const t = s.currentTaskId - && s.entities[s.currentTaskId] + const t = + (s.currentTaskId && + s.entities[s.currentTaskId] && + // @ts-ignore + s.entities[s.currentTaskId].parentId && + // @ts-ignore + s.entities[s.entities[s.currentTaskId].parentId]) || // @ts-ignore - && s.entities[s.currentTaskId].parentId - // @ts-ignore - && s.entities[s.entities[s.currentTaskId].parentId] || s.entities[s.currentTaskId]; + s.entities[s.currentTaskId]; return mapSubTasksToTask(t as Task, s); - }); + }, +); export const selectStartableTasks = createSelector( selectTaskFeatureState, (s): Task[] => { - return s.ids.map(id => s.entities[id] as Task) - .filter(task => !task.isDone && (!!task.parentId || task.subTaskIds.length === 0)); - }); + return s.ids + .map((id) => s.entities[id] as Task) + .filter( + (task) => !task.isDone && (!!task.parentId || task.subTaskIds.length === 0), + ); + }, +); // export const selectJiraTasks = createSelector( // selectTaskFeatureState, @@ -101,41 +118,54 @@ export const selectStartableTasks = createSelector( // .filter((task: Task) => task.issueType === GITLAB_TYPE); // }); -export const selectSelectedTaskId = createSelector(selectTaskFeatureState, (state) => state.selectedTaskId); -export const selectTaskAdditionalInfoTargetPanel = createSelector(selectTaskFeatureState, (state: TaskState) => state.taskAdditionalInfoTargetPanel); +export const selectSelectedTaskId = createSelector( + selectTaskFeatureState, + (state) => state.selectedTaskId, +); +export const selectTaskAdditionalInfoTargetPanel = createSelector( + selectTaskFeatureState, + (state: TaskState) => state.taskAdditionalInfoTargetPanel, +); export const selectSelectedTask = createSelector( selectTaskFeatureState, - (s): TaskWithSubTasks => { // @ts-ignore + (s): TaskWithSubTasks => { + // @ts-ignore // @ts-ignore return s.selectedTaskId && mapSubTasksToTask(s.entities[s.selectedTaskId], s); - }); + }, +); -export const selectCurrentTaskParentOrCurrent = createSelector(selectTaskFeatureState, (s): Task => - s.currentTaskId - // @ts-ignore - && s.entities[s.currentTaskId] && s.entities[s.currentTaskId].parentId - // @ts-ignore - && s.entities[s.entities[s.currentTaskId].parentId] - // @ts-ignore - || s.entities[s.currentTaskId] +export const selectCurrentTaskParentOrCurrent = createSelector( + selectTaskFeatureState, + (s): Task => + (s.currentTaskId && + s.entities[s.currentTaskId] && + // @ts-ignore + s.entities[s.currentTaskId].parentId && + // @ts-ignore + s.entities[s.entities[s.currentTaskId].parentId]) || + // @ts-ignore + s.entities[s.currentTaskId], ); export const selectPlannedTasks = createSelector(selectTaskFeatureState, (s): Task[] => { const allTasks: Task[] = []; const allParent = s.ids - .map(id => s.entities[id] as Task) - .filter(task => !task.parentId && (task.plannedAt || task.tagIds.includes(TODAY_TAG.id))); + .map((id) => s.entities[id] as Task) + .filter( + (task) => !task.parentId && (task.plannedAt || task.tagIds.includes(TODAY_TAG.id)), + ); allParent.forEach((pt) => { if (pt.subTaskIds.length) { - pt.subTaskIds.forEach(subId => { + pt.subTaskIds.forEach((subId) => { const st = s.entities[subId] as Task; // const par: Task = s.entities[st.parentId as string] as Task; allTasks.push({ ...st, - plannedAt: st.plannedAt || (!st.isDone - ? (s.entities[st.parentId as string] as Task).plannedAt - : null) + plannedAt: + st.plannedAt || + (!st.isDone ? (s.entities[st.parentId as string] as Task).plannedAt : null), }); }); } else { @@ -146,22 +176,26 @@ export const selectPlannedTasks = createSelector(selectTaskFeatureState, (s): Ta }); export const selectAllTasks = createSelector(selectTaskFeatureState, selectAll); -export const selectScheduledTasks = createSelector(selectAllTasks, (tasks) => tasks.filter(task => task.reminderId)); +export const selectScheduledTasks = createSelector(selectAllTasks, (tasks) => + tasks.filter((task) => task.reminderId), +); -export const selectAllTasksWithSubTasks = createSelector(selectAllTasks, mapSubTasksToTasks); +export const selectAllTasksWithSubTasks = createSelector( + selectAllTasks, + mapSubTasksToTasks, +); // DYNAMIC SELECTORS // ----------------- export const selectTaskById = createSelector( selectTaskFeatureState, - (state: TaskState, props: { id: string }): Task => state.entities[props.id] as Task + (state: TaskState, props: { id: string }): Task => state.entities[props.id] as Task, ); export const selectTasksById = createSelector( selectTaskFeatureState, - (state: TaskState, props: { ids: string[] }): Task[] => props.ids - ? props.ids.map(id => state.entities[id]) as Task[] - : [] + (state: TaskState, props: { ids: string[] }): Task[] => + props.ids ? (props.ids.map((id) => state.entities[id]) as Task[]) : [], ); export const selectTasksWithSubTasksByIds = createSelector( @@ -173,7 +207,7 @@ export const selectTasksWithSubTasksByIds = createSelector( devError('Task data not found for ' + id); } return mapSubTasksToTask(task as Task, state) as TaskWithSubTasks; - }) + }), ); export const selectTaskByIdWithSubTaskData = createSelector( @@ -184,34 +218,40 @@ export const selectTaskByIdWithSubTaskData = createSelector( devError('Task data not found for ' + props.id); } return mapSubTasksToTask(task as Task, state) as TaskWithSubTasks; - } + }, ); export const selectMainTasksWithoutTag = createSelector( selectAllTasks, - (tasks: Task[], props: { tagId: string }): Task[] => tasks.filter( - task => !task.parentId && !task.tagIds.includes(props.tagId) - ) + (tasks: Task[], props: { tagId: string }): Task[] => + tasks.filter((task) => !task.parentId && !task.tagIds.includes(props.tagId)), ); -export const selectTasksWorkedOnOrDoneFlat = createSelector(selectAllTasks, (tasks: Task[], props: { day: string }) => { - if (!props) { - return null; - } +export const selectTasksWorkedOnOrDoneFlat = createSelector( + selectAllTasks, + (tasks: Task[], props: { day: string }) => { + if (!props) { + return null; + } - const todayStr = props.day; - return tasks.filter( - (t: Task) => t.isDone || (t.timeSpentOnDay && t.timeSpentOnDay[todayStr] && t.timeSpentOnDay[todayStr] > 0) - ); -}); + const todayStr = props.day; + return tasks.filter( + (t: Task) => + t.isDone || + (t.timeSpentOnDay && + t.timeSpentOnDay[todayStr] && + t.timeSpentOnDay[todayStr] > 0), + ); + }, +); // REPEATABLE TASKS // ---------------- export const selectAllRepeatableTaskWithSubTasks = createSelector( selectAllTasksWithSubTasks, (tasks: TaskWithSubTasks[]) => { - return tasks.filter(task => !!task.repeatCfgId); - } + return tasks.filter((task) => !!task.repeatCfgId); + }, ); export const selectAllRepeatableTaskWithSubTasksFlat = createSelector( selectAllRepeatableTaskWithSubTasks, @@ -222,28 +262,28 @@ export const selectTasksByRepeatConfigId = createSelector( selectTaskFeatureState, (state: TaskState, props: { repeatCfgId: string }): Task[] => { const ids = state.ids as string[]; - const taskIds = ids.filter(idIN => state.entities[idIN] - // @ts-ignore - && state.entities[idIN].repeatCfgId === props.repeatCfgId); + const taskIds = ids.filter( + (idIN) => + state.entities[idIN] && + // @ts-ignore + state.entities[idIN].repeatCfgId === props.repeatCfgId, + ); // @ts-ignore - return (taskIds && taskIds.length) - ? taskIds.map(id => state.entities[id]) - : null; - } + return taskIds && taskIds.length ? taskIds.map((id) => state.entities[id]) : null; + }, ); export const selectTaskWithSubTasksByRepeatConfigId = createSelector( selectAllTasksWithSubTasks, (tasks: TaskWithSubTasks[], props: { repeatCfgId: string }) => { - return tasks.filter(task => task.repeatCfgId === props.repeatCfgId); - } + return tasks.filter((task) => task.repeatCfgId === props.repeatCfgId); + }, ); export const selectTasksByTag = createSelector( selectAllTasksWithSubTasks, (tasks: TaskWithSubTasks[], props: { tagId: string }) => { - return tasks.filter(task => task.tagIds.indexOf(props.tagId) !== -1); - } + return tasks.filter((task) => task.tagIds.indexOf(props.tagId) !== -1); + }, ); - diff --git a/src/app/features/tasks/task-additional-info/task-additional-info-item/task-additional-info-item.component.ts b/src/app/features/tasks/task-additional-info/task-additional-info-item/task-additional-info-item.component.ts index 39bbc2249..dd47cd712 100644 --- a/src/app/features/tasks/task-additional-info/task-additional-info-item/task-additional-info-item.component.ts +++ b/src/app/features/tasks/task-additional-info/task-additional-info-item/task-additional-info-item.component.ts @@ -6,14 +6,14 @@ import { HostBinding, HostListener, Input, - Output + Output, } from '@angular/core'; @Component({ selector: 'task-additional-info-item', templateUrl: './task-additional-info-item.component.html', styleUrls: ['./task-additional-info-item.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TaskAdditionalInfoItemComponent { @Input() type: 'input' | 'panel' = 'input'; @@ -26,10 +26,7 @@ export class TaskAdditionalInfoItemComponent { @HostBinding('tabindex') readonly tabindex: number = 3; - constructor( - public elementRef: ElementRef - ) { - } + constructor(public elementRef: ElementRef) {} @HostListener('keydown', ['$event']) onKeyDown(ev: KeyboardEvent) { const tagName = (ev.target as HTMLElement).tagName.toLowerCase(); diff --git a/src/app/features/tasks/task-additional-info/task-additional-info-wrapper/task-additional-info-wrapper.component.ts b/src/app/features/tasks/task-additional-info/task-additional-info-wrapper/task-additional-info-wrapper.component.ts index a46830a98..2f8e5e53d 100644 --- a/src/app/features/tasks/task-additional-info/task-additional-info-wrapper/task-additional-info-wrapper.component.ts +++ b/src/app/features/tasks/task-additional-info/task-additional-info-wrapper/task-additional-info-wrapper.component.ts @@ -9,7 +9,7 @@ import { TaskWithSubTasks } from '../../task.model'; selector: 'task-additional-info-wrapper', templateUrl: './task-additional-info-wrapper.component.html', styleUrls: ['./task-additional-info-wrapper.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TaskAdditionalInfoWrapperComponent { // NOTE: used for debugging @@ -17,15 +17,8 @@ export class TaskAdditionalInfoWrapperComponent { // to still display its data when panel is closing selectedTaskWithDelayForNone$: Observable = this.taskService.selectedTask$.pipe( - switchMap((task) => task - ? of(task) - : of(null).pipe(delay(200)) - ) + switchMap((task) => (task ? of(task) : of(null).pipe(delay(200)))), ); - constructor( - public taskService: TaskService, - public layoutService: LayoutService, - ) { - } + constructor(public taskService: TaskService, public layoutService: LayoutService) {} } diff --git a/src/app/features/tasks/task-additional-info/task-additional-info.ani.ts b/src/app/features/tasks/task-additional-info/task-additional-info.ani.ts index 92540d192..f1dc3fb24 100644 --- a/src/app/features/tasks/task-additional-info/task-additional-info.ani.ts +++ b/src/app/features/tasks/task-additional-info/task-additional-info.ani.ts @@ -2,11 +2,11 @@ import { animate, style, transition, trigger } from '@angular/animations'; import { ANI_ENTER_TIMING } from '../../../ui/animations/animation.const'; const ANI = [ - style({opacity: 0, transform: 'scale(0.9)'}), - animate(ANI_ENTER_TIMING, style({opacity: 1, transform: 'scale(1)'})), + style({ opacity: 0, transform: 'scale(0.9)' }), + animate(ANI_ENTER_TIMING, style({ opacity: 1, transform: 'scale(1)' })), ]; -export const taskAdditionalInfoTaskChangeAnimation = trigger('taskAdditionalInfoTaskChange', [ - transition('* <=> *', ANI), - transition(':enter', []), -]); +export const taskAdditionalInfoTaskChangeAnimation = trigger( + 'taskAdditionalInfoTaskChange', + [transition('* <=> *', ANI), transition(':enter', [])], +); diff --git a/src/app/features/tasks/task-additional-info/task-additional-info.component.ts b/src/app/features/tasks/task-additional-info/task-additional-info.component.ts index fae633064..c7e1f0ad7 100644 --- a/src/app/features/tasks/task-additional-info/task-additional-info.component.ts +++ b/src/app/features/tasks/task-additional-info/task-additional-info.component.ts @@ -9,22 +9,40 @@ import { OnDestroy, QueryList, ViewChild, - ViewChildren + ViewChildren, } from '@angular/core'; -import { ShowSubTasksMode, TaskAdditionalInfoTargetPanel, TaskWithSubTasks } from '../task.model'; +import { + ShowSubTasksMode, + TaskAdditionalInfoTargetPanel, + TaskWithSubTasks, +} from '../task.model'; import { IssueService } from '../../issue/issue.service'; import { TaskAttachmentService } from '../task-attachment/task-attachment.service'; import { BehaviorSubject, merge, Observable, of, Subject, Subscription } from 'rxjs'; -import { TaskAttachment, TaskAttachmentCopy } from '../task-attachment/task-attachment.model'; -import { catchError, delay, filter, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; +import { + TaskAttachment, + TaskAttachmentCopy, +} from '../task-attachment/task-attachment.model'; +import { + catchError, + delay, + filter, + map, + shareReplay, + switchMap, + withLatestFrom, +} from 'rxjs/operators'; import { T } from '../../../t.const'; import { TaskService } from '../task.service'; -import { expandAnimation, expandFadeInOnlyAnimation } from '../../../ui/animations/expand.ani'; +import { + expandAnimation, + expandFadeInOnlyAnimation, +} from '../../../ui/animations/expand.ani'; import { fadeAnimation } from '../../../ui/animations/fade.ani'; import { swirlAnimation } from '../../../ui/animations/swirl-in-out.ani'; import { DialogTimeEstimateComponent } from '../dialog-time-estimate/dialog-time-estimate.component'; import { MatDialog } from '@angular/material/dialog'; -import { isTouchOnly, IS_TOUCH_ONLY } from '../../../util/is-touch'; +import { IS_TOUCH_ONLY, isTouchOnly } from '../../../util/is-touch'; import { DialogAddTaskReminderComponent } from '../dialog-add-task-reminder/dialog-add-task-reminder.component'; import { AddTaskReminderInterface } from '../dialog-add-task-reminder/add-task-reminder-interface'; import { ReminderCopy } from '../../reminder/reminder.model'; @@ -60,14 +78,22 @@ interface IssueAndType { templateUrl: './task-additional-info.component.html', styleUrls: ['./task-additional-info.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation, expandFadeInOnlyAnimation, fadeAnimation, swirlAnimation, taskAdditionalInfoTaskChangeAnimation, noopAnimation] - + animations: [ + expandAnimation, + expandFadeInOnlyAnimation, + fadeAnimation, + swirlAnimation, + taskAdditionalInfoTaskChangeAnimation, + noopAnimation, + ], }) export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { @HostBinding('@noop') alwaysTrue: boolean = true; - @ViewChildren(TaskAdditionalInfoItemComponent) itemEls?: QueryList; - @ViewChild('attachmentPanelElRef') attachmentPanelElRef?: TaskAdditionalInfoItemComponent; + @ViewChildren(TaskAdditionalInfoItemComponent) + itemEls?: QueryList; + @ViewChild('attachmentPanelElRef') + attachmentPanelElRef?: TaskAdditionalInfoItemComponent; IS_TOUCH_ONLY: boolean = IS_TOUCH_ONLY; ShowSubTasksMode: typeof ShowSubTasksMode = ShowSubTasksMode; @@ -79,10 +105,7 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { issueAttachments: TaskAttachment[] = []; reminderId$: BehaviorSubject = new BehaviorSubject(null); reminderData$: Observable = this.reminderId$.pipe( - switchMap(id => id - ? this._reminderService.getById$(id) - : of(null) - ), + switchMap((id) => (id ? this._reminderService.getById$(id) : of(null))), ); issueIdAndType$: Subject = new Subject(); @@ -94,34 +117,42 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { issueDataTrigger$: Observable = merge( this.issueIdAndTypeShared$, - this.issueDataNullTrigger$ + this.issueDataNullTrigger$, ); issueData?: IssueData | null | false; repeatCfgId$: BehaviorSubject = new BehaviorSubject(null); repeatCfgDays$: Observable = this.repeatCfgId$.pipe( - switchMap(id => (id) - // TODO for some reason this can be undefined, maybe there is a better way - ? this._taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$(id).pipe( - map(repeatCfg => { - if (!repeatCfg) { - return null; - } - const days: (keyof TaskRepeatCfg)[] = ['sunday', 'monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday']; - const localWeekDays = moment.weekdaysMin(); - return days.filter(day => repeatCfg[day]) - .map((day, index) => localWeekDays[days.indexOf(day)]) - .join(', '); - }), - ) - - : of(null) + switchMap((id) => + id + ? // TODO for some reason this can be undefined, maybe there is a better way + this._taskRepeatCfgService.getTaskRepeatCfgByIdAllowUndefined$(id).pipe( + map((repeatCfg) => { + if (!repeatCfg) { + return null; + } + const days: (keyof TaskRepeatCfg)[] = [ + 'sunday', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + ]; + const localWeekDays = moment.weekdaysMin(); + return days + .filter((day) => repeatCfg[day]) + .map((day, index) => localWeekDays[days.indexOf(day)]) + .join(', '); + }), + ) + : of(null), ), ); parentId$: BehaviorSubject = new BehaviorSubject(null); - parentTaskData$: Observable = this.parentId$.pipe(switchMap((id) => !!id - ? this.taskService.getByIdWithSubTaskData$(id) - : of(null) - )); + parentTaskData$: Observable = this.parentId$.pipe( + switchMap((id) => (!!id ? this.taskService.getByIdWithSubTaskData$(id) : of(null))), + ); localAttachments?: TaskAttachment[]; @@ -133,12 +164,14 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { if (!this._taskData || !this._taskData.projectId) { throw new Error('task data not ready'); } - return this._issueService.getById$(args.type, args.id, this._taskData.projectId).pipe( - // NOTE we need this, otherwise the error is going to weird up the observable - catchError(() => { - return of(false); - }), - ) as Observable; + return this._issueService + .getById$(args.type, args.id, this._taskData.projectId) + .pipe( + // NOTE we need this, otherwise the error is going to weird up the observable + catchError(() => { + return of(false); + }), + ) as Observable; } return of(null); }), @@ -149,9 +182,9 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { ); issueAttachments$: Observable = this.issueData$.pipe( withLatestFrom(this.issueIdAndTypeShared$), - map(([data, {type}]) => (data && type) - ? this._issueService.getMappedAttachments(type, data) - : []) + map(([data, { type }]) => + data && type ? this._issueService.getMappedAttachments(type, data) : [], + ), ); IS_MOBILE: boolean = IS_MOBILE; defaultTaskNotes: string = ''; @@ -175,32 +208,51 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { private _cd: ChangeDetectorRef, ) { // NOTE: needs to be assigned here before any setter is called - this._subs.add(this.issueAttachments$.subscribe((attachments) => this.issueAttachments = attachments)); - this._subs.add(this._globalConfigService.misc$.subscribe((misc) => this.defaultTaskNotes = misc.taskNotesTpl)); - this._subs.add(this.issueData$.subscribe((issueData) => { - this.issueData = issueData; - this._cd.detectChanges(); - })); + this._subs.add( + this.issueAttachments$.subscribe( + (attachments) => (this.issueAttachments = attachments), + ), + ); + this._subs.add( + this._globalConfigService.misc$.subscribe( + (misc) => (this.defaultTaskNotes = misc.taskNotesTpl), + ), + ); + this._subs.add( + this.issueData$.subscribe((issueData) => { + this.issueData = issueData; + this._cd.detectChanges(); + }), + ); // NOTE: this works as long as there is no other place to display issue attachments for jira if (IS_ELECTRON) { - this._subs.add(this.issueIdAndTypeShared$.pipe( - filter(({id, type}) => type === JIRA_TYPE), - // not strictly reactive reactive but should work a 100% as issueIdAndType are triggered after task data - switchMap(() => { - if (!this._taskData || !this._taskData.projectId) { - throw new Error('task data not ready'); - } - return this._projectService.getJiraCfgForProject$(this._taskData.projectId); - }) - ).subscribe((jiraCfg) => { - if (jiraCfg.isEnabled) { - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.JIRA_SETUP_IMG_HEADERS, { - jiraCfg, - wonkyCookie: jiraCfg.isWonkyCookieMode && sessionStorage.getItem(SS_JIRA_WONKY_COOKIE) - }); - } - })); + this._subs.add( + this.issueIdAndTypeShared$ + .pipe( + filter(({ id, type }) => type === JIRA_TYPE), + // not strictly reactive reactive but should work a 100% as issueIdAndType are triggered after task data + switchMap(() => { + if (!this._taskData || !this._taskData.projectId) { + throw new Error('task data not ready'); + } + return this._projectService.getJiraCfgForProject$(this._taskData.projectId); + }), + ) + .subscribe((jiraCfg) => { + if (jiraCfg.isEnabled) { + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.JIRA_SETUP_IMG_HEADERS, + { + jiraCfg, + wonkyCookie: + jiraCfg.isWonkyCookieMode && + sessionStorage.getItem(SS_JIRA_WONKY_COOKIE), + }, + ); + } + }), + ); } // this.issueIdAndType$.subscribe((v) => console.log('issueIdAndType$', v)); // this.issueDataTrigger$.subscribe((v) => console.log('issueDataTrigger$', v)); @@ -216,37 +268,45 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { this._taskData = newVal; this.localAttachments = newVal.attachments; - if (!prev || !newVal || (prev.id !== newVal.id)) { + if (!prev || !newVal || prev.id !== newVal.id) { this._focusFirst(); } // NOTE: check for task change or issue update - if (!prev || (prev.issueId !== newVal.issueId || newVal.issueWasUpdated === true && !prev.issueWasUpdated)) { + if ( + !prev || + prev.issueId !== newVal.issueId || + (newVal.issueWasUpdated === true && !prev.issueWasUpdated) + ) { this.issueDataNullTrigger$.next(null); this.issueIdAndType$.next({ id: newVal.issueId, - type: newVal.issueType + type: newVal.issueType, }); } if (!newVal.issueId) { this.issueDataNullTrigger$.next(null); } - if (!prev || (prev.reminderId !== newVal.reminderId)) { + if (!prev || prev.reminderId !== newVal.reminderId) { this.reminderId$.next(newVal.reminderId); } - if (!prev || (prev.repeatCfgId !== newVal.repeatCfgId)) { + if (!prev || prev.repeatCfgId !== newVal.repeatCfgId) { this.repeatCfgId$.next(newVal.repeatCfgId); } - if (!prev || (prev.parentId !== newVal.parentId)) { + if (!prev || prev.parentId !== newVal.parentId) { this.parentId$.next(newVal.parentId); } } get progress() { - return this._taskData && this._taskData.timeEstimate && (this._taskData.timeSpent / this._taskData.timeEstimate) * 100; + return ( + this._taskData && + this._taskData.timeEstimate && + (this._taskData.timeSpent / this._taskData.timeEstimate) * 100 + ); } @HostListener('dragenter', ['$event']) onDragEnter(ev: DragEvent) { @@ -271,24 +331,28 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { } ngAfterViewInit(): void { - this._subs.add(this.taskService.taskAdditionalInfoTargetPanel$.pipe( - // hacky but we need a minimal delay to make sure selectedTaskId is ready - delay(50), - withLatestFrom(this.taskService.selectedTaskId$), - filter(([, id]) => !!id), - // delay(100), - ).subscribe(([v]) => { - if (v === TaskAdditionalInfoTargetPanel.Attachments) { - if (!this.attachmentPanelElRef) { - devError('this.attachmentPanelElRef not ready'); - this._focusFirst(); - } else { - this.focusItem(this.attachmentPanelElRef); - } - } else { - this._focusFirst(); - } - })); + this._subs.add( + this.taskService.taskAdditionalInfoTargetPanel$ + .pipe( + // hacky but we need a minimal delay to make sure selectedTaskId is ready + delay(50), + withLatestFrom(this.taskService.selectedTaskId$), + filter(([, id]) => !!id), + // delay(100), + ) + .subscribe(([v]) => { + if (v === TaskAdditionalInfoTargetPanel.Attachments) { + if (!this.attachmentPanelElRef) { + devError('this.attachmentPanelElRef not ready'); + this._focusFirst(); + } else { + this.focusItem(this.attachmentPanelElRef); + } + } else { + this._focusFirst(); + } + }), + ); } ngOnDestroy(): void { @@ -296,8 +360,11 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { } changeTaskNotes($event: string) { - if (!this.defaultTaskNotes || ($event && $event.trim() !== this.defaultTaskNotes.trim())) { - this.taskService.update(this.task.id, {notes: $event}); + if ( + !this.defaultTaskNotes || + ($event && $event.trim() !== this.defaultTaskNotes.trim()) + ) { + this.taskService.update(this.task.id, { notes: $event }); } } @@ -306,11 +373,10 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { } estimateTime() { - this._matDialog - .open(DialogTimeEstimateComponent, { - data: {task: this.task}, - autoFocus: !isTouchOnly(), - }); + this._matDialog.open(DialogTimeEstimateComponent, { + data: { task: this.task }, + autoFocus: !isTouchOnly(), + }); } editReminder() { @@ -320,7 +386,7 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { this._matDialog.open(DialogAddTaskReminderComponent, { restoreFocus: true, - data: {task: this.task} as AddTaskReminderInterface + data: { task: this.task } as AddTaskReminderInterface, }); } @@ -329,7 +395,7 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { restoreFocus: false, data: { task: this.task, - } + }, }); } @@ -339,7 +405,7 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { data: {}, }) .afterClosed() - .subscribe(result => { + .subscribe((result) => { if (result) { this.attachmentService.addAttachment(this.task.id, { ...result, @@ -364,10 +430,13 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { if (ev.key === 'ArrowUp' && this.selectedItemIndex > 0) { this.selectedItemIndex--; - (this.itemEls).toArray()[this.selectedItemIndex].focusEl(); - } else if (ev.key === 'ArrowDown' && (this.itemEls).toArray().length > (this.selectedItemIndex + 1)) { + this.itemEls.toArray()[this.selectedItemIndex].focusEl(); + } else if ( + ev.key === 'ArrowDown' && + this.itemEls.toArray().length > this.selectedItemIndex + 1 + ) { this.selectedItemIndex++; - (this.itemEls).toArray()[this.selectedItemIndex].focusEl(); + this.itemEls.toArray()[this.selectedItemIndex].focusEl(); } } @@ -378,7 +447,7 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { throw new Error(); } - const i = (this.itemEls).toArray().findIndex(el => el === cmpInstance); + const i = this.itemEls.toArray().findIndex((el) => el === cmpInstance); if (i === -1) { this.focusItem(cmpInstance); } else { @@ -394,7 +463,7 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { throw new Error('No task data'); } - this.taskService.update(this._taskData.id, {title: newTitle}); + this.taskService.update(this._taskData.id, { title: newTitle }); } } @@ -403,8 +472,7 @@ export class TaskAdditionalInfoComponent implements AfterViewInit, OnDestroy { if (!this.itemEls) { throw new Error(); } - this.focusItem((this.itemEls).first, 0); + this.focusItem(this.itemEls.first, 0); }, 150); } - } diff --git a/src/app/features/tasks/task-attachment/dialog-edit-attachment/dialog-edit-task-attachment.component.ts b/src/app/features/tasks/task-attachment/dialog-edit-attachment/dialog-edit-task-attachment.component.ts index 2619f5041..b50573a9d 100644 --- a/src/app/features/tasks/task-attachment/dialog-edit-attachment/dialog-edit-task-attachment.component.ts +++ b/src/app/features/tasks/task-attachment/dialog-edit-attachment/dialog-edit-task-attachment.component.ts @@ -1,7 +1,11 @@ import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { IS_ELECTRON } from '../../../../app.constants'; -import { TaskAttachment, TaskAttachmentCopy, TaskAttachmentType } from '../task-attachment.model'; +import { + TaskAttachment, + TaskAttachmentCopy, + TaskAttachmentType, +} from '../task-attachment.model'; import { T } from '../../../../t.const'; interface TaskAttachmentSelectType { @@ -13,7 +17,7 @@ interface TaskAttachmentSelectType { selector: 'dialog-edit-task-attachment', templateUrl: './dialog-edit-task-attachment.component.html', styleUrls: ['./dialog-edit-task-attachment.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogEditTaskAttachmentComponent { types: TaskAttachmentSelectType[]; @@ -22,19 +26,19 @@ export class DialogEditTaskAttachmentComponent { constructor( private _matDialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any + @Inject(MAT_DIALOG_DATA) public data: any, ) { - this.attachmentCopy = {...this.data.attachment} as TaskAttachmentCopy; + this.attachmentCopy = { ...this.data.attachment } as TaskAttachmentCopy; if (!this.attachmentCopy.type) { this.attachmentCopy.type = 'LINK'; } this.types = [ - {type: 'LINK', title: T.F.ATTACHMENT.DIALOG_EDIT.TYPES.LINK}, - {type: 'IMG', title: T.F.ATTACHMENT.DIALOG_EDIT.TYPES.IMG}, + { type: 'LINK', title: T.F.ATTACHMENT.DIALOG_EDIT.TYPES.LINK }, + { type: 'IMG', title: T.F.ATTACHMENT.DIALOG_EDIT.TYPES.IMG }, ]; if (IS_ELECTRON) { - this.types.push({type: 'FILE', title: T.F.ATTACHMENT.DIALOG_EDIT.TYPES.FILE}); + this.types.push({ type: 'FILE', title: T.F.ATTACHMENT.DIALOG_EDIT.TYPES.FILE }); } } @@ -48,7 +52,11 @@ export class DialogEditTaskAttachmentComponent { return; } - if (this.attachmentCopy.type === 'LINK' && this.attachmentCopy.path && !this.attachmentCopy.path.match(/(https?|ftp|file):\/\//)) { + if ( + this.attachmentCopy.type === 'LINK' && + this.attachmentCopy.path && + !this.attachmentCopy.path.match(/(https?|ftp|file):\/\//) + ) { this.attachmentCopy.path = 'http://' + this.attachmentCopy.path; } diff --git a/src/app/features/tasks/task-attachment/task-attachment-link/task-attachment-link.directive.ts b/src/app/features/tasks/task-attachment/task-attachment-link/task-attachment-link.directive.ts index a042aa4cd..db1ac891d 100644 --- a/src/app/features/tasks/task-attachment/task-attachment-link/task-attachment-link.directive.ts +++ b/src/app/features/tasks/task-attachment/task-attachment-link/task-attachment-link.directive.ts @@ -8,18 +8,16 @@ import { ElectronService } from '../../../../core/electron/electron.service'; import { ipcRenderer, shell } from 'electron'; @Directive({ - selector: '[taskAttachmentLink]' + selector: '[taskAttachmentLink]', }) export class TaskAttachmentLinkDirective { - @Input() type?: TaskAttachmentType; @Input() href?: TaskAttachmentType; constructor( private _electronService: ElectronService, private _snackService: SnackService, - ) { - } + ) {} @HostListener('click', ['$event']) onClick(ev: Event) { if (!this.href) { @@ -39,7 +37,7 @@ export class TaskAttachmentLinkDirective { } else if (this.type === 'COMMAND') { this._snackService.open({ msg: T.GLOBAL_SNACK.RUNNING_X, - translateParams: {str: this.href}, + translateParams: { str: this.href }, ico: 'laptop_windows', }); this._exec(this.href); 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 0591686b2..a4fb2efab 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 @@ -11,7 +11,7 @@ import { T } from '../../../../t.const'; templateUrl: './task-attachment-list.component.html', styleUrls: ['./task-attachment-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [standardListAnimation] + animations: [standardListAnimation], }) export class TaskAttachmentListComponent { @Input() taskId?: string; @@ -24,27 +24,32 @@ export class TaskAttachmentListComponent { constructor( public readonly attachmentService: TaskAttachmentService, private readonly _matDialog: MatDialog, - ) { - } + ) {} openEditDialog(attachment?: TaskAttachment) { if (!this.taskId) { throw new Error('No task id given'); } - this._matDialog.open(DialogEditTaskAttachmentComponent, { - restoreFocus: true, - data: { - attachment - }, - }).afterClosed() + this._matDialog + .open(DialogEditTaskAttachmentComponent, { + restoreFocus: true, + data: { + attachment, + }, + }) + .afterClosed() .subscribe((attachmentIN) => { if (!this.taskId) { throw new Error('No taskId'); } if (attachmentIN) { if (attachmentIN.id) { - this.attachmentService.updateAttachment(this.taskId, attachmentIN.id, attachmentIN); + this.attachmentService.updateAttachment( + this.taskId, + attachmentIN.id, + attachmentIN, + ); } else { this.attachmentService.addAttachment(this.taskId, attachmentIN); } @@ -60,8 +65,6 @@ export class TaskAttachmentListComponent { } trackByFn(i: number, attachment: TaskAttachment) { - return attachment - ? attachment.id - : i; + return attachment ? attachment.id : i; } } diff --git a/src/app/features/tasks/task-attachment/task-attachment.actions.ts b/src/app/features/tasks/task-attachment/task-attachment.actions.ts index 0b17eb50c..67eca97ae 100644 --- a/src/app/features/tasks/task-attachment/task-attachment.actions.ts +++ b/src/app/features/tasks/task-attachment/task-attachment.actions.ts @@ -12,26 +12,24 @@ export enum TaskAttachmentActionTypes { export class AddTaskAttachment implements Action { readonly type: string = TaskAttachmentActionTypes.AddTaskAttachment; - constructor(public payload: { taskId: string; taskAttachment: TaskAttachment }) { - } + constructor(public payload: { taskId: string; taskAttachment: TaskAttachment }) {} } export class UpdateTaskAttachment implements Action { readonly type: string = TaskAttachmentActionTypes.UpdateTaskAttachment; - constructor(public payload: { taskId: string; taskAttachment: Update }) { - } + constructor( + public payload: { taskId: string; taskAttachment: Update }, + ) {} } export class DeleteTaskAttachment implements Action { readonly type: string = TaskAttachmentActionTypes.DeleteTaskAttachment; - constructor(public payload: { taskId: string; id: string }) { - } + constructor(public payload: { taskId: string; id: string }) {} } export type TaskAttachmentActions = - AddTaskAttachment + | AddTaskAttachment | UpdateTaskAttachment - | DeleteTaskAttachment - ; + | DeleteTaskAttachment; 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 16fc4f955..ba5952eae 100644 --- a/src/app/features/tasks/task-attachment/task-attachment.model.ts +++ b/src/app/features/tasks/task-attachment/task-attachment.model.ts @@ -1,4 +1,7 @@ -import { DropPasteInput, DropPasteInputType } from '../../../core/drop-paste-input/drop-paste.model'; +import { + DropPasteInput, + DropPasteInputType, +} from '../../../core/drop-paste-input/drop-paste.model'; export type TaskAttachmentType = DropPasteInputType; diff --git a/src/app/features/tasks/task-attachment/task-attachment.module.ts b/src/app/features/tasks/task-attachment/task-attachment.module.ts index 8e3a2f070..b4180400c 100644 --- a/src/app/features/tasks/task-attachment/task-attachment.module.ts +++ b/src/app/features/tasks/task-attachment/task-attachment.module.ts @@ -7,19 +7,12 @@ import { TaskAttachmentLinkDirective } from './task-attachment-link/task-attachm import { TaskAttachmentListComponent } from './task-attachment-list/task-attachment-list.component'; @NgModule({ - imports: [ - CommonModule, - UiModule, - FormsModule, - ], + imports: [CommonModule, UiModule, FormsModule], declarations: [ DialogEditTaskAttachmentComponent, TaskAttachmentLinkDirective, TaskAttachmentListComponent, ], - exports: [ - TaskAttachmentListComponent, - ], + exports: [TaskAttachmentListComponent], }) -export class TaskAttachmentModule { -} +export class TaskAttachmentModule {} diff --git a/src/app/features/tasks/task-attachment/task-attachment.service.ts b/src/app/features/tasks/task-attachment/task-attachment.service.ts index 0983ffaac..fa5dcfc0f 100644 --- a/src/app/features/tasks/task-attachment/task-attachment.service.ts +++ b/src/app/features/tasks/task-attachment/task-attachment.service.ts @@ -5,7 +5,11 @@ import * as shortid from 'shortid'; import { DialogEditTaskAttachmentComponent } from './dialog-edit-attachment/dialog-edit-task-attachment.component'; import { MatDialog } from '@angular/material/dialog'; import { DropPasteInput } from '../../../core/drop-paste-input/drop-paste.model'; -import { AddTaskAttachment, DeleteTaskAttachment, UpdateTaskAttachment } from './task-attachment.actions'; +import { + AddTaskAttachment, + DeleteTaskAttachment, + UpdateTaskAttachment, +} from './task-attachment.actions'; import { TaskState } from '../task.model'; import { createFromDrop } from 'src/app/core/drop-paste-input/drop-paste-input'; @@ -13,12 +17,7 @@ import { createFromDrop } from 'src/app/core/drop-paste-input/drop-paste-input'; providedIn: 'root', }) export class TaskAttachmentService { - - constructor( - private _store$: Store, - private _matDialog: MatDialog, - ) { - } + constructor(private _store$: Store, private _matDialog: MatDialog) {} addAttachment(taskId: string, taskAttachment: TaskAttachment) { if (!taskAttachment) { @@ -26,21 +25,25 @@ export class TaskAttachmentService { return; } - this._store$.dispatch(new AddTaskAttachment({ - taskId, - taskAttachment: { - ...taskAttachment, - id: shortid() - } - })); + this._store$.dispatch( + new AddTaskAttachment({ + taskId, + taskAttachment: { + ...taskAttachment, + id: shortid(), + }, + }), + ); } deleteAttachment(taskId: string, id: string) { - this._store$.dispatch(new DeleteTaskAttachment({taskId, id})); + this._store$.dispatch(new DeleteTaskAttachment({ taskId, id })); } updateAttachment(taskId: string, id: string, changes: Partial) { - this._store$.dispatch(new UpdateTaskAttachment({taskId, taskAttachment: {id, changes}})); + this._store$.dispatch( + new UpdateTaskAttachment({ taskId, taskAttachment: { id, changes } }), + ); } // HANDLE INPUT @@ -68,12 +71,14 @@ export class TaskAttachmentService { ev.preventDefault(); ev.stopPropagation(); - this._matDialog.open(DialogEditTaskAttachmentComponent, { - restoreFocus: true, - data: { - attachment: {...attachment, taskId}, - }, - }).afterClosed() + this._matDialog + .open(DialogEditTaskAttachmentComponent, { + restoreFocus: true, + data: { + attachment: { ...attachment, taskId }, + }, + }) + .afterClosed() .subscribe((attachmentIN) => { if (attachmentIN) { if (attachmentIN.id) { diff --git a/src/app/features/tasks/task-list/task-list.component.ts b/src/app/features/tasks/task-list/task-list.component.ts index d1787e33b..bbffaa4b7 100644 --- a/src/app/features/tasks/task-list/task-list.component.ts +++ b/src/app/features/tasks/task-list/task-list.component.ts @@ -6,12 +6,18 @@ import { Input, OnDestroy, OnInit, - ViewChild + ViewChild, } from '@angular/core'; import { Task, TaskWithSubTasks } from '../task.model'; import { TaskService } from '../task.service'; import { DragulaService } from 'ng2-dragula'; -import { BehaviorSubject, combineLatest, Observable, ReplaySubject, Subscription } from 'rxjs'; +import { + BehaviorSubject, + combineLatest, + Observable, + ReplaySubject, + Subscription, +} from 'rxjs'; import { standardListAnimation } from '../../../ui/animations/standard-list.ani'; import { expandFadeFastAnimation } from '../../../ui/animations/expand.ani'; import { map } from 'rxjs/operators'; @@ -24,7 +30,6 @@ import { T } from '../../../t.const'; styleUrls: ['./task-list.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, animations: [standardListAnimation, expandFadeFastAnimation], - }) export class TaskListComponent implements OnDestroy, OnInit { T: typeof T = T; @@ -41,7 +46,7 @@ export class TaskListComponent implements OnDestroy, OnInit { @Input() noTasksMsg?: string; @Input() isBacklog: boolean = false; listId?: string; - @ViewChild('listEl', {static: true}) listEl?: ElementRef; + @ViewChild('listEl', { static: true }) listEl?: ElementRef; isBlockAni: boolean = false; doneTasksLength: number = 0; undoneTasksLength: number = 0; @@ -56,7 +61,7 @@ export class TaskListComponent implements OnDestroy, OnInit { this._taskService.currentTaskId$, ]).pipe( map(([tasks, isHideDone, isHideAll, currentId]) => - filterDoneTasks(tasks, currentId, isHideDone, isHideAll) + filterDoneTasks(tasks, currentId, isHideDone, isHideAll), ), ); @@ -64,8 +69,7 @@ export class TaskListComponent implements OnDestroy, OnInit { private _taskService: TaskService, private _dragulaService: DragulaService, private _cd: ChangeDetectorRef, - ) { - } + ) {} @Input('listId') set listIdIn(v: string) { this.listId = v; @@ -83,7 +87,7 @@ export class TaskListComponent implements OnDestroy, OnInit { if (!tasks) { return; } - this.doneTasksLength = this.tasksIN.filter(task => task.isDone).length; + this.doneTasksLength = this.tasksIN.filter((task) => task.isDone).length; this.allTasksLength = this.tasksIN.length; this.undoneTasksLength = this.tasksIN.length - this.doneTasksLength; } @@ -99,13 +103,15 @@ export class TaskListComponent implements OnDestroy, OnInit { } ngOnInit() { - this._subs.add(this._filteredTasks$.subscribe((tasks) => { - this.filteredTasks = tasks; - })); + this._subs.add( + this._filteredTasks$.subscribe((tasks) => { + this.filteredTasks = tasks; + }), + ); - this._subs.add(this._dragulaService.dropModel(this.listId) - .subscribe((params: any) => { - const {target, source, targetModel, item} = params; + this._subs.add( + this._dragulaService.dropModel(this.listId).subscribe((params: any) => { + const { target, source, targetModel, item } = params; if (this.listEl && this.listEl.nativeElement === target) { this._blockAnimation(); @@ -115,10 +121,12 @@ export class TaskListComponent implements OnDestroy, OnInit { const movedTaskId = item.id; this._taskService.move(movedTaskId, sourceModelId, targetModelId, targetNewIds); } - }) + }), ); - this._subs.add(this._taskService.currentTaskId$.subscribe(val => this.currentTaskId = val)); + this._subs.add( + this._taskService.currentTaskId$.subscribe((val) => (this.currentTaskId = val)), + ); } ngOnDestroy() { diff --git a/src/app/features/tasks/task-summary-table/task-summary-table.component.ts b/src/app/features/tasks/task-summary-table/task-summary-table.component.ts index bc56c9c2f..9427072a5 100644 --- a/src/app/features/tasks/task-summary-table/task-summary-table.component.ts +++ b/src/app/features/tasks/task-summary-table/task-summary-table.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; import { Task } from '../task.model'; import { getWorklogStr } from '../../../util/get-work-log-str'; import { TaskService } from '../task.service'; @@ -17,24 +23,21 @@ export class TaskSummaryTableComponent { T: typeof T = T; - constructor( - private _taskService: TaskService, - ) { - } + constructor(private _taskService: TaskService) {} updateTimeSpentTodayForTask(task: Task, newVal: number | string) { this._taskService.updateEverywhere(task.id, { timeSpentOnDay: { ...task.timeSpentOnDay, [this.day]: +newVal, - } + }, }); this.updated.emit(); } updateTaskTitle(task: Task, newVal: string) { this._taskService.updateEverywhere(task.id, { - title: newVal + title: newVal, }); this.updated.emit(); } diff --git a/src/app/features/tasks/task-summary-tables/map-to-project-with-tasks.spec.ts b/src/app/features/tasks/task-summary-tables/map-to-project-with-tasks.spec.ts index db3a9cd42..54ee1fab1 100644 --- a/src/app/features/tasks/task-summary-tables/map-to-project-with-tasks.spec.ts +++ b/src/app/features/tasks/task-summary-tables/map-to-project-with-tasks.spec.ts @@ -4,7 +4,7 @@ describe('mapToProjectWithTasks()', () => { it('should return a mapped project', () => { const project = { id: 'PID', - title: 'P_TITLE' + title: 'P_TITLE', }; expect(mapToProjectWithTasks(project as any, [], 'TODAY_STR')).toEqual({ id: 'PID', @@ -19,25 +19,25 @@ describe('mapToProjectWithTasks()', () => { it('should return a mapped project with accumulated time spent for tasks', () => { const project = { id: 'PID', - title: 'P_TITLE' + title: 'P_TITLE', }; const TDS = 'TODAY_STR'; const flatTasks = [ - {projectId: 'PID', parentId: 'IGNORE', timeSpentOnDay: {[TDS]: 100000}}, - {projectId: 'PID', timeSpentOnDay: {[TDS]: 111}}, - {projectId: 'PID', timeSpentOnDay: {[TDS]: 222}}, - {projectId: 'PID', timeSpentOnDay: {fakeOtherDayProp: 444}}, - {projectId: 'OTHER_PID', timeSpentOnDay: {[TDS]: 222}}, + { projectId: 'PID', parentId: 'IGNORE', timeSpentOnDay: { [TDS]: 100000 } }, + { projectId: 'PID', timeSpentOnDay: { [TDS]: 111 } }, + { projectId: 'PID', timeSpentOnDay: { [TDS]: 222 } }, + { projectId: 'PID', timeSpentOnDay: { fakeOtherDayProp: 444 } }, + { projectId: 'OTHER_PID', timeSpentOnDay: { [TDS]: 222 } }, ]; expect(mapToProjectWithTasks(project as any, flatTasks as any, TDS)).toEqual({ id: 'PID', title: 'P_TITLE', color: undefined, tasks: [ - {projectId: 'PID', parentId: 'IGNORE', timeSpentOnDay: {[TDS]: 100000}}, - {projectId: 'PID', timeSpentOnDay: {[TDS]: 111}}, - {projectId: 'PID', timeSpentOnDay: {[TDS]: 222}}, - {projectId: 'PID', timeSpentOnDay: {fakeOtherDayProp: 444}} + { projectId: 'PID', parentId: 'IGNORE', timeSpentOnDay: { [TDS]: 100000 } }, + { projectId: 'PID', timeSpentOnDay: { [TDS]: 111 } }, + { projectId: 'PID', timeSpentOnDay: { [TDS]: 222 } }, + { projectId: 'PID', timeSpentOnDay: { fakeOtherDayProp: 444 } }, ] as any, timeSpentToday: 333, timeSpentYesterday: undefined, @@ -47,26 +47,26 @@ describe('mapToProjectWithTasks()', () => { it('should return a mapped project with accumulated time spent for tasks yesterday', () => { const project = { id: 'PID', - title: 'P_TITLE' + title: 'P_TITLE', }; const TDS = 'TODAY_STR'; const YDS = 'YESTERDAY_STR'; const flatTasks = [ - {projectId: 'PID', parentId: 'IGNORE', timeSpentOnDay: {[TDS]: 100000}}, - {projectId: 'PID', timeSpentOnDay: {[TDS]: 111}}, - {projectId: 'PID', timeSpentOnDay: {[YDS]: 222}}, - {projectId: 'PID', timeSpentOnDay: {[YDS]: 444}}, - {projectId: 'OTHER_PID', timeSpentOnDay: {[YDS]: 222}}, + { projectId: 'PID', parentId: 'IGNORE', timeSpentOnDay: { [TDS]: 100000 } }, + { projectId: 'PID', timeSpentOnDay: { [TDS]: 111 } }, + { projectId: 'PID', timeSpentOnDay: { [YDS]: 222 } }, + { projectId: 'PID', timeSpentOnDay: { [YDS]: 444 } }, + { projectId: 'OTHER_PID', timeSpentOnDay: { [YDS]: 222 } }, ]; expect(mapToProjectWithTasks(project as any, flatTasks as any, TDS, YDS)).toEqual({ id: 'PID', title: 'P_TITLE', color: undefined, tasks: [ - {projectId: 'PID', parentId: 'IGNORE', timeSpentOnDay: {[TDS]: 100000}}, - {projectId: 'PID', timeSpentOnDay: {[TDS]: 111}}, - {projectId: 'PID', timeSpentOnDay: {[YDS]: 222}}, - {projectId: 'PID', timeSpentOnDay: {[YDS]: 444}} + { projectId: 'PID', parentId: 'IGNORE', timeSpentOnDay: { [TDS]: 100000 } }, + { projectId: 'PID', timeSpentOnDay: { [TDS]: 111 } }, + { projectId: 'PID', timeSpentOnDay: { [YDS]: 222 } }, + { projectId: 'PID', timeSpentOnDay: { [YDS]: 444 } }, ] as any, timeSpentToday: 111, timeSpentYesterday: 666, diff --git a/src/app/features/tasks/task-summary-tables/map-to-project-with-tasks.util.ts b/src/app/features/tasks/task-summary-tables/map-to-project-with-tasks.util.ts index f8164f5a0..29e6bd9ae 100644 --- a/src/app/features/tasks/task-summary-tables/map-to-project-with-tasks.util.ts +++ b/src/app/features/tasks/task-summary-tables/map-to-project-with-tasks.util.ts @@ -17,15 +17,15 @@ export const mapToProjectWithTasks = ( yesterdayStr?: string, ): ProjectWithTasks => { // NOTE: this only works, because projectIds is only triggered before assign flatTasks - const tasks = flatTasks.filter(task => task.projectId === project.id); + const tasks = flatTasks.filter((task) => task.projectId === project.id); const timeSpentToday = tasks.reduce((acc: number, task) => { return acc + ((!task.parentId && task.timeSpentOnDay[todayStr]) || 0); }, 0); const timeSpentYesterday = yesterdayStr ? tasks.reduce((acc: number, task) => { - return acc + ((!task.parentId && task.timeSpentOnDay[yesterdayStr]) || 0); - }, 0) + return acc + ((!task.parentId && task.timeSpentOnDay[yesterdayStr]) || 0); + }, 0) : undefined; return { diff --git a/src/app/features/tasks/task-summary-tables/task-summary-tables.component.ts b/src/app/features/tasks/task-summary-tables/task-summary-tables.component.ts index e2e5915f4..563ccd4d3 100644 --- a/src/app/features/tasks/task-summary-tables/task-summary-tables.component.ts +++ b/src/app/features/tasks/task-summary-tables/task-summary-tables.component.ts @@ -13,13 +13,16 @@ import { map, withLatestFrom } from 'rxjs/operators'; import { ProjectService } from '../../project/project.service'; import { unique } from '../../../util/unique'; import { TranslateService } from '@ngx-translate/core'; -import { mapToProjectWithTasks, ProjectWithTasks } from './map-to-project-with-tasks.util'; +import { + mapToProjectWithTasks, + ProjectWithTasks, +} from './map-to-project-with-tasks.util'; @Component({ selector: 'task-summary-tables', templateUrl: './task-summary-tables.component.html', styleUrls: ['./task-summary-tables.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TaskSummaryTablesComponent { T: typeof T = T; @@ -36,10 +39,10 @@ export class TaskSummaryTablesComponent { map(([pids, projects]) => { // NOTE: the order is like the ones in the menu const mappedProjects = projects - .filter(project => pids.includes(project.id)) + .filter((project) => pids.includes(project.id)) .map((project) => this._mapToProjectWithTasks(project)); - if (this.flatTasks.find(task => !task.projectId)) { + if (this.flatTasks.find((task) => !task.projectId)) { const noProjectProject: ProjectWithTasks = this._mapToProjectWithTasks({ id: null, title: this._translateService.instant(T.G.WITHOUT_PROJECT), @@ -47,7 +50,7 @@ export class TaskSummaryTablesComponent { return [...mappedProjects, noProjectProject]; } return mappedProjects; - }) + }), ); constructor( @@ -57,12 +60,13 @@ export class TaskSummaryTablesComponent { private readonly _matDialog: MatDialog, private readonly _worklogService: WorklogService, private readonly _projectService: ProjectService, - ) { - } + ) {} @Input('flatTasks') set flatTasksIn(v: Task[]) { this.flatTasks = v; - const pids = unique(v.map(t => t.projectId).filter(pid => typeof pid === 'string')) as string[]; + const pids = unique( + v.map((t) => t.projectId).filter((pid) => typeof pid === 'string'), + ) as string[]; this.projectIds$.next(pids); } @@ -78,14 +82,22 @@ export class TaskSummaryTablesComponent { projectId, rangeStart: new Date().setHours(0, 0, 0, 0), rangeEnd: new Date().setHours(23, 59, 59), - } + }, }); } - roundTimeForTasks(projectId: string, roundTo: RoundTimeOption, isRoundUp: boolean = false) { - const taskIds = this.flatTasks.map(task => task.id); + roundTimeForTasks( + projectId: string, + roundTo: RoundTimeOption, + isRoundUp: boolean = false, + ) { + const taskIds = this.flatTasks.map((task) => task.id); this._taskService.roundTimeSpentForDay({ - day: this.dayStr, taskIds, roundTo, isRoundUp, projectId + day: this.dayStr, + taskIds, + roundTo, + isRoundUp, + projectId, }); } @@ -93,7 +105,9 @@ export class TaskSummaryTablesComponent { return item.id; } - private _mapToProjectWithTasks(project: Project | { id: string | null; title: string }): ProjectWithTasks { + private _mapToProjectWithTasks( + project: Project | { id: string | null; title: string }, + ): ProjectWithTasks { let yesterdayStr: string | undefined; if (this.isShowYesterday && this.isForToday) { const t = new Date(); diff --git a/src/app/features/tasks/task.model.ts b/src/app/features/tasks/task.model.ts index c4757a5ab..4edf8f5c6 100644 --- a/src/app/features/tasks/task.model.ts +++ b/src/app/features/tasks/task.model.ts @@ -26,6 +26,7 @@ export enum TaskReminderOptionId { m30 = 'm30', h1 = 'h1', } + export interface TaskReminderOption { id: TaskReminderOptionId; title: string; diff --git a/src/app/features/tasks/task.service.ts b/src/app/features/tasks/task.service.ts index f93981a4b..df65ec1e4 100644 --- a/src/app/features/tasks/task.service.ts +++ b/src/app/features/tasks/task.service.ts @@ -1,5 +1,13 @@ import * as shortid from 'shortid'; -import { delay, filter, first, map, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { + delay, + filter, + first, + map, + switchMap, + take, + withLatestFrom, +} from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { @@ -12,7 +20,7 @@ import { TaskArchive, TaskReminderOptionId, TaskState, - TaskWithSubTasks + TaskWithSubTasks, } from './task.model'; import { select, Store } from '@ngrx/store'; import { @@ -41,7 +49,7 @@ import { UnsetCurrentTask, UpdateTask, UpdateTaskTags, - UpdateTaskUi + UpdateTaskUi, } from './store/task.actions'; import { PersistenceService } from '../../core/persistence/persistence.service'; import { IssueProviderKey } from '../issue/issue.model'; @@ -65,7 +73,7 @@ import { selectTasksById, selectTasksByRepeatConfigId, selectTasksByTag, - selectTaskWithSubTasksByRepeatConfigId + selectTaskWithSubTasksByRepeatConfigId, } from './store/task.selectors'; import { getWorklogStr } from '../../util/get-work-log-str'; import { RoundTimeOption } from '../project/project.model'; @@ -83,7 +91,7 @@ import { moveTaskToTodayList, moveTaskToTodayListAuto, moveTaskUpInBacklogList, - moveTaskUpInTodayList + moveTaskUpInTodayList, } from '../work-context/store/work-context-meta.actions'; import { Router } from '@angular/router'; import { unique } from '../../util/unique'; @@ -141,28 +149,23 @@ export class TaskService { select(selectTaskFeatureState), ); - allTasks$: Observable = this._store.pipe( - select(selectAllTasks), - ); + allTasks$: Observable = this._store.pipe(select(selectAllTasks)); - allStartableTasks$: Observable = this._store.pipe( - select(selectStartableTasks), - ); + allStartableTasks$: Observable = this._store.pipe(select(selectStartableTasks)); - plannedTasks$: Observable = this._store.pipe( - select(selectPlannedTasks), - ); + plannedTasks$: Observable = this._store.pipe(select(selectPlannedTasks)); // META FIELDS // ----------- currentTaskProgress$: Observable = this.currentTask$.pipe( - map((task) => (task && task.timeEstimate > 0) - ? task.timeSpent / task.timeEstimate - : 0 - ) + map((task) => + task && task.timeEstimate > 0 ? task.timeSpent / task.timeEstimate : 0, + ), ); - private _allTasksWithSubTaskData$: Observable = this._store.pipe(select(selectAllTasks)); + private _allTasksWithSubTaskData$: Observable = this._store.pipe( + select(selectAllTasks), + ); constructor( private readonly _store: Store, @@ -174,11 +177,13 @@ export class TaskService { private readonly _timeTrackingService: TimeTrackingService, private readonly _router: Router, ) { - this.currentTaskId$.subscribe((val) => this.currentTaskId = val); + this.currentTaskId$.subscribe((val) => (this.currentTaskId = val)); // time tracking this._timeTrackingService.tick$ - .pipe(withLatestFrom(this.currentTask$, this._imexMetaService.isDataImportInProgress$)) + .pipe( + withLatestFrom(this.currentTask$, this._imexMetaService.isDataImportInProgress$), + ) .subscribe(([tick, currentTask, isImportInProgress]) => { if (currentTask && !isImportInProgress) { this.addTimeSpent(currentTask, tick.duration, tick.date); @@ -187,7 +192,7 @@ export class TaskService { } getAllParentWithoutTag$(tagId: string) { - return this._store.pipe(select(selectMainTasksWithoutTag, {tagId})); + return this._store.pipe(select(selectMainTasksWithoutTag, { tagId })); } // META @@ -201,16 +206,21 @@ export class TaskService { } } - setSelectedId(id: string | null, taskAdditionalInfoTargetPanel: TaskAdditionalInfoTargetPanel = TaskAdditionalInfoTargetPanel.Default) { - this._store.dispatch(new SetSelectedTask({id, taskAdditionalInfoTargetPanel})); + setSelectedId( + id: string | null, + taskAdditionalInfoTargetPanel: TaskAdditionalInfoTargetPanel = TaskAdditionalInfoTargetPanel.Default, + ) { + this._store.dispatch(new SetSelectedTask({ id, taskAdditionalInfoTargetPanel })); } startFirstStartable() { - this._workContextService.startableTasksForActiveContext$.pipe(take(1)).subscribe(tasks => { - if (tasks[0] && !this.currentTaskId) { - this.setCurrentId(tasks[0].id); - } - }); + this._workContextService.startableTasksForActiveContext$ + .pipe(take(1)) + .subscribe((tasks) => { + if (tasks[0] && !this.currentTaskId) { + this.setCurrentId(tasks[0].id); + } + }); } pauseCurrent() { @@ -226,31 +236,41 @@ export class TaskService { isAddToBottom: boolean = false, ): string { const workContextId = this._workContextService.activeWorkContextId as string; - const workContextType = this._workContextService.activeWorkContextType as WorkContextType; - const task = this.createNewTaskWithDefaults({title, additional, workContextType, workContextId}); - - this._store.dispatch(new AddTask({ - task, - workContextId, + const workContextType = this._workContextService + .activeWorkContextType as WorkContextType; + const task = this.createNewTaskWithDefaults({ + title, + additional, workContextType, - isAddToBacklog, - isAddToBottom - })); + workContextId, + }); + + this._store.dispatch( + new AddTask({ + task, + workContextId, + workContextType, + isAddToBacklog, + isAddToBottom, + }), + ); return task && task.id; } remove(task: TaskWithSubTasks) { - this._store.dispatch(new DeleteTask({task})); + this._store.dispatch(new DeleteTask({ task })); } removeMultipleMainTasks(taskIds: string[]) { - this._store.dispatch(new DeleteMainTasks({taskIds})); + this._store.dispatch(new DeleteMainTasks({ taskIds })); } update(id: string, changedFields: Partial) { - this._store.dispatch(new UpdateTask({ - task: {id, changes: changedFields} - })); + this._store.dispatch( + new UpdateTask({ + task: { id, changes: changedFields }, + }), + ); } updateTags(task: Task, newTagIds: string[], oldTagIds: string[]) { @@ -259,30 +279,41 @@ export class TaskService { } if (!task.projectId && newTagIds.length === 0) { - this._snackService.open({type: 'ERROR', msg: T.F.TASK.S.LAST_TAG_DELETION_WARNING}); + this._snackService.open({ + type: 'ERROR', + msg: T.F.TASK.S.LAST_TAG_DELETION_WARNING, + }); } else { - this._store.dispatch(new UpdateTaskTags({ - task, - newTagIds: unique(newTagIds), - oldTagIds - })); + this._store.dispatch( + new UpdateTaskTags({ + task, + newTagIds: unique(newTagIds), + oldTagIds, + }), + ); } } removeTagsForAllTask(tagsToRemove: string[]) { - this._store.dispatch(new RemoveTagsForAllTasks({ - tagIdsToRemove: tagsToRemove, - })); + this._store.dispatch( + new RemoveTagsForAllTasks({ + tagIdsToRemove: tagsToRemove, + }), + ); } // TODO: Move logic away from service class (to actions)? // TODO: Should this reside in tagService? purgeUnusedTags(tagIds: string[]) { - tagIds.forEach(tagId => { + tagIds.forEach((tagId) => { this.getTasksByTag(tagId) .pipe(take(1)) - .subscribe(tasks => { - console.log(`Tag is present on ${tasks.length} tasks => ${tasks.length ? 'keeping...' : 'deleting...'}`); + .subscribe((tasks) => { + console.log( + `Tag is present on ${tasks.length} tasks => ${ + tasks.length ? 'keeping...' : 'deleting...' + }`, + ); if (tasks.length === 0 && tagId !== TODAY_TAG.id) { this._tagService.removeTag(tagId); } @@ -291,91 +322,111 @@ export class TaskService { } updateUi(id: string, changes: Partial) { - this._store.dispatch(new UpdateTaskUi({ - task: {id, changes} - })); + this._store.dispatch( + new UpdateTaskUi({ + task: { id, changes }, + }), + ); } - move(taskId: string, + move( + taskId: string, src: DropListModelSource, target: DropListModelSource, - newOrderedIds: string[]) { - const isSrcTodayList = (src === 'DONE' || src === 'UNDONE'); - const isTargetTodayList = (target === 'DONE' || target === 'UNDONE'); + newOrderedIds: string[], + ) { + const isSrcTodayList = src === 'DONE' || src === 'UNDONE'; + const isTargetTodayList = target === 'DONE' || target === 'UNDONE'; const workContextId = this._workContextService.activeWorkContextId as string; if (isSrcTodayList && isTargetTodayList) { // move inside today - const workContextType = this._workContextService.activeWorkContextType as WorkContextType; - this._store.dispatch(moveTaskInTodayList({taskId, newOrderedIds, src, target, workContextId, workContextType})); - + const workContextType = this._workContextService + .activeWorkContextType as WorkContextType; + this._store.dispatch( + moveTaskInTodayList({ + taskId, + newOrderedIds, + src, + target, + workContextId, + workContextType, + }), + ); } else if (src === 'BACKLOG' && target === 'BACKLOG') { // move inside backlog - this._store.dispatch(moveTaskInBacklogList({taskId, newOrderedIds, workContextId})); - + this._store.dispatch( + moveTaskInBacklogList({ taskId, newOrderedIds, workContextId }), + ); } else if (src === 'BACKLOG' && isTargetTodayList) { // move from backlog to today - this._store.dispatch(moveTaskToTodayList({taskId, newOrderedIds, src, target, workContextId})); - + this._store.dispatch( + moveTaskToTodayList({ taskId, newOrderedIds, src, target, workContextId }), + ); } else if (isSrcTodayList && target === 'BACKLOG') { // move from today to backlog - this._store.dispatch(moveTaskToBacklogList({taskId, newOrderedIds, workContextId})); - + this._store.dispatch( + moveTaskToBacklogList({ taskId, newOrderedIds, workContextId }), + ); } else { // move sub task - this._store.dispatch(new MoveSubTask({taskId, srcTaskId: src, targetTaskId: target, newOrderedIds})); + this._store.dispatch( + new MoveSubTask({ taskId, srcTaskId: src, targetTaskId: target, newOrderedIds }), + ); } } moveUp(id: string, parentId: string | null = null, isBacklog: boolean) { if (parentId) { - this._store.dispatch(new MoveSubTaskUp({id, parentId})); + this._store.dispatch(new MoveSubTaskUp({ id, parentId })); } else { - const workContextId = this._workContextService.activeWorkContextId as string; - const workContextType = this._workContextService.activeWorkContextType as WorkContextType; + const workContextType = this._workContextService + .activeWorkContextType as WorkContextType; if (isBacklog) { - this._store.dispatch(moveTaskUpInBacklogList({taskId: id, workContextId})); + this._store.dispatch(moveTaskUpInBacklogList({ taskId: id, workContextId })); } else { - this._store.dispatch(moveTaskUpInTodayList({taskId: id, workContextType, workContextId})); + this._store.dispatch( + moveTaskUpInTodayList({ taskId: id, workContextType, workContextId }), + ); } } } moveDown(id: string, parentId: string | null = null, isBacklog: boolean) { if (parentId) { - this._store.dispatch(new MoveSubTaskDown({id, parentId})); + this._store.dispatch(new MoveSubTaskDown({ id, parentId })); } else { - const workContextId = this._workContextService.activeWorkContextId as string; - const workContextType = this._workContextService.activeWorkContextType as WorkContextType; + const workContextType = this._workContextService + .activeWorkContextType as WorkContextType; if (isBacklog) { - this._store.dispatch(moveTaskDownInBacklogList({taskId: id, workContextId})); + this._store.dispatch(moveTaskDownInBacklogList({ taskId: id, workContextId })); } else { - this._store.dispatch(moveTaskDownInTodayList({taskId: id, workContextType, workContextId})); + this._store.dispatch( + moveTaskDownInTodayList({ taskId: id, workContextType, workContextId }), + ); } } } addSubTaskTo(parentId: string) { - this._store.dispatch(new AddSubTask({ - task: this.createNewTaskWithDefaults({title: ''}), - parentId - })); + this._store.dispatch( + new AddSubTask({ + task: this.createNewTaskWithDefaults({ title: '' }), + parentId, + }), + ); } - addTimeSpent(task: Task, - duration: number, - date: string = getWorklogStr()) { - this._store.dispatch(new AddTimeSpent({task, date, duration})); + addTimeSpent(task: Task, duration: number, date: string = getWorklogStr()) { + this._store.dispatch(new AddTimeSpent({ task, date, duration })); } - removeTimeSpent(id: string, - duration: number, - date: string = getWorklogStr()) { - this._store.dispatch(new RemoveTimeSpent({id, date, duration})); + removeTimeSpent(id: string, duration: number, date: string = getWorklogStr()) { + this._store.dispatch(new RemoveTimeSpent({ id, date, duration })); } focusTask(id: string) { @@ -403,17 +454,21 @@ export class TaskService { moveToToday(id: string, isMoveToTop: boolean = false) { const workContextId = this._workContextService.activeWorkContextId as string; - const workContextType = this._workContextService.activeWorkContextType as WorkContextType; + const workContextType = this._workContextService + .activeWorkContextType as WorkContextType; if (workContextType === WorkContextType.PROJECT) { - this._store.dispatch(moveTaskToTodayListAuto({taskId: id, isMoveToTop, workContextId})); + this._store.dispatch( + moveTaskToTodayListAuto({ taskId: id, isMoveToTop, workContextId }), + ); } } moveToBacklog(id: string) { const workContextId = this._workContextService.activeWorkContextId as string; - const workContextType = this._workContextService.activeWorkContextType as WorkContextType; + const workContextType = this._workContextService + .activeWorkContextType as WorkContextType; if (workContextType === WorkContextType.PROJECT) { - this._store.dispatch(moveTaskToBacklogListAuto({taskId: id, workContextId})); + this._store.dispatch(moveTaskToBacklogListAuto({ taskId: id, workContextId })); } } @@ -421,14 +476,14 @@ export class TaskService { if (!Array.isArray(tasks)) { tasks = [tasks]; } - this._store.dispatch(new MoveToArchive({tasks})); + this._store.dispatch(new MoveToArchive({ tasks })); } moveToProject(task: TaskWithSubTasks, projectId: string) { if (!!task.parentId || !!task.issueId) { throw new Error('Wrong task model'); } - this._store.dispatch(new MoveToOtherProject({task, targetProjectId: projectId})); + this._store.dispatch(new MoveToOtherProject({ task, targetProjectId: projectId })); } toggleStartTask() { @@ -436,43 +491,58 @@ export class TaskService { } restoreTask(task: Task, subTasks: Task[]) { - this._store.dispatch(new RestoreTask({task, subTasks})); + this._store.dispatch(new RestoreTask({ task, subTasks })); } - roundTimeSpentForDay({day, taskIds, roundTo, isRoundUp = false, projectId}: { - day: string; taskIds: string[]; roundTo: RoundTimeOption; isRoundUp: boolean; projectId?: string | null; + roundTimeSpentForDay({ + day, + taskIds, + roundTo, + isRoundUp = false, + projectId, + }: { + day: string; + taskIds: string[]; + roundTo: RoundTimeOption; + isRoundUp: boolean; + projectId?: string | null; }) { - this._store.dispatch(new RoundTimeSpentForDay({day, taskIds, roundTo, isRoundUp, projectId})); + this._store.dispatch( + new RoundTimeSpentForDay({ day, taskIds, roundTo, isRoundUp, projectId }), + ); } - startTaskFromOtherContext$(taskId: string, workContextType: WorkContextType, workContextId: string): Observable { - const base = (workContextType === WorkContextType.TAG ? 'tag' : 'project'); + startTaskFromOtherContext$( + taskId: string, + workContextType: WorkContextType, + workContextId: string, + ): Observable { + const base = workContextType === WorkContextType.TAG ? 'tag' : 'project'; this._router.navigate([`/${base}/${workContextId}/tasks`]); // NOTE: route is the only mechanism to trigger this // this._workContextService._setActiveContext(workContextId, workContextType); const contextChanged$ = this._workContextService.activeWorkContextId$.pipe( - filter(id => id === workContextId), + filter((id) => id === workContextId), // wait for actual data to be loaded switchMap(() => this._workContextService.activeWorkContext$), // dirty dirty fix delay(50), first(), - ) - ; + ); const task$ = contextChanged$.pipe( switchMap(() => this.getByIdOnce$(taskId)), take(1), ); if (workContextType === WorkContextType.PROJECT) { - task$.subscribe(task => { + task$.subscribe((task) => { if (!task) { console.log({ taskId, workContextType, workContextId, - activeWCId: this._workContextService.activeWorkContextId + activeWCId: this._workContextService.activeWorkContextId, }); throw new Error('Startable task not found'); } @@ -485,7 +555,7 @@ export class TaskService { }); return task$; } else if (workContextType === WorkContextType.TAG) { - task$.subscribe(task => { + task$.subscribe((task) => { this.setCurrentId(task.id); }); } else { @@ -496,20 +566,26 @@ export class TaskService { // REMINDER // -------- - scheduleTask(task: Task | TaskWithSubTasks, plannedAt: number, remindCfg: TaskReminderOptionId, isMoveToBacklog: boolean = false) { + scheduleTask( + task: Task | TaskWithSubTasks, + plannedAt: number, + remindCfg: TaskReminderOptionId, + isMoveToBacklog: boolean = false, + ) { console.log(remindOptionToMilliseconds(plannedAt, remindCfg), plannedAt); - console.log(remindOptionToMilliseconds(plannedAt, remindCfg) as number - plannedAt); + console.log((remindOptionToMilliseconds(plannedAt, remindCfg) as number) - plannedAt); console.log({ plannedAt, remindCfg, }); - this._store.dispatch(new ScheduleTask({ - task, - plannedAt, - remindAt: remindOptionToMilliseconds(plannedAt, remindCfg), - isMoveToBacklog - })); - + this._store.dispatch( + new ScheduleTask({ + task, + plannedAt, + remindAt: remindOptionToMilliseconds(plannedAt, remindCfg), + isMoveToBacklog, + }), + ); } reScheduleTask({ @@ -517,87 +593,105 @@ export class TaskService { plannedAt, reminderId, remindCfg, - title - }: { taskId: string; plannedAt: number; title: string; reminderId?: string; remindCfg: TaskReminderOptionId }) { - - this._store.dispatch(new ReScheduleTask({ - id: taskId, - plannedAt, - reminderId, - remindAt: remindOptionToMilliseconds(plannedAt, remindCfg), - title - })); + title, + }: { + taskId: string; + plannedAt: number; + title: string; + reminderId?: string; + remindCfg: TaskReminderOptionId; + }) { + this._store.dispatch( + new ReScheduleTask({ + id: taskId, + plannedAt, + reminderId, + remindAt: remindOptionToMilliseconds(plannedAt, remindCfg), + title, + }), + ); } unScheduleTask(taskId: string, reminderId?: string) { - console.log('unschedzle', {reminderId}); + console.log('unschedzle', { reminderId }); if (!taskId) { throw new Error('No task id'); } - this._store.dispatch(new UnScheduleTask({id: taskId, reminderId})); + this._store.dispatch(new UnScheduleTask({ id: taskId, reminderId })); } // HELPER // ------ getByIdOnce$(id: string): Observable { - return this._store.pipe(select(selectTaskById, {id}), take(1)); + return this._store.pipe(select(selectTaskById, { id }), take(1)); } getByIdLive$(id: string): Observable { - return this._store.pipe(select(selectTaskById, {id})); + return this._store.pipe(select(selectTaskById, { id })); } getByIdsLive$(ids: string[]): Observable { - return this._store.pipe(select(selectTasksById, {ids})); + return this._store.pipe(select(selectTasksById, { ids })); } getByIdWithSubTaskData$(id: string): Observable { - return this._store.pipe(select(selectTaskByIdWithSubTaskData, {id}), take(1)); + return this._store.pipe(select(selectTaskByIdWithSubTaskData, { id }), take(1)); } getTasksByRepeatCfgId$(repeatCfgId: string): Observable { - return this._store.pipe(select(selectTasksByRepeatConfigId, {repeatCfgId}), take(1)); + return this._store.pipe( + select(selectTasksByRepeatConfigId, { repeatCfgId }), + take(1), + ); } - getTasksWithSubTasksByRepeatCfgId$(repeatCfgId: string): Observable { + getTasksWithSubTasksByRepeatCfgId$( + repeatCfgId: string, + ): Observable { if (!repeatCfgId) { throw new Error('No repeatCfgId'); } - return this._store.pipe(select(selectTaskWithSubTasksByRepeatConfigId, {repeatCfgId})); + return this._store.pipe( + select(selectTaskWithSubTasksByRepeatConfigId, { repeatCfgId }), + ); } getTasksByTag(tagId: string): Observable { - return this._store.pipe(select(selectTasksByTag, {tagId})); + return this._store.pipe(select(selectTasksByTag, { tagId })); } setDone(id: string) { - this.update(id, {isDone: true}); + this.update(id, { isDone: true }); } markIssueUpdatesAsRead(id: string) { - this.update(id, {issueWasUpdated: false}); + this.update(id, { issueWasUpdated: false }); } setUnDone(id: string) { - this.update(id, {isDone: false}); + this.update(id, { isDone: false }); } showSubTasks(id: string) { - this.updateUi(id, {_showSubTasksMode: ShowSubTasksMode.Show}); + this.updateUi(id, { _showSubTasksMode: ShowSubTasksMode.Show }); } - toggleSubTaskMode(taskId: string, isShowLess: boolean = true, isEndless: boolean = false) { - this._store.dispatch(new ToggleTaskShowSubTasks({taskId, isShowLess, isEndless})); + toggleSubTaskMode( + taskId: string, + isShowLess: boolean = true, + isEndless: boolean = false, + ) { + this._store.dispatch(new ToggleTaskShowSubTasks({ taskId, isShowLess, isEndless })); } hideSubTasks(id: string) { - this.updateUi(id, {_showSubTasksMode: ShowSubTasksMode.HideAll}); + this.updateUi(id, { _showSubTasksMode: ShowSubTasksMode.HideAll }); } async convertToMainTask(task: Task) { const parent = await this.getByIdOnce$(task.parentId as string).toPromise(); - this._store.dispatch(new ConvertToMainTask({task, parentTagIds: parent.tagIds})); + this._store.dispatch(new ConvertToMainTask({ task, parentTagIds: parent.tagIds })); } // GLOBAL TASK MODEL STUFF @@ -606,7 +700,7 @@ export class TaskService { // BEWARE: does only work for task model updates, but not for related models async updateEverywhere(id: string, changedFields: Partial) { const state = await this.taskFeatureState$.pipe(first()).toPromise(); - const {entities} = state; + const { entities } = state; if (entities[id]) { this.update(id, changedFields); } else { @@ -616,44 +710,57 @@ export class TaskService { // BEWARE: does only work for task model updates, but not the meta models async updateArchiveTask(id: string, changedFields: Partial): Promise { - return await this._persistenceService.taskArchive.execAction(new UpdateTask({ - task: { - id, - changes: changedFields - } - })); + return await this._persistenceService.taskArchive.execAction( + new UpdateTask({ + task: { + id, + changes: changedFields, + }, + }), + ); } async getByIdFromEverywhere(id: string): Promise { - return await this._persistenceService.task.getById(id) - || await this._persistenceService.taskArchive.getById(id); + return ( + (await this._persistenceService.task.getById(id)) || + (await this._persistenceService.taskArchive.getById(id)) + ); } async getAllTasksForProject(projectId: string): Promise { const allTasks = await this._allTasksWithSubTaskData$.pipe(first()).toPromise(); const archiveTaskState: TaskArchive = await this._persistenceService.taskArchive.loadState(); - const ids = archiveTaskState && archiveTaskState.ids as string[] || []; - const archiveTasks = ids.map(id => archiveTaskState.entities[id]); + const ids = (archiveTaskState && (archiveTaskState.ids as string[])) || []; + const archiveTasks = ids.map((id) => archiveTaskState.entities[id]); return [...allTasks, ...archiveTasks].filter( - (task) => ((task as Task).projectId === projectId) + (task) => (task as Task).projectId === projectId, ) as Task[]; } - async getAllTaskByIssueTypeForProject$(projectId: string, issueProviderKey: IssueProviderKey): Promise { + async getAllTaskByIssueTypeForProject$( + projectId: string, + issueProviderKey: IssueProviderKey, + ): Promise { const allTasks = await this.getAllTasksForProject(projectId); - return allTasks - .filter(task => task.issueType === issueProviderKey); + return allTasks.filter((task) => task.issueType === issueProviderKey); } - async getAllIssueIdsForProject(projectId: string, issueProviderKey: IssueProviderKey): Promise { + async getAllIssueIdsForProject( + projectId: string, + issueProviderKey: IssueProviderKey, + ): Promise { const allTasks = await this.getAllTasksForProject(projectId); return allTasks - .filter(task => task.issueType === issueProviderKey) - .map(task => task.issueId) as string[] | number[]; + .filter((task) => task.issueType === issueProviderKey) + .map((task) => task.issueId) as string[] | number[]; } // TODO check with new archive - async checkForTaskWithIssueInProject(issueId: string | number, issueProviderKey: IssueProviderKey, projectId: string): Promise<{ + async checkForTaskWithIssueInProject( + issueId: string | number, + issueProviderKey: IssueProviderKey, + projectId: string, + ): Promise<{ task: Task; subTasks: Task[] | null; isFromArchive: boolean; @@ -663,8 +770,13 @@ export class TaskService { } const findTaskFn = (task: Task | ArchiveTask | undefined) => - task && task.issueId === issueId && task.issueType === issueProviderKey && task.projectId === projectId; - const allTasks = await this._allTasksWithSubTaskData$.pipe(first()).toPromise() as Task[]; + task && + task.issueId === issueId && + task.issueType === issueProviderKey && + task.projectId === projectId; + const allTasks = (await this._allTasksWithSubTaskData$ + .pipe(first()) + .toPromise()) as Task[]; const taskWithSameIssue: Task = allTasks.find(findTaskFn) as Task; if (taskWithSameIssue) { @@ -675,20 +787,22 @@ export class TaskService { }; } else { const archiveTaskState: TaskArchive = await this._persistenceService.taskArchive.loadState(); - const ids = archiveTaskState && archiveTaskState.ids as string[]; + const ids = archiveTaskState && (archiveTaskState.ids as string[]); if (ids) { const archiveTaskWithSameIssue = ids - .map(id => archiveTaskState.entities[id]) + .map((id) => archiveTaskState.entities[id]) .find(findTaskFn); return archiveTaskWithSameIssue ? { - task: archiveTaskWithSameIssue as Task, - subTasks: archiveTaskWithSameIssue.subTaskIds - ? archiveTaskWithSameIssue.subTaskIds.map(id => archiveTaskState.entities[id]) as Task[] - : null, - isFromArchive: true - } + task: archiveTaskWithSameIssue as Task, + subTasks: archiveTaskWithSameIssue.subTaskIds + ? (archiveTaskWithSameIssue.subTaskIds.map( + (id) => archiveTaskState.entities[id], + ) as Task[]) + : null, + isFromArchive: true, + } : null; } return null; @@ -699,7 +813,7 @@ export class TaskService { title, additional = {}, workContextType = this._workContextService.activeWorkContextType as WorkContextType, - workContextId = this._workContextService.activeWorkContextId as string + workContextId = this._workContextService.activeWorkContextId as string, }: { title: string | null; additional?: Partial; @@ -713,15 +827,13 @@ export class TaskService { title: title as string, id: shortid(), - projectId: (workContextType === WorkContextType.PROJECT) - ? workContextId - : null, - tagIds: (workContextType === WorkContextType.TAG && !additional.parentId) - ? [workContextId] - : [], + projectId: workContextType === WorkContextType.PROJECT ? workContextId : null, + tagIds: + workContextType === WorkContextType.TAG && !additional.parentId + ? [workContextId] + : [], ...additional, }; } - } diff --git a/src/app/features/tasks/task/task.component.ts b/src/app/features/tasks/task/task.component.ts index f54c854e3..4c1215c0c 100644 --- a/src/app/features/tasks/task/task.component.ts +++ b/src/app/features/tasks/task/task.component.ts @@ -10,11 +10,15 @@ import { OnDestroy, OnInit, Renderer2, - ViewChild + ViewChild, } from '@angular/core'; import { TaskService } from '../task.service'; import { Observable, of, ReplaySubject, Subject } from 'rxjs'; -import { ShowSubTasksMode, TaskAdditionalInfoTargetPanel, TaskWithSubTasks } from '../task.model'; +import { + ShowSubTasksMode, + TaskAdditionalInfoTargetPanel, + TaskWithSubTasks, +} from '../task.model'; import { MatDialog } from '@angular/material/dialog'; import { DialogTimeEstimateComponent } from '../dialog-time-estimate/dialog-time-estimate.component'; import { expandAnimation } from '../../../ui/animations/expand.ani'; @@ -45,7 +49,7 @@ import { throttle } from 'helpful-decorators'; templateUrl: './task.component.html', styleUrls: ['./task.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation, fadeAnimation, swirlAnimation] + animations: [expandAnimation, fadeAnimation, swirlAnimation], }) export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { task!: TaskWithSubTasks; @@ -58,16 +62,19 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { isPreventPointerEventsWhilePanning: boolean = false; isActionTriggered: boolean = false; ShowSubTasksMode: typeof ShowSubTasksMode = ShowSubTasksMode; - contextMenuPosition: { x: string; y: string } = {x: '0px', y: '0px'}; + contextMenuPosition: { x: string; y: string } = { x: '0px', y: '0px' }; progress: number = 0; isDev: boolean = !(environment.production || environment.stage); - @ViewChild('contentEditableOnClickEl', {static: true}) contentEditableOnClickEl?: ElementRef; + @ViewChild('contentEditableOnClickEl', { static: true }) + contentEditableOnClickEl?: ElementRef; @ViewChild('blockLeftEl') blockLeftElRef?: ElementRef; @ViewChild('blockRightEl') blockRightElRef?: ElementRef; - @ViewChild('innerWrapperEl', {static: true}) innerWrapperElRef?: ElementRef; + @ViewChild('innerWrapperEl', { static: true }) innerWrapperElRef?: ElementRef; // only works because item comes first in dom - @ViewChild('contextMenuTriggerEl', {static: true, read: MatMenuTrigger}) contextMenu?: MatMenuTrigger; - @ViewChild('projectMenuTriggerEl', {static: false, read: MatMenuTrigger}) projectMenuTrigger?: MatMenuTrigger; + @ViewChild('contextMenuTriggerEl', { static: true, read: MatMenuTrigger }) + contextMenu?: MatMenuTrigger; + @ViewChild('projectMenuTriggerEl', { static: false, read: MatMenuTrigger }) + projectMenuTrigger?: MatMenuTrigger; @HostBinding('tabindex') tabIndex: number = 1; @HostBinding('class.isDone') isDone: boolean = false; @HostBinding('id') taskIdWithPrefix: string = 'NO'; @@ -78,14 +85,14 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { private _task$: ReplaySubject = new ReplaySubject(1); issueUrl$: Observable = this._task$.pipe( switchMap((v) => { - return (v.issueType && v.issueId && v.projectId) + return v.issueType && v.issueId && v.projectId ? this._issueService.issueLink$(v.issueType, v.issueId, v.projectId) : of(null); }), take(1), ); moveToProjectList$: Observable = this._task$.pipe( - map(t => t.projectId), + map((t) => t.projectId), distinctUntilChanged(), switchMap((pid) => this._projectService.getProjectsWithoutId$(pid)), ); @@ -105,8 +112,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { private readonly _cd: ChangeDetectorRef, private readonly _projectService: ProjectService, public readonly workContextService: WorkContextService, - ) { - } + ) {} @Input('task') set taskSet(v: TaskWithSubTasks) { this.task = v; @@ -163,18 +169,14 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } ngOnInit() { - this._taskService.currentTaskId$ - .pipe(takeUntil(this._destroy$)) - .subscribe((id) => { - this.isCurrent = (this.task && id === this.task.id); - this._cd.markForCheck(); - }); - this._taskService.selectedTaskId$ - .pipe(takeUntil(this._destroy$)) - .subscribe((id) => { - this.isSelected = (this.task && id === this.task.id); - this._cd.markForCheck(); - }); + this._taskService.currentTaskId$.pipe(takeUntil(this._destroy$)).subscribe((id) => { + this.isCurrent = this.task && id === this.task.id; + this._cd.markForCheck(); + }); + this._taskService.selectedTaskId$.pipe(takeUntil(this._destroy$)).subscribe((id) => { + this.isSelected = this.task && id === this.task.id; + this._cd.markForCheck(); + }); } ngAfterViewInit() { @@ -210,9 +212,10 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { return; } - this._matDialog.open(DialogAddTaskReminderComponent, { - data: {task: this.task} as AddTaskReminderInterface - }) + this._matDialog + .open(DialogAddTaskReminderComponent, { + data: { task: this.task } as AddTaskReminderInterface, + }) .afterClosed() .pipe(takeUntil(this._destroy$)) .subscribe(() => this.focusSelf()); @@ -223,11 +226,12 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } editTaskRepeatCfg() { - this._matDialog.open(DialogEditTaskRepeatCfgComponent, { - data: { - task: this.task, - } - }) + this._matDialog + .open(DialogEditTaskRepeatCfgComponent, { + data: { + task: this.task, + }, + }) .afterClosed() .pipe(takeUntil(this._destroy$)) .subscribe(() => this.focusSelf()); @@ -258,31 +262,38 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { updateTaskTitleIfChanged({ isChanged, - newVal - }: { isChanged: boolean; newVal: string; $taskEl: HTMLElement | null; event: Event }) { + newVal, + }: { + isChanged: boolean; + newVal: string; + $taskEl: HTMLElement | null; + event: Event; + }) { if (isChanged) { - this._taskService.update(this.task.id, {title: newVal}); + this._taskService.update(this.task.id, { title: newVal }); } this.focusSelf(); } estimateTime() { - this._matDialog.open(DialogTimeEstimateComponent, { - data: {task: this.task}, - autoFocus: !isTouchOnly(), - }) + this._matDialog + .open(DialogTimeEstimateComponent, { + data: { task: this.task }, + autoFocus: !isTouchOnly(), + }) .afterClosed() .pipe(takeUntil(this._destroy$)) .subscribe(() => this.focusSelf()); } addAttachment() { - this._matDialog.open(DialogEditTaskAttachmentComponent, { - data: {}, - }) + this._matDialog + .open(DialogEditTaskAttachmentComponent, { + data: {}, + }) .afterClosed() .pipe(takeUntil(this._destroy$)) - .subscribe(result => { + .subscribe((result) => { this.focusSelf(); if (result) { this._attachmentService.addAttachment(this.task.id, result); @@ -294,7 +305,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { this._taskService.addSubTaskTo(this.task.parentId || this.task.id); } - @throttle(200, {leading: true, trailing: false}) + @throttle(200, { leading: true, trailing: false }) toggleDoneKeyboard() { this.toggleTaskDone(); if (!this.task.parentId) { @@ -334,7 +345,10 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } toggleShowAttachments() { - this._taskService.setSelectedId(this.task.id, TaskAdditionalInfoTargetPanel.Attachments); + this._taskService.setSelectedId( + this.task.id, + TaskAdditionalInfoTargetPanel.Attachments, + ); this.focusSelf(); } @@ -344,11 +358,12 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } editTags() { - this._matDialog.open(DialogEditTagsForTaskComponent, { - data: { - task: this.task - } - }) + this._matDialog + .open(DialogEditTagsForTaskComponent, { + data: { + task: this.task, + }, + }) .afterClosed() .pipe(takeUntil(this._destroy$)) .subscribe(() => this.focusSelf()); @@ -359,7 +374,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } removeFromMyDay() { - this.onTagsUpdated(this.task.tagIds.filter(tagId => tagId !== TODAY_TAG.id)); + this.onTagsUpdated(this.task.tagIds.filter((tagId) => tagId !== TODAY_TAG.id)); } convertToMainTask() { @@ -372,7 +387,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } const taskEls = Array.from(document.querySelectorAll('task')); - const currentIndex = taskEls.findIndex(el => document.activeElement === el); + const currentIndex = taskEls.findIndex((el) => document.activeElement === el); const prevEl = taskEls[currentIndex - 1] as HTMLElement; if (prevEl) { @@ -386,7 +401,6 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } else if (isFocusReverseIfNotPossible) { this.focusNext(); } - } focusNext(isFocusReverseIfNotPossible: boolean = false) { @@ -395,7 +409,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } const taskEls = Array.from(document.querySelectorAll('task')); - const currentIndex = taskEls.findIndex(el => document.activeElement === el); + const currentIndex = taskEls.findIndex((el) => document.activeElement === el); const nextEl = taskEls[currentIndex + 1] as HTMLElement; if (nextEl) { @@ -459,10 +473,10 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { this._resetAfterPan(); const targetEl: HTMLElement = ev.target as HTMLElement; if ( - (targetEl.className.indexOf && targetEl.className.indexOf('drag-handle') > -1) - || Math.abs(ev.deltaY) > Math.abs(ev.deltaX) - || document.activeElement === this.contentEditableOnClickEl.nativeElement - || ev.isFinal + (targetEl.className.indexOf && targetEl.className.indexOf('drag-handle') > -1) || + Math.abs(ev.deltaY) > Math.abs(ev.deltaX) || + document.activeElement === this.contentEditableOnClickEl.nativeElement || + ev.isFinal ) { return; } @@ -474,7 +488,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } onPanEnd() { - if (!IS_TOUCH_ONLY || !this.isLockPanLeft && !this.isLockPanRight) { + if (!IS_TOUCH_ONLY || (!this.isLockPanLeft && !this.isLockPanRight)) { return; } if (!this.blockLeftElRef || !this.blockRightElRef) { @@ -491,9 +505,12 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { if (this.isActionTriggered) { if (this.isLockPanLeft) { - this._renderer.setStyle(this.blockRightElRef.nativeElement, 'transform', `scaleX(1)`); + this._renderer.setStyle( + this.blockRightElRef.nativeElement, + 'transform', + `scaleX(1)`, + ); this._currentPanTimeout = window.setTimeout(() => { - if (this.workContextService.isToday) { if (this.task.repeatCfgId) { this.editTaskRepeatCfg(); @@ -514,7 +531,11 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { this._resetAfterPan(); }, 100); } else if (this.isLockPanRight) { - this._renderer.setStyle(this.blockLeftElRef.nativeElement, 'transform', `scaleX(1)`); + this._renderer.setStyle( + this.blockLeftElRef.nativeElement, + 'transform', + `scaleX(1)`, + ); this._currentPanTimeout = window.setTimeout(() => { this.toggleTaskDone(); this._resetAfterPan(); @@ -550,24 +571,24 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } private _handlePan(ev: any) { - if (!IS_TOUCH_ONLY - || !this.isLockPanLeft && !this.isLockPanRight - || ev.eventType === 8) { + if ( + !IS_TOUCH_ONLY || + (!this.isLockPanLeft && !this.isLockPanRight) || + ev.eventType === 8 + ) { return; } if (!this.innerWrapperElRef) { throw new Error('No el'); } - const targetRef = this.isLockPanRight - ? this.blockLeftElRef - : this.blockRightElRef; + const targetRef = this.isLockPanRight ? this.blockLeftElRef : this.blockRightElRef; const MAGIC_FACTOR = 2; this.isPreventPointerEventsWhilePanning = true; // this.contentEditableOnClickEl.nativeElement.blur(); if (targetRef) { - let scale = ev.deltaX / this._elementRef.nativeElement.offsetWidth * MAGIC_FACTOR; + let scale = (ev.deltaX / this._elementRef.nativeElement.offsetWidth) * MAGIC_FACTOR; scale = this.isLockPanLeft ? scale * -1 : scale; scale = Math.min(1, Math.max(0, scale)); if (scale > 0.5) { @@ -580,12 +601,21 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { const moveBy = this.isLockPanLeft ? ev.deltaX * -1 : ev.deltaX; this._renderer.setStyle(targetRef.nativeElement, 'width', `${moveBy}px`); this._renderer.setStyle(targetRef.nativeElement, 'transition', `none`); - this._renderer.setStyle(this.innerWrapperElRef.nativeElement, 'transform', `translateX(${ev.deltaX}px`); + this._renderer.setStyle( + this.innerWrapperElRef.nativeElement, + 'transform', + `translateX(${ev.deltaX}px`, + ); } } private _resetAfterPan() { - if (!this.contentEditableOnClickEl || !this.blockLeftElRef || !this.blockRightElRef || !this.innerWrapperElRef) { + if ( + !this.contentEditableOnClickEl || + !this.blockLeftElRef || + !this.blockRightElRef || + !this.innerWrapperElRef + ) { throw new Error('No el'); } @@ -611,7 +641,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { throw new Error(); } const keys = cfg.keyboard; - const isShiftOrCtrlPressed = (ev.shiftKey || ev.ctrlKey); + const isShiftOrCtrlPressed = ev.shiftKey || ev.ctrlKey; if (checkKeyCombo(ev, keys.taskEditTitle) || ev.key === 'Enter') { this.focusTitleForEdit(); @@ -675,23 +705,31 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } // move focus up - if ((!isShiftOrCtrlPressed && ev.key === 'ArrowUp') || checkKeyCombo(ev, keys.selectPreviousTask)) { + if ( + (!isShiftOrCtrlPressed && ev.key === 'ArrowUp') || + checkKeyCombo(ev, keys.selectPreviousTask) + ) { ev.preventDefault(); this.focusPrevious(); } // move focus down - if ((!isShiftOrCtrlPressed && ev.key === 'ArrowDown') || checkKeyCombo(ev, keys.selectNextTask)) { + if ( + (!isShiftOrCtrlPressed && ev.key === 'ArrowDown') || + checkKeyCombo(ev, keys.selectNextTask) + ) { ev.preventDefault(); this.focusNext(); } // collapse sub tasks - if ((ev.key === 'ArrowLeft') || checkKeyCombo(ev, keys.collapseSubTasks)) { - const hasSubTasks = this.task.subTasks - && (this.task.subTasks as any).length > 0; + if (ev.key === 'ArrowLeft' || checkKeyCombo(ev, keys.collapseSubTasks)) { + const hasSubTasks = this.task.subTasks && (this.task.subTasks as any).length > 0; if (this.isSelected) { this.hideAdditionalInfos(); - } else if (hasSubTasks && this.task._showSubTasksMode !== ShowSubTasksMode.HideAll) { + } else if ( + hasSubTasks && + this.task._showSubTasksMode !== ShowSubTasksMode.HideAll + ) { this._taskService.toggleSubTaskMode(this.task.id, true, false); // TODO find a solution // } else if (this.task.parentId) { @@ -702,9 +740,8 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } // expand sub tasks - if ((ev.key === 'ArrowRight') || checkKeyCombo(ev, keys.expandSubTasks)) { - const hasSubTasks = this.task.subTasks - && (this.task.subTasks as any).length > 0; + if (ev.key === 'ArrowRight' || checkKeyCombo(ev, keys.expandSubTasks)) { + const hasSubTasks = this.task.subTasks && (this.task.subTasks as any).length > 0; if (hasSubTasks && this.task._showSubTasksMode !== ShowSubTasksMode.Show) { this._taskService.toggleSubTaskMode(this.task.id, false, false); } else if (!this.isSelected) { diff --git a/src/app/features/tasks/tasks.module.ts b/src/app/features/tasks/tasks.module.ts index 413077206..da54eaac0 100644 --- a/src/app/features/tasks/tasks.module.ts +++ b/src/app/features/tasks/tasks.module.ts @@ -83,8 +83,6 @@ import { TaskSummaryTablesComponent } from './task-summary-tables/task-summary-t TaskAdditionalInfoComponent, TaskAdditionalInfoWrapperComponent, ], - providers: [TagService] - + providers: [TagService], }) -export class TasksModule { -} +export class TasksModule {} diff --git a/src/app/features/tasks/util/calc-total-time-spent.ts b/src/app/features/tasks/util/calc-total-time-spent.ts index eb2deec6a..c70b3fe10 100644 --- a/src/app/features/tasks/util/calc-total-time-spent.ts +++ b/src/app/features/tasks/util/calc-total-time-spent.ts @@ -2,9 +2,9 @@ import { TimeSpentOnDay } from '../task.model'; export const calcTotalTimeSpent = (timeSpentOnDay: TimeSpentOnDay) => { let totalTimeSpent = 0; - Object.keys(timeSpentOnDay).forEach(strDate => { + Object.keys(timeSpentOnDay).forEach((strDate) => { if (timeSpentOnDay[strDate]) { - totalTimeSpent += (+timeSpentOnDay[strDate]); + totalTimeSpent += +timeSpentOnDay[strDate]; } }); return totalTimeSpent; diff --git a/src/app/features/tasks/util/play-done-sound.ts b/src/app/features/tasks/util/play-done-sound.ts index c1506f481..0d4bcf123 100644 --- a/src/app/features/tasks/util/play-done-sound.ts +++ b/src/app/features/tasks/util/play-done-sound.ts @@ -15,17 +15,20 @@ export const playDoneSound = (soundCfg: SoundConfig, nrOfDoneTasks: number = 0) // a.play(); const pitchFactor = soundCfg.isIncreaseDoneSoundPitch - ? PITCH_OFFSET + (nrOfDoneTasks * 50) + ? PITCH_OFFSET + nrOfDoneTasks * 50 : 0; - const audioCtx = new ((window as any).AudioContext || (window as any).webkitAudioContext)(); + const audioCtx = new ((window as any).AudioContext || + (window as any).webkitAudioContext)(); const source = audioCtx.createBufferSource(); const request = new XMLHttpRequest(); request.open('GET', file, true); request.responseType = 'arraybuffer'; request.onload = () => { const audioData = request.response; - audioCtx.decodeAudioData(audioData, (buffer: AudioBuffer) => { + audioCtx.decodeAudioData( + audioData, + (buffer: AudioBuffer) => { source.buffer = buffer; source.playbackRate.value = speed; // source.detune.value = 100; // value in cents @@ -42,8 +45,8 @@ export const playDoneSound = (soundCfg: SoundConfig, nrOfDoneTasks: number = 0) }, (e: DOMException) => { throw new Error('Error with decoding audio data SP: ' + e.message); - }); - + }, + ); }; request.send(); source.start(0); diff --git a/src/app/features/tasks/util/remind-option-to-milliseconds.ts b/src/app/features/tasks/util/remind-option-to-milliseconds.ts index 49ea6fa1e..812b675f3 100644 --- a/src/app/features/tasks/util/remind-option-to-milliseconds.ts +++ b/src/app/features/tasks/util/remind-option-to-milliseconds.ts @@ -1,35 +1,41 @@ import { TaskReminderOptionId } from '../task.model'; -export const remindOptionToMilliseconds = (plannedAt: number, remindOptId: TaskReminderOptionId): number | undefined => { +export const remindOptionToMilliseconds = ( + plannedAt: number, + remindOptId: TaskReminderOptionId, +): number | undefined => { switch (remindOptId) { - case TaskReminderOptionId.AtStart : { + case TaskReminderOptionId.AtStart: { return plannedAt; } - case TaskReminderOptionId.m5 : { + case TaskReminderOptionId.m5: { return plannedAt - 5 * 60 * 1000; } - case TaskReminderOptionId.m10 : { + case TaskReminderOptionId.m10: { return plannedAt - 10 * 60 * 1000; } - case TaskReminderOptionId.m15 : { + case TaskReminderOptionId.m15: { return plannedAt - 15 * 60 * 1000; } - case TaskReminderOptionId.m30 : { + case TaskReminderOptionId.m30: { return plannedAt - 30 * 60 * 1000; } - case TaskReminderOptionId.h1 : { + case TaskReminderOptionId.h1: { return plannedAt - 60 * 60 * 1000; } } return undefined; }; -export const millisecondsDiffToRemindOption = (plannedAt: number, remindAt?: number): TaskReminderOptionId => { +export const millisecondsDiffToRemindOption = ( + plannedAt: number, + remindAt?: number, +): TaskReminderOptionId => { if (typeof remindAt !== 'number') { return TaskReminderOptionId.DoNotRemind; } - const diff: number = plannedAt as number - remindAt; + const diff: number = (plannedAt as number) - remindAt; if (diff >= 60 * 60 * 1000) { return TaskReminderOptionId.h1; } else if (diff >= 30 * 60 * 1000) { diff --git a/src/app/features/time-tracking/dialog-idle/dialog-idle.component.ts b/src/app/features/time-tracking/dialog-idle/dialog-idle.component.ts index 330590fd7..7790c087c 100644 --- a/src/app/features/time-tracking/dialog-idle/dialog-idle.component.ts +++ b/src/app/features/time-tracking/dialog-idle/dialog-idle.component.ts @@ -10,11 +10,13 @@ import { T } from '../../../t.const'; selector: 'dialog-idle', templateUrl: './dialog-idle.component.html', styleUrls: ['./dialog-idle.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogIdleComponent implements OnInit { T: typeof T = T; - lastCurrentTask$: Observable = this._taskService.getByIdOnce$(this.data.lastCurrentTaskId); + lastCurrentTask$: Observable = this._taskService.getByIdOnce$( + this.data.lastCurrentTaskId, + ); selectedTask: Task | null = null; newTaskTitle?: string; isCreate?: boolean; @@ -36,7 +38,7 @@ export class DialogIdleComponent implements OnInit { } onTaskChange(taskOrTaskTitle: Task | string) { - this.isCreate = (typeof taskOrTaskTitle === 'string'); + this.isCreate = typeof taskOrTaskTitle === 'string'; if (this.isCreate) { this.newTaskTitle = taskOrTaskTitle as string; this.selectedTask = null; @@ -49,7 +51,7 @@ export class DialogIdleComponent implements OnInit { skipTrack() { this._matDialogRef.close({ task: null, - isResetBreakTimer: true + isResetBreakTimer: true, }); } diff --git a/src/app/features/time-tracking/idle.service.ts b/src/app/features/time-tracking/idle.service.ts index e45324d15..8336f5e54 100644 --- a/src/app/features/time-tracking/idle.service.ts +++ b/src/app/features/time-tracking/idle.service.ts @@ -25,10 +25,9 @@ const IDLE_POLL_INTERVAL = 1000; export class IdleService { isIdle: boolean = false; private _isIdle$: BehaviorSubject = new BehaviorSubject(false); - isIdle$: Observable = this._isIdle$.asObservable().pipe( - distinctUntilChanged(), - shareReplay(1), - ); + isIdle$: Observable = this._isIdle$ + .asObservable() + .pipe(distinctUntilChanged(), shareReplay(1)); private _idleTime$: BehaviorSubject = new BehaviorSubject(0); idleTime$: Observable = this._idleTime$.asObservable(); @@ -47,20 +46,25 @@ export class IdleService { private _configService: GlobalConfigService, private _matDialog: MatDialog, private _uiHelperService: UiHelperService, - ) { - } + ) {} init() { if (IS_ELECTRON) { - (this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.IDLE_TIME, (ev, idleTimeInMs) => { - this.handleIdle(idleTimeInMs); - }); + (this._electronService.ipcRenderer as typeof ipcRenderer).on( + IPC.IDLE_TIME, + (ev, idleTimeInMs) => { + this.handleIdle(idleTimeInMs); + }, + ); } else { this._chromeExtensionInterfaceService.onReady$.subscribe(() => { - this._chromeExtensionInterfaceService.addEventListener(IPC.IDLE_TIME, (ev: Event, data?: unknown) => { - const idleTimeInMs = Number(data); - this.handleIdle(idleTimeInMs); - }); + this._chromeExtensionInterfaceService.addEventListener( + IPC.IDLE_TIME, + (ev: Event, data?: unknown) => { + const idleTimeInMs = Number(data); + this.handleIdle(idleTimeInMs); + }, + ); }); } @@ -79,7 +83,10 @@ export class IdleService { const minIdleTime = cfg.minIdleTime || DEFAULT_MIN_IDLE_TIME; // don't run if option is not enabled - if (!cfg.isEnableIdleTimeTracking || (cfg.isOnlyOpenIdleWhenCurrentTask && !this._taskService.currentTaskId)) { + if ( + !cfg.isEnableIdleTimeTracking || + (cfg.isOnlyOpenIdleWhenCurrentTask && !this._taskService.currentTaskId) + ) { this.isIdle = false; this._isIdle$.next(false); return; @@ -104,45 +111,56 @@ export class IdleService { this.isIdleDialogOpen = true; this.initIdlePoll(idleTime); - this._matDialog.open(DialogIdleComponent, { - restoreFocus: true, - disableClose: true, - data: { - lastCurrentTaskId: this.lastCurrentTaskId, - idleTime$: this.idleTime$, - } - }).afterClosed() - .subscribe((res: { task: Task | string; isResetBreakTimer: boolean; isTrackAsBreak: boolean }) => { - const {task, isResetBreakTimer, isTrackAsBreak} = res; - const timeSpent = this._idleTime$.getValue(); + this._matDialog + .open(DialogIdleComponent, { + restoreFocus: true, + disableClose: true, + data: { + lastCurrentTaskId: this.lastCurrentTaskId, + idleTime$: this.idleTime$, + }, + }) + .afterClosed() + .subscribe( + (res: { + task: Task | string; + isResetBreakTimer: boolean; + isTrackAsBreak: boolean; + }) => { + const { task, isResetBreakTimer, isTrackAsBreak } = res; + const timeSpent = this._idleTime$.getValue(); - if (isResetBreakTimer || isTrackAsBreak) { - this._triggerResetBreakTimer$.next(true); - } - - if (isTrackAsBreak) { - this._workContextService.addToBreakTimeForActiveContext(undefined, timeSpent); - } - - if (task) { - if (typeof task === 'string') { - const currId = this._taskService.add(task, false, { - timeSpent, - timeSpentOnDay: { - [getWorklogStr()]: timeSpent - } - }); - this._taskService.setCurrentId(currId); - } else { - this._taskService.addTimeSpent(task, timeSpent); - this._taskService.setCurrentId(task.id); + if (isResetBreakTimer || isTrackAsBreak) { + this._triggerResetBreakTimer$.next(true); } - } - this.cancelIdlePoll(); - this._isIdle$.next(false); - this.isIdleDialogOpen = false; - }); + if (isTrackAsBreak) { + this._workContextService.addToBreakTimeForActiveContext( + undefined, + timeSpent, + ); + } + + if (task) { + if (typeof task === 'string') { + const currId = this._taskService.add(task, false, { + timeSpent, + timeSpentOnDay: { + [getWorklogStr()]: timeSpent, + }, + }); + this._taskService.setCurrentId(currId); + } else { + this._taskService.addTimeSpent(task, timeSpent); + this._taskService.setCurrentId(task.id); + } + } + + this.cancelIdlePoll(); + this._isIdle$.next(false); + this.isIdleDialogOpen = false; + }, + ); } } } diff --git a/src/app/features/time-tracking/take-a-break/take-a-break.module.ts b/src/app/features/time-tracking/take-a-break/take-a-break.module.ts index d8e5b2140..bf4ce6091 100644 --- a/src/app/features/time-tracking/take-a-break/take-a-break.module.ts +++ b/src/app/features/time-tracking/take-a-break/take-a-break.module.ts @@ -3,9 +3,6 @@ import { CommonModule } from '@angular/common'; @NgModule({ declarations: [], - imports: [ - CommonModule - ], + imports: [CommonModule], }) -export class TakeABreakModule { -} +export class TakeABreakModule {} diff --git a/src/app/features/time-tracking/take-a-break/take-a-break.service.ts b/src/app/features/time-tracking/take-a-break/take-a-break.service.ts index ea32d4c8a..5cf83b877 100644 --- a/src/app/features/time-tracking/take-a-break/take-a-break.service.ts +++ b/src/app/features/time-tracking/take-a-break/take-a-break.service.ts @@ -13,7 +13,7 @@ import { startWith, switchMap, throttleTime, - withLatestFrom + withLatestFrom, } from 'rxjs/operators'; import { GlobalConfigService } from '../../config/global-config.service'; import { msToString } from '../../../ui/duration/ms-to-string.pipe'; @@ -60,25 +60,25 @@ export class TakeABreakService { private _isIdleResetEnabled$: Observable = this._configService.idle$.pipe( switchMap((idleCfg) => { - const isConfigured = (idleCfg.isEnableIdleTimeTracking && idleCfg.isUnTrackedIdleResetsBreakTimer); - if (IS_ELECTRON) { - return [isConfigured]; - } else if (isConfigured) { - return this._chromeExtensionInterfaceService.isReady$; - } else { - return [false]; - } + const isConfigured = + idleCfg.isEnableIdleTimeTracking && idleCfg.isUnTrackedIdleResetsBreakTimer; + if (IS_ELECTRON) { + return [isConfigured]; + } else if (isConfigured) { + return this._chromeExtensionInterfaceService.isReady$; + } else { + return [false]; } - ), + }), distinctUntilChanged(), ); private _triggerSimpleBreakReset$: Observable = this._timeWithNoCurrentTask$.pipe( - filter(timeWithNoTask => timeWithNoTask > BREAK_TRIGGER_DURATION), + filter((timeWithNoTask) => timeWithNoTask > BREAK_TRIGGER_DURATION), ); private _tick$: Observable = this._timeTrackingService.tick$.pipe( - map(tick => tick.duration), + map((tick) => tick.duration), ); private _triggerSnooze$: Subject = new Subject(); @@ -88,10 +88,7 @@ export class TakeABreakService { if (val === false) { return [false]; } else { - return timer(+val).pipe( - mapTo(false), - startWith(true), - ); + return timer(+val).pipe(mapTo(false), startWith(true)); } }), ); @@ -109,9 +106,7 @@ export class TakeABreakService { private _triggerReset$: Observable = merge( this._triggerProgrammaticReset$, this._triggerManualReset$, - ).pipe( - mapTo(0), - ); + ).pipe(mapTo(0)); timeWorkingWithoutABreak$: Observable = merge( this._tick$, @@ -119,47 +114,54 @@ export class TakeABreakService { // of(9999999).pipe(delay(4000)), ).pipe( scan((acc, value) => { - return (value > 0) - ? acc + value - : value; + return value > 0 ? acc + value : value; }), shareReplay(1), ); private _triggerLockScreenCounter$: Subject = new Subject(); - private _triggerLockScreenThrottledAndDelayed$: Observable = this._triggerLockScreenCounter$.pipe( + private _triggerLockScreenThrottledAndDelayed$: Observable< + unknown | never + > = this._triggerLockScreenCounter$.pipe( filter(() => IS_ELECTRON), distinctUntilChanged(), - switchMap((v) => !!(v) - ? of(v).pipe( - throttleTime(LOCK_SCREEN_THROTTLE), - delay(LOCK_SCREEN_DELAY), - ) - : EMPTY, + switchMap((v) => + !!v + ? of(v).pipe(throttleTime(LOCK_SCREEN_THROTTLE), delay(LOCK_SCREEN_DELAY)) + : EMPTY, ), ); - private _triggerBanner$: Observable<[number, GlobalConfigState, boolean, boolean]> = this.timeWorkingWithoutABreak$.pipe( + private _triggerBanner$: Observable< + [number, GlobalConfigState, boolean, boolean] + > = this.timeWorkingWithoutABreak$.pipe( withLatestFrom( this._configService.cfg$, this._idleService.isIdle$, this._snoozeActive$, ), - filter(([timeWithoutBreak, cfg, isIdle, isSnoozeActive]: - [number, GlobalConfigState, boolean, boolean]): boolean => - cfg && cfg.takeABreak && cfg.takeABreak.isTakeABreakEnabled - && !isSnoozeActive - && (timeWithoutBreak > cfg.takeABreak.takeABreakMinWorkingTime) - // we don't wanna show if idle to avoid conflicts with the idle modal - && (!isIdle || !cfg.idle.isEnableIdleTimeTracking) + filter( + ([timeWithoutBreak, cfg, isIdle, isSnoozeActive]: [ + number, + GlobalConfigState, + boolean, + boolean, + ]): boolean => + cfg && + cfg.takeABreak && + cfg.takeABreak.isTakeABreakEnabled && + !isSnoozeActive && + timeWithoutBreak > cfg.takeABreak.takeABreakMinWorkingTime && + // we don't wanna show if idle to avoid conflicts with the idle modal + (!isIdle || !cfg.idle.isEnableIdleTimeTracking), ), // throttleTime(5 * 1000), throttleTime(PING_UPDATE_BANNER_INTERVAL), ); - private _triggerDesktopNotification$: Observable<[number, GlobalConfigState, boolean, boolean]> = this._triggerBanner$.pipe( - throttleTime(DESKTOP_NOTIFICATION_THROTTLE) - ); + private _triggerDesktopNotification$: Observable< + [number, GlobalConfigState, boolean, boolean] + > = this._triggerBanner$.pipe(throttleTime(DESKTOP_NOTIFICATION_THROTTLE)); constructor( private _taskService: TaskService, @@ -173,12 +175,14 @@ export class TakeABreakService { private _chromeExtensionInterfaceService: ChromeExtensionInterfaceService, private _uiHelperService: UiHelperService, ) { - this._triggerReset$.pipe( - withLatestFrom(this._configService.takeABreak$), - filter(([reset, cfg]) => cfg && cfg.isTakeABreakEnabled), - ).subscribe(() => { - this._bannerService.dismiss(BANNER_ID); - }); + this._triggerReset$ + .pipe( + withLatestFrom(this._configService.takeABreak$), + filter(([reset, cfg]) => cfg && cfg.isTakeABreakEnabled), + ) + .subscribe(() => { + this._bannerService.dismiss(BANNER_ID); + }); this._triggerLockScreenThrottledAndDelayed$.subscribe(() => { if (IS_ELECTRON) { @@ -207,19 +211,18 @@ export class TakeABreakService { ico: 'free_breakfast', msg, translateParams: { - time: '15m' + time: '15m', }, action: { label: T.F.TIME_TRACKING.B.ALREADY_DID, - fn: () => this.resetTimerAndCountAsBreak() + fn: () => this.resetTimerAndCountAsBreak(), }, action2: { label: T.F.TIME_TRACKING.B.SNOOZE, - fn: () => this.snooze() + fn: () => this.snooze(), }, - img: cfg.takeABreak.motivationalImg || undefined + img: cfg.takeABreak.motivationalImg || undefined, }); - }); } @@ -243,8 +246,7 @@ export class TakeABreakService { private _createMessage(duration: number, cfg: TakeABreakConfig): string | undefined { if (cfg && cfg.takeABreakMessage) { const durationStr = msToString(duration); - return cfg.takeABreakMessage - .replace(/\$\{duration\}/gi, durationStr); + return cfg.takeABreakMessage.replace(/\$\{duration\}/gi, durationStr); } return undefined; } diff --git a/src/app/features/time-tracking/time-tracking.module.ts b/src/app/features/time-tracking/time-tracking.module.ts index 3372191f3..29793d000 100644 --- a/src/app/features/time-tracking/time-tracking.module.ts +++ b/src/app/features/time-tracking/time-tracking.module.ts @@ -9,24 +9,12 @@ import { TasksModule } from '../tasks/tasks.module'; import { DialogTrackingReminderComponent } from './tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component'; @NgModule({ - imports: [ - CommonModule, - UiModule, - FormsModule, - TasksModule, - ], - declarations: [ - DialogIdleComponent, - DialogTrackingReminderComponent - ], - exports: [ - TakeABreakModule, - ] + imports: [CommonModule, UiModule, FormsModule, TasksModule], + declarations: [DialogIdleComponent, DialogTrackingReminderComponent], + exports: [TakeABreakModule], }) export class TimeTrackingModule { - constructor( - private readonly _idleService: IdleService, - ) { + constructor(private readonly _idleService: IdleService) { this._idleService.init(); } } diff --git a/src/app/features/time-tracking/time-tracking.service.spec.ts b/src/app/features/time-tracking/time-tracking.service.spec.ts index 63892acef..aed80fa0f 100644 --- a/src/app/features/time-tracking/time-tracking.service.spec.ts +++ b/src/app/features/time-tracking/time-tracking.service.spec.ts @@ -15,11 +15,11 @@ describe('TimeTrackingService', () => { }); it('should provide a steady interval', () => { - const values = {a: 1, b: 2, c: 3, x: 2, y: 3, z: 4}; + const values = { a: 1, b: 2, c: 3, x: 2, y: 3, z: 4 }; const source = cold('-a-b-c-|', values); const expected = cold('-x-y-z-|', values); - const result = source.pipe(map(x => x + 1)); + const result = source.pipe(map((x) => x + 1)); expect(result).toBeObservable(expected); }); }); diff --git a/src/app/features/time-tracking/time-tracking.service.ts b/src/app/features/time-tracking/time-tracking.service.ts index 2ceb64320..6c3c901ce 100644 --- a/src/app/features/time-tracking/time-tracking.service.ts +++ b/src/app/features/time-tracking/time-tracking.service.ts @@ -22,7 +22,7 @@ export class TimeTrackingService { }; }), // important because we want the same interval for everyone - share() + share(), ); constructor() { diff --git a/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.ts b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.ts index 3da40738c..e3aaff498 100644 --- a/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.ts +++ b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.ts @@ -9,11 +9,13 @@ import { T } from '../../../../t.const'; selector: 'dialog-tracking-reminder', templateUrl: './dialog-tracking-reminder.component.html', styleUrls: ['./dialog-tracking-reminder.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogTrackingReminderComponent implements OnInit { T: typeof T = T; - lastCurrentTask$: Observable = this._taskService.getByIdOnce$(this.data.lastCurrentTaskId); + lastCurrentTask$: Observable = this._taskService.getByIdOnce$( + this.data.lastCurrentTaskId, + ); selectedTask: Task | null = null; newTaskTitle?: string; isCreate?: boolean; @@ -34,7 +36,7 @@ export class DialogTrackingReminderComponent implements OnInit { } onTaskChange(taskOrTaskTitle: Task | string) { - this.isCreate = (typeof taskOrTaskTitle === 'string'); + this.isCreate = typeof taskOrTaskTitle === 'string'; if (this.isCreate) { this.newTaskTitle = taskOrTaskTitle as string; this.selectedTask = null; diff --git a/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.ts b/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.ts index 76edd9d1b..f7bafd9ec 100644 --- a/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.ts +++ b/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.ts @@ -3,7 +3,14 @@ import { IdleService } from '../idle.service'; import { TaskService } from '../../tasks/task.service'; import { GlobalConfigService } from '../../config/global-config.service'; import { combineLatest, EMPTY, merge, Observable, of, Subject } from 'rxjs'; -import { distinctUntilChanged, filter, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + map, + shareReplay, + switchMap, + withLatestFrom, +} from 'rxjs/operators'; import { realTimer$ } from '../../../util/real-timer'; import { BannerService } from '../../../core/banner/banner.service'; import { BannerId } from '../../../core/banner/banner.model'; @@ -18,42 +25,39 @@ import { TrackingReminderConfig } from '../../config/global-config.model'; import { IS_TOUCH_ONLY } from '../../../util/is-touch'; @Injectable({ - providedIn: 'root' + providedIn: 'root', }) export class TrackingReminderService { - _cfg$: Observable = this._globalConfigService.cfg$.pipe(map(cfg => cfg?.trackingReminder)); + _cfg$: Observable = this._globalConfigService.cfg$.pipe( + map((cfg) => cfg?.trackingReminder), + ); _counter$: Observable = realTimer$(1000); _manualReset$: Subject = new Subject(); - _resetableCounter$: Observable = merge( - of('INITIAL'), - this._manualReset$, - ).pipe( + _resetableCounter$: Observable = merge(of('INITIAL'), this._manualReset$).pipe( switchMap(() => this._counter$), ); _hideTrigger$: Observable = merge( - this._taskService.currentTaskId$.pipe(filter(currentId => !!currentId)), - this._idleService.isIdle$.pipe(filter(isIdle => isIdle)), + this._taskService.currentTaskId$.pipe(filter((currentId) => !!currentId)), + this._idleService.isIdle$.pipe(filter((isIdle) => isIdle)), ); remindCounter$: Observable = this._cfg$.pipe( - switchMap((cfg) => !cfg.isEnabled || (!cfg.isShowOnMobile && IS_TOUCH_ONLY) - ? EMPTY - : combineLatest([ - this._taskService.currentTaskId$, - this._idleService.isIdle$, - ]).pipe( - map(([currentTaskId, isIdle]) => !currentTaskId && !isIdle), - distinctUntilChanged(), - switchMap((isEnabled) => isEnabled - ? this._resetableCounter$ - : of(0) - ), - filter(time => time > cfg.minTime), - ) + switchMap((cfg) => + !cfg.isEnabled || (!cfg.isShowOnMobile && IS_TOUCH_ONLY) + ? EMPTY + : combineLatest([ + this._taskService.currentTaskId$, + this._idleService.isIdle$, + ]).pipe( + map(([currentTaskId, isIdle]) => !currentTaskId && !isIdle), + distinctUntilChanged(), + switchMap((isEnabled) => (isEnabled ? this._resetableCounter$ : of(0))), + filter((time) => time > cfg.minTime), + ), ), shareReplay(), ); @@ -65,8 +69,7 @@ export class TrackingReminderService { private _bannerService: BannerService, private _matDialog: MatDialog, private _translateService: TranslateService, - ) { - } + ) {} init() { this.remindCounter$.subscribe((count) => { @@ -92,7 +95,9 @@ export class TrackingReminderService { this._bannerService.open({ id: BannerId.StartTrackingReminder, ico: 'timer', - msg: this._translateService.instant(T.F.TIME_TRACKING.B_TTR.MSG, {time: durationStr}), + msg: this._translateService.instant(T.F.TIME_TRACKING.B_TTR.MSG, { + time: durationStr, + }), action: { label: T.F.TIME_TRACKING.B_TTR.ADD_TO_TASK, fn: () => this._openDialog(), @@ -105,35 +110,40 @@ export class TrackingReminderService { } private _openDialog() { - this._matDialog.open(DialogTrackingReminderComponent, { - data: { - disableClose: true, - remindCounter$: this.remindCounter$, - } - }).afterClosed() - .pipe( - withLatestFrom(this.remindCounter$), - ) - .subscribe(async ([{task} = {task: undefined}, remindCounter]: [{ task: Task | string | undefined }, number]): Promise => { - this._manualReset$.next(); - const timeSpent = remindCounter; + this._matDialog + .open(DialogTrackingReminderComponent, { + data: { + disableClose: true, + remindCounter$: this.remindCounter$, + }, + }) + .afterClosed() + .pipe(withLatestFrom(this.remindCounter$)) + .subscribe( + async ([{ task } = { task: undefined }, remindCounter]: [ + { task: Task | string | undefined }, + number, + ]): Promise => { + this._manualReset$.next(); + const timeSpent = remindCounter; - if (task) { - if (typeof task === 'string') { - const currId = this._taskService.add(task, false, { - timeSpent, - timeSpentOnDay: { - [getWorklogStr()]: timeSpent - } - }); - this._taskService.setCurrentId(currId); - } else { - this._taskService.addTimeSpent(task, timeSpent); - this._taskService.setCurrentId(task.id); + if (task) { + if (typeof task === 'string') { + const currId = this._taskService.add(task, false, { + timeSpent, + timeSpentOnDay: { + [getWorklogStr()]: timeSpent, + }, + }); + this._taskService.setCurrentId(currId); + } else { + this._taskService.addTimeSpent(task, timeSpent); + this._taskService.setCurrentId(task.id); + } } - } - this._dismissBanner(); - }); + this._dismissBanner(); + }, + ); } private _dismissBanner() { diff --git a/src/app/features/ui-helper/ui-helper.service.ts b/src/app/features/ui-helper/ui-helper.service.ts index 85c8f04cb..e9e1de996 100644 --- a/src/app/features/ui-helper/ui-helper.service.ts +++ b/src/app/features/ui-helper/ui-helper.service.ts @@ -11,15 +11,14 @@ import { fromEvent } from 'rxjs'; import { throttleTime } from 'rxjs/operators'; import { ipcRenderer, webFrame } from 'electron'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class UiHelperService { - private _webFrame: typeof webFrame = (this._electronService.webFrame as typeof webFrame); + private _webFrame: typeof webFrame = this._electronService.webFrame as typeof webFrame; constructor( @Inject(DOCUMENT) private _document: Document, private _electronService: ElectronService, - ) { - } + ) {} initElectron() { this._initMousewheelZoomForElectron(); @@ -32,7 +31,7 @@ export class UiHelperService { } this._webFrame.setZoomFactor(zoomFactor); - this._updateLocalUiHelperSettings({zoomFactor}); + this._updateLocalUiHelperSettings({ zoomFactor }); } zoomBy(zoomBy: number) { @@ -44,12 +43,11 @@ export class UiHelperService { const zoomFactor = currentZoom + zoomBy; this._webFrame.setZoomFactor(zoomFactor); - this._updateLocalUiHelperSettings({zoomFactor}); + this._updateLocalUiHelperSettings({ zoomFactor }); } focusApp() { if (IS_ELECTRON) { - // otherwise the last focused task get's focused again leading to unintended keyboard events if (document.activeElement) { (document.activeElement as HTMLElement).blur(); @@ -67,27 +65,30 @@ export class UiHelperService { // set initial zoom this.zoomTo(this._getLocalUiHelperSettings().zoomFactor); - fromEvent(this._document, 'mousewheel').pipe( - throttleTime(20) - ).subscribe((event: any) => { - if (event && event.ctrlKey) { - // this does not prevent scrolling unfortunately - // event.preventDefault(); + fromEvent(this._document, 'mousewheel') + .pipe(throttleTime(20)) + .subscribe((event: any) => { + if (event && event.ctrlKey) { + // this does not prevent scrolling unfortunately + // event.preventDefault(); - let zoomFactor = this._webFrame.getZoomFactor(); - if (event.deltaY > 0) { - zoomFactor -= ZOOM_DELTA; - } else if (event.deltaY < 0) { - zoomFactor += ZOOM_DELTA; + let zoomFactor = this._webFrame.getZoomFactor(); + if (event.deltaY > 0) { + zoomFactor -= ZOOM_DELTA; + } else if (event.deltaY < 0) { + zoomFactor += ZOOM_DELTA; + } + zoomFactor = Math.min(Math.max(zoomFactor, 0.1), 4); + this.zoomTo(zoomFactor); } - zoomFactor = Math.min(Math.max(zoomFactor, 0.1), 4); - this.zoomTo(zoomFactor); - } - }); + }); } private _getLocalUiHelperSettings(): LocalUiHelperSettings { - return loadFromRealLs(LS_LOCAL_UI_HELPER) as LocalUiHelperSettings || UI_LOCAL_HELPER_DEFAULT; + return ( + (loadFromRealLs(LS_LOCAL_UI_HELPER) as LocalUiHelperSettings) || + UI_LOCAL_HELPER_DEFAULT + ); } private _updateLocalUiHelperSettings(newCfg: Partial) { diff --git a/src/app/features/work-context/store/work-context-meta.helper.ts b/src/app/features/work-context/store/work-context-meta.helper.ts index 30cb1b389..abaa63721 100644 --- a/src/app/features/work-context/store/work-context-meta.helper.ts +++ b/src/app/features/work-context/store/work-context-meta.helper.ts @@ -5,13 +5,11 @@ export const moveTaskForWorkContextLikeState = ( taskId: string, newOrderedIds: string[], target: DropListModelSource | null, - taskIdsBefore: string[] + taskIdsBefore: string[], ): string[] => { const idsFilteredMoving = taskIdsBefore.filter(filterOutId(taskId)); // NOTE: move to end of complete list for done tasks - const emptyListVal = (target === 'DONE') - ? idsFilteredMoving.length - : 0; + const emptyListVal = target === 'DONE' ? idsFilteredMoving.length : 0; return moveItemInList(taskId, idsFilteredMoving, newOrderedIds, emptyListVal); }; @@ -22,7 +20,12 @@ items might be hidden (done sub tasks). Please note that the completeList depending on the circumstances might or might not include the itemId, while the partialList always should. */ -export const moveItemInList = (itemId: string, completeList: string[], partialList: string[], emptyListVal = 0): string[] => { +export const moveItemInList = ( + itemId: string, + completeList: string[], + partialList: string[], + emptyListVal = 0, +): string[] => { // console.log(itemId, completeList, partialList); let newIndex; diff --git a/src/app/features/work-context/store/work-context.actions.ts b/src/app/features/work-context/store/work-context.actions.ts index 12dc06904..1d170d06a 100644 --- a/src/app/features/work-context/store/work-context.actions.ts +++ b/src/app/features/work-context/store/work-context.actions.ts @@ -10,4 +10,3 @@ export const setActiveWorkContext = createAction( '[WorkContext] Set Active Work Context', props<{ activeId: string; activeType: WorkContextType }>(), ); - diff --git a/src/app/features/work-context/store/work-context.effects.ts b/src/app/features/work-context/store/work-context.effects.ts index 2c36b8356..549d97b1c 100644 --- a/src/app/features/work-context/store/work-context.effects.ts +++ b/src/app/features/work-context/store/work-context.effects.ts @@ -21,15 +21,16 @@ export class WorkContextEffects { // tap(this._saveToLs.bind(this)), // ), {dispatch: false}); - dismissContextScopeBannersOnContextChange: Observable = createEffect(() => this._actions$ - .pipe( - ofType( - contextActions.setActiveWorkContext, + dismissContextScopeBannersOnContextChange: Observable = createEffect( + () => + this._actions$.pipe( + ofType(contextActions.setActiveWorkContext), + tap(() => { + this._bannerService.dismiss(BannerId.JiraUnblock); + }), ), - tap(() => { - this._bannerService.dismiss(BannerId.JiraUnblock); - }), - ), {dispatch: false}); + { dispatch: false }, + ); // EXTERNAL // -------- @@ -40,17 +41,18 @@ export class WorkContextEffects { // map(() => new UnsetCurrentTask()), // )); - unselectSelectedTask$: Observable = createEffect(() => this._actions$.pipe( - ofType(contextActions.setActiveWorkContext), - withLatestFrom(this._taskService.isTaskDataLoaded$), - filter(([, isDataLoaded]) => isDataLoaded), - map(() => new SetSelectedTask({id: null})), - )); + unselectSelectedTask$: Observable = createEffect(() => + this._actions$.pipe( + ofType(contextActions.setActiveWorkContext), + withLatestFrom(this._taskService.isTaskDataLoaded$), + filter(([, isDataLoaded]) => isDataLoaded), + map(() => new SetSelectedTask({ id: null })), + ), + ); constructor( private _actions$: Actions, private _taskService: TaskService, private _bannerService: BannerService, - ) { - } + ) {} } diff --git a/src/app/features/work-context/store/work-context.reducer.ts b/src/app/features/work-context/store/work-context.reducer.ts index 2f10a2b42..3c5b59bb2 100644 --- a/src/app/features/work-context/store/work-context.reducer.ts +++ b/src/app/features/work-context/store/work-context.reducer.ts @@ -1,40 +1,63 @@ import * as contextActions from './work-context.actions'; import { WorkContextState, WorkContextType } from '../work-context.model'; -import { Action, createFeatureSelector, createReducer, createSelector, on } from '@ngrx/store'; +import { + Action, + createFeatureSelector, + createReducer, + createSelector, + on, +} from '@ngrx/store'; export const WORK_CONTEXT_FEATURE_NAME = 'context'; -export const selectContextFeatureState = createFeatureSelector(WORK_CONTEXT_FEATURE_NAME); -export const selectActiveContextId = createSelector(selectContextFeatureState, (state) => state.activeId); -export const selectActiveContextType = createSelector(selectContextFeatureState, (state) => state.activeType); +export const selectContextFeatureState = createFeatureSelector( + WORK_CONTEXT_FEATURE_NAME, +); +export const selectActiveContextId = createSelector( + selectContextFeatureState, + (state) => state.activeId, +); +export const selectActiveContextType = createSelector( + selectContextFeatureState, + (state) => state.activeType, +); -export const selectActiveContextTypeAndId = createSelector(selectContextFeatureState, (state: WorkContextState): { - activeId: string; - activeType: WorkContextType; - // additional entities state properties -} => ({ - activeType: state.activeType as WorkContextType, - activeId: state.activeId as string, -})); +export const selectActiveContextTypeAndId = createSelector( + selectContextFeatureState, + ( + state: WorkContextState, + ): { + activeId: string; + activeType: WorkContextType; + // additional entities state properties + } => ({ + activeType: state.activeType as WorkContextType, + activeId: state.activeId as string, + }), +); export const initialContextState: WorkContextState = { activeId: null, - activeType: null + activeType: null, }; const _reducer = createReducer( initialContextState, - on(contextActions.setActiveWorkContext, (oldState, {activeId, activeType}) => ({...oldState, activeId, activeType})), - on(contextActions.loadWorkContextState, (oldState, {state}) => ({...oldState, ...state})), + on(contextActions.setActiveWorkContext, (oldState, { activeId, activeType }) => ({ + ...oldState, + activeId, + activeType, + })), + on(contextActions.loadWorkContextState, (oldState, { state }) => ({ + ...oldState, + ...state, + })), ); export function workContextReducer( state: WorkContextState = initialContextState, action: Action, ): WorkContextState { - return _reducer(state, action); } - - diff --git a/src/app/features/work-context/work-context.const.ts b/src/app/features/work-context/work-context.const.ts index 29fdb7d09..107abb1e0 100644 --- a/src/app/features/work-context/work-context.const.ts +++ b/src/app/features/work-context/work-context.const.ts @@ -9,7 +9,7 @@ export const WORKLOG_EXPORT_DEFAULTS: WorklogExportSettings = { roundStartTimeTo: null, roundEndTimeTo: null, separateTasksBy: ' | ', - groupBy: WorklogGrouping.DATE + groupBy: WorklogGrouping.DATE, }; export const DEFAULT_PROJECT_COLOR = '#29a1aa'; @@ -41,16 +41,16 @@ export const WORK_CONTEXT_DEFAULT_COMMON: WorkContextCommon = { }; export const HUES = [ - {value: '50', label: '50'}, - {value: '100', label: '100'}, - {value: '200', label: '200'}, - {value: '300', label: '300'}, - {value: '400', label: '400'}, - {value: '500', label: '500'}, - {value: '600', label: '600'}, - {value: '700', label: '700'}, - {value: '800', label: '800'}, - {value: '900', label: '900'}, + { value: '50', label: '50' }, + { value: '100', label: '100' }, + { value: '200', label: '200' }, + { value: '300', label: '300' }, + { value: '400', label: '400' }, + { value: '500', label: '500' }, + { value: '600', label: '600' }, + { value: '700', label: '700' }, + { value: '800', label: '800' }, + { value: '900', label: '900' }, ]; export const WORK_CONTEXT_THEME_CONFIG_FORM_CONFIG: ConfigFormSection = { @@ -99,7 +99,7 @@ export const WORK_CONTEXT_THEME_CONFIG_FORM_CONFIG: ConfigFormSection = this.activeWorkContextTypeAndId$.pipe( - map(({activeType}) => activeType === WorkContextType.PROJECT), + map(({ activeType }) => activeType === WorkContextType.PROJECT), shareReplay(1), ); @@ -94,12 +101,12 @@ export class WorkContextService { ); activeWorkContextIdIfProject$: Observable = this.activeWorkContextTypeAndId$.pipe( - map(({activeType, activeId}) => { + map(({ activeType, activeId }) => { if (activeType !== WorkContextType.PROJECT) { throw Error('Not in project context'); } return activeId; - }) + }), ); // for convenience... @@ -107,31 +114,31 @@ export class WorkContextService { activeWorkContextType?: WorkContextType; activeWorkContext$: Observable = this.activeWorkContextTypeAndId$.pipe( - switchMap(({activeId, activeType}) => { + switchMap(({ activeId, activeType }) => { if (activeType === WorkContextType.TAG) { return this._tagService.getTagById$(activeId).pipe( // TODO find out why this is sometimes undefined - filter(p => !!p), - map(tag => ({ + filter((p) => !!p), + map((tag) => ({ ...tag, type: WorkContextType.TAG, - routerLink: `tag/${tag.id}` - })) + routerLink: `tag/${tag.id}`, + })), ); } if (activeType === WorkContextType.PROJECT) { // return this._projectService.getByIdLive$(activeId).pipe( // NOTE: temporary work around to be able to sync current id - return this._store$.pipe(select(selectProjectById, {id: activeId})).pipe( + return this._store$.pipe(select(selectProjectById, { id: activeId })).pipe( // TODO find out why this is sometimes undefined - filter(p => !!p), - map(project => ({ + filter((p) => !!p), + map((project) => ({ ...project, icon: null, taskIds: project.taskIds || [], backlogTaskIds: project.backlogTaskIds || [], type: WorkContextType.PROJECT, - routerLink: `project/${project.id}` + routerLink: `project/${project.id}`, })), ); } @@ -139,46 +146,40 @@ export class WorkContextService { return EMPTY; }), // TODO find out why this is sometimes undefined - filter(ctx => !!ctx), + filter((ctx) => !!ctx), shareReplay(1), ); - mainWorkContexts$: Observable = - this._isAllDataLoaded$.pipe( - concatMap(() => this._tagService.getTagById$(TODAY_TAG.id)), - switchMap(myDayTag => of([ - ({ - ...myDayTag, - type: WorkContextType.TAG, - routerLink: `tag/${myDayTag.id}` - } as WorkContext) - ]) - ), - ); + mainWorkContexts$: Observable = this._isAllDataLoaded$.pipe( + concatMap(() => this._tagService.getTagById$(TODAY_TAG.id)), + switchMap((myDayTag) => + of([ + { + ...myDayTag, + type: WorkContextType.TAG, + routerLink: `tag/${myDayTag.id}`, + } as WorkContext, + ]), + ), + ); currentTheme$: Observable = this.activeWorkContext$.pipe( - map(awc => awc.theme) + map((awc) => awc.theme), ); advancedCfg$: Observable = this.activeWorkContext$.pipe( - map(awc => awc.advancedCfg) + map((awc) => awc.advancedCfg), ); - onWorkContextChange$: Observable = this._actions$.pipe(ofType(setActiveWorkContext)); + onWorkContextChange$: Observable = this._actions$.pipe( + ofType(setActiveWorkContext), + ); isContextChanging$: Observable = this.onWorkContextChange$.pipe( - switchMap(() => - timer(50).pipe( - mapTo(false), - startWith(true) - ) - ), + switchMap(() => timer(50).pipe(mapTo(false), startWith(true))), startWith(false), ); isContextChangingWithDelay$: Observable = this.isContextChanging$.pipe( - delayWhen(val => val - ? of(undefined) - : interval(60) - ) + delayWhen((val) => (val ? of(undefined) : interval(60))), ); // TASK LEVEL @@ -197,7 +198,7 @@ export class WorkContextService { todaysTasks$: Observable = this.todaysTaskIds$.pipe( // tap(() => console.log('TRIGGER TODAY TASKS')), - switchMap(taskIds => this._getTasksByIds$(taskIds)), + switchMap((taskIds) => this._getTasksByIds$(taskIds)), // TODO find out why this is triggered so often // tap(() => console.log('AFTER SWITCHMAP TODAYSTASKS')), // map(to => to.filter(t => !!t)), @@ -205,33 +206,29 @@ export class WorkContextService { ); undoneTasks$: Observable = this.todaysTasks$.pipe( - map(tasks => tasks.filter(task => task && !task.isDone)), + map((tasks) => tasks.filter((task) => task && !task.isDone)), ); doneTasks$: Observable = this.todaysTasks$.pipe( - map(tasks => tasks.filter(task => task && task.isDone)) + map((tasks) => tasks.filter((task) => task && task.isDone)), ); backlogTasks$: Observable = this.backlogTaskIds$.pipe( - switchMap(ids => this._getTasksByIds$(ids)), + switchMap((ids) => this._getTasksByIds$(ids)), ); allTasksForCurrentContext$: Observable = combineLatest([ this.todaysTasks$, this.backlogTasks$, - ]).pipe( - map(([today, backlog]) => [...today, ...backlog]) - ); + ]).pipe(map(([today, backlog]) => [...today, ...backlog])); startableTasksForActiveContext$: Observable = combineLatest([ this.activeWorkContext$, - this._store$.pipe( - select(selectTaskEntities), - ) + this._store$.pipe(select(selectTaskEntities)), ]).pipe( map(([activeContext, entities]) => { let startableTasks: Task[] = []; - activeContext.taskIds.forEach(id => { + activeContext.taskIds.forEach((id) => { const task: Task | undefined = entities[id]; if (!task) { // NOTE: there is the rare chance that activeWorkContext$ and selectTaskEntities @@ -239,20 +236,20 @@ export class WorkContextService { // only use devError devError('Task not found'); } else if (task.subTaskIds && task.subTaskIds.length) { - startableTasks = startableTasks.concat(task.subTaskIds.map(sid => entities[sid] as Task)); + startableTasks = startableTasks.concat( + task.subTaskIds.map((sid) => entities[sid] as Task), + ); } else { startableTasks.push(task); } }); - return startableTasks.filter(task => !task.isDone); + return startableTasks.filter((task) => !task.isDone); }), ); workingToday$: Observable = this.getTimeWorkedForDay$(getWorklogStr()); - onMoveToBacklog$: Observable = this._actions$.pipe(ofType( - moveTaskToBacklogList, - )); + onMoveToBacklog$: Observable = this._actions$.pipe(ofType(moveTaskToBacklogList)); isHasTasksToWorkOn$: Observable = this.todaysTasks$.pipe( map(hasTasksToWorkOn), @@ -275,16 +272,16 @@ export class WorkContextService { // ); flatDoneTodayNr$: Observable = this.todaysTasks$.pipe( - map(tasks => flattenTasks(tasks)), - map(tasks => { - const done = tasks.filter(task => task.isDone); + map((tasks) => flattenTasks(tasks)), + map((tasks) => { + const done = tasks.filter((task) => task.isDone); return done.length; - }) + }), ); isToday: boolean = false; isToday$: Observable = this.activeWorkContextId$.pipe( - map(id => id === TODAY_TAG.id), + map((id) => id === TODAY_TAG.id), shareReplay(1), ); @@ -294,28 +291,31 @@ export class WorkContextService { private _tagService: TagService, private _router: Router, ) { - this.isToday$.subscribe((v) => this.isToday = v); + this.isToday$.subscribe((v) => (this.isToday = v)); - this.activeWorkContextTypeAndId$.subscribe(v => { + this.activeWorkContextTypeAndId$.subscribe((v) => { this.activeWorkContextId = v.activeId; this.activeWorkContextType = v.activeType; }); // we need all data to be loaded before we dispatch a setActiveContext action - this._router.events.pipe( - // NOTE: when we use any other router event than NavigationEnd, the changes triggered - // by the active context may occur before the current page component is unloaded - filter(event => event instanceof NavigationEnd), - withLatestFrom(this._isAllDataLoaded$), - concatMap(([next, isAllDataLoaded]) => isAllDataLoaded - ? of(next as NavigationEnd) - : this._isAllDataLoaded$.pipe( - filter(isLoaded => isLoaded), - take(1), - mapTo(next as NavigationEnd), - ) - ), - ).subscribe(({urlAfterRedirects}: NavigationEnd) => { + this._router.events + .pipe( + // NOTE: when we use any other router event than NavigationEnd, the changes triggered + // by the active context may occur before the current page component is unloaded + filter((event) => event instanceof NavigationEnd), + withLatestFrom(this._isAllDataLoaded$), + concatMap(([next, isAllDataLoaded]) => + isAllDataLoaded + ? of(next as NavigationEnd) + : this._isAllDataLoaded$.pipe( + filter((isLoaded) => isLoaded), + take(1), + mapTo(next as NavigationEnd), + ), + ), + ) + .subscribe(({ urlAfterRedirects }: NavigationEnd) => { const split = urlAfterRedirects.split('/'); const id = split[2]; @@ -329,21 +329,24 @@ export class WorkContextService { } else if (urlAfterRedirects.match(/project\/.+/)) { this._setActiveContext(id, WorkContextType.PROJECT); } - } - ); + }); } // TODO could be done better getTimeWorkedForDay$(day: string = getWorklogStr()): Observable { return this.todaysTasks$.pipe( map((tasks) => { - return tasks && tasks.length && tasks.reduce((acc, task) => { - return acc + ( - (task.timeSpentOnDay && +task.timeSpentOnDay[day]) + return ( + tasks && + tasks.length && + tasks.reduce((acc, task) => { + return ( + acc + + (task.timeSpentOnDay && +task.timeSpentOnDay[day] ? +task.timeSpentOnDay[day] - : 0 + : 0) ); - }, 0 + }, 0) ); }), distinctUntilChanged(), @@ -351,27 +354,19 @@ export class WorkContextService { } getWorkStart$(day: string = getWorklogStr()): Observable { - return this.activeWorkContext$.pipe( - map(ctx => ctx.workStart[day]), - ); + return this.activeWorkContext$.pipe(map((ctx) => ctx.workStart[day])); } getWorkEnd$(day: string = getWorklogStr()): Observable { - return this.activeWorkContext$.pipe( - map(ctx => ctx.workEnd[day]), - ); + return this.activeWorkContext$.pipe(map((ctx) => ctx.workEnd[day])); } getBreakTime$(day: string = getWorklogStr()): Observable { - return this.activeWorkContext$.pipe( - map(ctx => ctx.breakTime[day]), - ); + return this.activeWorkContext$.pipe(map((ctx) => ctx.breakTime[day])); } getBreakNr$(day: string = getWorklogStr()): Observable { - return this.activeWorkContext$.pipe( - map(ctx => ctx.breakNr[day]), - ); + return this.activeWorkContext$.pipe(map((ctx) => ctx.breakNr[day])); } async load() { @@ -393,9 +388,10 @@ export class WorkContextService { date, newVal, }; - const action = (this.activeWorkContextType === WorkContextType.PROJECT) - ? new UpdateProjectWorkStart(payload) - : updateWorkStartForTag(payload); + const action = + this.activeWorkContextType === WorkContextType.PROJECT + ? new UpdateProjectWorkStart(payload) + : updateWorkStartForTag(payload); this._store$.dispatch(action); } @@ -405,9 +401,10 @@ export class WorkContextService { date, newVal, }; - const action = (this.activeWorkContextType === WorkContextType.PROJECT) - ? new UpdateProjectWorkEnd(payload) - : updateWorkEndForTag(payload); + const action = + this.activeWorkContextType === WorkContextType.PROJECT + ? new UpdateProjectWorkEnd(payload) + : updateWorkEndForTag(payload); this._store$.dispatch(action); } @@ -417,25 +414,33 @@ export class WorkContextService { date, valToAdd, }; - const action = (this.activeWorkContextType === WorkContextType.PROJECT) - ? new AddToProjectBreakTime(payload) - : addToBreakTimeForTag(payload); + const action = + this.activeWorkContextType === WorkContextType.PROJECT + ? new AddToProjectBreakTime(payload) + : addToBreakTimeForTag(payload); this._store$.dispatch(action); } - private _updateAdvancedCfgForCurrentContext(sectionKey: WorkContextAdvancedCfgKey, data: any) { + private _updateAdvancedCfgForCurrentContext( + sectionKey: WorkContextAdvancedCfgKey, + data: any, + ) { if (this.activeWorkContextType === WorkContextType.PROJECT) { - this._store$.dispatch(new UpdateProjectAdvancedCfg({ - projectId: this.activeWorkContextId as string, - sectionKey, - data, - })); + this._store$.dispatch( + new UpdateProjectAdvancedCfg({ + projectId: this.activeWorkContextId as string, + sectionKey, + data, + }), + ); } else if (this.activeWorkContextType === WorkContextType.TAG) { - this._store$.dispatch(updateAdvancedConfigForTag({ - tagId: this.activeWorkContextId as string, - sectionKey, - data - })); + this._store$.dispatch( + updateAdvancedConfigForTag({ + tagId: this.activeWorkContextId as string, + sectionKey, + data, + }), + ); } } @@ -444,12 +449,11 @@ export class WorkContextService { if (!Array.isArray(ids)) { throw new Error('Invalid param provided for getByIds$ :('); } - return this._store$.pipe(select(selectTasksWithSubTasksByIds, {ids})); + return this._store$.pipe(select(selectTasksWithSubTasksByIds, { ids })); } // NOTE: NEVER call this from some place other than the route change stuff private async _setActiveContext(activeId: string, activeType: WorkContextType) { - this._store$.dispatch(setActiveWorkContext({activeId, activeType})); + this._store$.dispatch(setActiveWorkContext({ activeId, activeType })); } - } diff --git a/src/app/features/work-context/work-context.util.ts b/src/app/features/work-context/work-context.util.ts index 17715abfc..2c48f760e 100644 --- a/src/app/features/work-context/work-context.util.ts +++ b/src/app/features/work-context/work-context.util.ts @@ -1,25 +1,32 @@ import { TaskWithSubTasks } from '../tasks/task.model'; -export const mapEstimateRemainingFromTasks = - (tasks: TaskWithSubTasks[]): number => tasks && tasks.length && tasks.reduce((acc: number, task: TaskWithSubTasks): number => { +export const mapEstimateRemainingFromTasks = (tasks: TaskWithSubTasks[]): number => + tasks && + tasks.length && + tasks.reduce((acc: number, task: TaskWithSubTasks): number => { let estimateRemaining; if (task.subTasks && task.subTasks.length > 0) { estimateRemaining = task.subTasks.reduce((subAcc, subTask) => { - const estimateRemainingSub = (+subTask.timeEstimate) - (+subTask.timeSpent); - const isTrackValSub = ((estimateRemainingSub > 0) && !subTask.isDone); - return subAcc + ((isTrackValSub) ? estimateRemainingSub : 0); + const estimateRemainingSub = +subTask.timeEstimate - +subTask.timeSpent; + const isTrackValSub = estimateRemainingSub > 0 && !subTask.isDone; + return subAcc + (isTrackValSub ? estimateRemainingSub : 0); }, 0); } else { - estimateRemaining = (+task.timeEstimate) - (+task.timeSpent); + estimateRemaining = +task.timeEstimate - +task.timeSpent; } - const isTrackVal = ((estimateRemaining > 0) && !task.isDone); - return acc + ((isTrackVal) ? estimateRemaining : 0); + const isTrackVal = estimateRemaining > 0 && !task.isDone; + return acc + (isTrackVal ? estimateRemaining : 0); }, 0); export const hasTasksToWorkOn = (tasks: TaskWithSubTasks[]): boolean => { const _tasksToWorkOn = tasks.filter((t) => { - return !t.isDone && !t.repeatCfgId && - ((!t.subTasks || t.subTasks.length === 0) || t.subTasks.filter((st) => !st.isDone).length > 0); + return ( + !t.isDone && + !t.repeatCfgId && + (!t.subTasks || + t.subTasks.length === 0 || + t.subTasks.filter((st) => !st.isDone).length > 0) + ); }); - return (_tasksToWorkOn && _tasksToWorkOn.length > 0); + return _tasksToWorkOn && _tasksToWorkOn.length > 0; }; diff --git a/src/app/features/work-view/backlog/backlog.component.ts b/src/app/features/work-view/backlog/backlog.component.ts index 5bb48889c..199020a05 100644 --- a/src/app/features/work-view/backlog/backlog.component.ts +++ b/src/app/features/work-view/backlog/backlog.component.ts @@ -1,6 +1,15 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; import { TaskService } from '../../../features/tasks/task.service'; -import { TaskWithReminderData, TaskWithSubTasks } from '../../../features/tasks/task.model'; +import { + TaskWithReminderData, + TaskWithSubTasks, +} from '../../../features/tasks/task.model'; import { standardListAnimation } from '../../../ui/animations/standard-list.ani'; import { T } from '../../../t.const'; @@ -9,7 +18,7 @@ import { T } from '../../../t.const'; templateUrl: './backlog.component.html', styleUrls: ['./backlog.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [standardListAnimation] + animations: [standardListAnimation], }) export class BacklogComponent { @Input() backlogTasks: TaskWithSubTasks[] = []; @@ -26,10 +35,7 @@ export class BacklogComponent { // ); // backlogTasks$: Observable = this.taskService.backlogTasks$; - constructor( - public taskService: TaskService, - ) { - } + constructor(public taskService: TaskService) {} trackByFn(i: number, task: TaskWithReminderData) { return task.id; @@ -41,5 +47,4 @@ export class BacklogComponent { } this.taskService.unScheduleTask(task.id, task.reminderId); } - } diff --git a/src/app/features/work-view/split/split.component.ts b/src/app/features/work-view/split/split.component.ts index 06f923003..0d3630c28 100644 --- a/src/app/features/work-view/split/split.component.ts +++ b/src/app/features/work-view/split/split.component.ts @@ -7,7 +7,7 @@ import { Input, Output, Renderer2, - ViewChild + ViewChild, } from '@angular/core'; import { fromEvent, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @@ -19,7 +19,7 @@ const ANIMATABLE_CLASS = 'isAnimatable'; selector: 'split', templateUrl: './split.component.html', styleUrls: ['./split.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SplitComponent implements AfterViewInit { @Input() splitTopEl?: ElementRef; @@ -31,12 +31,11 @@ export class SplitComponent implements AfterViewInit { pos: number = 100; eventSubs?: Subscription; - @ViewChild('buttonEl', {static: true}) buttonEl?: ElementRef; + @ViewChild('buttonEl', { static: true }) buttonEl?: ElementRef; private _isDrag: boolean = false; private _isViewInitialized: boolean = false; - constructor(private _renderer: Renderer2) { - } + constructor(private _renderer: Renderer2) {} @Input() set splitPos(pos: number) { if (pos !== this.pos) { @@ -117,7 +116,7 @@ export class SplitComponent implements AfterViewInit { const h = this.containerEl.offsetHeight; const headerHeight = bounds.top; - let percentage = (clientY - headerHeight) / h * 100; + let percentage = ((clientY - headerHeight) / h) * 100; if (percentage > 100) { percentage = 100; } @@ -134,16 +133,8 @@ export class SplitComponent implements AfterViewInit { this.pos = pos; if (this.splitTopEl && this.splitBottomEl) { - this._renderer.setStyle( - this.splitTopEl, - 'height', - `${pos}%`, - ); - this._renderer.setStyle( - this.splitBottomEl, - 'height', - `${100 - pos}%`, - ); + this._renderer.setStyle(this.splitTopEl, 'height', `${pos}%`); + this._renderer.setStyle(this.splitBottomEl, 'height', `${100 - pos}%`); // this._renderer.setStyle( // this._el.nativeElement, // 'top', diff --git a/src/app/features/work-view/split/split.module.ts b/src/app/features/work-view/split/split.module.ts index 06d3bd791..e38b57b0b 100644 --- a/src/app/features/work-view/split/split.module.ts +++ b/src/app/features/work-view/split/split.module.ts @@ -5,13 +5,8 @@ import { MatButtonModule } from '@angular/material/button'; import { MatIconModule } from '@angular/material/icon'; @NgModule({ - imports: [ - CommonModule, - MatIconModule, - MatButtonModule, - ], + imports: [CommonModule, MatIconModule, MatButtonModule], exports: [SplitComponent], - declarations: [SplitComponent] + declarations: [SplitComponent], }) -export class SplitModule { -} +export class SplitModule {} diff --git a/src/app/features/work-view/work-view.component.ts b/src/app/features/work-view/work-view.component.ts index 7bb9f1aa0..4ea195057 100644 --- a/src/app/features/work-view/work-view.component.ts +++ b/src/app/features/work-view/work-view.component.ts @@ -6,7 +6,7 @@ import { Input, OnDestroy, OnInit, - ViewChild + ViewChild, } from '@angular/core'; import { TaskService } from '../tasks/task.service'; import { expandAnimation, expandFadeAnimation } from '../../ui/animations/expand.ani'; @@ -14,7 +14,15 @@ import { LayoutService } from '../../core-ui/layout/layout.service'; import { DragulaService } from 'ng2-dragula'; import { TakeABreakService } from '../time-tracking/take-a-break/take-a-break.service'; import { ActivatedRoute } from '@angular/router'; -import { from, fromEvent, Observable, ReplaySubject, Subscription, timer, zip } from 'rxjs'; +import { + from, + fromEvent, + Observable, + ReplaySubject, + Subscription, + timer, + zip, +} from 'rxjs'; import { TaskWithSubTasks } from '../tasks/task.model'; import { delay, filter, map, switchMap } from 'rxjs/operators'; import { fadeAnimation } from '../../ui/animations/fade.ani'; @@ -31,7 +39,12 @@ const PARENT = 'PARENT'; selector: 'work-view', templateUrl: './work-view.component.html', styleUrls: ['./work-view.component.scss'], - animations: [expandFadeAnimation, expandAnimation, fadeAnimation, workViewProjectChangeAnimation], + animations: [ + expandFadeAnimation, + expandAnimation, + fadeAnimation, + workViewProjectChangeAnimation, + ], changeDetection: ChangeDetectionStrategy.OnPush, }) export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit { @@ -47,19 +60,14 @@ export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit { // NOTE: not perfect but good enough for now isTriggerBacklogIconAni$: Observable = this.workContextService.onMoveToBacklog$.pipe( - switchMap(() => - zip( - from([true, false]), - timer(1, 200), - ), - ), - map(v => v[0]), + switchMap(() => zip(from([true, false]), timer(1, 200))), + map((v) => v[0]), ); splitTopEl$: ReplaySubject = new ReplaySubject(1); // TODO make this work for tag page without backlog upperContainerScroll$: Observable = this.workContextService.isContextChanging$.pipe( - filter(isChanging => !isChanging), + filter((isChanging) => !isChanging), delay(50), switchMap(() => this.splitTopEl$), switchMap((el) => fromEvent(el, 'scroll')), @@ -76,10 +84,9 @@ export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit { public workContextService: WorkContextService, private _dragulaService: DragulaService, private _activatedRoute: ActivatedRoute, - ) { - } + ) {} - @ViewChild('splitTopEl', {read: ElementRef}) set splitTopElRef(ref: ElementRef) { + @ViewChild('splitTopEl', { read: ElementRef }) set splitTopElRef(ref: ElementRef) { if (ref) { this.splitTopEl$.next(ref.nativeElement); } @@ -93,39 +100,48 @@ export class WorkViewComponent implements OnInit, OnDestroy, AfterContentInit { this._dragulaService.createGroup(SUB, { direction: 'vertical', moves: (el, container, handle) => { - return !!handle && handle.className.indexOf && handle.className.indexOf('handle-sub') > -1; - } + return ( + !!handle && + handle.className.indexOf && + handle.className.indexOf('handle-sub') > -1 + ); + }, }); } if (!par) { this._dragulaService.createGroup(PARENT, { direction: 'vertical', moves: (el, container, handle) => { - return !!handle && handle.className.indexOf && handle.className.indexOf('handle-par') > -1; - } + return ( + !!handle && + handle.className.indexOf && + handle.className.indexOf('handle-par') > -1 + ); + }, }); } // preload // TODO check // this._subs.add(this.workContextService.backlogTasks$.subscribe()); - this._subs.add(this._activatedRoute.queryParams - .subscribe((params) => { + this._subs.add( + this._activatedRoute.queryParams.subscribe((params) => { if (params && params.backlogPos) { this.splitInputPos = params.backlogPos; } - })); + }), + ); } ngAfterContentInit(): void { this._subs.add( - this.upperContainerScroll$.subscribe(({target}) => { + this.upperContainerScroll$.subscribe(({ target }) => { if ((target as HTMLElement).scrollTop !== 0) { this.layoutService.isScrolled$.next(true); } else { this.layoutService.isScrolled$.next(false); } - }) + }), ); } diff --git a/src/app/features/work-view/work-view.module.ts b/src/app/features/work-view/work-view.module.ts index cad0fcfad..d3a641434 100644 --- a/src/app/features/work-view/work-view.module.ts +++ b/src/app/features/work-view/work-view.module.ts @@ -26,13 +26,7 @@ import { WorkViewComponent } from './work-view.component'; MatSidenavModule, BetterDrawerModule, ], - declarations: [ - WorkViewComponent, - BacklogComponent, - ], - exports: [ - WorkViewComponent, - ] + declarations: [WorkViewComponent, BacklogComponent], + exports: [WorkViewComponent], }) -export class WorkViewModule { -} +export class WorkViewModule {} diff --git a/src/app/features/worklog/dialog-worklog-export/dialog-worklog-export.component.ts b/src/app/features/worklog/dialog-worklog-export/dialog-worklog-export.component.ts index aadc58592..554d74558 100644 --- a/src/app/features/worklog/dialog-worklog-export/dialog-worklog-export.component.ts +++ b/src/app/features/worklog/dialog-worklog-export/dialog-worklog-export.component.ts @@ -9,7 +9,7 @@ import { WORKLOG_EXPORT_DEFAULTS } from '../../work-context/work-context.const'; selector: 'dialog-worklog-export', templateUrl: './dialog-worklog-export.component.html', styleUrls: ['./dialog-worklog-export.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogWorklogExportComponent { T: typeof T = T; @@ -27,7 +27,7 @@ export class DialogWorklogExportComponent { this.strStart = moment(data.rangeStart).format('l'); this.strEnd = moment(data.rangeEnd).format('l'); - this.isSingleDay = (this.strStart === this.strEnd); + this.isSingleDay = this.strStart === this.strEnd; } close() { diff --git a/src/app/features/worklog/util/get-complete-state-for-work-context.util.spec.ts b/src/app/features/worklog/util/get-complete-state-for-work-context.util.spec.ts index 75bbf592b..9a49fd07e 100644 --- a/src/app/features/worklog/util/get-complete-state-for-work-context.util.spec.ts +++ b/src/app/features/worklog/util/get-complete-state-for-work-context.util.spec.ts @@ -83,7 +83,11 @@ describe('getCompleteStateForWorkContext', () => { }, ]); const r = getCompleteStateForWorkContext(TAG_CTX, ts, archiveS); - expect(r.completeStateForWorkContext.ids).toEqual(['PT_TODAY', 'PT', 'SUB_B', 'SUB_C']); + expect(r.completeStateForWorkContext.ids).toEqual([ + 'PT_TODAY', + 'PT', + 'SUB_B', + 'SUB_C', + ]); }); }); - diff --git a/src/app/features/worklog/util/get-complete-state-for-work-context.util.ts b/src/app/features/worklog/util/get-complete-state-for-work-context.util.ts index f15d0243f..b2b3c6599 100644 --- a/src/app/features/worklog/util/get-complete-state-for-work-context.util.ts +++ b/src/app/features/worklog/util/get-complete-state-for-work-context.util.ts @@ -3,7 +3,11 @@ import { Dictionary, EntityState } from '@ngrx/entity'; import { Task } from '../../tasks/task.model'; import { TODAY_TAG } from '../../tag/tag.const'; -export const getCompleteStateForWorkContext = (workContext: WorkContext, taskState: EntityState, archive: EntityState): { +export const getCompleteStateForWorkContext = ( + workContext: WorkContext, + taskState: EntityState, + archive: EntityState, +): { completeStateForWorkContext: EntityState; unarchivedIds: string[]; } => { @@ -12,7 +16,7 @@ export const getCompleteStateForWorkContext = (workContext: WorkContext, taskSta if (wid === TODAY_TAG.id) { return { completeStateForWorkContext: { - ids: [...taskState.ids as string[], ...archive.ids as string[]], + ids: [...(taskState.ids as string[]), ...(archive.ids as string[])], entities: { ...archive.entities, ...taskState.entities, @@ -22,13 +26,15 @@ export const getCompleteStateForWorkContext = (workContext: WorkContext, taskSta }; } - const unarchivedIds: string[] = (workContext.type === WorkContextType.TAG) - ? _filterIdsForTag(taskState, wid) - : _filterIdsForProject(taskState, wid); + const unarchivedIds: string[] = + workContext.type === WorkContextType.TAG + ? _filterIdsForTag(taskState, wid) + : _filterIdsForProject(taskState, wid); - const archivedIdsForTag: string[] = (workContext.type === WorkContextType.TAG) - ? _filterIdsForTag(archive, wid) - : _filterIdsForProject(archive, wid); + const archivedIdsForTag: string[] = + workContext.type === WorkContextType.TAG + ? _filterIdsForTag(archive, wid) + : _filterIdsForProject(archive, wid); const unarchivedEntities = _limitStateToIds(taskState, unarchivedIds); const archivedEntities = _limitStateToIds(archive, archivedIdsForTag); @@ -45,26 +51,31 @@ export const getCompleteStateForWorkContext = (workContext: WorkContext, taskSta }; }; -const _filterIdsForProject = (state: EntityState, workContextId: string): string[] => (state.ids as string[]).filter( - id => { +const _filterIdsForProject = ( + state: EntityState, + workContextId: string, +): string[] => + (state.ids as string[]).filter((id) => { const t = state.entities[id] as Task; - return !!(t.parentId) + return !!t.parentId ? (state.entities[t.parentId] as Task).projectId === workContextId : t.projectId === workContextId; - } -); + }); -const _filterIdsForTag = (state: EntityState, workContextId: string): string[] => (state.ids as string[]).filter( - id => { +const _filterIdsForTag = (state: EntityState, workContextId: string): string[] => + (state.ids as string[]).filter((id) => { const t = state.entities[id] as Task; - return !!(t.parentId) + return !!t.parentId ? (state.entities[t.parentId] as Task).tagIds.includes(workContextId) : t.tagIds.includes(workContextId); }); -const _limitStateToIds = (stateIn: EntityState, ids: string[]): Dictionary => { +const _limitStateToIds = ( + stateIn: EntityState, + ids: string[], +): Dictionary => { const newState: any = {}; - ids.forEach(id => { + ids.forEach((id) => { newState[id] = stateIn.entities[id]; }); return newState; diff --git a/src/app/features/worklog/util/map-archive-to-worklog.spec.ts b/src/app/features/worklog/util/map-archive-to-worklog.spec.ts index a9901ef60..8da85435b 100644 --- a/src/app/features/worklog/util/map-archive-to-worklog.spec.ts +++ b/src/app/features/worklog/util/map-archive-to-worklog.spec.ts @@ -9,8 +9,8 @@ const START_END_ALL = { '1200-05-05': 10713600000, }, workEnd: { - '2022-05-08': 1651968000000 - } + '2022-05-08': 1651968000000, + }, }; const fakeTaskStateFromArray = (tasks: TaskCopy[]): EntityState => { @@ -32,8 +32,8 @@ describe('mapArchiveToWorklog', () => { timeSpentOnDay: { '2015-01-15': 9999, '2018-02-16': 3333, - } - } + }, + }, ]); const r = mapArchiveToWorklog(ts, [], START_END_ALL); @@ -69,7 +69,7 @@ describe('mapArchiveToWorklog', () => { timeSpentOnDay: { '2015-01-15': 9999, '2018-02-16': 3333, - } + }, }, { ...DEFAULT_TASK, @@ -78,7 +78,7 @@ describe('mapArchiveToWorklog', () => { parentId: 'A', timeSpentOnDay: { '2015-01-15': 9999, - } + }, }, { ...DEFAULT_TASK, @@ -87,7 +87,7 @@ describe('mapArchiveToWorklog', () => { parentId: 'A', timeSpentOnDay: { '2018-02-16': 3333, - } + }, }, ]); @@ -124,8 +124,8 @@ describe('mapArchiveToWorklog', () => { subTaskIds: ['SUB_B', 'SUB_C'], timeSpent: 10000, timeSpentOnDay: { - '2015-01-15': 10000 - } + '2015-01-15': 10000, + }, }, { ...DEFAULT_TASK, @@ -135,7 +135,7 @@ describe('mapArchiveToWorklog', () => { timeSpent: 3333, timeSpentOnDay: { '2015-01-15': 3333, - } + }, }, { ...DEFAULT_TASK, @@ -145,7 +145,7 @@ describe('mapArchiveToWorklog', () => { timeSpent: 4000, timeSpentOnDay: { '2015-01-15': 4000, - } + }, }, { ...DEFAULT_TASK, @@ -155,7 +155,7 @@ describe('mapArchiveToWorklog', () => { timeSpent: 6000, timeSpentOnDay: { '2015-01-15': 6000, - } + }, }, ]); diff --git a/src/app/features/worklog/util/map-archive-to-worklog.ts b/src/app/features/worklog/util/map-archive-to-worklog.ts index 87f56b523..551eff368 100644 --- a/src/app/features/worklog/util/map-archive-to-worklog.ts +++ b/src/app/features/worklog/util/map-archive-to-worklog.ts @@ -3,38 +3,41 @@ import { Task } from '../../tasks/task.model'; import { getWeeksInMonth } from '../../../util/get-weeks-in-month'; import { getWeekNumber } from '../../../util/get-week-number'; import * as moment from 'moment'; -import { Worklog, WorklogDay, WorklogMonth, WorklogWeek, WorklogYear } from '../worklog.model'; +import { Worklog, WorklogDay, WorklogMonth, WorklogWeek, WorklogYear, } from '../worklog.model'; import { getWorklogStr } from '../../../util/get-work-log-str'; import { WorkStartEnd } from '../../work-context/work-context.model'; // Provides defaults to display tasks without time spent on them const _getTimeSpentOnDay = (entities: any, task: Task): { [key: string]: number } => { - const isTimeSpentTracked = (task.timeSpentOnDay && !!Object.keys(task.timeSpentOnDay).length); + const isTimeSpentTracked = + task.timeSpentOnDay && !!Object.keys(task.timeSpentOnDay).length; if (isTimeSpentTracked) { return task.timeSpentOnDay; } else if (task.parentId) { const parentSpentOnDay = task.parentId && entities[task.parentId].timeSpentOnDay; - const parentLogEntryDate = parentSpentOnDay && ( - Object.keys(parentSpentOnDay)[0] - || getWorklogStr(entities[task.parentId].created)); - return {[parentLogEntryDate]: 1}; + const parentLogEntryDate = + parentSpentOnDay && + (Object.keys(parentSpentOnDay)[0] || + getWorklogStr(entities[task.parentId].created)); + return { [parentLogEntryDate]: 1 }; } else { - return {[getWorklogStr(task.created)]: 1}; + return { [getWorklogStr(task.created)]: 1 }; } }; export const mapArchiveToWorklog = ( taskState: EntityState, noRestoreIds: string[] = [], - startEnd: { workStart: WorkStartEnd; workEnd: WorkStartEnd }): { worklog: Worklog; totalTimeSpent: number } => { + startEnd: { workStart: WorkStartEnd; workEnd: WorkStartEnd }, +): { worklog: Worklog; totalTimeSpent: number } => { const entities = taskState.entities; const worklog: Worklog = {}; let totalTimeSpent = 0; - Object.keys(entities).forEach(id => { + Object.keys(entities).forEach((id) => { const task = entities[id] as Task; const timeSpentOnDay = _getTimeSpentOnDay(entities, task); - Object.keys(timeSpentOnDay).forEach(dateStr => { + Object.keys(timeSpentOnDay).forEach((dateStr) => { const split = dateStr.split('-'); const year = parseInt(split[0], 10); const month = parseInt(split[1], 10); @@ -44,7 +47,7 @@ export const mapArchiveToWorklog = ( timeSpent: 0, daysWorked: 0, monthWorked: 0, - ent: {} + ent: {}, }; } if (!worklog[year].ent[month]) { @@ -64,19 +67,14 @@ export const mapArchiveToWorklog = ( workStart: startEnd.workStart && startEnd.workStart[dateStr], workEnd: startEnd.workEnd && startEnd.workEnd[dateStr], }; - } if (task.subTaskIds.length === 0) { const timeSpentForTask = +timeSpentOnDay[dateStr]; - worklog[year].ent[month].ent[day].timeSpent - = worklog[year].ent[month].ent[day].timeSpent - + timeSpentForTask; - worklog[year].ent[month].timeSpent - = worklog[year].ent[month].timeSpent - + timeSpentForTask; - worklog[year].timeSpent - = worklog[year].timeSpent - + timeSpentForTask; + worklog[year].ent[month].ent[day].timeSpent = + worklog[year].ent[month].ent[day].timeSpent + timeSpentForTask; + worklog[year].ent[month].timeSpent = + worklog[year].ent[month].timeSpent + timeSpentForTask; + worklog[year].timeSpent = worklog[year].timeSpent + timeSpentForTask; totalTimeSpent += timeSpentForTask; } @@ -85,18 +83,18 @@ export const mapArchiveToWorklog = ( task, parentId: task.parentId, isNoRestore: noRestoreIds.includes(task.id), - timeSpent: timeSpentOnDay[dateStr] + timeSpent: timeSpentOnDay[dateStr], }; if (task.parentId) { let insertIndex; insertIndex = worklog[year].ent[month].ent[day].logEntries.findIndex( // sibling - t => t.task.parentId === task.parentId + (t) => t.task.parentId === task.parentId, ); if (insertIndex === -1) { insertIndex = worklog[year].ent[month].ent[day].logEntries.findIndex( // parent - t => t.task.id === task.parentId + (t) => t.task.id === task.parentId, ); } @@ -118,30 +116,32 @@ export const mapArchiveToWorklog = ( month.daysWorked = days.length; year.daysWorked += days.length; - const weeks = getWeeksInMonth((+monthIN - 1), +yearIN); + const weeks = getWeeksInMonth(+monthIN - 1, +yearIN); - month.weeks = weeks.map((week) => { - const weekForMonth: WorklogWeek = { - ...week, - timeSpent: 0, - daysWorked: 0, - ent: {}, - weekNr: getWeekNumber(new Date(+yearIN, +monthIN - 1, week.start)), - }; + month.weeks = weeks + .map((week) => { + const weekForMonth: WorklogWeek = { + ...week, + timeSpent: 0, + daysWorked: 0, + ent: {}, + weekNr: getWeekNumber(new Date(+yearIN, +monthIN - 1, week.start)), + }; - days.forEach((dayIN: string) => { - const day: WorklogDay = month.ent[dayIN as any]; - if (+dayIN >= week.start && +dayIN <= week.end) { - weekForMonth.timeSpent += month.ent[dayIN as any].timeSpent; - weekForMonth.daysWorked += 1; - weekForMonth.ent[dayIN as any] = day; - } - }); + days.forEach((dayIN: string) => { + const day: WorklogDay = month.ent[dayIN as any]; + if (+dayIN >= week.start && +dayIN <= week.end) { + weekForMonth.timeSpent += month.ent[dayIN as any].timeSpent; + weekForMonth.daysWorked += 1; + weekForMonth.ent[dayIN as any] = day; + } + }); - return weekForMonth; - }).filter(week => week.daysWorked > 0); + return weekForMonth; + }) + .filter((week) => week.daysWorked > 0); }); }); - return {worklog, totalTimeSpent}; + return { worklog, totalTimeSpent }; }; 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 aa83aa943..b12d90265 100644 --- a/src/app/features/worklog/worklog-export/worklog-export.component.ts +++ b/src/app/features/worklog/worklog-export/worklog-export.component.ts @@ -6,7 +6,7 @@ import { Input, OnDestroy, OnInit, - Output + Output, } from '@angular/core'; import { combineLatest, Subscription } from 'rxjs'; import { getWorklogStr } from '../../../util/get-work-log-str'; @@ -15,7 +15,11 @@ import 'moment-duration-format'; import Clipboard from 'clipboard'; import { SnackService } from '../../../core/snack/snack.service'; import { WorklogService } from '../worklog.service'; -import { WorklogColTypes, WorklogExportSettingsCopy, WorklogGrouping } from '../worklog.model'; +import { + WorklogColTypes, + WorklogExportSettingsCopy, + WorklogGrouping, +} from '../worklog.model'; import { T } from '../../../t.const'; import { distinctUntilChanged } from 'rxjs/operators'; import { distinctUntilChangedObject } from '../../../util/distinct-until-changed-object'; @@ -30,7 +34,7 @@ import { createRows, formatRows, formatText } from './worklog-export.util'; selector: 'worklog-export', templateUrl: './worklog-export.component.html', styleUrls: ['./worklog-export.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class WorklogExportComponent implements OnInit, OnDestroy { @Input() rangeStart?: Date; @@ -52,33 +56,36 @@ export class WorklogExportComponent implements OnInit, OnDestroy { txt: string = ''; fileName: string = 'tasks.csv'; roundTimeOptions: { id: string; title: string }[] = [ - {id: 'QUARTER', title: T.F.WORKLOG.EXPORT.O.FULL_QUARTERS}, - {id: 'HALF', title: T.F.WORKLOG.EXPORT.O.FULL_HALF_HOURS}, - {id: 'HOUR', title: T.F.WORKLOG.EXPORT.O.FULL_HOURS}, + { id: 'QUARTER', title: T.F.WORKLOG.EXPORT.O.FULL_QUARTERS }, + { id: 'HALF', title: T.F.WORKLOG.EXPORT.O.FULL_HALF_HOURS }, + { id: 'HOUR', title: T.F.WORKLOG.EXPORT.O.FULL_HOURS }, ]; colOpts: { id: string; title: string }[] = [ - {id: 'DATE', title: T.F.WORKLOG.EXPORT.O.DATE}, - {id: 'START', title: T.F.WORKLOG.EXPORT.O.STARTED_WORKING}, - {id: 'END', title: T.F.WORKLOG.EXPORT.O.ENDED_WORKING}, - {id: 'TITLES', title: T.F.WORKLOG.EXPORT.O.PARENT_TASK_TITLES_ONLY}, - {id: 'TITLES_INCLUDING_SUB', title: T.F.WORKLOG.EXPORT.O.TITLES_AND_SUB_TASK_TITLES}, - {id: 'NOTES', title: T.F.WORKLOG.EXPORT.O.NOTES}, - {id: 'PROJECTS', title: T.F.WORKLOG.EXPORT.O.PROJECTS}, - {id: 'TAGS', title: T.F.WORKLOG.EXPORT.O.TAGS}, - {id: 'TIME_MS', title: T.F.WORKLOG.EXPORT.O.TIME_AS_MILLISECONDS}, - {id: 'TIME_STR', title: T.F.WORKLOG.EXPORT.O.TIME_AS_STRING}, - {id: 'TIME_CLOCK', title: T.F.WORKLOG.EXPORT.O.TIME_AS_CLOCK}, - {id: 'ESTIMATE_MS', title: T.F.WORKLOG.EXPORT.O.ESTIMATE_AS_MILLISECONDS}, - {id: 'ESTIMATE_STR', title: T.F.WORKLOG.EXPORT.O.ESTIMATE_AS_STRING}, - {id: 'ESTIMATE_CLOCK', title: T.F.WORKLOG.EXPORT.O.ESTIMATE_AS_CLOCK}, + { id: 'DATE', title: T.F.WORKLOG.EXPORT.O.DATE }, + { id: 'START', title: T.F.WORKLOG.EXPORT.O.STARTED_WORKING }, + { id: 'END', title: T.F.WORKLOG.EXPORT.O.ENDED_WORKING }, + { id: 'TITLES', title: T.F.WORKLOG.EXPORT.O.PARENT_TASK_TITLES_ONLY }, + { + id: 'TITLES_INCLUDING_SUB', + title: T.F.WORKLOG.EXPORT.O.TITLES_AND_SUB_TASK_TITLES, + }, + { id: 'NOTES', title: T.F.WORKLOG.EXPORT.O.NOTES }, + { id: 'PROJECTS', title: T.F.WORKLOG.EXPORT.O.PROJECTS }, + { id: 'TAGS', title: T.F.WORKLOG.EXPORT.O.TAGS }, + { id: 'TIME_MS', title: T.F.WORKLOG.EXPORT.O.TIME_AS_MILLISECONDS }, + { id: 'TIME_STR', title: T.F.WORKLOG.EXPORT.O.TIME_AS_STRING }, + { id: 'TIME_CLOCK', title: T.F.WORKLOG.EXPORT.O.TIME_AS_CLOCK }, + { id: 'ESTIMATE_MS', title: T.F.WORKLOG.EXPORT.O.ESTIMATE_AS_MILLISECONDS }, + { id: 'ESTIMATE_STR', title: T.F.WORKLOG.EXPORT.O.ESTIMATE_AS_STRING }, + { id: 'ESTIMATE_CLOCK', title: T.F.WORKLOG.EXPORT.O.ESTIMATE_AS_CLOCK }, ]; groupByOptions: { id: string; title: string }[] = [ - {id: WorklogGrouping.DATE, title: T.F.WORKLOG.EXPORT.O.DATE}, - {id: WorklogGrouping.TASK, title: T.F.WORKLOG.EXPORT.O.TASK_SUBTASK}, - {id: WorklogGrouping.PARENT, title: T.F.WORKLOG.EXPORT.O.PARENT_TASK}, - {id: WorklogGrouping.WORKLOG, title: T.F.WORKLOG.EXPORT.O.WORKLOG} + { id: WorklogGrouping.DATE, title: T.F.WORKLOG.EXPORT.O.DATE }, + { id: WorklogGrouping.TASK, title: T.F.WORKLOG.EXPORT.O.TASK_SUBTASK }, + { id: WorklogGrouping.PARENT, title: T.F.WORKLOG.EXPORT.O.PARENT_TASK }, + { id: WorklogGrouping.WORKLOG, title: T.F.WORKLOG.EXPORT.O.WORKLOG }, ]; private _subs: Subscription = new Subscription(); @@ -89,103 +96,109 @@ export class WorklogExportComponent implements OnInit, OnDestroy { private _workContextService: WorkContextService, private _changeDetectorRef: ChangeDetectorRef, private _projectService: ProjectService, - private _tagService: TagService - ) { - } + private _tagService: TagService, + ) {} ngOnInit() { if (!this.rangeStart || !this.rangeEnd) { throw new Error('Worklog: Invalid date range'); } - this.fileName - = 'tasks' - + getWorklogStr(this.rangeStart) - + '-' - + getWorklogStr(this.rangeEnd) - + '.csv' - ; + this.fileName = + 'tasks' + + getWorklogStr(this.rangeStart) + + '-' + + getWorklogStr(this.rangeEnd) + + '.csv'; - this._subs.add(this._workContextService.advancedCfg$.pipe( - distinctUntilChanged(distinctUntilChangedObject) - ).subscribe((advancedCfg: WorkContextAdvancedCfg) => { - if (advancedCfg.worklogExportSettings) { - this.options = { - ...WORKLOG_EXPORT_DEFAULTS, - ...advancedCfg.worklogExportSettings, - // NOTE: if we don't do this typescript(?) get's aggressive - cols: [...( - advancedCfg.worklogExportSettings - ? [...advancedCfg.worklogExportSettings.cols] - : [...WORKLOG_EXPORT_DEFAULTS.cols] - )] - }; - } else { - this.options = { - ...WORKLOG_EXPORT_DEFAULTS, - cols: [...WORKLOG_EXPORT_DEFAULTS.cols], - }; - } - this._changeDetectorRef.detectChanges(); - })); + this._subs.add( + this._workContextService.advancedCfg$ + .pipe(distinctUntilChanged(distinctUntilChangedObject)) + .subscribe((advancedCfg: WorkContextAdvancedCfg) => { + if (advancedCfg.worklogExportSettings) { + this.options = { + ...WORKLOG_EXPORT_DEFAULTS, + ...advancedCfg.worklogExportSettings, + // NOTE: if we don't do this typescript(?) get's aggressive + cols: [ + ...(advancedCfg.worklogExportSettings + ? [...advancedCfg.worklogExportSettings.cols] + : [...WORKLOG_EXPORT_DEFAULTS.cols]), + ], + }; + } else { + this.options = { + ...WORKLOG_EXPORT_DEFAULTS, + cols: [...WORKLOG_EXPORT_DEFAULTS.cols], + }; + } + this._changeDetectorRef.detectChanges(); + }), + ); this._subs.add( combineLatest([ - this._worklogService.getTaskListForRange$(this.rangeStart, this.rangeEnd, true, this.projectId), + this._worklogService.getTaskListForRange$( + this.rangeStart, + this.rangeEnd, + true, + this.projectId, + ), this._workContextService.activeWorkContext$, this._projectService.list$, - this._tagService.tags$ + this._tagService.tags$, ]) - .pipe( - ).subscribe(([tasks, ac, projects, tags]) => { - if (tasks) { - const workTimes = { start: ac.workStart, end: ac.workEnd }; - const data = { tasks, projects, tags, workTimes }; - const rows = createRows(data, this.options.groupBy); - this.formattedRows = formatRows(rows, this.options); - // TODO format to csv + .pipe() + .subscribe(([tasks, ac, projects, tags]) => { + if (tasks) { + const workTimes = { start: ac.workStart, end: ac.workEnd }; + const data = { tasks, projects, tags, workTimes }; + const rows = createRows(data, this.options.groupBy); + this.formattedRows = formatRows(rows, this.options); + // TODO format to csv - this.headlineCols = this.options.cols.map(col => { - switch (col) { - case 'DATE': - return 'Date'; - case 'START': - return 'Start'; - case 'END': - return 'End'; - case 'TITLES': - return 'Titles'; - case 'TITLES_INCLUDING_SUB': - return 'Titles'; - case 'NOTES': - return 'Descriptions'; - case 'PROJECTS': - return 'Projects'; - case 'TAGS': - return 'Tags'; - case 'TIME_MS': - case 'TIME_STR': - case 'TIME_CLOCK': - return 'Worked'; - case 'ESTIMATE_MS': - case 'ESTIMATE_STR': - case 'ESTIMATE_CLOCK': - return 'Estimate'; - default: - return 'INVALID COL'; - } - }); + this.headlineCols = this.options.cols.map((col) => { + switch (col) { + case 'DATE': + return 'Date'; + case 'START': + return 'Start'; + case 'END': + return 'End'; + case 'TITLES': + return 'Titles'; + case 'TITLES_INCLUDING_SUB': + return 'Titles'; + case 'NOTES': + return 'Descriptions'; + case 'PROJECTS': + return 'Projects'; + case 'TAGS': + return 'Tags'; + case 'TIME_MS': + case 'TIME_STR': + case 'TIME_CLOCK': + return 'Worked'; + case 'ESTIMATE_MS': + case 'ESTIMATE_STR': + case 'ESTIMATE_CLOCK': + return 'Estimate'; + default: + return 'INVALID COL'; + } + }); - this.txt = formatText(this.headlineCols, this.formattedRows); - this._changeDetectorRef.detectChanges(); - } - })); + this.txt = formatText(this.headlineCols, this.formattedRows); + this._changeDetectorRef.detectChanges(); + } + }), + ); // dirty but good enough for now const clipboard = new Clipboard('#clipboard-btn'); clipboard.on('success', (e: any) => { this._snackService.open({ msg: T.GLOBAL_SNACK.COPY_TO_CLIPPBOARD, - type: 'SUCCESS' + type: 'SUCCESS', }); e.clearSelection(); }); @@ -200,7 +213,7 @@ export class WorklogExportComponent implements OnInit, OnDestroy { } onOptionsChange() { - this.options.cols = this.options.cols.filter(col => !!col); + this.options.cols = this.options.cols.filter((col) => !!col); this._workContextService.updateWorklogExportSettingsForCurrentContext(this.options); } diff --git a/src/app/features/worklog/worklog-export/worklog-export.util.spec.ts b/src/app/features/worklog/worklog-export/worklog-export.util.spec.ts index 3fac0fa97..a92990709 100644 --- a/src/app/features/worklog/worklog-export/worklog-export.util.spec.ts +++ b/src/app/features/worklog/worklog-export/worklog-export.util.spec.ts @@ -12,17 +12,19 @@ const startTime1 = new Date('5/2/2021 10:00:00').getTime(); const endTime1 = new Date('5/2/2021 12:00:00').getTime(); const startTime2 = new Date('6/2/2021 14:00:00').getTime(); const endTime2 = new Date('6/2/2021 16:00:00').getTime(); -const dateKey1 = '2021-02-05', dateKey2 = '2021-02-06'; +const dateKey1 = '2021-02-05', + dateKey2 = '2021-02-06'; const start: WorkStartEnd = { [dateKey1]: startTime1, [dateKey2]: startTime2 }; -const end: WorkStartEnd = { [dateKey1]: endTime1, [dateKey2]: endTime2 }; +const end: WorkStartEnd = { [dateKey1]: endTime1, [dateKey2]: endTime2 }; const workTimes: WorkTimes = { start, end }; -const oneHour = 3600000, twoHours = 7200000; +const oneHour = 3600000, + twoHours = 7200000; const createTask = (partialTask: Partial): WorklogTask => { const deepCopy = JSON.parse(JSON.stringify(DEFAULT_TASK)); return { ...deepCopy, - timeSpentOnDay: { [dateKey1]: oneHour, }, + timeSpentOnDay: { [dateKey1]: oneHour }, title: partialTask.id as string, ...partialTask, dateStr: '', @@ -36,7 +38,7 @@ const createSubTask = (partialTask: Partial, parentTask: WorklogTas const createTag = (tagId: string, ...tasks: WorklogTask[]) => { const taskIds: string[] = []; - tasks.forEach(task => { + tasks.forEach((task) => { taskIds.push(task.id); task.tagIds.push(tagId); }); @@ -45,37 +47,57 @@ const createTag = (tagId: string, ...tasks: WorklogTask[]) => { const createProject = (projectId: string, ...tasks: WorklogTask[]): Project => { const taskIds: string[] = []; - tasks.forEach(task => { + tasks.forEach((task) => { taskIds.push(task.id); Object.defineProperty(task, 'projectId', { value: projectId }); }); return { ...DEFAULT_PROJECT, id: projectId, taskIds, title: projectId }; }; -const createWorklogData = (partialData: Partial): WorklogExportData => { - return { tasks: [], tags: [], projects: [], workTimes: { start: {}, end: {} }, ...partialData }; +const createWorklogData = ( + partialData: Partial, +): WorklogExportData => { + return { + tasks: [], + tags: [], + projects: [], + workTimes: { start: {}, end: {} }, + ...partialData, + }; }; describe('createRows', () => { describe('time', () => { it('should have correct time fields', () => { const tasks = [ - createTask({ id: 'T1', timeSpentOnDay: { [dateKey1]: oneHour }, timeEstimate: twoHours }), - createTask({ id: 'T2', timeSpentOnDay: { [dateKey1]: oneHour }, timeEstimate: twoHours }), - createTask({ id: 'T3', timeSpentOnDay: { [dateKey1]: oneHour }, timeEstimate: twoHours }) + createTask({ + id: 'T1', + timeSpentOnDay: { [dateKey1]: oneHour }, + timeEstimate: twoHours, + }), + createTask({ + id: 'T2', + timeSpentOnDay: { [dateKey1]: oneHour }, + timeEstimate: twoHours, + }), + createTask({ + id: 'T3', + timeSpentOnDay: { [dateKey1]: oneHour }, + timeEstimate: twoHours, + }), ]; const data = createWorklogData({ tasks }); const rows = createRows(data, WorklogGrouping.DATE); expect(rows.length).toBe(1); - expect(rows[0].timeSpent).toBe(oneHour*3); - expect(rows[0].timeEstimate).toBe(twoHours*3); + expect(rows[0].timeSpent).toBe(oneHour * 3); + expect(rows[0].timeEstimate).toBe(twoHours * 3); }); it('should have correct WorkStartEnd fields', () => { const tasks = [ createTask({ id: 'T1' }), createTask({ id: 'T2' }), - createTask({ id: 'T3', timeSpentOnDay: { [dateKey2]: oneHour }}) + createTask({ id: 'T3', timeSpentOnDay: { [dateKey2]: oneHour } }), ]; const data = createWorklogData({ tasks, workTimes }); const rows = createRows(data, WorklogGrouping.DATE); @@ -90,22 +112,32 @@ describe('createRows', () => { describe('date', () => { it('should sort date fields', () => { const tasks = [ - createTask({ id: 'T1', timeSpentOnDay: { [dateKey1]: oneHour, '2021-02-20': oneHour, [dateKey2]: oneHour} }), + createTask({ + id: 'T1', + timeSpentOnDay: { + [dateKey1]: oneHour, + '2021-02-20': oneHour, + [dateKey2]: oneHour, + }, + }), ]; const data = createWorklogData({ tasks }); const rows = createRows(data, WorklogGrouping.TASK); expect(rows.length).toBe(1); - expect(rows[0].dates).toEqual([ dateKey1, dateKey2, '2021-02-20']); + expect(rows[0].dates).toEqual([dateKey1, dateKey2, '2021-02-20']); }); }); describe('titles', () => { - const parentTaskId = 'PT1', subTaskId1 = 'S2', subTaskId2 = 'S3', otherTaskId = 'T4'; - const task1 = createTask({ id:parentTaskId }); - const task2 = createSubTask({ id:subTaskId1 }, task1); - const task3 = createSubTask({ id:subTaskId2 }, task1); - const task4 = createTask({ id:otherTaskId }); - const tasks = [ task1, task2, task3, task4 ]; + const parentTaskId = 'PT1', + subTaskId1 = 'S2', + subTaskId2 = 'S3', + otherTaskId = 'T4'; + const task1 = createTask({ id: parentTaskId }); + const task2 = createSubTask({ id: subTaskId1 }, task1); + const task3 = createSubTask({ id: subTaskId2 }, task1); + const task4 = createTask({ id: otherTaskId }); + const tasks = [task1, task2, task3, task4]; let data: WorklogExportData; beforeEach(() => { @@ -115,42 +147,51 @@ describe('createRows', () => { it('should have correct titles when grouping by parent', () => { const rows = createRows(data, WorklogGrouping.PARENT); expect(rows.length).toBe(2); - expect(rows[0].titles).toEqual([ parentTaskId ]); - expect(rows[1].titles).toEqual([ otherTaskId ]); - expect(rows[0].titlesWithSub).toEqual([ parentTaskId ]); - expect(rows[1].titlesWithSub).toEqual([ otherTaskId ]); + expect(rows[0].titles).toEqual([parentTaskId]); + expect(rows[1].titles).toEqual([otherTaskId]); + expect(rows[0].titlesWithSub).toEqual([parentTaskId]); + expect(rows[1].titlesWithSub).toEqual([otherTaskId]); }); it('should have correct titles when grouping by task', () => { const rows = createRows(data, WorklogGrouping.TASK); expect(rows.length).toBe(3); - expect(rows[0].titles).toEqual([ parentTaskId ]); - expect(rows[1].titles).toEqual([ parentTaskId ]); - expect(rows[2].titles).toEqual([ otherTaskId ]); - expect(rows[0].titlesWithSub).toEqual([ subTaskId1 ]); - expect(rows[1].titlesWithSub).toEqual([ subTaskId2 ]); - expect(rows[2].titlesWithSub).toEqual([ otherTaskId ]); + expect(rows[0].titles).toEqual([parentTaskId]); + expect(rows[1].titles).toEqual([parentTaskId]); + expect(rows[2].titles).toEqual([otherTaskId]); + expect(rows[0].titlesWithSub).toEqual([subTaskId1]); + expect(rows[1].titlesWithSub).toEqual([subTaskId2]); + expect(rows[2].titlesWithSub).toEqual([otherTaskId]); }); it('should have correct titles when grouping by date', () => { const rows = createRows(data, WorklogGrouping.DATE); expect(rows.length).toBe(1); - expect(rows[0].titles).toEqual([ parentTaskId, otherTaskId ]); - expect(rows[0].titlesWithSub).toEqual([ parentTaskId, subTaskId1, subTaskId2, otherTaskId ]); + expect(rows[0].titles).toEqual([parentTaskId, otherTaskId]); + expect(rows[0].titlesWithSub).toEqual([ + parentTaskId, + subTaskId1, + subTaskId2, + otherTaskId, + ]); }); }); describe('projects', () => { - const task1P1 = '1T', task2P1 = '2T', parentTaskP2 = '3PT', subTask = '4ST'; - const project1 = 'P1', project2 = 'P2'; - const task1 = createTask({ id:task1P1 }); - const task2 = createTask({ id:task2P1 }); - const task3 = createTask({ id:parentTaskP2 }); - const task4 = createSubTask({ id:subTask }, task3); - const tasks = [ task1, task2, task3, task4 ]; + const task1P1 = '1T', + task2P1 = '2T', + parentTaskP2 = '3PT', + subTask = '4ST'; + const project1 = 'P1', + project2 = 'P2'; + const task1 = createTask({ id: task1P1 }); + const task2 = createTask({ id: task2P1 }); + const task3 = createTask({ id: parentTaskP2 }); + const task4 = createSubTask({ id: subTask }, task3); + const tasks = [task1, task2, task3, task4]; const p1 = createProject(project1, task1, task2); const p2 = createProject(project2, task3, task4); - const projects = [ p1, p2 ]; + const projects = [p1, p2]; let data: WorklogExportData; beforeEach(() => { @@ -160,28 +201,31 @@ describe('createRows', () => { it('should not duplicate', () => { const rows = createRows(data, WorklogGrouping.DATE); expect(rows.length).toBe(1); - expect(rows[0].projects).toEqual([ project1, project2 ]); + expect(rows[0].projects).toEqual([project1, project2]); }); it('should show project of parent task', () => { const rows = createRows(data, WorklogGrouping.TASK); expect(rows.length).toBe(3); - expect(rows[0].projects).toEqual([ project1 ]); - expect(rows[1].projects).toEqual([ project1 ]); - expect(rows[2].projects).toEqual([ project2 ]); + expect(rows[0].projects).toEqual([project1]); + expect(rows[1].projects).toEqual([project1]); + expect(rows[2].projects).toEqual([project2]); }); }); describe('tags', () => { - const parentTaskId = '1PT1', subTaskId = '2ST1', otherTaskId = '3OT1'; - const tagId1 = 'Tag1', tagId2 = 'Tag2'; + const parentTaskId = '1PT1', + subTaskId = '2ST1', + otherTaskId = '3OT1'; + const tagId1 = 'Tag1', + tagId2 = 'Tag2'; const parentTask = createTask({ id: parentTaskId }); const subTask = createSubTask({ id: subTaskId }, parentTask); const otherTask = createTask({ id: otherTaskId }); - const tasks = [ parentTask, subTask, otherTask ]; + const tasks = [parentTask, subTask, otherTask]; const tag1 = createTag(tagId1, parentTask); const tag2 = createTag(tagId2, parentTask, otherTask); - const tags = [ tag1, tag2 ]; + const tags = [tag1, tag2]; let data: WorklogExportData; beforeEach(() => { @@ -191,80 +235,88 @@ describe('createRows', () => { it('should not duplicate tags when grouping by date', () => { const rows = createRows(data, WorklogGrouping.DATE); expect(rows.length).toBe(1); - expect(rows[0].tags).toEqual([ tagId1, tagId2 ]); + expect(rows[0].tags).toEqual([tagId1, tagId2]); }); it('should not duplicate tags when grouping by parent', () => { const rows = createRows(data, WorklogGrouping.PARENT); expect(rows.length).toBe(2); - expect(rows[0].tags).toEqual([ tagId1, tagId2 ]); - expect(rows[1].tags).toEqual([ tagId2 ]); + expect(rows[0].tags).toEqual([tagId1, tagId2]); + expect(rows[1].tags).toEqual([tagId2]); }); it('should use tags of the parent task when grouping by task', () => { const rows = createRows(data, WorklogGrouping.TASK); expect(rows.length).toBe(2); - expect(rows[0].tags).toEqual([ tagId1, tagId2 ]); - expect(rows[1].tags).toEqual([ tagId2 ]); + expect(rows[0].tags).toEqual([tagId1, tagId2]); + expect(rows[1].tags).toEqual([tagId2]); }); it('should use tags of the parent task when grouping by worklog', () => { const rows = createRows(data, WorklogGrouping.WORKLOG); expect(rows.length).toBe(3); - expect(rows[0].tags).toEqual([ tagId1, tagId2 ]); - expect(rows[1].tags).toEqual([ tagId1, tagId2 ]); - expect(rows[2].tags).toEqual([ tagId2 ]); + expect(rows[0].tags).toEqual([tagId1, tagId2]); + expect(rows[1].tags).toEqual([tagId1, tagId2]); + expect(rows[2].tags).toEqual([tagId2]); }); it('should have today tags', () => { - const todayTaskId = 'T1', todayTagId = 'Tag1'; + const todayTaskId = 'T1', + todayTagId = 'Tag1'; const todayTask = createTask({ id: todayTaskId }); const todayTag = createTag(todayTagId, todayTask); - data = createWorklogData({ tasks: [ todayTask ], tags: [ todayTag ]}); + data = createWorklogData({ tasks: [todayTask], tags: [todayTag] }); const rows = createRows(data, WorklogGrouping.DATE); expect(rows.length).toBe(1); - expect(rows[0].tags).toEqual([ todayTagId ]); + expect(rows[0].tags).toEqual([todayTagId]); }); }); describe('notes', () => { - const note1 = 'N1', note2 = 'N2'; - const task1 = createTask({ id: 'T1', notes: note1, timeSpentOnDay: { [dateKey1]: oneHour, [dateKey2]: oneHour }}); + const note1 = 'N1', + note2 = 'N2'; + const task1 = createTask({ + id: 'T1', + notes: note1, + timeSpentOnDay: { [dateKey1]: oneHour, [dateKey2]: oneHour }, + }); const task2 = createTask({ id: 'T2', notes: note2 }); let data: WorklogExportData; beforeEach(() => { - data = createWorklogData({ tasks: [ task1, task2 ] }); + data = createWorklogData({ tasks: [task1, task2] }); }); it('should not duplicate notes when grouping by date', () => { const rows = createRows(data, WorklogGrouping.DATE); expect(rows.length).toBe(2); - expect(rows[0].notes).toEqual([ note1, note2 ]); - expect(rows[1].notes).toEqual([ note1 ]); + expect(rows[0].notes).toEqual([note1, note2]); + expect(rows[1].notes).toEqual([note1]); }); it('should not duplicate notes when grouping by task', () => { const rows = createRows(data, WorklogGrouping.TASK); expect(rows.length).toBe(2); - expect(rows[0].notes).toEqual([ note1 ]); - expect(rows[1].notes).toEqual([ note2 ]); + expect(rows[0].notes).toEqual([note1]); + expect(rows[1].notes).toEqual([note2]); }); it('should show correct notes of sub/parent tasks', () => { - const parentTaskId = 'P1', subTaskId = 'ST1'; + const parentTaskId = 'P1', + subTaskId = 'ST1'; const t1 = createTask({ id: parentTaskId, notes: note1 }); const t2 = createSubTask({ id: subTaskId, notes: note2 }, t1); - data = createWorklogData({ tasks: [ t1, t2 ] }); + data = createWorklogData({ tasks: [t1, t2] }); const rows = createRows(data, WorklogGrouping.DATE); expect(rows.length).toBe(1); - expect(rows[0].notes).toEqual([ note1, note2 ]); + expect(rows[0].notes).toEqual([note1, note2]); }); it('should replace \\n with dashes', () => { - const noteWithNewLine = 'N1\nN1', expectedNote = 'N1 - N1'; + const noteWithNewLine = 'N1\nN1', + expectedNote = 'N1 - N1'; const t1 = createTask({ id: 'T1', notes: noteWithNewLine }); - data = createWorklogData({ tasks: [ t1 ] }); + data = createWorklogData({ tasks: [t1] }); const rows = createRows(data, WorklogGrouping.DATE); expect(rows.length).toBe(1); expect(rows[0].notes).toEqual([expectedNote]); @@ -272,20 +324,24 @@ describe('createRows', () => { }); describe('group by worklog', () => { - const taskId1 = 'T1', taskId2 = 'T2'; - const task1 = createTask({ id:taskId1 }); - const task2 = createTask({ id:taskId2, timeSpentOnDay: { [dateKey1]: oneHour, [dateKey2]: oneHour } }); - const data = createWorklogData({ tasks: [ task1, task2 ] }); + const taskId1 = 'T1', + taskId2 = 'T2'; + const task1 = createTask({ id: taskId1 }); + const task2 = createTask({ + id: taskId2, + timeSpentOnDay: { [dateKey1]: oneHour, [dateKey2]: oneHour }, + }); + const data = createWorklogData({ tasks: [task1, task2] }); it('should list all tasks and not group by task/date', () => { const rows = createRows(data, WorklogGrouping.WORKLOG); expect(rows.length).toBe(3); - expect(rows[0].titlesWithSub).toEqual([ taskId1 ]); - expect(rows[0].dates).toEqual([ dateKey1 ]); - expect(rows[1].titlesWithSub).toEqual([ taskId2 ]); - expect(rows[1].dates).toEqual([ dateKey1 ]); - expect(rows[2].titlesWithSub).toEqual([ taskId2 ]); - expect(rows[2].dates).toEqual([ dateKey2 ]); + expect(rows[0].titlesWithSub).toEqual([taskId1]); + expect(rows[0].dates).toEqual([dateKey1]); + expect(rows[1].titlesWithSub).toEqual([taskId2]); + expect(rows[1].dates).toEqual([dateKey1]); + expect(rows[2].titlesWithSub).toEqual([taskId2]); + expect(rows[2].dates).toEqual([dateKey2]); }); }); }); diff --git a/src/app/features/worklog/worklog-export/worklog-export.util.ts b/src/app/features/worklog/worklog-export/worklog-export.util.ts index a54edda0e..ac034a514 100644 --- a/src/app/features/worklog/worklog-export/worklog-export.util.ts +++ b/src/app/features/worklog/worklog-export/worklog-export.util.ts @@ -8,7 +8,12 @@ import { ProjectCopy } from '../../project/project.model'; import { TagCopy } from '../../tag/tag.model'; import { WorklogTask } from '../../tasks/task.model'; import { WorklogExportSettingsCopy, WorklogGrouping } from '../worklog.model'; -import { ItemsByKey, RowItem, TaskFields, WorklogExportData } from './worklog-export.model'; +import { + ItemsByKey, + RowItem, + TaskFields, + WorklogExportData, +} from './worklog-export.model'; const LINE_SEPARATOR = '\n'; const EMPTY_VAL = ' - '; @@ -17,7 +22,10 @@ const EMPTY_VAL = ' - '; * Depending on groupBy it gets a map of RowItems by groupKeys (date, task.id, date_task.id). * Then it sorts and reiterates on groupKey and converts the map into a simple array of RowItems. */ -export const createRows = (data: WorklogExportData, groupBy: WorklogGrouping): RowItem[] => { +export const createRows = ( + data: WorklogExportData, + groupBy: WorklogGrouping, +): RowItem[] => { let groups: ItemsByKey = {}; switch (groupBy) { @@ -27,14 +35,17 @@ export const createRows = (data: WorklogExportData, groupBy: WorklogGrouping): R case WorklogGrouping.WORKLOG: // don't group at all groups = handleWorklogGroup(data); break; - default: // group by TASK/PARENT + default: + // group by TASK/PARENT groups = handleTaskGroup(data, groupBy); } const rows: RowItem[] = []; - Object.keys(groups).sort().forEach((key => { - rows.push(groups[key]); - })); + Object.keys(groups) + .sort() + .forEach((key) => { + rows.push(groups[key]); + }); return rows; }; @@ -46,43 +57,57 @@ export const createRows = (data: WorklogExportData, groupBy: WorklogGrouping): R const handleDateGroup = (data: WorklogExportData): ItemsByKey => { const taskGroups: ItemsByKey = {}; for (const task of data.tasks) { - if (!task.timeSpentOnDay) { continue; } + if (!task.timeSpentOnDay) { + continue; + } const taskFields = getTaskFields(task, data); const numDays = Object.keys(task.timeSpentOnDay).length; let timeEstimate = 0; let timeSpent = 0; - Object.keys(task.timeSpentOnDay).forEach(day => { + Object.keys(task.timeSpentOnDay).forEach((day) => { if (!task.subTaskIds || task.subTaskIds.length === 0) { timeSpent = task.timeSpentOnDay[day]; timeEstimate = task.timeEstimate / numDays; } const rowItem: RowItem = { - dates: [day], - workStart: data.workTimes.start[day], - workEnd: data.workTimes.end[day], - timeSpent, - timeEstimate, - ...taskFields + dates: [day], + workStart: data.workTimes.start[day], + workEnd: data.workTimes.end[day], + timeSpent, + timeEstimate, + ...taskFields, }; if (!taskGroups[day]) { taskGroups[day] = rowItem; } else { - taskGroups[day].titles = unique([ ...taskGroups[day].titles, ...rowItem.titles ]); - taskGroups[day].titlesWithSub = [ ...taskGroups[day].titlesWithSub, ...rowItem.titlesWithSub ]; - taskGroups[day].tasks = [ ...taskGroups[day].tasks, ...rowItem.tasks ]; - taskGroups[day].notes = [ ...taskGroups[day].notes, ...rowItem.notes ]; - taskGroups[day].projects = unique([ ...taskGroups[day].projects, ...rowItem.projects ]); - taskGroups[day].tags = unique([ ...taskGroups[day].tags, ...rowItem.tags ]); + taskGroups[day].titles = unique([...taskGroups[day].titles, ...rowItem.titles]); + taskGroups[day].titlesWithSub = [ + ...taskGroups[day].titlesWithSub, + ...rowItem.titlesWithSub, + ]; + taskGroups[day].tasks = [...taskGroups[day].tasks, ...rowItem.tasks]; + taskGroups[day].notes = [...taskGroups[day].notes, ...rowItem.notes]; + taskGroups[day].projects = unique([ + ...taskGroups[day].projects, + ...rowItem.projects, + ]); + taskGroups[day].tags = unique([...taskGroups[day].tags, ...rowItem.tags]); if (taskGroups[day].workStart !== undefined) { // TODO check if this works as intended - taskGroups[day].workStart = Math.min(taskGroups[day].workStart as number, rowItem.workStart as number); + taskGroups[day].workStart = Math.min( + taskGroups[day].workStart as number, + rowItem.workStart as number, + ); } if (taskGroups[day].workEnd !== undefined) { // TODO check if this works as intended - taskGroups[day].workEnd = Math.min(taskGroups[day].workEnd as number, rowItem.workEnd as number); + taskGroups[day].workEnd = Math.min( + taskGroups[day].workEnd as number, + rowItem.workEnd as number, + ); } taskGroups[day].timeEstimate += rowItem.timeEstimate; taskGroups[day].timeSpent += rowItem.timeSpent; @@ -97,18 +122,25 @@ const handleDateGroup = (data: WorklogExportData): ItemsByKey => { * If we're grouping by task ignore parent tasks */ const skipTask = (task: WorklogTask, groupBy: WorklogGrouping): boolean => { - return (groupBy === WorklogGrouping.PARENT && task.parentId !== null) - || (groupBy === WorklogGrouping.TASK && task.subTaskIds.length > 0); + return ( + (groupBy === WorklogGrouping.PARENT && task.parentId !== null) || + (groupBy === WorklogGrouping.TASK && task.subTaskIds.length > 0) + ); }; /** * For each task creates a new rowItem without needing to push to previous taskGroups, unlike handleDateGroup. * We're still creating a map since we will use the key for sorting in the next step. */ -const handleTaskGroup = (data: WorklogExportData, groupBy: WorklogGrouping): ItemsByKey => { +const handleTaskGroup = ( + data: WorklogExportData, + groupBy: WorklogGrouping, +): ItemsByKey => { const taskGroups: ItemsByKey = {}; for (const task of data.tasks) { - if (skipTask(task, groupBy)) { continue; }; + if (skipTask(task, groupBy)) { + continue; + } const taskFields = getTaskFields(task, data); const dates = sortDateStrings(Object.keys(task.timeSpentOnDay)); taskGroups[task.id] = { @@ -117,7 +149,7 @@ const handleTaskGroup = (data: WorklogExportData, groupBy: WorklogGrouping): Ite timeSpent: Object.values(task.timeSpentOnDay).reduce((acc, curr) => acc + curr, 0), workStart: 0, workEnd: 0, - ...taskFields + ...taskFields, }; } return taskGroups; @@ -130,7 +162,7 @@ const handleTaskGroup = (data: WorklogExportData, groupBy: WorklogGrouping): Ite const handleWorklogGroup = (data: WorklogExportData): ItemsByKey => { const taskGroups: ItemsByKey = {}; for (const task of data.tasks) { - Object.keys(task.timeSpentOnDay).forEach(day => { + Object.keys(task.timeSpentOnDay).forEach((day) => { const groupKey = day + '_' + task.id; const taskFields = getTaskFields(task, data); taskGroups[groupKey] = { @@ -139,7 +171,7 @@ const handleWorklogGroup = (data: WorklogExportData): ItemsByKey => { timeSpent: task.subTaskIds.length > 0 ? 0 : task.timeSpentOnDay[day], workStart: data.workTimes.start[day], workEnd: data.workTimes.end[day], - ...taskFields + ...taskFields, }; }); } @@ -152,21 +184,27 @@ const handleWorklogGroup = (data: WorklogExportData): ItemsByKey => { const getTaskFields = (task: WorklogTask, data: WorklogExportData): TaskFields => { const titlesWithSub = [task.title]; const titles = task.parentId - ? [(data.tasks.find(ptIN => ptIN.id === task.parentId) as WorklogTask).title] + ? [(data.tasks.find((ptIN) => ptIN.id === task.parentId) as WorklogTask).title] : [task.title]; const notes = task.notes ? [task.notes.replace(/\n/g, ' - ')] : []; const projects = task.projectId - ? [(data.projects.find(project => project.id === task.projectId) as ProjectCopy).title] + ? [ + (data.projects.find((project) => project.id === task.projectId) as ProjectCopy) + .title, + ] : []; // by design subtasks don't have tags, so we must set its parent's tags - let tags = task.parentId !== null - ? (data.tasks.find(t => t.id === task.parentId) as WorklogTask).tagIds - : task.tagIds; - tags = tags.map( tagId => (data.tags.find(tag => tag.id === tagId) as TagCopy).title); + let tags = + task.parentId !== null + ? (data.tasks.find((t) => t.id === task.parentId) as WorklogTask).tagIds + : task.tagIds; + tags = tags.map( + (tagId) => (data.tags.find((tag) => tag.id === tagId) as TagCopy).title, + ); - const tasks = [ task ]; + const tasks = [task]; return { tasks, titlesWithSub, titles, notes, projects, tags }; }; @@ -186,9 +224,12 @@ const sortDateStrings = (dates: string[]): string[] => { /** * Reiterates cell by cell and applies formatting based on requested Column Type */ -export const formatRows = (rows: RowItem[], options: WorklogExportSettingsCopy): (string | number | undefined)[][] => { +export const formatRows = ( + rows: RowItem[], + options: WorklogExportSettingsCopy, +): (string | number | undefined)[][] => { return rows.map((row: RowItem) => { - return options.cols.map(col => { + return options.cols.map((col) => { // TODO check if this is possible if (!col) { return; @@ -197,16 +238,21 @@ export const formatRows = (rows: RowItem[], options: WorklogExportSettingsCopy): throw new Error('Worklog: No titles'); } - const timeSpent = (options.roundWorkTimeTo) + const timeSpent = options.roundWorkTimeTo ? roundDuration(row.timeSpent, options.roundWorkTimeTo, true).asMilliseconds() : row.timeSpent; // If we're exporting raw worklogs, spread estimated time over worklogs belonging to a task based on // its share in time spent on the task let timeEstimate = row.timeEstimate; - if (options.groupBy === WorklogGrouping.WORKLOG - && (col === 'ESTIMATE_MS' || col === 'ESTIMATE_STR' || col === 'ESTIMATE_CLOCK')) { - const timeSpentTotal = Object.values(row.tasks[0].timeSpentOnDay).reduce((acc, curr) => acc + curr, 0); + if ( + options.groupBy === WorklogGrouping.WORKLOG && + (col === 'ESTIMATE_MS' || col === 'ESTIMATE_STR' || col === 'ESTIMATE_CLOCK') + ) { + const timeSpentTotal = Object.values(row.tasks[0].timeSpentOnDay).reduce( + (acc, curr) => acc + curr, + 0, + ); const timeSpentPart = row.timeSpent / timeSpentTotal; console.log(`${row.timeSpent} / ${timeSpentTotal} = ${timeSpentPart}`); timeEstimate = timeEstimate * timeSpentPart; @@ -220,31 +266,37 @@ export const formatRows = (rows: RowItem[], options: WorklogExportSettingsCopy): return row.dates[0]; case 'START': const workStart = !row.workStart ? 0 : row.workStart; - return (workStart) + return workStart ? moment( - (options.roundStartTimeTo) - ? roundTime(workStart, options.roundStartTimeTo) - : workStart - ).format('HH:mm') + options.roundStartTimeTo + ? roundTime(workStart, options.roundStartTimeTo) + : workStart, + ).format('HH:mm') : EMPTY_VAL; case 'END': - return (row.workEnd) + return row.workEnd ? moment( - (options.roundEndTimeTo && row.workEnd) - ? roundTime(row.workEnd, options.roundEndTimeTo) - : row.workEnd - ).format('HH:mm') + options.roundEndTimeTo && row.workEnd + ? roundTime(row.workEnd, options.roundEndTimeTo) + : row.workEnd, + ).format('HH:mm') : EMPTY_VAL; case 'TITLES': return row.titles.join(options.separateTasksBy || '
'); case 'TITLES_INCLUDING_SUB': return row.titlesWithSub.join(options.separateTasksBy || '
'); case 'NOTES': - return (row.notes.length !== 0) ? row.notes.join(options.separateTasksBy) : EMPTY_VAL; + return row.notes.length !== 0 + ? row.notes.join(options.separateTasksBy) + : EMPTY_VAL; case 'PROJECTS': - return (row.projects.length !== 0) ? row.projects.join(options.separateTasksBy) : EMPTY_VAL; + return row.projects.length !== 0 + ? row.projects.join(options.separateTasksBy) + : EMPTY_VAL; case 'TAGS': - return (row.tags.length !== 0) ? row.tags.join(options.separateTasksBy) : EMPTY_VAL; + return row.tags.length !== 0 + ? row.tags.join(options.separateTasksBy) + : EMPTY_VAL; case 'TIME_MS': return timeSpent; case 'TIME_STR': @@ -267,9 +319,12 @@ export const formatRows = (rows: RowItem[], options: WorklogExportSettingsCopy): /** * Prepares the csv for export */ -export const formatText = (headlineCols: string[], rows: (string | number | undefined)[][]): string => { +export const formatText = ( + headlineCols: string[], + rows: (string | number | undefined)[][], +): string => { let txt = ''; txt += headlineCols.join(';') + LINE_SEPARATOR; - txt += rows.map(cols => cols.join(';')).join(LINE_SEPARATOR); + txt += rows.map((cols) => cols.join(';')).join(LINE_SEPARATOR); return txt; }; diff --git a/src/app/features/worklog/worklog-week/worklog-week.component.ts b/src/app/features/worklog/worklog-week/worklog-week.component.ts index 78d50dc35..df991792d 100644 --- a/src/app/features/worklog/worklog-week/worklog-week.component.ts +++ b/src/app/features/worklog/worklog-week/worklog-week.component.ts @@ -17,7 +17,7 @@ import { SimpleCounterService } from '../../simple-counter/simple-counter.servic templateUrl: './worklog-week.component.html', styleUrls: ['./worklog-week.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation, expandFadeAnimation, fadeAnimation] + animations: [expandAnimation, expandFadeAnimation, fadeAnimation], }) export class WorklogWeekComponent { visibility: boolean[] = []; @@ -29,8 +29,7 @@ export class WorklogWeekComponent { public readonly simpleCounterService: SimpleCounterService, private readonly _matDialog: MatDialog, private readonly _taskService: TaskService, - ) { - } + ) {} sortDays(a: any, b: any) { return a.key - b.key; @@ -42,7 +41,7 @@ export class WorklogWeekComponent { const weekNr = getWeekNumber(now); // get for whole week - const {rangeStart, rangeEnd} = getDateRangeForWeek(year, weekNr); + const { rangeStart, rangeEnd } = getDateRangeForWeek(year, weekNr); this._matDialog.open(DialogWorklogExportComponent, { restoreFocus: true, @@ -50,16 +49,20 @@ export class WorklogWeekComponent { data: { rangeStart, rangeEnd, - } + }, }); } - async updateTimeSpentTodayForTask(task: Task, dateStr: string, newVal: number | string) { + async updateTimeSpentTodayForTask( + task: Task, + dateStr: string, + newVal: number | string, + ) { await this._taskService.updateEverywhere(task.id, { timeSpentOnDay: { ...task.timeSpentOnDay, [dateStr]: +newVal, - } + }, }); this.worklogService.refreshWorklog(); } diff --git a/src/app/features/worklog/worklog.component.ts b/src/app/features/worklog/worklog.component.ts index 29fcd26ba..99dd6a15b 100644 --- a/src/app/features/worklog/worklog.component.ts +++ b/src/app/features/worklog/worklog.component.ts @@ -21,7 +21,7 @@ import { WorkContextService } from '../work-context/work-context.service'; templateUrl: './worklog.component.html', styleUrls: ['./worklog.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandFadeAnimation, standardListAnimation, fadeAnimation] + animations: [expandFadeAnimation, standardListAnimation, fadeAnimation], }) export class WorklogComponent { T: typeof T = T; @@ -34,13 +34,18 @@ export class WorklogComponent { private readonly _taskService: TaskService, private readonly _matDialog: MatDialog, private readonly _router: Router, - ) { - } + ) {} - exportData(monthData: WorklogMonth, year: number, month: string | number, week?: number) { - const {rangeStart, rangeEnd} = (typeof week === 'number') - ? getDateRangeForWeek(year, week, +month) - : getDateRangeForMonth(year, +month); + exportData( + monthData: WorklogMonth, + year: number, + month: string | number, + week?: number, + ) { + const { rangeStart, rangeEnd } = + typeof week === 'number' + ? getDateRangeForWeek(year, week, +month) + : getDateRangeForMonth(year, +month); this._matDialog.open(DialogWorklogExportComponent, { restoreFocus: true, @@ -48,36 +53,37 @@ export class WorklogComponent { data: { rangeStart, rangeEnd, - } + }, }); } restoreTask(task: TaskCopy) { - this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - okTxt: T.G.DO_IT, - message: T.F.WORKLOG.D_CONFIRM_RESTORE, - translateParams: {title: task.title} - } - }).afterClosed() + this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + okTxt: T.G.DO_IT, + message: T.F.WORKLOG.D_CONFIRM_RESTORE, + translateParams: { title: task.title }, + }, + }) + .afterClosed() .subscribe(async (isConfirm: boolean) => { - // because we navigate away we don't need to worry about updating the worklog itself - if (isConfirm) { - let subTasks; - if (task.subTaskIds && task.subTaskIds.length) { - const archiveState = await this._persistenceService.taskArchive.loadState(); - subTasks = task.subTaskIds - .map(id => archiveState.entities[id]) - .filter(v => !!v); - } - - console.log('RESTORE', task, subTasks); - this._taskService.restoreTask(task, (subTasks || []) as Task[]); - this._router.navigate(['/active/tasks']); + // because we navigate away we don't need to worry about updating the worklog itself + if (isConfirm) { + let subTasks; + if (task.subTaskIds && task.subTaskIds.length) { + const archiveState = await this._persistenceService.taskArchive.loadState(); + subTasks = task.subTaskIds + .map((id) => archiveState.entities[id]) + .filter((v) => !!v); } + + console.log('RESTORE', task, subTasks); + this._taskService.restoreTask(task, (subTasks || []) as Task[]); + this._router.navigate(['/active/tasks']); } - ); + }); } sortWorklogItems(a: any, b: any) { @@ -100,12 +106,16 @@ export class WorklogComponent { return val.weekNr; } - async updateTimeSpentTodayForTask(task: Task, dateStr: string, newVal: number | string) { + async updateTimeSpentTodayForTask( + task: Task, + dateStr: string, + newVal: number | string, + ) { await this._taskService.updateEverywhere(task.id, { timeSpentOnDay: { ...task.timeSpentOnDay, [dateStr]: +newVal, - } + }, }); this.worklogService.refreshWorklog(); } diff --git a/src/app/features/worklog/worklog.model.ts b/src/app/features/worklog/worklog.model.ts index 552c9e704..a3245b099 100644 --- a/src/app/features/worklog/worklog.model.ts +++ b/src/app/features/worklog/worklog.model.ts @@ -51,7 +51,7 @@ export interface Worklog { } export type WorklogColTypes = - 'EMPTY' + | 'EMPTY' | 'DATE' | 'START' | 'END' @@ -65,14 +65,13 @@ export type WorklogColTypes = | 'TIME_CLOCK' | 'ESTIMATE_MS' | 'ESTIMATE_STR' - | 'ESTIMATE_CLOCK' - ; + | 'ESTIMATE_CLOCK'; export enum WorklogGrouping { DATE = 'DATE', PARENT = 'PARENT', TASK = 'TASK', - WORKLOG = 'WORKLOG' + WORKLOG = 'WORKLOG', } export interface WorklogExportSettingsCopy { diff --git a/src/app/features/worklog/worklog.module.ts b/src/app/features/worklog/worklog.module.ts index 3fa62e9ff..b9d12b7d5 100644 --- a/src/app/features/worklog/worklog.module.ts +++ b/src/app/features/worklog/worklog.module.ts @@ -8,11 +8,7 @@ import { FormsModule } from '@angular/forms'; import { WorklogWeekComponent } from './worklog-week/worklog-week.component'; @NgModule({ - imports: [ - CommonModule, - UiModule, - FormsModule, - ], + imports: [CommonModule, UiModule, FormsModule], declarations: [ WorklogComponent, DialogWorklogExportComponent, @@ -26,5 +22,4 @@ import { WorklogWeekComponent } from './worklog-week/worklog-week.component'; WorklogWeekComponent, ], }) -export class WorklogModule { -} +export class WorklogModule {} diff --git a/src/app/features/worklog/worklog.service.ts b/src/app/features/worklog/worklog.service.ts index 3022005e1..e97a1498e 100644 --- a/src/app/features/worklog/worklog.service.ts +++ b/src/app/features/worklog/worklog.service.ts @@ -3,7 +3,16 @@ import { Worklog, WorklogDay, WorklogWeek } 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, filter, first, map, shareReplay, startWith, switchMap, take } from 'rxjs/operators'; +import { + concatMap, + filter, + first, + map, + shareReplay, + startWith, + switchMap, + take, +} from 'rxjs/operators'; import { getWeekNumber } from '../../util/get-week-number'; import { WorkContextService } from '../work-context/work-context.service'; import { WorkContext } from '../work-context/work-context.model'; @@ -15,56 +24,71 @@ import { NavigationEnd, Router } from '@angular/router'; import { DataInitService } from '../../core/data-init/data-init.service'; import { WorklogTask } from '../tasks/task.model'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class WorklogService { // treated as private but needs to be assigned first - archiveUpdateManualTrigger$: BehaviorSubject = new BehaviorSubject(true); + archiveUpdateManualTrigger$: BehaviorSubject = new BehaviorSubject( + true, + ); _archiveUpdateTrigger$: Observable = this._dataInitService.isAllDataLoadedInitially$.pipe( - concatMap(() => merge( - // this._workContextService.activeWorkContextOnceOnContextChange$, - this.archiveUpdateManualTrigger$, - this._router.events.pipe( - filter((event: any) => event instanceof NavigationEnd), - filter(({urlAfterRedirects}: NavigationEnd) => - urlAfterRedirects.includes('worklog') - || urlAfterRedirects.includes('daily-summary') + concatMap(() => + merge( + // this._workContextService.activeWorkContextOnceOnContextChange$, + this.archiveUpdateManualTrigger$, + this._router.events.pipe( + filter((event: any) => event instanceof NavigationEnd), + filter( + ({ urlAfterRedirects }: NavigationEnd) => + urlAfterRedirects.includes('worklog') || + urlAfterRedirects.includes('daily-summary'), + ), ), ), - )), + ), ); // NOTE: task updates are not reflected // TODO improve to reflect task updates or load when route is changed to worklog or daily summary - worklogData$: Observable<{ worklog: Worklog; totalTimeSpent: number }> = this._archiveUpdateTrigger$.pipe( + worklogData$: Observable<{ + worklog: Worklog; + totalTimeSpent: number; + }> = this._archiveUpdateTrigger$.pipe( switchMap(() => this._workContextService.activeWorkContext$.pipe(take(1))), - switchMap((curCtx) => from(this._loadForWorkContext(curCtx)).pipe( - startWith(null), - )), - shareReplay({bufferSize: 1, refCount: true}), + switchMap((curCtx) => + from(this._loadForWorkContext(curCtx)).pipe(startWith(null)), + ), + shareReplay({ bufferSize: 1, refCount: true }), ); - _worklogDataIfDefined$: Observable<{ worklog: Worklog; totalTimeSpent: number }> = this.worklogData$.pipe( - filter(wd => !!wd), - ); + _worklogDataIfDefined$: Observable<{ + worklog: Worklog; + totalTimeSpent: number; + }> = this.worklogData$.pipe(filter((wd) => !!wd)); - worklog$: Observable = this._worklogDataIfDefined$.pipe(map(data => data.worklog)); - totalTimeSpent$: Observable = this._worklogDataIfDefined$.pipe(map(data => data.totalTimeSpent)); + worklog$: Observable = this._worklogDataIfDefined$.pipe( + map((data) => data.worklog), + ); + totalTimeSpent$: Observable = this._worklogDataIfDefined$.pipe( + map((data) => data.totalTimeSpent), + ); currentWeek$: Observable = this.worklog$.pipe( - map(worklog => { + map((worklog) => { const now = new Date(); const year = now.getFullYear(); const month = now.getMonth() + 1; const weekNr = getWeekNumber(now); if (worklog[year] && worklog[year].ent[month]) { - return worklog[year].ent[month].weeks.find(week => week.weekNr === weekNr) || null; + return ( + worklog[year].ent[month].weeks.find((week) => week.weekNr === weekNr) || null + ); } return null; }), ); worklogTasks$: Observable = this.worklog$.pipe( - map(worklog => { + map((worklog) => { let tasks: WorklogTask[] = []; Object.keys(worklog).forEach((yearKeyIN) => { @@ -72,13 +96,13 @@ export class WorklogService { const year = worklog[yearKey]; if (year && year.ent) { - Object.keys(year.ent).forEach(monthKeyIN => { + Object.keys(year.ent).forEach((monthKeyIN) => { // needs de-normalization const monthKey = +monthKeyIN; const month = year.ent[monthKey]; if (month && month.ent) { - Object.keys(month.ent).forEach(dayKeyIN => { + Object.keys(month.ent).forEach((dayKeyIN) => { const dayKey = +dayKeyIN; const day: WorklogDay = month.ent[dayKey]; if (day) { @@ -90,7 +114,7 @@ export class WorklogService { } }); return tasks; - }) + }), ); constructor( @@ -99,45 +123,50 @@ export class WorklogService { private readonly _dataInitService: DataInitService, private readonly _taskService: TaskService, private readonly _router: Router, - ) { - } + ) {} refreshWorklog() { this.archiveUpdateManualTrigger$.next(true); } // TODO this is not waiting for worklog data - getTaskListForRange$(rangeStart: Date, + getTaskListForRange$( + rangeStart: Date, rangeEnd: Date, isFilterOutTimeSpentOnOtherDays: boolean = false, - projectId?: string | null): Observable { + projectId?: string | null, + ): Observable { const isProjectIdProvided: boolean = !!projectId || projectId === null; return this.worklogTasks$.pipe( - map(tasks => { + map((tasks) => { tasks = tasks.filter((task: WorklogTask) => { const taskDate = new Date(task.dateStr); - return (!isProjectIdProvided || task.projectId === projectId) - && (taskDate >= rangeStart && taskDate <= rangeEnd); + return ( + (!isProjectIdProvided || task.projectId === projectId) && + taskDate >= rangeStart && + taskDate <= rangeEnd + ); }); if (isFilterOutTimeSpentOnOtherDays) { - tasks = tasks.map((task): WorklogTask => { + tasks = tasks.map( + (task): WorklogTask => { + const timeSpentOnDay: any = {}; + Object.keys(task.timeSpentOnDay).forEach((dateStr) => { + const date = new Date(dateStr); - const timeSpentOnDay: any = {}; - Object.keys(task.timeSpentOnDay).forEach(dateStr => { - const date = new Date(dateStr); + if (date >= rangeStart && date <= rangeEnd) { + timeSpentOnDay[dateStr] = task.timeSpentOnDay[dateStr]; + } + }); - if (date >= rangeStart && date <= rangeEnd) { - timeSpentOnDay[dateStr] = task.timeSpentOnDay[dateStr]; - } - }); - - return { - ...task, - timeSpentOnDay - }; - }); + return { + ...task, + timeSpentOnDay, + }; + }, + ); } return dedupeByKey(tasks, 'id'); @@ -145,15 +174,21 @@ export class WorklogService { ); } - private async _loadForWorkContext(workContext: WorkContext): Promise<{ worklog: Worklog; totalTimeSpent: number }> { - const archive = await this._persistenceService.taskArchive.loadState() || createEmptyEntity(); - const taskState = await this._taskService.taskFeatureState$.pipe(first()).toPromise() || createEmptyEntity(); + private async _loadForWorkContext( + workContext: WorkContext, + ): Promise<{ worklog: Worklog; totalTimeSpent: number }> { + const archive = + (await this._persistenceService.taskArchive.loadState()) || createEmptyEntity(); + const taskState = + (await this._taskService.taskFeatureState$.pipe(first()).toPromise()) || + createEmptyEntity(); // console.time('calcTime'); - const { - completeStateForWorkContext, - unarchivedIds - } = getCompleteStateForWorkContext(workContext, taskState, archive); + const { completeStateForWorkContext, unarchivedIds } = getCompleteStateForWorkContext( + workContext, + taskState, + archive, + ); // console.timeEnd('calcTime'); const startEnd = { @@ -162,7 +197,11 @@ export class WorklogService { }; if (completeStateForWorkContext) { - const {worklog, totalTimeSpent} = mapArchiveToWorklog(completeStateForWorkContext, unarchivedIds, startEnd); + const { worklog, totalTimeSpent } = mapArchiveToWorklog( + completeStateForWorkContext, + unarchivedIds, + startEnd, + ); return { worklog, totalTimeSpent, @@ -170,12 +209,12 @@ export class WorklogService { } return { worklog: {}, - totalTimeSpent: 0 + totalTimeSpent: 0, }; } private _createTasksForDay(data: WorklogDay): WorklogTask[] { - const dayData = {...data}; + const dayData = { ...data }; return dayData.logEntries.map((entry) => { return { diff --git a/src/app/imex/file-imex/file-imex.component.ts b/src/app/imex/file-imex/file-imex.component.ts index 00284152d..2b30fa220 100644 --- a/src/app/imex/file-imex/file-imex.component.ts +++ b/src/app/imex/file-imex/file-imex.component.ts @@ -11,18 +11,17 @@ import { Router } from '@angular/router'; selector: 'file-imex', templateUrl: './file-imex.component.html', styleUrls: ['./file-imex.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class FileImexComponent { - @ViewChild('fileInput', {static: true}) fileInputRef?: ElementRef; + @ViewChild('fileInput', { static: true }) fileInputRef?: ElementRef; T: typeof T = T; constructor( private _dataImportService: DataImportService, private _snackService: SnackService, private _router: Router, - ) { - } + ) {} async handleFileInput(ev: any) { const files = ev.target.files; @@ -36,7 +35,7 @@ export class FileImexComponent { try { data = oldData = JSON.parse((textData as any).toString()); } catch (e) { - this._snackService.open({type: 'ERROR', msg: T.FILE_IMEX.S_ERR_INVALID_DATA}); + this._snackService.open({ type: 'ERROR', msg: T.FILE_IMEX.S_ERR_INVALID_DATA }); } if (oldData.config && Array.isArray(oldData.tasks)) { diff --git a/src/app/imex/file-imex/file-imex.module.ts b/src/app/imex/file-imex/file-imex.module.ts index 3cfd0cbdb..f6891100e 100644 --- a/src/app/imex/file-imex/file-imex.module.ts +++ b/src/app/imex/file-imex/file-imex.module.ts @@ -5,17 +5,8 @@ import { UiModule } from '../../ui/ui.module'; import { FormsModule } from '@angular/forms'; @NgModule({ - imports: [ - CommonModule, - FormsModule, - UiModule, - ], - declarations: [ - FileImexComponent, - ], - exports: [ - FileImexComponent, - ] + imports: [CommonModule, FormsModule, UiModule], + declarations: [FileImexComponent], + exports: [FileImexComponent], }) -export class FileImexModule { -} +export class FileImexModule {} diff --git a/src/app/imex/imex-meta/imex-meta.module.ts b/src/app/imex/imex-meta/imex-meta.module.ts index 5bbd5798c..7b007e4b2 100644 --- a/src/app/imex/imex-meta/imex-meta.module.ts +++ b/src/app/imex/imex-meta/imex-meta.module.ts @@ -3,9 +3,6 @@ import { CommonModule } from '@angular/common'; @NgModule({ declarations: [], - imports: [ - CommonModule - ] + imports: [CommonModule], }) -export class ImexMetaModule { -} +export class ImexMetaModule {} diff --git a/src/app/imex/imex-meta/imex-meta.service.ts b/src/app/imex/imex-meta/imex-meta.service.ts index c53ee1276..3994feadf 100644 --- a/src/app/imex/imex-meta/imex-meta.service.ts +++ b/src/app/imex/imex-meta/imex-meta.service.ts @@ -1,15 +1,17 @@ import { Injectable } from '@angular/core'; import { BehaviorSubject, Observable } from 'rxjs'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class ImexMetaService { // TODO check if this is needed isDataImportInProgress: boolean = false; - private _isDataImportInProgress$: BehaviorSubject = new BehaviorSubject(false); + private _isDataImportInProgress$: BehaviorSubject = new BehaviorSubject( + false, + ); isDataImportInProgress$: Observable = this._isDataImportInProgress$.asObservable(); constructor() { - this.isDataImportInProgress$.subscribe((val) => this.isDataImportInProgress = val); + this.isDataImportInProgress$.subscribe((val) => (this.isDataImportInProgress = val)); } setDataImportInProgress(isInProgress: boolean) { diff --git a/src/app/imex/local-backup/local-backup.effects.ts b/src/app/imex/local-backup/local-backup.effects.ts index 214f1a6b8..a40db0662 100644 --- a/src/app/imex/local-backup/local-backup.effects.ts +++ b/src/app/imex/local-backup/local-backup.effects.ts @@ -12,33 +12,44 @@ import * as moment from 'moment'; @Injectable() export class LocalBackupEffects { - checkForBackupIfNoTasks$: any = IS_ELECTRON && createEffect(() => this._actions$.pipe( - ofType( - loadAllData - ), - take(1), - tap(({appDataComplete}) => { - this._checkForBackupIfEmpty(appDataComplete); - }) - ), {dispatch: false}); + checkForBackupIfNoTasks$: any = + IS_ELECTRON && + createEffect( + () => + this._actions$.pipe( + ofType(loadAllData), + take(1), + tap(({ appDataComplete }) => { + this._checkForBackupIfEmpty(appDataComplete); + }), + ), + { dispatch: false }, + ); constructor( private _actions$: Actions, private _localBackupService: LocalBackupService, private _dataImportService: DataImportService, private _translateService: TranslateService, - ) { - } + ) {} private async _checkForBackupIfEmpty(appDataComplete: AppDataComplete) { if (IS_ELECTRON) { - if (appDataComplete.task.ids.length === 0 && appDataComplete.taskArchive.ids.length === 0 && !appDataComplete.lastLocalSyncModelChange) { + if ( + appDataComplete.task.ids.length === 0 && + appDataComplete.taskArchive.ids.length === 0 && + !appDataComplete.lastLocalSyncModelChange + ) { const backupMeta = await this._localBackupService.isBackupAvailable(); if (backupMeta) { - if (confirm(this._translateService.instant(T.CONFIRM.RESTORE_FILE_BACKUP, { - dir: backupMeta.folder, - from: this._formatDate(backupMeta.created), - }))) { + if ( + confirm( + this._translateService.instant(T.CONFIRM.RESTORE_FILE_BACKUP, { + dir: backupMeta.folder, + from: this._formatDate(backupMeta.created), + }), + ) + ) { const backupData = await this._localBackupService.loadBackup(backupMeta.path); console.log('backupData', backupData); await this._dataImportService.importCompleteSyncData(JSON.parse(backupData)); diff --git a/src/app/imex/local-backup/local-backup.module.ts b/src/app/imex/local-backup/local-backup.module.ts index c1d02bab8..8b589a584 100644 --- a/src/app/imex/local-backup/local-backup.module.ts +++ b/src/app/imex/local-backup/local-backup.module.ts @@ -14,5 +14,4 @@ export class LocalBackupModule { this._localBackupService.init(); } } - } diff --git a/src/app/imex/local-backup/local-backup.service.ts b/src/app/imex/local-backup/local-backup.service.ts index 25ac7515d..2ad197183 100644 --- a/src/app/imex/local-backup/local-backup.service.ts +++ b/src/app/imex/local-backup/local-backup.service.ts @@ -15,30 +15,36 @@ const DEFAULT_BACKUP_INTERVAL = 2 * 60 * 1000; @Injectable() export class LocalBackupService { - private _cfg$: Observable = this._configService.cfg$.pipe(map(cfg => cfg.localBackup)); + private _cfg$: Observable = this._configService.cfg$.pipe( + map((cfg) => cfg.localBackup), + ); private _triggerBackups: Observable = this._cfg$.pipe( - filter(cfg => cfg.isEnabled), + filter((cfg) => cfg.isEnabled), switchMap(() => interval(DEFAULT_BACKUP_INTERVAL)), - tap(() => this._backup()) + tap(() => this._backup()), ); constructor( private _configService: GlobalConfigService, private _persistenceService: PersistenceService, private _electronService: ElectronService, - ) { - } + ) {} init() { this._triggerBackups.subscribe(); } isBackupAvailable(): Promise { - return this._electronService.callMain(IPC.BACKUP_IS_AVAILABLE, null) as Promise; + return this._electronService.callMain(IPC.BACKUP_IS_AVAILABLE, null) as Promise< + false | LocalBackupMeta + >; } loadBackup(backupPath: string): Promise { - return this._electronService.callMain(IPC.BACKUP_LOAD_DATA, backupPath) as Promise; + return this._electronService.callMain( + IPC.BACKUP_LOAD_DATA, + backupPath, + ) as Promise; } private async _backup() { diff --git a/src/app/imex/sync/check-for-update.util.spec.ts b/src/app/imex/sync/check-for-update.util.spec.ts index fda18e096..9daddc392 100644 --- a/src/app/imex/sync/check-for-update.util.spec.ts +++ b/src/app/imex/sync/check-for-update.util.spec.ts @@ -2,42 +2,42 @@ import { checkForUpdate, UpdateCheckResult } from './check-for-update.util'; describe('checkForUpdate', () => { it('l > s >= r -> update remote', () => { - const r1 = checkForUpdate({local: 2, lastSync: 1, remote: 0}); + const r1 = checkForUpdate({ local: 2, lastSync: 1, remote: 0 }); expect(r1).toBe(UpdateCheckResult.RemoteUpdateRequired); - const r2 = checkForUpdate({local: 1, lastSync: 0, remote: 0}); + const r2 = checkForUpdate({ local: 1, lastSync: 0, remote: 0 }); expect(r2).toBe(UpdateCheckResult.RemoteUpdateRequired); }); it('l = s = r -> no update', () => { - const r = checkForUpdate({local: 0, lastSync: 0, remote: 0}); + const r = checkForUpdate({ local: 0, lastSync: 0, remote: 0 }); expect(r).toBe(UpdateCheckResult.InSync); }); it('l = s < r -> update local', () => { - const r = checkForUpdate({local: 0, lastSync: 0, remote: 1}); + const r = checkForUpdate({ local: 0, lastSync: 0, remote: 1 }); expect(r).toBe(UpdateCheckResult.LocalUpdateRequired); }); it('l > s < r -> conflict', () => { - const r1 = checkForUpdate({local: 3, lastSync: 0, remote: 4}); + const r1 = checkForUpdate({ local: 3, lastSync: 0, remote: 4 }); expect(r1).toBe(UpdateCheckResult.DataDiverged); - const r2 = checkForUpdate({local: 5, lastSync: 2, remote: 3}); + const r2 = checkForUpdate({ local: 5, lastSync: 2, remote: 3 }); expect(r2).toBe(UpdateCheckResult.DataDiverged); }); it('l = s > r -> update local', () => { - const r = checkForUpdate({local: 3, lastSync: 3, remote: 0}); + const r = checkForUpdate({ local: 3, lastSync: 3, remote: 0 }); expect(r).toBe(UpdateCheckResult.RemoteNotUpToDateDespiteSync); }); it('l = r > s -> update last sync', () => { - const r = checkForUpdate({local: 4, lastSync: 0, remote: 4}); + const r = checkForUpdate({ local: 4, lastSync: 0, remote: 4 }); expect(r).toBe(UpdateCheckResult.LastSyncNotUpToDate); }); it('l < s -> error', () => { - const r = checkForUpdate({local: 0, lastSync: 3, remote: 4}); + const r = checkForUpdate({ local: 0, lastSync: 3, remote: 4 }); expect(r).toBe(UpdateCheckResult.ErrorLastSyncNewerThanLocal); // expect(() => { // checkForUpdate({local: 0, lastSync: 3, remote: 4}); diff --git a/src/app/imex/sync/check-for-update.util.ts b/src/app/imex/sync/check-for-update.util.ts index 32534c9c0..14c64dd06 100644 --- a/src/app/imex/sync/check-for-update.util.ts +++ b/src/app/imex/sync/check-for-update.util.ts @@ -9,9 +9,13 @@ export enum UpdateCheckResult { ErrorInvalidTimeValues = 'ErrorInvalidTimeValues', } -export const checkForUpdate = (params: { remote: number; local: number; lastSync: number }) => { +export const checkForUpdate = (params: { + remote: number; + local: number; + lastSync: number; +}) => { _logHelper(params); - const {remote, local, lastSync} = params; + const { remote, local, lastSync } = params; const n = Date.now(); if (remote > n || local > n || lastSync > n) { @@ -21,7 +25,9 @@ export const checkForUpdate = (params: { remote: number; local: number; lastSync if (lastSync > local) { console.error('This should not happen. lastSyncTo > local'); - alert('Sync Error: last sync value is newer than local, which should never happen if you weren`t manually manipulating the data!'); + alert( + 'Sync Error: last sync value is newer than local, which should never happen if you weren`t manually manipulating the data!', + ); return UpdateCheckResult.ErrorLastSyncNewerThanLocal; } @@ -38,7 +44,9 @@ export const checkForUpdate = (params: { remote: number; local: number; lastSync } else if (lastSync < local) { return UpdateCheckResult.RemoteUpdateRequired; } else if (lastSync === local) { - alert('Sync Warning: Dropbox date not up to date despite seemingly successful sync. (This might happen when: 1. You have conflict changes and decide to take the local version. 2. You open the other instance and also decide to use the local version.)'); + alert( + 'Sync Warning: Dropbox date not up to date despite seemingly successful sync. (This might happen when: 1. You have conflict changes and decide to take the local version. 2. You open the other instance and also decide to use the local version.)', + ); return UpdateCheckResult.RemoteNotUpToDateDespiteSync; } } else if (local < remote) { @@ -55,13 +63,16 @@ export const checkForUpdate = (params: { remote: number; local: number; lastSync const _logHelper = (params: { remote: number; local: number; lastSync: number }) => { console.log(params); - const oldestFirst = Object.keys(params).sort((k1: string, k2: string) => (params as any)[k1] - (params as any)[k2]); + const oldestFirst = Object.keys(params).sort( + (k1: string, k2: string) => (params as any)[k1] - (params as any)[k2], + ); const keyOfOldest = oldestFirst[0]; - const zeroed = oldestFirst.reduce((acc, key) => - ({ - ...acc, - [key]: ((params as any)[key] - (params as any)[keyOfOldest]) / 1000, - }), - {}); + const zeroed = oldestFirst.reduce( + (acc, key) => ({ + ...acc, + [key]: ((params as any)[key] - (params as any)[keyOfOldest]) / 1000, + }), + {}, + ); console.log(zeroed, (Date.now() - (params as any)[keyOfOldest]) / 1000); }; diff --git a/src/app/imex/sync/data-import.service.ts b/src/app/imex/sync/data-import.service.ts index ceed71383..70cbdb56c 100644 --- a/src/app/imex/sync/data-import.service.ts +++ b/src/app/imex/sync/data-import.service.ts @@ -18,7 +18,6 @@ import { TranslateService } from '@ngx-translate/core'; providedIn: 'root', }) export class DataImportService { - constructor( private _persistenceService: PersistenceService, private _snackService: SnackService, @@ -36,13 +35,17 @@ export class DataImportService { return await this._persistenceService.loadComplete(); } - async importCompleteSyncData(data: AppDataComplete, isBackupReload: boolean = false, isSkipStrayBackupCheck: boolean = false) { - this._snackService.open({msg: T.F.SYNC.S.IMPORTING, ico: 'cloud_download'}); + async importCompleteSyncData( + data: AppDataComplete, + isBackupReload: boolean = false, + isSkipStrayBackupCheck: boolean = false, + ) { + this._snackService.open({ msg: T.F.SYNC.S.IMPORTING, ico: 'cloud_download' }); this._imexMetaService.setDataImportInProgress(true); // get rid of outdated project data if (!isBackupReload) { - if (!isSkipStrayBackupCheck && await this._isCheckForStrayBackupAndImport()) { + if (!isSkipStrayBackupCheck && (await this._isCheckForStrayBackupAndImport())) { return; } @@ -58,8 +61,7 @@ export class DataImportService { await this._loadAllFromDatabaseToStore(); await this._persistenceService.clearBackup(); this._imexMetaService.setDataImportInProgress(false); - this._snackService.open({type: 'SUCCESS', msg: T.F.SYNC.S.SUCCESS}); - + this._snackService.open({ type: 'SUCCESS', msg: T.F.SYNC.S.SUCCESS }); } catch (e) { this._snackService.open({ type: 'ERROR', @@ -73,7 +75,7 @@ export class DataImportService { const fixedData = this._dataRepairService.repairData(data); await this.importCompleteSyncData(fixedData, isBackupReload, true); } else { - this._snackService.open({type: 'ERROR', msg: T.F.SYNC.S.ERROR_INVALID_DATA}); + this._snackService.open({ type: 'ERROR', msg: T.F.SYNC.S.ERROR_INVALID_DATA }); console.error(data); this._imexMetaService.setDataImportInProgress(false); } diff --git a/src/app/imex/sync/dialog-dbx-sync-conflict/dialog-sync-conflict.component.ts b/src/app/imex/sync/dialog-dbx-sync-conflict/dialog-sync-conflict.component.ts index d1a4a45de..fda13f64a 100644 --- a/src/app/imex/sync/dialog-dbx-sync-conflict/dialog-sync-conflict.component.ts +++ b/src/app/imex/sync/dialog-dbx-sync-conflict/dialog-sync-conflict.component.ts @@ -8,7 +8,7 @@ import { DialogConflictResolutionResult } from '../sync.model'; selector: 'dialog-sync-conflict', templateUrl: './dialog-sync-conflict.component.html', styleUrls: ['./dialog-sync-conflict.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogSyncConflictComponent { T: typeof T = T; @@ -26,11 +26,12 @@ export class DialogSyncConflictComponent { constructor( private _matDialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: { + @Inject(MAT_DIALOG_DATA) + public data: { remote: number; local: number; lastSync: number; - } + }, ) { _matDialogRef.disableClose = true; } diff --git a/src/app/imex/sync/dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component.ts b/src/app/imex/sync/dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component.ts index 34b2310bf..108077b35 100644 --- a/src/app/imex/sync/dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component.ts +++ b/src/app/imex/sync/dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component.ts @@ -6,7 +6,7 @@ import { T } from 'src/app/t.const'; selector: 'dialog-get-and-enter-auth-code', templateUrl: './dialog-get-and-enter-auth-code.component.html', styleUrls: ['./dialog-get-and-enter-auth-code.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogGetAndEnterAuthCodeComponent { T: typeof T = T; @@ -15,12 +15,12 @@ export class DialogGetAndEnterAuthCodeComponent { constructor( private _matDialogRef: MatDialogRef, // @ts-ignore - @Inject(MAT_DIALOG_DATA) public data: { + @Inject(MAT_DIALOG_DATA) + public data: { providerName: string; url: string; - } - ) { - } + }, + ) {} close(token?: string) { this._matDialogRef.close(token); diff --git a/src/app/imex/sync/dropbox/dropbox-api.service.ts b/src/app/imex/sync/dropbox/dropbox-api.service.ts index 48c67ed41..91fbf9cfd 100644 --- a/src/app/imex/sync/dropbox/dropbox-api.service.ts +++ b/src/app/imex/sync/dropbox/dropbox-api.service.ts @@ -9,11 +9,15 @@ import { stringify } from 'query-string'; import { DropboxFileMetadata } from './dropbox.model'; import { toDropboxIsoString } from './iso-date-without-ms.util.'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class DropboxApiService { - authCode$: Observable = this._globalConfigService.cfg$.pipe(map(cfg => cfg?.sync.dropboxSync.authCode)); + authCode$: Observable = this._globalConfigService.cfg$.pipe( + map((cfg) => cfg?.sync.dropboxSync.authCode), + ); - private _accessToken$: Observable = this._globalConfigService.cfg$.pipe(map(cfg => cfg?.sync.dropboxSync.accessToken)); + private _accessToken$: Observable = this._globalConfigService.cfg$.pipe( + map((cfg) => cfg?.sync.dropboxSync.accessToken), + ); isTokenAvailable$: Observable = this._accessToken$.pipe( map((token) => !!token), @@ -28,8 +32,7 @@ export class DropboxApiService { constructor( private _globalConfigService: GlobalConfigService, private _dataInitService: DataInitService, - ) { - } + ) {} async getMetaData(path: string): Promise { await this._isReady$.toPromise(); @@ -37,26 +40,29 @@ export class DropboxApiService { return this._request({ method: 'POST', url: 'https://api.dropboxapi.com/2/files/get_metadata', - data: {path}, + data: { path }, }).then((res) => res.data); } async download({ path, - localRev - }: { path: string; localRev?: string | null }): Promise<{ meta: DropboxFileMetadata; data: T }> { + localRev, + }: { + path: string; + localRev?: string | null; + }): Promise<{ meta: DropboxFileMetadata; data: T }> { await this._isReady$.toPromise(); return this._request({ method: 'POST', url: 'https://content.dropboxapi.com/2/files/download', headers: { - 'Dropbox-API-Arg': JSON.stringify({path}), + 'Dropbox-API-Arg': JSON.stringify({ path }), // NOTE: doesn't do much, because we rarely get to the case where it would be // useful due to our pre meta checks and because data often changes after // we're checking it. // If it messes up => Check service worker! - ...(localRev ? {'If-None-Match': localRev} : {}), + ...(localRev ? { 'If-None-Match': localRev } : {}), // circumvent: // https://github.com/angular/angular/issues/37133 // https://github.com/johannesjo/super-productivity/issues/645 @@ -64,11 +70,17 @@ export class DropboxApiService { }, }).then((res) => { const meta = JSON.parse(res.headers['dropbox-api-result']); - return {meta, data: res.data}; + return { meta, data: res.data }; }); } - async upload({path, localRev, data, clientModified, isForceOverwrite = false}: { + async upload({ + path, + localRev, + data, + clientModified, + isForceOverwrite = false, + }: { path: string; clientModified?: number; localRev?: string | null; @@ -78,17 +90,17 @@ export class DropboxApiService { await this._isReady$.toPromise(); const args = { - mode: {'.tag': 'overwrite'}, + mode: { '.tag': 'overwrite' }, path, mute: true, - ...((typeof clientModified === 'number') - // we need to use ISO 8601 "combined date and time representation" format: - ? {client_modified: toDropboxIsoString(clientModified)} + ...(typeof clientModified === 'number' + ? // we need to use ISO 8601 "combined date and time representation" format: + { client_modified: toDropboxIsoString(clientModified) } : {}), }; if (localRev && !isForceOverwrite) { - args.mode = {'.tag': 'update', update: localRev} as any; + args.mode = { '.tag': 'update', update: localRev } as any; } return this._request({ @@ -111,7 +123,14 @@ export class DropboxApiService { }).then((res) => res.data); } - async _request({url, method = 'GET', data, headers = {}, params, accessToken}: { + async _request({ + url, + method = 'GET', + data, + headers = {}, + params, + accessToken, + }: { url: string; method?: Method; headers?: { [key: string]: any }; @@ -120,11 +139,10 @@ export class DropboxApiService { accessToken?: string; }): Promise { await this._isReady$.toPromise(); - accessToken = accessToken || await this._accessToken$.pipe(first()).toPromise() || undefined; + accessToken = + accessToken || (await this._accessToken$.pipe(first()).toPromise()) || undefined; return axios.request({ - url: (params && Object.keys(params).length) - ? `${url}?${stringify(params)}` - : url, + url: params && Object.keys(params).length ? `${url}?${stringify(params)}` : url, method, data, headers: { @@ -136,23 +154,25 @@ export class DropboxApiService { } async getAccessTokenFromAuthCode(authCode: string): Promise { - return axios.request({ - url: 'https://api.dropboxapi.com/oauth2/token', - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', - }, - data: stringify({ - code: authCode, - grant_type: 'authorization_code', - client_id: DROPBOX_APP_KEY, - code_verifier: DROPBOX_CODE_VERIFIER, - }), - }).then(res => { - return res.data.access_token; - // Not necessary as it is highly unlikely that we get a wrong on - // const accessToken = res.data.access_token; - // return this.checkUser(accessToken).then(() => accessToken); - }); + return axios + .request({ + url: 'https://api.dropboxapi.com/oauth2/token', + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }, + data: stringify({ + code: authCode, + grant_type: 'authorization_code', + client_id: DROPBOX_APP_KEY, + code_verifier: DROPBOX_CODE_VERIFIER, + }), + }) + .then((res) => { + return res.data.access_token; + // Not necessary as it is highly unlikely that we get a wrong on + // const accessToken = res.data.access_token; + // return this.checkUser(accessToken).then(() => accessToken); + }); } } diff --git a/src/app/imex/sync/dropbox/dropbox-sync.service.ts b/src/app/imex/sync/dropbox/dropbox-sync.service.ts index 1c2e47002..33f8e0ab2 100644 --- a/src/app/imex/sync/dropbox/dropbox-sync.service.ts +++ b/src/app/imex/sync/dropbox/dropbox-sync.service.ts @@ -10,7 +10,7 @@ import { environment } from '../../../../environments/environment'; import { T } from '../../../t.const'; import { SyncProvider, SyncProviderServiceInterface } from '../sync-provider.model'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class DropboxSyncService implements SyncProviderServiceInterface { id: SyncProvider = SyncProvider.Dropbox; isUploadForcePossible: boolean = true; @@ -24,12 +24,13 @@ export class DropboxSyncService implements SyncProviderServiceInterface { private _dropboxApiService: DropboxApiService, private _dataInitService: DataInitService, private _snackService: SnackService, - ) { - } + ) {} // TODO refactor in a way that it doesn't need to trigger uploadAppData itself // NOTE: this does not include milliseconds, which could lead to uncool edge cases... :( - async getRevAndLastClientUpdate(localRev: string): Promise<{ rev: string; clientUpdate: number } | SyncGetRevResult> { + async getRevAndLastClientUpdate( + localRev: string, + ): Promise<{ rev: string; clientUpdate: number } | SyncGetRevResult> { try { const r = await this._dropboxApiService.getMetaData(DROPBOX_SYNC_FILE_PATH); const d = new Date(r.client_modified); @@ -39,10 +40,14 @@ export class DropboxSyncService implements SyncProviderServiceInterface { }; } catch (e) { const isAxiosError = !!(e && e.response && e.response.status); - if (isAxiosError && e.response.data && e.response.data.error_summary === 'path/not_found/..') { + if ( + isAxiosError && + e.response.data && + e.response.data.error_summary === 'path/not_found/..' + ) { return 'NO_REMOTE_DATA'; } else if (isAxiosError && e.response.status === 401) { - this._snackService.open({msg: T.F.DROPBOX.S.AUTH_ERROR, type: 'ERROR'}); + this._snackService.open({ msg: T.F.DROPBOX.S.AUTH_ERROR, type: 'ERROR' }); return 'HANDLED_ERROR'; } else { console.error(e); @@ -55,7 +60,9 @@ export class DropboxSyncService implements SyncProviderServiceInterface { } } - async downloadAppData(localRev: string): Promise<{ rev: string; data: AppDataComplete }> { + async downloadAppData( + localRev: string, + ): Promise<{ rev: string; data: AppDataComplete }> { const r = await this._dropboxApiService.download({ path: DROPBOX_SYNC_FILE_PATH, localRev, @@ -66,14 +73,18 @@ export class DropboxSyncService implements SyncProviderServiceInterface { }; } - async uploadAppData(data: AppDataComplete, localRev: string, isForceOverwrite: boolean = false): Promise { + async uploadAppData( + data: AppDataComplete, + localRev: string, + isForceOverwrite: boolean = false, + ): Promise { try { const r = await this._dropboxApiService.upload({ path: DROPBOX_SYNC_FILE_PATH, data, clientModified: data.lastLocalSyncModelChange, localRev, - isForceOverwrite + isForceOverwrite, }); return r.rev; } catch (e) { diff --git a/src/app/imex/sync/dropbox/dropbox.const.ts b/src/app/imex/sync/dropbox/dropbox.const.ts index f1b64eff6..2b3898094 100644 --- a/src/app/imex/sync/dropbox/dropbox.const.ts +++ b/src/app/imex/sync/dropbox/dropbox.const.ts @@ -3,15 +3,14 @@ import { generatePKCECodes } from './generate-pkce-codes'; export const DROPBOX_APP_KEY = 'm7w85uty7m745ph'; export const DROPBOX_APP_FOLDER = 'super_productivity'; -export const DROPBOX_SYNC_FILE_NAME = environment.production - ? 'sp.json' - : 'sp-dev.json'; +export const DROPBOX_SYNC_FILE_NAME = environment.production ? 'sp.json' : 'sp-dev.json'; export const DROPBOX_SYNC_FILE_PATH = `/${DROPBOX_APP_FOLDER}/${DROPBOX_SYNC_FILE_NAME}`; -const {codeVerifier, codeChallenge} = generatePKCECodes(); +const { codeVerifier, codeChallenge } = generatePKCECodes(); export const DROPBOX_CODE_VERIFIER = codeVerifier; -export const DROPBOX_AUTH_CODE_URL = `https://www.dropbox.com/oauth2/authorize` - + `?response_type=code&client_id=${DROPBOX_APP_KEY}` - + '&code_challenge_method=S256' - + `&code_challenge=${codeChallenge}`; +export const DROPBOX_AUTH_CODE_URL = + `https://www.dropbox.com/oauth2/authorize` + + `?response_type=code&client_id=${DROPBOX_APP_KEY}` + + '&code_challenge_method=S256' + + `&code_challenge=${codeChallenge}`; diff --git a/src/app/imex/sync/dropbox/dropbox.module.ts b/src/app/imex/sync/dropbox/dropbox.module.ts index 9197ffc33..2198d8e0c 100644 --- a/src/app/imex/sync/dropbox/dropbox.module.ts +++ b/src/app/imex/sync/dropbox/dropbox.module.ts @@ -11,8 +11,7 @@ import { UiModule } from '../../../ui/ui.module'; CommonModule, FormsModule, UiModule, - EffectsModule.forFeature([DropboxEffects]) - ] + EffectsModule.forFeature([DropboxEffects]), + ], }) -export class DropboxModule { -} +export class DropboxModule {} diff --git a/src/app/imex/sync/dropbox/generate-pkce-codes.ts b/src/app/imex/sync/dropbox/generate-pkce-codes.ts index 68c8f95b7..70d8e9bb8 100644 --- a/src/app/imex/sync/dropbox/generate-pkce-codes.ts +++ b/src/app/imex/sync/dropbox/generate-pkce-codes.ts @@ -1,7 +1,8 @@ import pkceChallenge from 'pkce-challenge'; -export const generatePKCECodes = (length?: number): { codeVerifier: string; codeChallenge: string } => { - const {code_verifier, code_challenge} = pkceChallenge(length); - return {codeVerifier: code_verifier, codeChallenge: code_challenge}; +export const generatePKCECodes = ( + length?: number, +): { codeVerifier: string; codeChallenge: string } => { + const { code_verifier, code_challenge } = pkceChallenge(length); + return { codeVerifier: code_verifier, codeChallenge: code_challenge }; }; - diff --git a/src/app/imex/sync/dropbox/store/dropbox.effects.ts b/src/app/imex/sync/dropbox/store/dropbox.effects.ts index b4526cc0c..e80255cee 100644 --- a/src/app/imex/sync/dropbox/store/dropbox.effects.ts +++ b/src/app/imex/sync/dropbox/store/dropbox.effects.ts @@ -2,9 +2,18 @@ import { Injectable } from '@angular/core'; import { Actions, Effect, ofType } from '@ngrx/effects'; import { GlobalConfigActionTypes, - UpdateGlobalConfigSection + UpdateGlobalConfigSection, } from '../../../../features/config/store/global-config.actions'; -import { catchError, filter, map, pairwise, shareReplay, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { + catchError, + filter, + map, + pairwise, + shareReplay, + switchMap, + tap, + withLatestFrom, +} from 'rxjs/operators'; import { DropboxApiService } from '../dropbox-api.service'; import { DataInitService } from '../../../../core/data-init/data-init.service'; import { EMPTY, from, Observable } from 'rxjs'; @@ -24,44 +33,59 @@ export class DropboxEffects { ); @Effect() getAuthTokenFromAccessCode: any = this._actions$.pipe( - ofType( - GlobalConfigActionTypes.UpdateGlobalConfigSection, + ofType(GlobalConfigActionTypes.UpdateGlobalConfigSection), + filter( + ({ payload }: UpdateGlobalConfigSection): boolean => payload.sectionKey === 'sync', ), - filter(({payload}: UpdateGlobalConfigSection): boolean => payload.sectionKey === 'sync'), - map(({payload}) => payload.sectionCfg as SyncConfig), - filter(syncConfig => syncConfig.syncProvider === SyncProvider.Dropbox), + map(({ payload }) => payload.sectionCfg as SyncConfig), + filter((syncConfig) => syncConfig.syncProvider === SyncProvider.Dropbox), withLatestFrom(this._isChangedAuthCode$), switchMap(([syncConfig, isChanged]: [SyncConfig, boolean]) => { if (isChanged && typeof syncConfig.dropboxSync.authCode === 'string') { - return from(this._dropboxApiService.getAccessTokenFromAuthCode(syncConfig.dropboxSync.authCode)).pipe( + return from( + this._dropboxApiService.getAccessTokenFromAuthCode( + syncConfig.dropboxSync.authCode, + ), + ).pipe( // NOTE: catch needs to be limited to request only, otherwise we break the chain catchError((e) => { console.error(e); - this._snackService.open({type: 'ERROR', msg: T.F.DROPBOX.S.ACCESS_TOKEN_ERROR}); + this._snackService.open({ + type: 'ERROR', + msg: T.F.DROPBOX.S.ACCESS_TOKEN_ERROR, + }); // filter return EMPTY; }), - map((accessToken) => ({accessToken, sync: syncConfig as SyncConfig})), + map((accessToken) => ({ accessToken, sync: syncConfig as SyncConfig })), ); } else { return EMPTY; } }), - tap((): any => setTimeout(() => this._snackService.open({ - type: 'SUCCESS', - msg: T.F.DROPBOX.S.ACCESS_TOKEN_GENERATED - }), 200) + tap((): any => + setTimeout( + () => + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.DROPBOX.S.ACCESS_TOKEN_GENERATED, + }), + 200, + ), + ), + map( + ({ accessToken, sync }: { accessToken: string; sync: SyncConfig }) => + new UpdateGlobalConfigSection({ + sectionKey: 'sync', + sectionCfg: { + ...sync, + dropboxSync: { + ...sync.dropboxSync, + accessToken, + }, + } as SyncConfig, + }), ), - map(({accessToken, sync}: { accessToken: string; sync: SyncConfig }) => new UpdateGlobalConfigSection({ - sectionKey: 'sync', - sectionCfg: ({ - ...sync, - dropboxSync: { - ...sync.dropboxSync, - accessToken - } - } as SyncConfig) - })), ); constructor( @@ -69,6 +93,5 @@ export class DropboxEffects { private _dropboxApiService: DropboxApiService, private _snackService: SnackService, private _dataInitService: DataInitService, - ) { - } + ) {} } diff --git a/src/app/imex/sync/get-sync-error-str.ts b/src/app/imex/sync/get-sync-error-str.ts index 8536a0f36..34aa44aca 100644 --- a/src/app/imex/sync/get-sync-error-str.ts +++ b/src/app/imex/sync/get-sync-error-str.ts @@ -3,10 +3,13 @@ import { HANDLED_ERROR_PROP_STR } from '../../app.constants'; // ugly little helper to make sure we get the most information out of it for the user export const getSyncErrorStr = (err: unknown): string => { - let errorAsString: string = err && (err as any)?.toString - ? (err as any).toString() - : '???'; - if (errorAsString === '[object Object]' && err && (err as any)[HANDLED_ERROR_PROP_STR]) { + let errorAsString: string = + err && (err as any)?.toString ? (err as any).toString() : '???'; + if ( + errorAsString === '[object Object]' && + err && + (err as any)[HANDLED_ERROR_PROP_STR] + ) { errorAsString = (err as any)[HANDLED_ERROR_PROP_STR] as string; } return truncate(errorAsString.toString(), 100); diff --git a/src/app/imex/sync/google/get-google-auth-url.ts b/src/app/imex/sync/google/get-google-auth-url.ts index a73052b5d..97ab62255 100644 --- a/src/app/imex/sync/google/get-google-auth-url.ts +++ b/src/app/imex/sync/google/get-google-auth-url.ts @@ -2,12 +2,14 @@ import * as querystring from 'querystring'; import { generatePKCECodes } from '../dropbox/generate-pkce-codes'; import { GOOGLE_API_SCOPES_ARRAY, GOOGLE_SETTINGS_ELECTRON } from './google.const'; -const {codeVerifier} = generatePKCECodes(80); +const { codeVerifier } = generatePKCECodes(80); export const GOOGLE_AUTH_CODE_VERIFIER = codeVerifier; const _getGoogleAuthUrl = (opts: any = {}) => { if (opts.code_challenge_method && !opts.code_challenge) { - throw new Error('If a code_challenge_method is provided, code_challenge must be included.'); + throw new Error( + 'If a code_challenge_method is provided, code_challenge must be included.', + ); } opts.response_type = opts.response_type || 'code'; opts.client_id = opts.client_id || GOOGLE_SETTINGS_ELECTRON.CLIENT_ID; @@ -20,13 +22,14 @@ const _getGoogleAuthUrl = (opts: any = {}) => { return rootUrl + '?' + querystring.stringify(opts); }; -export const getGoogleAuthUrl = (opts = {}) => _getGoogleAuthUrl({ - access_type: 'offline', - scope: GOOGLE_API_SCOPES_ARRAY, - redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - // TODO make real code challenge work - // code_challenge: codeChallenge, - code_challenge: codeVerifier, -}); +export const getGoogleAuthUrl = (opts = {}) => + _getGoogleAuthUrl({ + access_type: 'offline', + scope: GOOGLE_API_SCOPES_ARRAY, + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + // TODO make real code challenge work + // code_challenge: codeChallenge, + code_challenge: codeVerifier, + }); export const GOOGLE_AUTH_URL = getGoogleAuthUrl(); diff --git a/src/app/imex/sync/google/google-api.service.ts b/src/app/imex/sync/google/google-api.service.ts index ccac6ce8f..81f86dd64 100644 --- a/src/app/imex/sync/google/google-api.service.ts +++ b/src/app/imex/sync/google/google-api.service.ts @@ -4,7 +4,7 @@ import { GOOGLE_DEFAULT_FIELDS_FOR_DRIVE, GOOGLE_DISCOVERY_DOCS, GOOGLE_SETTINGS_ELECTRON, - GOOGLE_SETTINGS_WEB + GOOGLE_SETTINGS_WEB, } from './google.const'; import * as moment from 'moment'; import { HANDLED_ERROR_PROP_STR, IS_ELECTRON } from '../../../app.constants'; @@ -12,8 +12,25 @@ import { MultiPartBuilder } from './util/multi-part-builder'; import { HttpClient, HttpHeaders, HttpParams, HttpRequest } from '@angular/common/http'; import { SnackService } from '../../../core/snack/snack.service'; import { SnackType } from '../../../core/snack/snack.model'; -import { catchError, concatMap, filter, map, shareReplay, switchMap, take } from 'rxjs/operators'; -import { BehaviorSubject, EMPTY, from, merge, Observable, of, throwError, timer } from 'rxjs'; +import { + catchError, + concatMap, + filter, + map, + shareReplay, + switchMap, + take, +} from 'rxjs/operators'; +import { + BehaviorSubject, + EMPTY, + from, + merge, + Observable, + of, + throwError, + timer, +} from 'rxjs'; import { BannerService } from '../../../core/banner/banner.service'; import { BannerId } from '../../../core/banner/banner.model'; import { T } from '../../../t.const'; @@ -35,19 +52,19 @@ const EXPIRES_SAFETY_MARGIN = 5 * 60 * 1000; }) export class GoogleApiService { public isLoggedIn: boolean = false; - private _session$: BehaviorSubject = new BehaviorSubject(getGoogleSession()); + private _session$: BehaviorSubject = new BehaviorSubject( + getGoogleSession(), + ); private _onTokenExpire$: Observable = this._session$.pipe( switchMap((session) => { if (!session.accessToken) { return EMPTY; } - const expiresAt = session && session.expiresAt || 0; + const expiresAt = (session && session.expiresAt) || 0; const expiresIn = expiresAt - (moment().valueOf() + EXPIRES_SAFETY_MARGIN); - return this._isTokenExpired(session) - ? timer(expiresIn) - : EMPTY; - }) + return this._isTokenExpired(session) ? timer(expiresIn) : EMPTY; + }), ); public isLoggedIn$: Observable = merge( this._session$, @@ -75,7 +92,7 @@ export class GoogleApiService { private readonly _bannerService: BannerService, private readonly _matDialog: MatDialog, ) { - this.isLoggedIn$.subscribe((isLoggedIn) => this.isLoggedIn = isLoggedIn); + this.isLoggedIn$.subscribe((isLoggedIn) => (this.isLoggedIn = isLoggedIn)); } private get _session(): GoogleSession { @@ -84,7 +101,7 @@ export class GoogleApiService { login(isSkipSuccessMsg: boolean = false): Promise { const showSuccessMsg = () => { - if (!(isSkipSuccessMsg)) { + if (!isSkipSuccessMsg) { this._snackIt('SUCCESS', T.F.GOOGLE.S_API.SUCCESS_LOGIN); } }; @@ -97,36 +114,41 @@ export class GoogleApiService { this._saveToken({ access_token: token, // TODO check if we can get a real value if existant - expires_at: Date.now() + (1000 * 60 * 30), + expires_at: Date.now() + 1000 * 60 * 30, }); showSuccessMsg(); }); } else { - return this._initClientLibraryIfNotDone() - .then((user: any) => { - // TODO implement offline access - // const authInstance = this._gapi.auth2.getAuthInstance(); - // authInstance.grantOfflineAccess() - // .then((res) => { - // this._updateSession({ - // refreshToken: res.code - // }); - // }); - const successHandler = (res: any) => { - this._saveToken(res); - showSuccessMsg(); - }; + return this._initClientLibraryIfNotDone().then((user: any) => { + // TODO implement offline access + // const authInstance = this._gapi.auth2.getAuthInstance(); + // authInstance.grantOfflineAccess() + // .then((res) => { + // this._updateSession({ + // refreshToken: res.code + // }); + // }); + const successHandler = (res: any) => { + this._saveToken(res); + showSuccessMsg(); + }; - if (user && user.Zi && user.Zi.access_token) { - successHandler(user); - } else { - return this._gapi.auth2.getAuthInstance().currentUser.get().reloadAuthResponse().then(successHandler.bind(this)) - .catch(() => { - return this._gapi.auth2.getAuthInstance().signIn() - .then(successHandler.bind(this)); - }); - } - }); + if (user && user.Zi && user.Zi.access_token) { + successHandler(user); + } else { + return this._gapi.auth2 + .getAuthInstance() + .currentUser.get() + .reloadAuthResponse() + .then(successHandler.bind(this)) + .catch(() => { + return this._gapi.auth2 + .getAuthInstance() + .signIn() + .then(successHandler.bind(this)); + }); + } + }); } } @@ -157,7 +179,7 @@ export class GoogleApiService { getFileInfo$(fileId: string | null): Observable { if (!fileId) { this._snackIt('ERROR', T.F.GOOGLE.S_API.ERR_NO_FILE_ID); - return throwError({[HANDLED_ERROR_PROP_STR]: 'No file id given'}); + return throwError({ [HANDLED_ERROR_PROP_STR]: 'No file id given' }); } return this._mapHttp$({ @@ -166,7 +188,7 @@ export class GoogleApiService { params: { key: GOOGLE_SETTINGS_WEB.API_KEY, supportsTeamDrives: true, - fields: GOOGLE_DEFAULT_FIELDS_FOR_DRIVE + fields: GOOGLE_DEFAULT_FIELDS_FOR_DRIVE, }, }); } @@ -174,7 +196,7 @@ export class GoogleApiService { findFile$(fileName: string): Observable { if (!fileName) { this._snackIt('ERROR', T.F.GOOGLE.S_API.ERR_NO_FILE_NAME); - return throwError({[HANDLED_ERROR_PROP_STR]: 'No file name given'}); + return throwError({ [HANDLED_ERROR_PROP_STR]: 'No file name given' }); } return this._mapHttp$({ @@ -189,32 +211,36 @@ export class GoogleApiService { } // NOTE: file will always be returned as text (makes sense) - loadFile$(fileId: string | null): Observable<{ backup: string | undefined; meta: GoogleDriveFileMeta }> { + loadFile$( + fileId: string | null, + ): Observable<{ backup: string | undefined; meta: GoogleDriveFileMeta }> { if (!fileId) { this._snackIt('ERROR', T.F.GOOGLE.S_API.ERR_NO_FILE_ID); - return throwError({[HANDLED_ERROR_PROP_STR]: 'No file id given'}); + return throwError({ [HANDLED_ERROR_PROP_STR]: 'No file id given' }); } return this.getFileInfo$(fileId).pipe( - concatMap((meta) => this._mapHttp$({ - method: 'GET', - // workaround for: https://issuetracker.google.com/issues/149891169 - url: `https://www.googleapis.com/drive/v2/files/${encodeURIComponent(fileId)}`, - params: { - key: GOOGLE_SETTINGS_WEB.API_KEY, - supportsTeamDrives: true, - alt: 'media', - }, - responseType: 'text', - }).pipe( - map((res) => { - // console.log('GOOGLE RES', res); - return { - backup: res, - meta, - }; - }), - )), + concatMap((meta) => + this._mapHttp$({ + method: 'GET', + // workaround for: https://issuetracker.google.com/issues/149891169 + url: `https://www.googleapis.com/drive/v2/files/${encodeURIComponent(fileId)}`, + params: { + key: GOOGLE_SETTINGS_WEB.API_KEY, + supportsTeamDrives: true, + alt: 'media', + }, + responseType: 'text', + }).pipe( + map((res) => { + // console.log('GOOGLE RES', res); + return { + backup: res, + meta, + }; + }), + ), + ), ); } @@ -234,7 +260,7 @@ export class GoogleApiService { metadata.mimeType = 'application/json'; } - const multipart: any = (new (MultiPartBuilder as any)()) + const multipart: any = new (MultiPartBuilder as any)() .append('application/json', JSON.stringify(metadata)) .append(metadata.mimeType, content) .finish(); @@ -246,12 +272,12 @@ export class GoogleApiService { key: GOOGLE_SETTINGS_WEB.API_KEY, uploadType: 'multipart', supportsTeamDrives: true, - fields: GOOGLE_DEFAULT_FIELDS_FOR_DRIVE + fields: GOOGLE_DEFAULT_FIELDS_FOR_DRIVE, }, headers: { - 'Content-Type': multipart.type + 'Content-Type': multipart.type, }, - data: multipart.body + data: multipart.body, }); } @@ -259,9 +285,12 @@ export class GoogleApiService { const session = this._session; if (this.isLoggedIn && !this._isTokenExpired(session)) { return new Promise((resolve) => resolve(true)); - } else if (session.refreshToken && (!this._session.accessToken || this._isTokenExpired(session))) { + } else if ( + session.refreshToken && + (!this._session.accessToken || this._isTokenExpired(session)) + ) { try { - const {data} = await this._getAccessTokenFromRefreshToken(session.refreshToken); + const { data } = await this._getAccessTokenFromRefreshToken(session.refreshToken); if (data) { this._updateSession({ accessToken: data.access_token, @@ -275,16 +304,19 @@ export class GoogleApiService { return Promise.reject('Token refresh failed'); } } else { - const authCode = await this._matDialog.open(DialogGetAndEnterAuthCodeComponent, { - restoreFocus: true, - data: { - providerName: 'Google Drive', - url: GOOGLE_AUTH_URL, - }, - }).afterClosed().toPromise(); + const authCode = await this._matDialog + .open(DialogGetAndEnterAuthCodeComponent, { + restoreFocus: true, + data: { + providerName: 'Google Drive', + url: GOOGLE_AUTH_URL, + }, + }) + .afterClosed() + .toPromise(); if (authCode) { try { - const {data} = await this._getTokenFromAuthCode(authCode); + const { data } = await this._getTokenFromAuthCode(authCode); if (data) { this._updateSession({ accessToken: data.access_token, @@ -303,39 +335,51 @@ export class GoogleApiService { } } - private _getTokenFromAuthCode(code: string): Promise> { + private _getTokenFromAuthCode( + code: string, + ): Promise< + AxiosResponse<{ + access_token: string; + expires_in: number; + token_type: string; + scope: string; + refresh_token: string; + }> + > { return axios.request({ - url: 'https://oauth2.googleapis.com/token?' + querystring.stringify({ - client_id: GOOGLE_SETTINGS_ELECTRON.CLIENT_ID, - client_secret: GOOGLE_SETTINGS_ELECTRON.API_KEY, - grant_type: 'authorization_code', - redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', - code_verifier: GOOGLE_AUTH_CODE_VERIFIER, - code, - }), + url: + 'https://oauth2.googleapis.com/token?' + + querystring.stringify({ + client_id: GOOGLE_SETTINGS_ELECTRON.CLIENT_ID, + client_secret: GOOGLE_SETTINGS_ELECTRON.API_KEY, + grant_type: 'authorization_code', + redirect_uri: 'urn:ietf:wg:oauth:2.0:oob', + code_verifier: GOOGLE_AUTH_CODE_VERIFIER, + code, + }), method: 'POST', }); } - private _getAccessTokenFromRefreshToken(refreshToken: string): Promise> { + private _getAccessTokenFromRefreshToken( + refreshToken: string, + ): Promise< + AxiosResponse<{ + access_token: string; + expires_in: number; + token_type: string; + scope: string; + }> + > { return axios.request({ - url: 'https://oauth2.googleapis.com/token?' + querystring.stringify({ - client_id: GOOGLE_SETTINGS_ELECTRON.CLIENT_ID, - client_secret: GOOGLE_SETTINGS_ELECTRON.API_KEY, - grant_type: 'refresh_token', - refresh_token: refreshToken, - }), + url: + 'https://oauth2.googleapis.com/token?' + + querystring.stringify({ + client_id: GOOGLE_SETTINGS_ELECTRON.CLIENT_ID, + client_secret: GOOGLE_SETTINGS_ELECTRON.API_KEY, + grant_type: 'refresh_token', + refresh_token: refreshToken, + }), method: 'POST', }); } @@ -353,7 +397,7 @@ export class GoogleApiService { apiKey: GOOGLE_SETTINGS_WEB.API_KEY, clientId: GOOGLE_SETTINGS_WEB.CLIENT_ID, discoveryDocs: GOOGLE_DISCOVERY_DOCS, - scope: GOOGLE_API_SCOPES + scope: GOOGLE_API_SCOPES, }); } @@ -371,23 +415,25 @@ export class GoogleApiService { this._snackService.open({ type: 'ERROR', msg: T.G.NO_CON, - ico: 'cloud_off' + ico: 'cloud_off', }); return Promise.reject('No internet'); } return new Promise((resolve, reject) => { - return this._loadJs().then(() => { - // eslint-disable-next-line - this._gapi = (window as any)['gapi']; - this._gapi.load('client:auth2', () => { - this.initClient() - .then(() => { - resolve(getUser()); - }) - .catch(reject); - }); - }).catch(reject); + return this._loadJs() + .then(() => { + // eslint-disable-next-line + this._gapi = (window as any)['gapi']; + this._gapi.load('client:auth2', () => { + this.initClient() + .then(() => { + resolve(getUser()); + }) + .catch(reject); + }); + }) + .catch(reject); }); } @@ -404,16 +450,26 @@ export class GoogleApiService { }; }) { const r: any = res; - const accessToken = r.accessToken || r.access_token || r.Zi?.access_token || r.uc?.access_token; - const expiresAt = +(r.expiresAt || r.expires_at || r.Zi?.expires_at || r.uc?.expires_at || Date.now() + r.expire_in); + const accessToken = + r.accessToken || r.access_token || r.Zi?.access_token || r.uc?.access_token; + const expiresAt = +( + r.expiresAt || + r.expires_at || + r.Zi?.expires_at || + r.uc?.expires_at || + Date.now() + r.expire_in + ); if (!accessToken) { console.log(res); throw new Error('No access token in response'); } - if (accessToken !== this._session.accessToken || expiresAt !== this._session.expiresAt) { - this._updateSession({accessToken, expiresAt}); + if ( + accessToken !== this._session.accessToken || + expiresAt !== this._session.expiresAt + ) { + this._updateSession({ accessToken, expiresAt }); } } @@ -425,8 +481,8 @@ export class GoogleApiService { id: BannerId.GoogleLogin, action: { label: T.G.LOGIN, - fn: () => this.login() - } + fn: () => this.login(), + }, }); console.error(err); } @@ -448,7 +504,7 @@ export class GoogleApiService { this._handleUnAuthenticated(err); } else { console.warn(err); - this._snackIt('ERROR', T.F.GOOGLE.S_API.ERR, {errStr}); + this._snackIt('ERROR', T.F.GOOGLE.S_API.ERR, { errStr }); } } @@ -470,65 +526,69 @@ export class GoogleApiService { } else { return of(true); } - }) + }), ); - return from(loginObs) - .pipe( - concatMap(() => { - const p: any = { - ...paramsIN, - headers: { - ...(paramsIN.headers || {}), - Authorization: `Bearer ${this._session.accessToken}`, - } - }; + return from(loginObs).pipe( + concatMap(() => { + const p: any = { + ...paramsIN, + headers: { + ...(paramsIN.headers || {}), + Authorization: `Bearer ${this._session.accessToken}`, + }, + }; - const bodyArg = p.data ? [p.data] : []; - const allArgs = [...bodyArg, { + const bodyArg = p.data ? [p.data] : []; + const allArgs = [ + ...bodyArg, + { headers: new HttpHeaders(p.headers), params: new HttpParams({ fromObject: { ...p.params, // needed because negative globs are not working as they should // @see https://github.com/angular/angular/issues/21191 - 'ngsw-bypass': true - } + 'ngsw-bypass': true, + }, }), reportProgress: false, observe: 'response', responseType: paramsIN.responseType, - }]; - const req = new HttpRequest(p.method, p.url, ...allArgs); - return this._http.request(req); - }), + }, + ]; + const req = new HttpRequest(p.method, p.url, ...allArgs); + return this._http.request(req); + }), - // TODO remove type: 0 @see https://brianflove.com/2018/09/03/angular-http-client-observe-response/ - // tap(res => console.log(res)), - filter(res => !(res === Object(res) && res.type === 0)), - map((res: any) => (res && res.body) ? res.body : res), - catchError((res) => { - if (!isOnline()) { - this._snackService.open({ - type: 'ERROR', - msg: T.G.NO_CON, - ico: 'cloud_off' - }); - } else if (!res) { - this._handleError('No response body'); - } else if (res && res.status === 401) { - this._handleUnAuthenticated(res); - return throwError({ - [HANDLED_ERROR_PROP_STR]: 'Auth Error ' + res.status + ': ' + res.message - }); - } else if (res && (res.status >= 300)) { - this._handleError(res); - } else if (res && (res.status >= 0)) { - this._handleError('Could not connect to google. Check your internet connection.'); - } - return throwError({[HANDLED_ERROR_PROP_STR]: res}); - }), - ); + // TODO remove type: 0 @see https://brianflove.com/2018/09/03/angular-http-client-observe-response/ + // tap(res => console.log(res)), + filter((res) => !(res === Object(res) && res.type === 0)), + map((res: any) => (res && res.body ? res.body : res)), + catchError((res) => { + if (!isOnline()) { + this._snackService.open({ + type: 'ERROR', + msg: T.G.NO_CON, + ico: 'cloud_off', + }); + } else if (!res) { + this._handleError('No response body'); + } else if (res && res.status === 401) { + this._handleUnAuthenticated(res); + return throwError({ + [HANDLED_ERROR_PROP_STR]: 'Auth Error ' + res.status + ': ' + res.message, + }); + } else if (res && res.status >= 300) { + this._handleError(res); + } else if (res && res.status >= 0) { + this._handleError( + 'Could not connect to google. Check your internet connection.', + ); + } + return throwError({ [HANDLED_ERROR_PROP_STR]: res }); + }), + ); } private _loadJs(): Promise { @@ -564,11 +624,10 @@ export class GoogleApiService { // script['onreadystatechange'] = loadFunction.bind(this); document.getElementsByTagName('head')[0].appendChild(script); }); - } private _isTokenExpired(session: any): boolean { - const expiresAt = session && session.expiresAt || 0; + const expiresAt = (session && session.expiresAt) || 0; const expiresIn = expiresAt - (moment().valueOf() + EXPIRES_SAFETY_MARGIN); return expiresIn <= 0; } diff --git a/src/app/imex/sync/google/google-drive-sync.service.ts b/src/app/imex/sync/google/google-drive-sync.service.ts index 5347269eb..9ebea0add 100644 --- a/src/app/imex/sync/google/google-drive-sync.service.ts +++ b/src/app/imex/sync/google/google-drive-sync.service.ts @@ -16,12 +16,19 @@ import { IS_F_DROID_APP } from '../../../util/is-android-web-view'; export class GoogleDriveSyncService implements SyncProviderServiceInterface { id: SyncProvider = SyncProvider.GoogleDrive; - cfg$: Observable = this._globalConfigService.cfg$.pipe(map(cfg => cfg.sync.googleDriveSync)); + cfg$: Observable = this._globalConfigService.cfg$.pipe( + map((cfg) => cfg.sync.googleDriveSync), + ); isReady$: Observable = this._dataInitService.isAllDataLoadedInitially$.pipe( concatMap(() => this._googleApiService.isLoggedIn$), - concatMap(() => this.cfg$.pipe( - map(cfg => cfg.syncFileName === cfg._syncFileNameForBackupDocId && !!cfg._backupDocId)), + concatMap(() => + this.cfg$.pipe( + map( + (cfg) => + cfg.syncFileName === cfg._syncFileNameForBackupDocId && !!cfg._backupDocId, + ), + ), ), distinctUntilChanged(), ); @@ -37,17 +44,21 @@ export class GoogleDriveSyncService implements SyncProviderServiceInterface { // if (IS_ELECTRON) { // } // }, 45 * 1000); - } - async getRevAndLastClientUpdate(localRev: string): Promise<{ rev: string; clientUpdate: number } | SyncGetRevResult> { + async getRevAndLastClientUpdate( + localRev: string, + ): Promise<{ rev: string; clientUpdate: number } | SyncGetRevResult> { if (IS_F_DROID_APP) { throw new Error('Google Drive Sync not supported on FDroid'); } const cfg = await this.cfg$.pipe(first()).toPromise(); const fileId = cfg._backupDocId; - const r: any = await this._googleApiService.getFileInfo$(fileId).pipe(first()).toPromise(); + const r: any = await this._googleApiService + .getFileInfo$(fileId) + .pipe(first()) + .toPromise(); const d = new Date(r.client_modified); // TODO error handling if we happen to need it return { @@ -62,16 +73,21 @@ export class GoogleDriveSyncService implements SyncProviderServiceInterface { } const cfg = await this.cfg$.pipe(first()).toPromise(); - const {backup, meta} = await this._googleApiService.loadFile$(cfg._backupDocId).pipe(first()).toPromise(); + const { backup, meta } = await this._googleApiService + .loadFile$(cfg._backupDocId) + .pipe(first()) + .toPromise(); // console.log(backup, meta); - const data = !!backup - ? await this._decodeAppDataIfNeeded(backup) - : undefined; - return {rev: meta.md5Checksum as string, data}; + const data = !!backup ? await this._decodeAppDataIfNeeded(backup) : undefined; + return { rev: meta.md5Checksum as string, data }; } - async uploadAppData(data: AppDataComplete, localRev: string, isForceOverwrite: boolean = false): Promise { + async uploadAppData( + data: AppDataComplete, + localRev: string, + isForceOverwrite: boolean = false, + ): Promise { if (IS_F_DROID_APP) { throw new Error('Google Drive Sync not supported on FDroid'); } @@ -82,12 +98,14 @@ export class GoogleDriveSyncService implements SyncProviderServiceInterface { const uploadData = cfg.isCompressData ? await this._compressionService.compressUTF16(JSON.stringify(data)) : JSON.stringify(data); - const r = await this._googleApiService.saveFile$(uploadData, { - title: cfg.syncFileName, - id: cfg._backupDocId, - editable: true, - mimeType: cfg.isCompressData ? 'text/plain' : 'application/json', - }).toPromise(); + const r = await this._googleApiService + .saveFile$(uploadData, { + title: cfg.syncFileName, + id: cfg._backupDocId, + editable: true, + mimeType: cfg.isCompressData ? 'text/plain' : 'application/json', + }) + .toPromise(); if (!(r as any).md5Checksum) { throw new Error('No md5Checksum'); } @@ -98,7 +116,9 @@ export class GoogleDriveSyncService implements SyncProviderServiceInterface { } } - private async _decodeAppDataIfNeeded(backupStr: string | AppDataComplete): Promise { + private async _decodeAppDataIfNeeded( + backupStr: string | AppDataComplete, + ): Promise { let backupData: AppDataComplete | undefined; // we attempt this regardless of the option, because data might be compressed anyway @@ -107,7 +127,9 @@ export class GoogleDriveSyncService implements SyncProviderServiceInterface { backupData = JSON.parse(backupStr) as AppDataComplete; } catch (e) { try { - const decompressedData = await this._compressionService.decompressUTF16(backupStr); + const decompressedData = await this._compressionService.decompressUTF16( + backupStr, + ); backupData = JSON.parse(decompressedData) as AppDataComplete; } catch (ex) { console.error('Drive Sync, invalid data'); diff --git a/src/app/imex/sync/google/google-session.ts b/src/app/imex/sync/google/google-session.ts index f29df5f09..24350468c 100644 --- a/src/app/imex/sync/google/google-session.ts +++ b/src/app/imex/sync/google/google-session.ts @@ -11,11 +11,11 @@ export type GoogleSession = Readonly<{ const DEFAULT_GOOGLE_SESSION: GoogleSession = { accessToken: null, refreshToken: null, - expiresAt: null + expiresAt: null, }; export const getGoogleSession = (): GoogleSession => { - return loadFromRealLs(LS_GOOGLE_SESSION) as GoogleSession || DEFAULT_GOOGLE_SESSION; + return (loadFromRealLs(LS_GOOGLE_SESSION) as GoogleSession) || DEFAULT_GOOGLE_SESSION; }; export const updateGoogleSession = (googleSession: Partial) => { diff --git a/src/app/imex/sync/google/google.module.ts b/src/app/imex/sync/google/google.module.ts index b9e3a14ec..2240db354 100644 --- a/src/app/imex/sync/google/google.module.ts +++ b/src/app/imex/sync/google/google.module.ts @@ -15,5 +15,4 @@ import { GoogleDriveSyncEffects } from './store/google-drive-sync.effects'; declarations: [], exports: [], }) -export class GoogleModule { -} +export class GoogleModule {} diff --git a/src/app/imex/sync/google/store/google-drive-sync.effects.ts b/src/app/imex/sync/google/store/google-drive-sync.effects.ts index c85eb005e..881be129d 100644 --- a/src/app/imex/sync/google/store/google-drive-sync.effects.ts +++ b/src/app/imex/sync/google/store/google-drive-sync.effects.ts @@ -4,7 +4,7 @@ import { catchError, concatMap, filter, map, switchMap, tap } from 'rxjs/operato import { Actions, Effect, ofType } from '@ngrx/effects'; import { GlobalConfigActionTypes, - UpdateGlobalConfigSection + UpdateGlobalConfigSection, } from '../../../../features/config/store/global-config.actions'; import { SyncConfig } from '../../../../features/config/global-config.model'; import { SnackService } from '../../../../core/snack/snack.service'; @@ -40,103 +40,130 @@ import { HANDLED_ERROR_PROP_STR } from '../../../../app.constants'; @Injectable() export class GoogleDriveSyncEffects { @Effect() createSyncFile$: any = this._actions$.pipe( - ofType( - GlobalConfigActionTypes.UpdateGlobalConfigSection, + ofType(GlobalConfigActionTypes.UpdateGlobalConfigSection), + filter( + ({ payload }: UpdateGlobalConfigSection): boolean => payload.sectionKey === 'sync', ), - filter(({payload}: UpdateGlobalConfigSection): boolean => payload.sectionKey === 'sync'), - map(({payload}) => payload.sectionCfg as SyncConfig), - switchMap((syncConfig: SyncConfig): Observable<{ - syncFileName: string; - _backupDocId: string; - _syncFileNameForBackupDocId: string; - sync: SyncConfig; - } | never> => { - const isChanged = (syncConfig.googleDriveSync.syncFileName !== syncConfig.googleDriveSync._syncFileNameForBackupDocId); - if (syncConfig.syncProvider === SyncProvider.GoogleDrive - && (isChanged || !syncConfig.googleDriveSync._backupDocId) - && syncConfig.googleDriveSync.syncFileName.length > 0 - ) { - const newFileName = syncConfig.googleDriveSync.syncFileName || DEFAULT_SYNC_FILE_NAME; - return this._googleApiService.findFile$(newFileName).pipe( - concatMap((res: any): any => { - const filesFound = res.items; - if (filesFound.length && filesFound.length > 1) { + map(({ payload }) => payload.sectionCfg as SyncConfig), + switchMap( + ( + syncConfig: SyncConfig, + ): Observable< + | { + syncFileName: string; + _backupDocId: string; + _syncFileNameForBackupDocId: string; + sync: SyncConfig; + } + | never + > => { + const isChanged = + syncConfig.googleDriveSync.syncFileName !== + syncConfig.googleDriveSync._syncFileNameForBackupDocId; + if ( + syncConfig.syncProvider === SyncProvider.GoogleDrive && + (isChanged || !syncConfig.googleDriveSync._backupDocId) && + syncConfig.googleDriveSync.syncFileName.length > 0 + ) { + const newFileName = + syncConfig.googleDriveSync.syncFileName || DEFAULT_SYNC_FILE_NAME; + return this._googleApiService.findFile$(newFileName).pipe( + concatMap((res: any): any => { + const filesFound = res.items; + if (filesFound.length && filesFound.length > 1) { + this._snackService.open({ + type: 'ERROR', + msg: T.F.GOOGLE.S.MULTIPLE_SYNC_FILES_WITH_SAME_NAME, + translateParams: { newFileName }, + }); + return EMPTY; + } else if (!filesFound || filesFound.length === 0) { + return this._confirmSaveNewFile$(newFileName).pipe( + concatMap((isSave) => { + return !isSave + ? EMPTY + : this._googleApiService.saveFile$('', { + title: newFileName, + editable: true, + }); + }), + map((res2: any) => ({ + syncFileName: res2.title, + _syncFileNameForBackupDocId: res2.title, + _backupDocId: res2.id, + sync: syncConfig, + })), + ); + } else if (filesFound.length === 1) { + return this._confirmUsingExistingFileDialog$(newFileName).pipe( + concatMap((isConfirmUseExisting) => { + const fileToUpdate = filesFound[0]; + return isConfirmUseExisting + ? of({ + syncFileName: newFileName, + _syncFileNameForBackupDocId: newFileName, + _backupDocId: fileToUpdate.id, + sync: syncConfig, + }) + : EMPTY; + }), + ); + } + return EMPTY; + }), + map((v) => v as any), + catchError((err: any) => { this._snackService.open({ type: 'ERROR', - msg: T.F.GOOGLE.S.MULTIPLE_SYNC_FILES_WITH_SAME_NAME, - translateParams: {newFileName}, + msg: T.F.GOOGLE.S.SYNC_FILE_CREATION_ERROR, + translateParams: { err: this._getApiErrorString(err) }, }); - return EMPTY; - } else if (!filesFound || filesFound.length === 0) { - return this._confirmSaveNewFile$(newFileName).pipe( - concatMap((isSave) => { - return !isSave - ? EMPTY - : this._googleApiService.saveFile$('', { - title: newFileName, - editable: true - }); - }), - map((res2: any) => ({ - syncFileName: res2.title, - _syncFileNameForBackupDocId: res2.title, - _backupDocId: res2.id, - sync: syncConfig, - })) - ); - } else if (filesFound.length === 1) { - return this._confirmUsingExistingFileDialog$(newFileName).pipe( - concatMap((isConfirmUseExisting) => { - const fileToUpdate = filesFound[0]; - return isConfirmUseExisting - ? of({ - syncFileName: newFileName, - _syncFileNameForBackupDocId: newFileName, - _backupDocId: fileToUpdate.id, - sync: syncConfig, - }) - : EMPTY; - }) - ); - } - return EMPTY; - }), - map((v) => v as any), - catchError((err: any) => { - this._snackService.open({ - type: 'ERROR', - msg: T.F.GOOGLE.S.SYNC_FILE_CREATION_ERROR, - translateParams: {err: this._getApiErrorString(err)}, - }); - return throwError({[HANDLED_ERROR_PROP_STR]: 'GD File creation: ' + this._getApiErrorString(err)}); - }) - ); - } else { - return EMPTY; - } - }), - tap((): any => setTimeout(() => this._snackService.open({ - type: 'SUCCESS', - msg: T.F.GOOGLE.S.UPDATED_SYNC_FILE_NAME - }), 200) - ), - map(({syncFileName, _backupDocId, _syncFileNameForBackupDocId, sync}: { - syncFileName: string; - _backupDocId: string; - _syncFileNameForBackupDocId: string; - sync: SyncConfig; - }) => new UpdateGlobalConfigSection({ - sectionKey: 'sync', - sectionCfg: ({ - ...sync, - googleDriveSync: { - ...sync.googleDriveSync, - _backupDocId, - _syncFileNameForBackupDocId, - syncFileName, + return throwError({ + [HANDLED_ERROR_PROP_STR]: + 'GD File creation: ' + this._getApiErrorString(err), + }); + }), + ); + } else { + return EMPTY; } - } as SyncConfig) - })), + }, + ), + tap((): any => + setTimeout( + () => + this._snackService.open({ + type: 'SUCCESS', + msg: T.F.GOOGLE.S.UPDATED_SYNC_FILE_NAME, + }), + 200, + ), + ), + map( + ({ + syncFileName, + _backupDocId, + _syncFileNameForBackupDocId, + sync, + }: { + syncFileName: string; + _backupDocId: string; + _syncFileNameForBackupDocId: string; + sync: SyncConfig; + }) => + new UpdateGlobalConfigSection({ + sectionKey: 'sync', + sectionCfg: { + ...sync, + googleDriveSync: { + ...sync.googleDriveSync, + _backupDocId, + _syncFileNameForBackupDocId, + syncFileName, + }, + } as SyncConfig, + }), + ), ); constructor( @@ -144,28 +171,30 @@ export class GoogleDriveSyncEffects { private _snackService: SnackService, private _googleApiService: GoogleApiService, private _matDialog: MatDialog, - ) { - - } + ) {} private _confirmSaveNewFile$(fileName: string): Observable { - return this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - message: T.F.GOOGLE.DIALOG.CREATE_SYNC_FILE, - translateParams: {fileName}, - } - }).afterClosed(); + return this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + message: T.F.GOOGLE.DIALOG.CREATE_SYNC_FILE, + translateParams: { fileName }, + }, + }) + .afterClosed(); } private _confirmUsingExistingFileDialog$(fileName: string): Observable { - return this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - message: T.F.GOOGLE.DIALOG.USE_EXISTING_SYNC_FILE, - translateParams: {fileName}, - } - }).afterClosed(); + return this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + message: T.F.GOOGLE.DIALOG.USE_EXISTING_SYNC_FILE, + translateParams: { fileName }, + }, + }) + .afterClosed(); } private _getApiErrorString(err: any): string { diff --git a/src/app/imex/sync/google/util/multi-part-builder.ts b/src/app/imex/sync/google/util/multi-part-builder.ts index ff63d4caf..13fcd25a6 100644 --- a/src/app/imex/sync/google/util/multi-part-builder.ts +++ b/src/app/imex/sync/google/util/multi-part-builder.ts @@ -3,7 +3,7 @@ /** * Helper for building multipart requests for uploading to Drive. */ -export const MultiPartBuilder = function(this: any) { +export const MultiPartBuilder = function (this: any) { this.boundary = Math.random().toString(36).slice(2); this.mimeType = 'multipart/mixed; boundary="' + this.boundary + '"'; this.parts = []; @@ -13,14 +13,19 @@ export const MultiPartBuilder = function(this: any) { /** * Appends a part. */ -MultiPartBuilder.prototype.append = function(mimeType: string, content: any) { +MultiPartBuilder.prototype.append = function (mimeType: string, content: any) { if (this.body !== null) { throw new Error('Builder has already been finalized.'); } this.parts.push( - '\r\n--', this.boundary, '\r\n', - 'Content-Type: ', mimeType, '\r\n\r\n', - content); + '\r\n--', + this.boundary, + '\r\n', + 'Content-Type: ', + mimeType, + '\r\n\r\n', + content, + ); return this; }; @@ -29,7 +34,7 @@ MultiPartBuilder.prototype.append = function(mimeType: string, content: any) { * the request. Once finalized, appending additional parts will result in an * error. */ -MultiPartBuilder.prototype.finish = function() { +MultiPartBuilder.prototype.finish = function () { if (this.parts.length === 0) { throw new Error('No parts have been added.'); } @@ -41,6 +46,6 @@ MultiPartBuilder.prototype.finish = function() { } return { type: this.mimeType, - body: this.body + body: this.body, }; }; diff --git a/src/app/imex/sync/is-valid-app-data.util.spec.ts b/src/app/imex/sync/is-valid-app-data.util.spec.ts index fc3850407..56ec410b1 100644 --- a/src/app/imex/sync/is-valid-app-data.util.spec.ts +++ b/src/app/imex/sync/is-valid-app-data.util.spec.ts @@ -34,277 +34,355 @@ describe('isValidAppData()', () => { }); describe('should return false for', () => { - ['note', 'bookmark', 'improvement', 'obstruction', 'metric', 'task', 'tag', 'globalConfig', 'taskArchive'].forEach((prop) => { + [ + 'note', + 'bookmark', + 'improvement', + 'obstruction', + 'metric', + 'task', + 'tag', + 'globalConfig', + 'taskArchive', + ].forEach((prop) => { it('missing prop ' + prop, () => { - expect(isValidAppData({ - ...mock, - [prop]: null, - })).toBe(false); + expect( + isValidAppData({ + ...mock, + [prop]: null, + }), + ).toBe(false); }); }); }); describe('should error for', () => { describe('inconsistent entity state', () => { - ['task', 'taskArchive', 'taskRepeatCfg', 'tag', 'project', 'simpleCounter'].forEach(prop => { - it(prop, () => { - expect(() => isValidAppData({ - ...mock, - [prop]: { - ...mock[prop], - entities: {}, - ids: ['asasdasd'] - }, - })).toThrowError(`Inconsistent entity state "${prop}"`); - }); - }); + ['task', 'taskArchive', 'taskRepeatCfg', 'tag', 'project', 'simpleCounter'].forEach( + (prop) => { + it(prop, () => { + expect(() => + isValidAppData({ + ...mock, + [prop]: { + ...mock[prop], + entities: {}, + ids: ['asasdasd'], + }, + }), + ).toThrowError(`Inconsistent entity state "${prop}"`); + }); + }, + ); }); it('inconsistent task state', () => { - expect(() => isValidAppData({ - ...mock, - task: { - ...mock.task, - entities: {'A asdds': DEFAULT_TASK}, - ids: ['asasdasd'] - }, - })).toThrowError(`Inconsistent entity state "task"`); + expect(() => + isValidAppData({ + ...mock, + task: { + ...mock.task, + entities: { 'A asdds': DEFAULT_TASK }, + ids: ['asasdasd'], + }, + }), + ).toThrowError(`Inconsistent entity state "task"`); }); it('missing today task data for projects', () => { - expect(() => isValidAppData({ - ...mock, - // NOTE: it's empty - task: mock.task, - project: { - ...fakeEntityStateFromArray([{ - title: 'TEST_T', - id: 'TEST_ID', - taskIds: ['gone'], - }] as Partial []), - [MODEL_VERSION_KEY]: 5 - }, - })).toThrowError(`Missing task data (tid: gone) for Project TEST_T`); + expect(() => + isValidAppData({ + ...mock, + // NOTE: it's empty + task: mock.task, + project: { + ...fakeEntityStateFromArray([ + { + title: 'TEST_T', + id: 'TEST_ID', + taskIds: ['gone'], + }, + ] as Partial[]), + [MODEL_VERSION_KEY]: 5, + }, + }), + ).toThrowError(`Missing task data (tid: gone) for Project TEST_T`); }); it('missing backlog task data for projects', () => { - expect(() => isValidAppData({ - ...mock, - // NOTE: it's empty - task: mock.task, - project: { - ...fakeEntityStateFromArray([{ - title: 'TEST_T', - id: 'TEST_ID', - taskIds: [], - backlogTaskIds: ['goneBL'], - }] as Partial []), - [MODEL_VERSION_KEY]: 5 - }, - })).toThrowError(`Missing task data (tid: goneBL) for Project TEST_T`); + expect(() => + isValidAppData({ + ...mock, + // NOTE: it's empty + task: mock.task, + project: { + ...fakeEntityStateFromArray([ + { + title: 'TEST_T', + id: 'TEST_ID', + taskIds: [], + backlogTaskIds: ['goneBL'], + }, + ] as Partial[]), + [MODEL_VERSION_KEY]: 5, + }, + }), + ).toThrowError(`Missing task data (tid: goneBL) for Project TEST_T`); }); it('missing today task data for tags', () => { - expect(() => isValidAppData({ - ...mock, - // NOTE: it's empty - task: mock.task, - tag: { - ...fakeEntityStateFromArray([{ - title: 'TEST_TAG', - id: 'TEST_ID_TAG', - taskIds: ['goneTag'], - }] as Partial []), - [MODEL_VERSION_KEY]: 5 - }, - })).toThrowError(`Inconsistent Task State: Missing task id goneTag for Project/Tag TEST_TAG`); + expect(() => + isValidAppData({ + ...mock, + // NOTE: it's empty + task: mock.task, + tag: { + ...fakeEntityStateFromArray([ + { + title: 'TEST_TAG', + id: 'TEST_ID_TAG', + taskIds: ['goneTag'], + }, + ] as Partial[]), + [MODEL_VERSION_KEY]: 5, + }, + }), + ).toThrowError( + `Inconsistent Task State: Missing task id goneTag for Project/Tag TEST_TAG`, + ); }); it('orphaned archived sub tasks', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskUnarchived', - title: 'subTaskUnarchived', - parentId: 'parent', - }, { - ...DEFAULT_TASK, - id: 'parent', - title: 'parent', - parentId: null, - subTaskIds: ['subTaskUnarchived'] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'subTaskUnarchived', + title: 'subTaskUnarchived', + parentId: 'parent', + }, + { + ...DEFAULT_TASK, + id: 'parent', + title: 'parent', + parentId: null, + subTaskIds: ['subTaskUnarchived'], + }, + ]), } as any; const taskArchiveState = { ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskArchived', - title: 'subTaskArchived', - parentId: 'parent', - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'subTaskArchived', + title: 'subTaskArchived', + parentId: 'parent', + }, + ]), } as any; - expect(() => isValidAppData({ - ...mock, - // NOTE: it's empty - task: taskState, - taskArchive: taskArchiveState, - })).toThrowError(`Inconsistent Task State: Lonely Sub Task in Archive`); + expect(() => + isValidAppData({ + ...mock, + // NOTE: it's empty + task: taskState, + taskArchive: taskArchiveState, + }), + ).toThrowError(`Inconsistent Task State: Lonely Sub Task in Archive`); }); it('orphaned today sub tasks', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskUnarchived', - title: 'subTaskUnarchived', - parentId: 'parent', - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'subTaskUnarchived', + title: 'subTaskUnarchived', + parentId: 'parent', + }, + ]), } as any; const taskArchiveState = { ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'subTaskArchived', - title: 'subTaskArchived', - parentId: 'parent', - }, { - ...DEFAULT_TASK, - id: 'parent', - title: 'parent', - parentId: null, - subTaskIds: ['subTaskArchived'] - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'subTaskArchived', + title: 'subTaskArchived', + parentId: 'parent', + }, + { + ...DEFAULT_TASK, + id: 'parent', + title: 'parent', + parentId: null, + subTaskIds: ['subTaskArchived'], + }, + ]), } as any; - expect(() => isValidAppData({ - ...mock, - // NOTE: it's empty - task: taskState, - taskArchive: taskArchiveState, - })).toThrowError(`Inconsistent Task State: Lonely Sub Task in Today`); + expect(() => + isValidAppData({ + ...mock, + // NOTE: it's empty + task: taskState, + taskArchive: taskArchiveState, + }), + ).toThrowError(`Inconsistent Task State: Lonely Sub Task in Today`); }); xit('missing tag for task', () => { - expect(() => isValidAppData({ - ...mock, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - tagIds: ['Non existent'] - }]) - } as any, - })).toThrowError(`No tagX`); + expect(() => + isValidAppData({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + tagIds: ['Non existent'], + }, + ]), + } as any, + }), + ).toThrowError(`No tagX`); }); it('missing projectIds for task', () => { - expect(() => isValidAppData({ - ...mock, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - projectId: 'NON_EXISTENT' - }]) - } as any, - })).toThrowError(`projectId NON_EXISTENT from task not existing`); + expect(() => + isValidAppData({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + projectId: 'NON_EXISTENT', + }, + ]), + } as any, + }), + ).toThrowError(`projectId NON_EXISTENT from task not existing`); }); it('wrong projectIds for listed tasks', () => { - expect(() => isValidAppData({ - ...mock, - taskArchive: { - ...mock.taskArchive, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - projectId: 'NON_EXISTENT' - }]) - } as any, - })).toThrowError(`projectId NON_EXISTENT from archive task not existing`); + expect(() => + isValidAppData({ + ...mock, + taskArchive: { + ...mock.taskArchive, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + projectId: 'NON_EXISTENT', + }, + ]), + } as any, + }), + ).toThrowError(`projectId NON_EXISTENT from archive task not existing`); }); }); it('missing projectIds for task', () => { - expect(() => isValidAppData({ - ...mock, - project: { - ...mock.project, - ...fakeEntityStateFromArray([{ - ...DEFAULT_PROJECT, - id: 'p1', - taskIds: ['t1', 't2'] - }, { - ...DEFAULT_PROJECT, - id: 'p2', - taskIds: ['t1'] - }]) - } as any, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 't1', - projectId: 'p1' - }, { - ...DEFAULT_TASK, - id: 't2', - projectId: 'p1' - }]) - } as any, - })).toThrowError(`Inconsistent task projectId`); + expect(() => + isValidAppData({ + ...mock, + project: { + ...mock.project, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_PROJECT, + id: 'p1', + taskIds: ['t1', 't2'], + }, + { + ...DEFAULT_PROJECT, + id: 'p2', + taskIds: ['t1'], + }, + ]), + } as any, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 't1', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 't2', + projectId: 'p1', + }, + ]), + } as any, + }), + ).toThrowError(`Inconsistent task projectId`); }); it('missing task data', () => { - expect(() => isValidAppData({ - ...mock, - project: { - ...mock.project, - ...fakeEntityStateFromArray([{ - ...DEFAULT_PROJECT, - id: 'p1', - taskIds: ['t1', 't2'] - }, { - ...DEFAULT_PROJECT, - id: 'p2', - taskIds: ['t1'] - }]) - } as any, - task: { - ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 't1', - projectId: 'p1' - }, { - ...DEFAULT_TASK, - id: 't2', - projectId: 'p1' - }]) - } as any, - })).toThrowError(`Inconsistent task projectId`); + expect(() => + isValidAppData({ + ...mock, + project: { + ...mock.project, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_PROJECT, + id: 'p1', + taskIds: ['t1', 't2'], + }, + { + ...DEFAULT_PROJECT, + id: 'p2', + taskIds: ['t1'], + }, + ]), + } as any, + task: { + ...mock.task, + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 't1', + projectId: 'p1', + }, + { + ...DEFAULT_TASK, + id: 't2', + projectId: 'p1', + }, + ]), + } as any, + }), + ).toThrowError(`Inconsistent task projectId`); }); it('task without neither tagId nor projectId', () => { const taskState = { ...mock.task, - ...fakeEntityStateFromArray([{ - ...DEFAULT_TASK, - id: 'parent', - title: 'parent', - parentId: null, - }]) + ...fakeEntityStateFromArray([ + { + ...DEFAULT_TASK, + id: 'parent', + title: 'parent', + parentId: null, + }, + ]), } as any; - expect(() => isValidAppData({ - ...mock, - // NOTE: it's empty - task: taskState, - })).toThrowError(`Task without project or tag`); + expect(() => + isValidAppData({ + ...mock, + // NOTE: it's empty + task: taskState, + }), + ).toThrowError(`Task without project or tag`); }); }); diff --git a/src/app/imex/sync/is-valid-app-data.util.ts b/src/app/imex/sync/is-valid-app-data.util.ts index 92d618a65..c235fc933 100644 --- a/src/app/imex/sync/is-valid-app-data.util.ts +++ b/src/app/imex/sync/is-valid-app-data.util.ts @@ -6,41 +6,50 @@ import { Tag } from '../../features/tag/tag.model'; import { Project } from '../../features/project/project.model'; import { Task } from '../../features/tasks/task.model'; -export const isValidAppData = (d: AppDataComplete, isSkipInconsistentTaskStateError = false): boolean => { +export const isValidAppData = ( + d: AppDataComplete, + isSkipInconsistentTaskStateError = false, +): boolean => { const dAny: any = d; // TODO remove this later on const isCapableModelVersion = - (typeof dAny === 'object') - && d.project - && d.project[MODEL_VERSION_KEY] - && typeof d.project[MODEL_VERSION_KEY] === 'number' - && (d.project[MODEL_VERSION_KEY] as number) >= 5; + typeof dAny === 'object' && + d.project && + d.project[MODEL_VERSION_KEY] && + typeof d.project[MODEL_VERSION_KEY] === 'number' && + (d.project[MODEL_VERSION_KEY] as number) >= 5; // console.time('time isValidAppData'); - const isValid = (isCapableModelVersion) - - ? (typeof dAny === 'object') && dAny !== null - && typeof dAny.note === 'object' && dAny.note !== null - && typeof dAny.bookmark === 'object' && dAny.bookmark !== null - && typeof dAny.improvement === 'object' && dAny.improvement !== null - && typeof dAny.obstruction === 'object' && dAny.obstruction !== null - && typeof dAny.metric === 'object' && dAny.metric !== null - && typeof dAny.task === 'object' && dAny.task !== null - && typeof dAny.tag === 'object' && dAny.tag !== null - && typeof dAny.globalConfig === 'object' && dAny.globalConfig !== null - && typeof dAny.taskArchive === 'object' && dAny.taskArchive !== null - && typeof dAny.project === 'object' && dAny.project !== null - && Array.isArray(d.reminders) - && _isEntityStatesConsistent(d) - && (isSkipInconsistentTaskStateError || - _isAllTasksAvailableAndListConsistent(d) - && _isNoLonelySubTasks(d) - ) - && _isAllProjectsAvailable(d) - && _isAllTasksHaveAProjectOrTag(d) - - : typeof dAny === 'object' - ; + const isValid = isCapableModelVersion + ? typeof dAny === 'object' && + dAny !== null && + typeof dAny.note === 'object' && + dAny.note !== null && + typeof dAny.bookmark === 'object' && + dAny.bookmark !== null && + typeof dAny.improvement === 'object' && + dAny.improvement !== null && + typeof dAny.obstruction === 'object' && + dAny.obstruction !== null && + typeof dAny.metric === 'object' && + dAny.metric !== null && + typeof dAny.task === 'object' && + dAny.task !== null && + typeof dAny.tag === 'object' && + dAny.tag !== null && + typeof dAny.globalConfig === 'object' && + dAny.globalConfig !== null && + typeof dAny.taskArchive === 'object' && + dAny.taskArchive !== null && + typeof dAny.project === 'object' && + dAny.project !== null && + Array.isArray(d.reminders) && + _isEntityStatesConsistent(d) && + (isSkipInconsistentTaskStateError || + (_isAllTasksAvailableAndListConsistent(d) && _isNoLonelySubTasks(d))) && + _isAllProjectsAvailable(d) && + _isAllTasksHaveAProjectOrTag(d) + : typeof dAny === 'object'; // console.timeEnd('time isValidAppData'); return isValid; @@ -82,12 +91,12 @@ const _isAllTasksHaveAProjectOrTag = (data: AppDataComplete): boolean => { }; const _isAllTasksAvailableAndListConsistent = (data: AppDataComplete): boolean => { - let allIds: string [] = []; + let allIds: string[] = []; let isInconsistentProjectId: boolean = false; let isMissingTaskData: boolean = false; (data.tag.ids as string[]) - .map(id => data.tag.entities[id]) + .map((id) => data.tag.entities[id]) .forEach((tag) => { if (!tag) { console.log(data.tag); @@ -97,40 +106,50 @@ const _isAllTasksAvailableAndListConsistent = (data: AppDataComplete): boolean = }); (data.project.ids as string[]) - .map(id => data.project.entities[id]) - .forEach(project => { - if (!project) { - console.log(data.project); - throw new Error('No project'); - } - const allTaskIdsForProject: string[] = project.taskIds.concat(project.backlogTaskIds); - allIds = allIds.concat(allTaskIdsForProject); - allTaskIdsForProject.forEach(tid => { - const task = data.task.entities[tid]; - if (!task) { - isMissingTaskData = true; - devError('Missing task data (tid: ' + tid + ') for Project ' + project.title); - } else if (task?.projectId !== project.id) { - isInconsistentProjectId = true; - console.log({task}); - devError('Inconsistent task projectId'); - } - }); + .map((id) => data.project.entities[id]) + .forEach((project) => { + if (!project) { + console.log(data.project); + throw new Error('No project'); } - ); + const allTaskIdsForProject: string[] = project.taskIds.concat( + project.backlogTaskIds, + ); + allIds = allIds.concat(allTaskIdsForProject); + allTaskIdsForProject.forEach((tid) => { + const task = data.task.entities[tid]; + if (!task) { + isMissingTaskData = true; + devError('Missing task data (tid: ' + tid + ') for Project ' + project.title); + } else if (task?.projectId !== project.id) { + isInconsistentProjectId = true; + console.log({ task }); + devError('Inconsistent task projectId'); + } + }); + }); // check ids as well - const idNotFound = allIds.find(id => !(data.task.ids.includes(id))); + const idNotFound = allIds.find((id) => !data.task.ids.includes(id)); if (idNotFound) { const tag = (data.tag.ids as string[]) - .map(id => data.tag.entities[id]) - .find(tagI => (tagI as Tag).taskIds.includes(idNotFound)); + .map((id) => data.tag.entities[id]) + .find((tagI) => (tagI as Tag).taskIds.includes(idNotFound)); const project = (data.project.ids as string[]) - .map(id => data.project.entities[id]) - .find(projectI => (projectI as Project).taskIds.includes(idNotFound) || (projectI as Project).backlogTaskIds.includes(idNotFound)); + .map((id) => data.project.entities[id]) + .find( + (projectI) => + (projectI as Project).taskIds.includes(idNotFound) || + (projectI as Project).backlogTaskIds.includes(idNotFound), + ); - devError('Inconsistent Task State: Missing task id ' + idNotFound + ' for Project/Tag ' + ((tag as Tag) || (project as Project)).title); + devError( + 'Inconsistent Task State: Missing task id ' + + idNotFound + + ' for Project/Tag ' + + ((tag as Tag) || (project as Project)).title, + ); } return !idNotFound && !isInconsistentProjectId && !isMissingTaskData; @@ -150,24 +169,23 @@ const _isEntityStatesConsistent = (data: AppDataComplete): boolean => { // 'improvement', // 'obstruction', ]; - const projectStateKeys: (keyof AppDataForProjects)[] = [ - 'note', - 'bookmark', - ]; + const projectStateKeys: (keyof AppDataForProjects)[] = ['note', 'bookmark']; const brokenItem = - baseStateKeys.find(key => !isEntityStateConsistent(data[key], key)) - || - projectStateKeys.find(projectModelKey => { + baseStateKeys.find((key) => !isEntityStateConsistent(data[key], key)) || + projectStateKeys.find((projectModelKey) => { const dataForProjects = data[projectModelKey]; if (typeof (dataForProjects as any) !== 'object') { throw new Error('No dataForProjects'); } - return Object.keys(dataForProjects).find(projectId => - // also allow undefined for project models - (((data as any)[projectId]) !== undefined) - && - (!isEntityStateConsistent((data as any)[projectId], `${projectModelKey} pId:${projectId}`)) + return Object.keys(dataForProjects).find( + (projectId) => + // also allow undefined for project models + (data as any)[projectId] !== undefined && + !isEntityStateConsistent( + (data as any)[projectId], + `${projectModelKey} pId:${projectId}`, + ), ); }); diff --git a/src/app/imex/sync/sync-provider.model.ts b/src/app/imex/sync/sync-provider.model.ts index b0b183b3b..806d8bfbe 100644 --- a/src/app/imex/sync/sync-provider.model.ts +++ b/src/app/imex/sync/sync-provider.model.ts @@ -17,9 +17,17 @@ export interface SyncProviderServiceInterface { isUploadForcePossible?: boolean; isReady$: Observable; - getRevAndLastClientUpdate(localRev: string | null): Promise<{ rev: string; clientUpdate?: number } | SyncGetRevResult>; + getRevAndLastClientUpdate( + localRev: string | null, + ): Promise<{ rev: string; clientUpdate?: number } | SyncGetRevResult>; - uploadAppData(data: AppDataComplete, localRev: string | null, isForceOverwrite?: boolean): Promise; + uploadAppData( + data: AppDataComplete, + localRev: string | null, + isForceOverwrite?: boolean, + ): Promise; - downloadAppData(localRev: string | null): Promise<{ rev: string; data: AppDataComplete | undefined }>; + downloadAppData( + localRev: string | null, + ): Promise<{ rev: string; data: AppDataComplete | undefined }>; } diff --git a/src/app/imex/sync/sync-provider.service.ts b/src/app/imex/sync/sync-provider.service.ts index 94f5747c4..1a3405eca 100644 --- a/src/app/imex/sync/sync-provider.service.ts +++ b/src/app/imex/sync/sync-provider.service.ts @@ -3,7 +3,15 @@ import { combineLatest, Observable } from 'rxjs'; import { DropboxSyncService } from './dropbox/dropbox-sync.service'; import { SyncProvider, SyncProviderServiceInterface } from './sync-provider.model'; import { GlobalConfigService } from '../../features/config/global-config.service'; -import { distinctUntilChanged, filter, first, map, shareReplay, switchMap, take } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + first, + map, + shareReplay, + switchMap, + take, +} from 'rxjs/operators'; import { SyncConfig } from '../../features/config/global-config.model'; import { GoogleDriveSyncService } from './google/google-drive-sync.service'; import { AppDataComplete, DialogConflictResolutionResult } from './sync.model'; @@ -25,7 +33,9 @@ import { getSyncErrorStr } from './get-sync-error-str'; providedIn: 'root', }) export class SyncProviderService { - syncCfg$: Observable = this._globalConfigService.cfg$.pipe(map(cfg => cfg?.sync)); + syncCfg$: Observable = this._globalConfigService.cfg$.pipe( + map((cfg) => cfg?.sync), + ); currentProvider$: Observable = this.syncCfg$.pipe( map((cfg: SyncConfig): SyncProvider | null => cfg.syncProvider), distinctUntilChanged(), @@ -42,20 +52,16 @@ export class SyncProviderService { return null; } }), - filter(p => !!p), + filter((p) => !!p), map((v) => v as SyncProviderServiceInterface), shareReplay(1), ); - syncInterval$: Observable = this.syncCfg$.pipe(map(cfg => cfg.syncInterval)); - isEnabled$: Observable = this.syncCfg$.pipe(map(cfg => cfg.isEnabled)); + syncInterval$: Observable = this.syncCfg$.pipe(map((cfg) => cfg.syncInterval)); + isEnabled$: Observable = this.syncCfg$.pipe(map((cfg) => cfg.isEnabled)); isEnabledAndReady$: Observable = combineLatest([ - this.currentProvider$.pipe( - switchMap(currentProvider => currentProvider.isReady$), - ), - this.syncCfg$.pipe(map(cfg => cfg.isEnabled)), - ]).pipe( - map(([isReady, isEnabled]) => isReady && isEnabled), - ); + this.currentProvider$.pipe(switchMap((currentProvider) => currentProvider.isReady$)), + this.syncCfg$.pipe(map((cfg) => cfg.isEnabled)), + ]).pipe(map(([isReady, isEnabled]) => isReady && isEnabled)); constructor( private _dropboxSyncService: DropboxSyncService, @@ -68,8 +74,7 @@ export class SyncProviderService { private _syncService: SyncService, private _snackService: SnackService, private _matDialog: MatDialog, - ) { - } + ) {} async sync(): Promise { const currentProvider = await this.currentProvider$.pipe(take(1)).toPromise(); @@ -86,7 +91,7 @@ export class SyncProviderService { if (!isReady) { this._snackService.open({ msg: T.F.SYNC.S.INCOMPLETE_CFG, - type: 'ERROR' + type: 'ERROR', }); return; } @@ -102,7 +107,9 @@ export class SyncProviderService { if (typeof revRes === 'string') { if (revRes === 'NO_REMOTE_DATA' && this._c(T.F.SYNC.C.NO_REMOTE_DATA)) { this._log(cp, '↑ Update Remote after no getRevAndLastClientUpdate()'); - const localLocal = await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise(); + const localLocal = await this._syncService.inMemoryComplete$ + .pipe(take(1)) + .toPromise(); return await this._uploadAppData(cp, localLocal); } // NOTE: includes HANDLED_ERROR @@ -113,11 +120,11 @@ export class SyncProviderService { translateParams: { err: getSyncErrorStr(revRes), }, - type: 'ERROR' + type: 'ERROR', }); } - const {rev, clientUpdate} = revRes as { rev: string; clientUpdate: number }; + const { rev, clientUpdate } = revRes as { rev: string; clientUpdate: number }; if (rev && rev === localRev) { this._log(cp, 'PRE1: ↔ Same Rev', rev); @@ -133,10 +140,11 @@ export class SyncProviderService { // simple check based on local meta // ------------------------------------ // if not defined yet - local = local || await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise(); + local = + local || (await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise()); if (!local.lastLocalSyncModelChange || local.lastLocalSyncModelChange === 0) { - if (!(this._c(T.F.SYNC.C.EMPTY_SYNC))) { + if (!this._c(T.F.SYNC.C.EMPTY_SYNC)) { this._log(cp, 'PRE2: Abort'); return; } @@ -158,16 +166,16 @@ export class SyncProviderService { // getting lost, might be unlikely and ok after all // local > remote && lastSync >= remote && lastSync < local if ( - Math.floor(local.lastLocalSyncModelChange / 1000) > remoteClientUpdate - && remoteClientUpdate === Math.floor(lastSync / 1000) - && lastSync < local.lastLocalSyncModelChange + Math.floor(local.lastLocalSyncModelChange / 1000) > remoteClientUpdate && + remoteClientUpdate === Math.floor(lastSync / 1000) && + lastSync < local.lastLocalSyncModelChange ) { this._log(cp, 'PRE3: ↑ Update Remote'); return await this._uploadAppData(cp, local); } // DOWNLOAD OF REMOTE - const r = (await this._downloadAppData(cp)); + const r = await this._downloadAppData(cp); // PRE CHECK 4 // check if there is no data or no valid remote data @@ -186,7 +194,7 @@ export class SyncProviderService { const timestamps = { local: local.lastLocalSyncModelChange, lastSync, - remote: remote.lastLocalSyncModelChange + remote: remote.lastLocalSyncModelChange, }; switch (checkForUpdate(timestamps)) { @@ -210,14 +218,14 @@ export class SyncProviderService { if (this._c(T.F.SYNC.C.TRY_LOAD_REMOTE_AGAIN)) { return this.sync(); } else { - return this._handleConflict(cp, {remote, local, lastSync, rev: r.rev}); + return this._handleConflict(cp, { remote, local, lastSync, rev: r.rev }); } } case UpdateCheckResult.DataDiverged: { this._log(cp, '^--------^-------^'); this._log(cp, '⇎ X Diverged Data'); - return this._handleConflict(cp, {remote, local, lastSync, rev: r.rev}); + return this._handleConflict(cp, { remote, local, lastSync, rev: r.rev }); } case UpdateCheckResult.LastSyncNotUpToDate: { @@ -244,12 +252,18 @@ export class SyncProviderService { // WRAPPER // ------- - private async _downloadAppData(cp: SyncProviderServiceInterface): Promise<{ rev: string; data: AppDataComplete | undefined }> { + private async _downloadAppData( + cp: SyncProviderServiceInterface, + ): Promise<{ rev: string; data: AppDataComplete | undefined }> { const rev = await this._getLocalRev(cp); return cp.downloadAppData(rev); } - private async _uploadAppData(cp: SyncProviderServiceInterface, data: AppDataComplete, isForceOverwrite: boolean = false): Promise { + private async _uploadAppData( + cp: SyncProviderServiceInterface, + data: AppDataComplete, + isForceOverwrite: boolean = false, + ): Promise { if (!isValidAppData(data)) { console.log(data); alert('The data you are trying to upload is invalid'); @@ -259,7 +273,11 @@ export class SyncProviderService { const successRev = await cp.uploadAppData(data, localRev, isForceOverwrite); if (typeof successRev === 'string') { this._log(cp, '↑ Uploaded Data ↑ ✓'); - return await this._setLocalRevAndLastSync(cp, successRev, data.lastLocalSyncModelChange) as Promise; + return (await this._setLocalRevAndLastSync( + cp, + successRev, + data.lastLocalSyncModelChange, + )) as Promise; } else { this._log(cp, 'X Upload Request Error'); if (cp.isUploadForcePossible && this._c(T.F.SYNC.C.FORCE_UPLOAD_AFTER_ERROR)) { @@ -268,19 +286,24 @@ export class SyncProviderService { this._snackService.open({ msg: T.F.SYNC.S.UPLOAD_ERROR, translateParams: { - err: truncate(successRev?.toString - ? successRev.toString() - : successRev as any, 100), + err: truncate( + successRev?.toString ? successRev.toString() : (successRev as any), + 100, + ), }, - type: 'ERROR' + type: 'ERROR', }); } } } - private async _importAppData(cp: SyncProviderServiceInterface, data: AppDataComplete, rev: string): Promise { + private async _importAppData( + cp: SyncProviderServiceInterface, + data: AppDataComplete, + rev: string, + ): Promise { if (!data) { - const r = (await this._downloadAppData(cp)); + const r = await this._downloadAppData(cp); data = r.data as AppDataComplete; rev = r.rev; } @@ -300,7 +323,11 @@ export class SyncProviderService { } // NOTE: last sync should always equal localLastChange - private async _setLocalRevAndLastSync(cp: SyncProviderServiceInterface, rev: string, lastSync: number): Promise { + private async _setLocalRevAndLastSync( + cp: SyncProviderServiceInterface, + rev: string, + lastSync: number, + ): Promise { if (!rev) { console.log(cp, rev); throw new Error('No rev given'); @@ -314,22 +341,30 @@ export class SyncProviderService { [cp.id]: { rev, lastSync, - } + }, }); } // OTHER // ----- - private async _handleConflict(cp: SyncProviderServiceInterface, {remote, local, lastSync, rev}: { - remote: AppDataComplete; - local: AppDataComplete; - lastSync: number; - rev: string; - }) { + private async _handleConflict( + cp: SyncProviderServiceInterface, + { + remote, + local, + lastSync, + rev, + }: { + remote: AppDataComplete; + local: AppDataComplete; + lastSync: number; + rev: string; + }, + ) { const dr = await this._openConflictDialog$({ local: local.lastLocalSyncModelChange, lastSync, - remote: remote.lastLocalSyncModelChange + remote: remote.lastLocalSyncModelChange, }).toPromise(); if (dr === 'USE_LOCAL') { @@ -342,20 +377,26 @@ export class SyncProviderService { return; } - private _openConflictDialog$({remote, local, lastSync}: { + private _openConflictDialog$({ + remote, + local, + lastSync, + }: { remote: number; local: number; lastSync: number; }): Observable { - return this._matDialog.open(DialogSyncConflictComponent, { - restoreFocus: true, - disableClose: true, - data: { - remote, - local, - lastSync, - } - }).afterClosed(); + return this._matDialog + .open(DialogSyncConflictComponent, { + restoreFocus: true, + disableClose: true, + data: { + remote, + local, + lastSync, + }, + }) + .afterClosed(); } private _c(str: string): boolean { @@ -365,5 +406,4 @@ export class SyncProviderService { private _log(cp: SyncProviderServiceInterface, ...args: any | any[]) { return console.log(cp.id, ...args); } - } diff --git a/src/app/imex/sync/sync.effects.ts b/src/app/imex/sync/sync.effects.ts index 37c743d64..4928d7466 100644 --- a/src/app/imex/sync/sync.effects.ts +++ b/src/app/imex/sync/sync.effects.ts @@ -13,11 +13,15 @@ import { switchMap, take, tap, - withLatestFrom + withLatestFrom, } from 'rxjs/operators'; import { DataInitService } from '../../core/data-init/data-init.service'; import { SyncService } from '../../imex/sync/sync.service'; -import { SYNC_BEFORE_CLOSE_ID, SYNC_INITIAL_SYNC_TRIGGER, SYNC_MIN_INTERVAL } from '../../imex/sync/sync.const'; +import { + SYNC_BEFORE_CLOSE_ID, + SYNC_INITIAL_SYNC_TRIGGER, + SYNC_MIN_INTERVAL, +} from '../../imex/sync/sync.const'; import { combineLatest, EMPTY, merge, Observable, of } from 'rxjs'; import { isOnline$ } from '../../util/is-online'; import { SnackService } from '../../core/snack/snack.service'; @@ -31,81 +35,87 @@ import { getSyncErrorStr } from './get-sync-error-str'; @Injectable() export class SyncEffects { - @Effect({dispatch: false}) syncBeforeQuit$: any = !IS_ELECTRON + @Effect({ dispatch: false }) syncBeforeQuit$: any = !IS_ELECTRON ? EMPTY : this._dataInitService.isAllDataLoadedInitially$.pipe( - concatMap(() => this._syncProviderService.isEnabledAndReady$), - distinctUntilChanged(), - tap((isEnabled) => isEnabled - ? this._execBeforeCloseService.schedule(SYNC_BEFORE_CLOSE_ID) - : this._execBeforeCloseService.unschedule(SYNC_BEFORE_CLOSE_ID) - ), - switchMap((isEnabled) => isEnabled - ? this._execBeforeCloseService.onBeforeClose$ - : EMPTY - ), - filter(ids => ids.includes(SYNC_BEFORE_CLOSE_ID)), - tap(() => { - this._taskService.setCurrentId(null); - this._simpleCounterService.turnOffAll(); - }), - // minimally hacky delay to wait for inMemoryDatabase update... - delay(100), - switchMap(() => this._syncProviderService.sync() - .then(() => { - this._execBeforeCloseService.setDone(SYNC_BEFORE_CLOSE_ID); - }) - .catch((e: unknown) => { - console.error(e); - this._snackService.open({msg: T.F.DROPBOX.S.SYNC_ERROR, type: 'ERROR'}); - if (confirm('Sync failed. Close App anyway?')) { - this._execBeforeCloseService.setDone(SYNC_BEFORE_CLOSE_ID); - } - }) - ), - ); + concatMap(() => this._syncProviderService.isEnabledAndReady$), + distinctUntilChanged(), + tap((isEnabled) => + isEnabled + ? this._execBeforeCloseService.schedule(SYNC_BEFORE_CLOSE_ID) + : this._execBeforeCloseService.unschedule(SYNC_BEFORE_CLOSE_ID), + ), + switchMap((isEnabled) => + isEnabled ? this._execBeforeCloseService.onBeforeClose$ : EMPTY, + ), + filter((ids) => ids.includes(SYNC_BEFORE_CLOSE_ID)), + tap(() => { + this._taskService.setCurrentId(null); + this._simpleCounterService.turnOffAll(); + }), + // minimally hacky delay to wait for inMemoryDatabase update... + delay(100), + switchMap(() => + this._syncProviderService + .sync() + .then(() => { + this._execBeforeCloseService.setDone(SYNC_BEFORE_CLOSE_ID); + }) + .catch((e: unknown) => { + console.error(e); + this._snackService.open({ msg: T.F.DROPBOX.S.SYNC_ERROR, type: 'ERROR' }); + if (confirm('Sync failed. Close App anyway?')) { + this._execBeforeCloseService.setDone(SYNC_BEFORE_CLOSE_ID); + } + }), + ), + ); // private _wasJustEnabled$: Observable = of(false); private _wasJustEnabled$: Observable = this._dataInitService.isAllDataLoadedInitially$.pipe( // NOTE: it is important that we don't use distinct until changed here switchMap(() => this._syncProviderService.isEnabledAndReady$), pairwise(), map(([a, b]) => !a && !!b), - filter(wasJustEnabled => wasJustEnabled), + filter((wasJustEnabled) => wasJustEnabled), shareReplay(), ); - @Effect({dispatch: false}) triggerSync$: any = this._dataInitService.isAllDataLoadedInitially$.pipe( - switchMap(() => merge( - // dynamic - combineLatest([ - this._syncProviderService.isEnabledAndReady$, - this._syncProviderService.syncInterval$, - ]).pipe( - switchMap(([isEnabledAndReady, syncInterval]) => isEnabledAndReady - ? this._syncService.getSyncTrigger$(syncInterval, SYNC_MIN_INTERVAL) - : EMPTY + @Effect({ dispatch: false }) + triggerSync$: any = this._dataInitService.isAllDataLoadedInitially$.pipe( + switchMap(() => + merge( + // dynamic + combineLatest([ + this._syncProviderService.isEnabledAndReady$, + this._syncProviderService.syncInterval$, + ]).pipe( + switchMap(([isEnabledAndReady, syncInterval]) => + isEnabledAndReady + ? this._syncService.getSyncTrigger$(syncInterval, SYNC_MIN_INTERVAL) + : EMPTY, + ), ), - ), - // initial after starting app - this._syncProviderService.isEnabledAndReady$.pipe( - take(1), - switchMap((isEnabledAndReady) => { - if (isEnabledAndReady) { - return of(SYNC_INITIAL_SYNC_TRIGGER); - } else { - this._syncService.setInitialSyncDone(true); - this._snackService.open({ - msg: T.F.SYNC.S.INITIAL_SYNC_ERROR, - type: 'ERROR' - }); - return EMPTY; - } - }), - ), + // initial after starting app + this._syncProviderService.isEnabledAndReady$.pipe( + take(1), + switchMap((isEnabledAndReady) => { + if (isEnabledAndReady) { + return of(SYNC_INITIAL_SYNC_TRIGGER); + } else { + this._syncService.setInitialSyncDone(true); + this._snackService.open({ + msg: T.F.SYNC.S.INITIAL_SYNC_ERROR, + type: 'ERROR', + }); + return EMPTY; + } + }), + ), - // initial after enabling it, - this._wasJustEnabled$.pipe(take(1), mapTo('SYNC_DBX_AFTER_ENABLE')), - )), + // initial after enabling it, + this._wasJustEnabled$.pipe(take(1), mapTo('SYNC_DBX_AFTER_ENABLE')), + ), + ), tap((x) => console.log('sync(effect).....', x)), withLatestFrom(isOnline$), // don't run multiple after each other when dialog is open @@ -118,7 +128,8 @@ export class SyncEffects { // we need to return something return of(null); } - return this._syncProviderService.sync() + return this._syncProviderService + .sync() .then(() => { if (trigger === SYNC_INITIAL_SYNC_TRIGGER) { this._syncService.setInitialSyncDone(true); @@ -131,7 +142,7 @@ export class SyncEffects { translateParams: { err: getSyncErrorStr(err), }, - type: 'ERROR' + type: 'ERROR', }); }); }), @@ -145,7 +156,5 @@ export class SyncEffects { private _simpleCounterService: SimpleCounterService, private _dataInitService: DataInitService, private _execBeforeCloseService: ExecBeforeCloseService, - ) { - } - + ) {} } diff --git a/src/app/imex/sync/sync.model.ts b/src/app/imex/sync/sync.model.ts index 3face145c..feee159cc 100644 --- a/src/app/imex/sync/sync.model.ts +++ b/src/app/imex/sync/sync.model.ts @@ -1,4 +1,4 @@ -import { initialProjectState, ProjectState } from '../../features/project/store/project.reducer'; +import { initialProjectState, ProjectState, } from '../../features/project/store/project.reducer'; import { GlobalConfigState } from '../../features/config/global-config.model'; import { TaskArchive, TaskState } from '../../features/tasks/task.model'; import { BookmarkState } from '../../features/bookmark/store/bookmark.reducer'; @@ -57,7 +57,7 @@ export interface LocalSyncMetaModel { } export type AppBaseDataEntityLikeStates = - ProjectState + | ProjectState | TaskState | TaskRepeatCfgState | TaskArchive @@ -73,7 +73,9 @@ export interface AppDataForProjects { }; } -export interface AppDataCompleteOptionalSyncModelChange extends AppBaseData, AppDataForProjects { +export interface AppDataCompleteOptionalSyncModelChange + extends AppBaseData, + AppDataForProjects { lastLocalSyncModelChange?: number; } diff --git a/src/app/imex/sync/sync.module.ts b/src/app/imex/sync/sync.module.ts index de89ea1f5..59bee8955 100644 --- a/src/app/imex/sync/sync.module.ts +++ b/src/app/imex/sync/sync.module.ts @@ -12,10 +12,7 @@ import { DialogSyncConflictComponent } from './dialog-dbx-sync-conflict/dialog-s import { DialogGetAndEnterAuthCodeComponent } from './dialog-get-and-enter-auth-code/dialog-get-and-enter-auth-code.component'; @NgModule({ - declarations: [ - DialogSyncConflictComponent, - DialogGetAndEnterAuthCodeComponent, - ], + declarations: [DialogSyncConflictComponent, DialogGetAndEnterAuthCodeComponent], imports: [ FormsModule, UiModule, @@ -28,5 +25,4 @@ import { DialogGetAndEnterAuthCodeComponent } from './dialog-get-and-enter-auth- WebDavModule, ], }) -export class SyncModule { -} +export class SyncModule {} diff --git a/src/app/imex/sync/sync.service.ts b/src/app/imex/sync/sync.service.ts index 3dd4a0e30..5d15522af 100644 --- a/src/app/imex/sync/sync.service.ts +++ b/src/app/imex/sync/sync.service.ts @@ -1,5 +1,14 @@ import { Injectable } from '@angular/core'; -import { EMPTY, fromEvent, merge, Observable, of, ReplaySubject, throwError, timer } from 'rxjs'; +import { + EMPTY, + fromEvent, + merge, + Observable, + of, + ReplaySubject, + throwError, + timer, +} from 'rxjs'; import { auditTime, catchError, @@ -16,7 +25,7 @@ import { take, tap, throttleTime, - timeout + timeout, } from 'rxjs/operators'; import { GlobalConfigService } from '../../features/config/global-config.service'; import { DataInitService } from '../../core/data-init/data-init.service'; @@ -25,7 +34,7 @@ import { PersistenceService } from '../../core/persistence/persistence.service'; import { SYNC_ACTIVITY_AFTER_SOMETHING_ELSE_THROTTLE_TIME, SYNC_BEFORE_GOING_TO_SLEEP_THROTTLE_TIME, - SYNC_DEFAULT_AUDIT_TIME + SYNC_DEFAULT_AUDIT_TIME, } from './sync.const'; import { IS_TOUCH_ONLY } from '../../util/is-touch'; import { AllowedDBKeys } from '../../core/persistence/ls-keys.const'; @@ -49,55 +58,62 @@ export class SyncService { catchError(() => throwError('Error while trying to get inMemoryComplete$')), ); - private _onUpdateLocalDataTrigger$: Observable<{ appDataKey: AllowedDBKeys; data: any; isDataImport: boolean; projectId?: string }> = - this._persistenceService.onAfterSave$.pipe( - filter(({appDataKey, data, isDataImport, isSyncModelChange}) => !!data && !isDataImport && isSyncModelChange), - ); + private _onUpdateLocalDataTrigger$: Observable<{ + appDataKey: AllowedDBKeys; + data: any; + isDataImport: boolean; + projectId?: string; + }> = this._persistenceService.onAfterSave$.pipe( + filter( + ({ appDataKey, data, isDataImport, isSyncModelChange }) => + !!data && !isDataImport && isSyncModelChange, + ), + ); // IMMEDIATE TRIGGERS // ---------------------- - private _mouseMoveAfterIdle$: Observable = this._idleService.isIdle$.pipe( + private _mouseMoveAfterIdle$: Observable< + string | never + > = this._idleService.isIdle$.pipe( distinctUntilChanged(), - switchMap((isIdle) => isIdle - ? fromEvent(window, 'mousemove').pipe( - take(1), - mapTo('I_MOUSE_MOVE_AFTER_IDLE'), - ) - : EMPTY - ) + switchMap((isIdle) => + isIdle + ? fromEvent(window, 'mousemove').pipe(take(1), mapTo('I_MOUSE_MOVE_AFTER_IDLE')) + : EMPTY, + ), ); private _activityAfterSomethingElseTriggers$: Observable = merge( fromEvent(window, 'focus').pipe(mapTo('I_FOCUS_THROTTLED')), IS_ELECTRON - ? fromEvent((this._electronService.ipcRenderer as IpcRenderer), IPC.RESUME).pipe(mapTo('I_IPC_RESUME')) + ? fromEvent(this._electronService.ipcRenderer as IpcRenderer, IPC.RESUME).pipe( + mapTo('I_IPC_RESUME'), + ) : EMPTY, IS_TOUCH_ONLY ? merge( - fromEvent(window, 'touchstart'), - fromEvent(window, 'visibilitychange'), - ).pipe(mapTo('I_MOUSE_TOUCH_MOVE_OR_VISIBILITYCHANGE')) + fromEvent(window, 'touchstart'), + fromEvent(window, 'visibilitychange'), + ).pipe(mapTo('I_MOUSE_TOUCH_MOVE_OR_VISIBILITYCHANGE')) : EMPTY, - this._mouseMoveAfterIdle$ - ).pipe( - throttleTime(SYNC_ACTIVITY_AFTER_SOMETHING_ELSE_THROTTLE_TIME), - ); + this._mouseMoveAfterIdle$, + ).pipe(throttleTime(SYNC_ACTIVITY_AFTER_SOMETHING_ELSE_THROTTLE_TIME)); private _beforeGoingToSleepTriggers$: Observable = merge( IS_ELECTRON - ? fromEvent((this._electronService.ipcRenderer as IpcRenderer), IPC.SUSPEND).pipe(mapTo('I_IPC_SUSPEND')) + ? fromEvent(this._electronService.ipcRenderer as IpcRenderer, IPC.SUSPEND).pipe( + mapTo('I_IPC_SUSPEND'), + ) : EMPTY, - ).pipe( - throttleTime(SYNC_BEFORE_GOING_TO_SLEEP_THROTTLE_TIME) - ); + ).pipe(throttleTime(SYNC_BEFORE_GOING_TO_SLEEP_THROTTLE_TIME)); private _isOnlineTrigger$: Observable = isOnline$.pipe( // skip initial online which always fires on page load skip(1), - filter(isOnline => isOnline), + filter((isOnline) => isOnline), mapTo('IS_ONLINE'), ); @@ -116,16 +132,16 @@ export class SyncService { ); // keep it super simple for now - private _isInitialSyncDoneManual$: ReplaySubject = new ReplaySubject(1); + private _isInitialSyncDoneManual$: ReplaySubject = new ReplaySubject( + 1, + ); private _isInitialSyncDone$: Observable = this._isInitialSyncEnabled$.pipe( switchMap((isActive) => { - return isActive - ? this._isInitialSyncDoneManual$.asObservable() - : of(true); + return isActive ? this._isInitialSyncDoneManual$.asObservable() : of(true); }), ); private _afterInitialSyncDoneAndDataLoadedInitially$: Observable = this._isInitialSyncDone$.pipe( - filter(isDone => isDone), + filter((isDone) => isDone), take(1), // should normally be already loaded, but if there is NO initial sync we need to wait here concatMap(() => this._dataInitService.isAllDataLoadedInitially$), @@ -133,10 +149,7 @@ export class SyncService { afterInitialSyncDoneAndDataLoadedInitially$: Observable = merge( this._afterInitialSyncDoneAndDataLoadedInitially$, - timer(MAX_WAIT_FOR_INITIAL_SYNC).pipe(mapTo(true), - ).pipe( - shareReplay(1), - ) + timer(MAX_WAIT_FOR_INITIAL_SYNC).pipe(mapTo(true)).pipe(shareReplay(1)), ); constructor( @@ -149,7 +162,10 @@ export class SyncService { // this.getSyncTrigger$(5000).subscribe((v) => console.log('.getSyncTrigger$(5000)', v)); } - getSyncTrigger$(syncInterval: number = SYNC_DEFAULT_AUDIT_TIME, minSyncInterval: number = 5000): Observable { + getSyncTrigger$( + syncInterval: number = SYNC_DEFAULT_AUDIT_TIME, + minSyncInterval: number = 5000, + ): Observable { return merge( this._immediateSyncTrigger$, @@ -157,15 +173,17 @@ export class SyncService { this._immediateSyncTrigger$.pipe( // NOTE: startWith needs to come before switchMap! startWith(false), - switchMap(() => this._onUpdateLocalDataTrigger$.pipe( - tap((ev) => console.log('__trigger_sync__', ev.appDataKey, ev)), - auditTime(Math.max(syncInterval, minSyncInterval)), - tap((ev) => console.log('__trigger_sync after auditTime__', ev.appDataKey, ev)), - )), - ) - ).pipe( - debounceTime(100) - ); + switchMap(() => + this._onUpdateLocalDataTrigger$.pipe( + tap((ev) => console.log('__trigger_sync__', ev.appDataKey, ev)), + auditTime(Math.max(syncInterval, minSyncInterval)), + tap((ev) => + console.log('__trigger_sync after auditTime__', ev.appDataKey, ev), + ), + ), + ), + ), + ).pipe(debounceTime(100)); } setInitialSyncDone(val: boolean) { diff --git a/src/app/imex/sync/web-dav/web-dav-api.service.ts b/src/app/imex/sync/web-dav/web-dav-api.service.ts index eff636fac..e7158edb2 100644 --- a/src/app/imex/sync/web-dav/web-dav-api.service.ts +++ b/src/app/imex/sync/web-dav/web-dav-api.service.ts @@ -9,14 +9,16 @@ import { createClient } from 'webdav/web'; import { AppDataComplete } from '../sync.model'; import { AxiosResponse } from 'axios'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class WebDavApiService { private _cfg$: Observable = this._globalConfigService.cfg$.pipe( - map((cfg) => cfg?.sync.webDav) + map((cfg) => cfg?.sync.webDav), ); isAllConfigDataAvailable$: Observable = this._cfg$.pipe( - map((cfg) => !!(cfg && cfg.userName && cfg.baseUrl && cfg.syncFilePath && cfg.password)), + map( + (cfg) => !!(cfg && cfg.userName && cfg.baseUrl && cfg.syncFilePath && cfg.password), + ), ); private _isReady$: Observable = this._dataInitService.isAllDataLoadedInitially$.pipe( @@ -28,10 +30,13 @@ export class WebDavApiService { constructor( private _globalConfigService: GlobalConfigService, private _dataInitService: DataInitService, - ) { - } + ) {} - async upload({localRev, data, isForceOverwrite = false}: { + async upload({ + localRev, + data, + isForceOverwrite = false, + }: { localRev?: string | null; data: AppDataComplete; isForceOverwrite?: boolean; @@ -48,7 +53,9 @@ export class WebDavApiService { return r; } - async getMetaData(path: string): Promise<{ + async getMetaData( + path: string, + ): Promise<{ basename: string; etag: string; filename: string; @@ -68,14 +75,20 @@ export class WebDavApiService { return r; } - async download({path, localRev}: { path: string; localRev?: string | null }): Promise { + async download({ + path, + localRev, + }: { + path: string; + localRev?: string | null; + }): Promise { await this._isReady$.toPromise(); const cfg = await this._cfg$.pipe(first()).toPromise(); const client = createClient(cfg.baseUrl, { username: cfg.userName, password: cfg.password, }); - const r = await client.getFileContents(path, {format: 'text'}); + const r = await client.getFileContents(path, { format: 'text' }); console.log(r); return r; } diff --git a/src/app/imex/sync/web-dav/web-dav-sync.service.ts b/src/app/imex/sync/web-dav/web-dav-sync.service.ts index 6b627cf4f..b0f2f39e6 100644 --- a/src/app/imex/sync/web-dav/web-dav-sync.service.ts +++ b/src/app/imex/sync/web-dav/web-dav-sync.service.ts @@ -12,7 +12,7 @@ import { GlobalConfigService } from '../../../features/config/global-config.serv import { GlobalProgressBarService } from '../../../core-ui/global-progress-bar/global-progress-bar.service'; import { T } from '../../../t.const'; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class WebDavSyncService implements SyncProviderServiceInterface { id: SyncProvider = SyncProvider.WebDAV; @@ -22,7 +22,7 @@ export class WebDavSyncService implements SyncProviderServiceInterface { ); private _cfg$: Observable = this._globalConfigService.cfg$.pipe( - map((cfg) => cfg?.sync.webDav) + map((cfg) => cfg?.sync.webDav), ); // @@ -31,10 +31,11 @@ export class WebDavSyncService implements SyncProviderServiceInterface { private _dataInitService: DataInitService, private _globalConfigService: GlobalConfigService, private _globalProgressBarService: GlobalProgressBarService, - ) { - } + ) {} - async getRevAndLastClientUpdate(localRev: string): Promise<{ rev: string; clientUpdate: number } | SyncGetRevResult> { + async getRevAndLastClientUpdate( + localRev: string, + ): Promise<{ rev: string; clientUpdate: number } | SyncGetRevResult> { const cfg = await this._cfg$.pipe(first()).toPromise(); try { @@ -58,7 +59,9 @@ export class WebDavSyncService implements SyncProviderServiceInterface { } } - async downloadAppData(localRev: string): Promise<{ rev: string; data: AppDataComplete }> { + async downloadAppData( + localRev: string, + ): Promise<{ rev: string; data: AppDataComplete }> { this._globalProgressBarService.countUp(T.GPB.WEB_DAV_DOWNLOAD); const cfg = await this._cfg$.pipe(first()).toPromise(); try { @@ -78,13 +81,17 @@ export class WebDavSyncService implements SyncProviderServiceInterface { } } - async uploadAppData(data: AppDataComplete, localRev: string, isForceOverwrite: boolean = false): Promise { + async uploadAppData( + data: AppDataComplete, + localRev: string, + isForceOverwrite: boolean = false, + ): Promise { this._globalProgressBarService.countUp(T.GPB.WEB_DAV_UPLOAD); try { const r = await this._webDavApiService.upload({ data, localRev, - isForceOverwrite + isForceOverwrite, }); console.log(r); this._globalProgressBarService.countDown(); diff --git a/src/app/imex/sync/web-dav/web-dav.module.ts b/src/app/imex/sync/web-dav/web-dav.module.ts index 13a468a24..45e8322b5 100644 --- a/src/app/imex/sync/web-dav/web-dav.module.ts +++ b/src/app/imex/sync/web-dav/web-dav.module.ts @@ -3,7 +3,6 @@ import { NgModule } from '@angular/core'; @NgModule({ declarations: [], exports: [], - imports: [] + imports: [], }) -export class WebDavModule { -} +export class WebDavModule {} diff --git a/src/app/pages/config-page/config-page.component.ts b/src/app/pages/config-page/config-page.component.ts index e3606436c..6b047752a 100644 --- a/src/app/pages/config-page/config-page.component.ts +++ b/src/app/pages/config-page/config-page.component.ts @@ -1,16 +1,22 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, +} from '@angular/core'; import { GlobalConfigService } from '../../features/config/global-config.service'; import { GLOBAL_CONFIG_FORM_CONFIG, GLOBAL_PRODUCTIVITY_FORM_CONFIG, - GLOBAL_SYNC_FORM_CONFIG + GLOBAL_SYNC_FORM_CONFIG, } from '../../features/config/global-config-form-config.const'; import { ConfigFormConfig, ConfigFormSection, GlobalConfigSectionKey, GlobalConfigState, - GlobalSectionConfig + GlobalSectionConfig, } from '../../features/config/global-config.model'; import { Subscription } from 'rxjs'; import { ProjectCfgFormKey } from '../../features/project/project.model'; @@ -47,10 +53,12 @@ export class ConfigPageComponent implements OnInit, OnDestroy { } ngOnInit() { - this._subs.add(this.configService.cfg$.subscribe((cfg) => { - this.globalCfg = cfg; - this._cd.detectChanges(); - })); + this._subs.add( + this.configService.cfg$.subscribe((cfg) => { + this.globalCfg = cfg; + this._cd.detectChanges(); + }), + ); } ngOnDestroy() { @@ -61,7 +69,10 @@ export class ConfigPageComponent implements OnInit, OnDestroy { return section.key; } - saveGlobalCfg($event: { sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; config: any }) { + saveGlobalCfg($event: { + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; + config: any; + }) { const config = $event.config; const sectionKey = $event.sectionKey as GlobalConfigSectionKey; @@ -73,10 +84,12 @@ export class ConfigPageComponent implements OnInit, OnDestroy { } toggleDarkMode(change: MatSlideToggleChange) { - this.configService.updateSection('misc', {isDarkMode: change.checked}); + this.configService.updateSection('misc', { isDarkMode: change.checked }); } - getGlobalCfgSection(sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey): GlobalSectionConfig { + getGlobalCfgSection( + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey, + ): GlobalSectionConfig { return (this.globalCfg as any)[sectionKey]; } } diff --git a/src/app/pages/config-page/config-page.module.ts b/src/app/pages/config-page/config-page.module.ts index 7d86533f7..5a1867f94 100644 --- a/src/app/pages/config-page/config-page.module.ts +++ b/src/app/pages/config-page/config-page.module.ts @@ -6,13 +6,7 @@ import { UiModule } from '../../ui/ui.module'; import { JiraViewComponentsModule } from '../../features/issue/providers/jira/jira-view-components/jira-view-components.module'; @NgModule({ - imports: [ - CommonModule, - ConfigModule, - UiModule, - JiraViewComponentsModule, - ], - declarations: [ConfigPageComponent] + imports: [CommonModule, ConfigModule, UiModule, JiraViewComponentsModule], + declarations: [ConfigPageComponent], }) -export class ConfigPageModule { -} +export class ConfigPageModule {} diff --git a/src/app/pages/daily-summary/daily-summary.component.ts b/src/app/pages/daily-summary/daily-summary.component.ts index 2647764d9..75a75ac3c 100644 --- a/src/app/pages/daily-summary/daily-summary.component.ts +++ b/src/app/pages/daily-summary/daily-summary.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, +} from '@angular/core'; import { TaskService } from '../../features/tasks/task.service'; import { ActivatedRoute, Router } from '@angular/router'; import { IS_ELECTRON } from '../../app.constants'; @@ -7,7 +13,16 @@ import { combineLatest, from, merge, Observable, Subscription } from 'rxjs'; import { IPC } from '../../../../electron/ipc-events.const'; import { DialogConfirmComponent } from '../../ui/dialog-confirm/dialog-confirm.component'; import { GlobalConfigService } from '../../features/config/global-config.service'; -import { delay, filter, map, shareReplay, startWith, switchMap, take, withLatestFrom } from 'rxjs/operators'; +import { + delay, + filter, + map, + shareReplay, + startWith, + switchMap, + take, + withLatestFrom, +} from 'rxjs/operators'; import { getWorklogStr } from '../../util/get-work-log-str'; import * as moment from 'moment'; import { T } from '../../t.const'; @@ -30,7 +45,7 @@ const MAGIC_YESTERDAY_MARGIN = 4 * 60 * 60 * 1000; selector: 'daily-summary', templateUrl: './daily-summary.component.html', styleUrls: ['./daily-summary.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DailySummaryComponent implements OnInit, OnDestroy { T: typeof T = T; @@ -47,7 +62,7 @@ export class DailySummaryComponent implements OnInit, OnDestroy { dayStr: string = getWorklogStr(); dayStr$: Observable = this._activatedRoute.paramMap.pipe( - startWith({params: {dayStr: getWorklogStr()}}), + startWith({ params: { dayStr: getWorklogStr() } }), map((s: any) => { if (s && s.params.dayStr) { return s.params.dayStr; @@ -55,7 +70,7 @@ export class DailySummaryComponent implements OnInit, OnDestroy { return getWorklogStr(); } }), - shareReplay(1) + shareReplay(1), ); tasksWorkedOnOrDoneOrRepeatableFlat$: Observable = this.dayStr$.pipe( @@ -63,52 +78,73 @@ export class DailySummaryComponent implements OnInit, OnDestroy { shareReplay(1), ); - hasTasksForToday$: Observable = this.tasksWorkedOnOrDoneOrRepeatableFlat$.pipe(map(tasks => tasks && !!tasks.length)); + hasTasksForToday$: Observable = this.tasksWorkedOnOrDoneOrRepeatableFlat$.pipe( + map((tasks) => tasks && !!tasks.length), + ); nrOfDoneTasks$: Observable = this.tasksWorkedOnOrDoneOrRepeatableFlat$.pipe( - map(tasks => tasks && tasks.filter(task => !!task.isDone).length), + map((tasks) => tasks && tasks.filter((task) => !!task.isDone).length), ); totalNrOfTasks$: Observable = this.tasksWorkedOnOrDoneOrRepeatableFlat$.pipe( - map(tasks => tasks && tasks.length), + map((tasks) => tasks && tasks.length), ); estimatedOnTasksWorkedOn$: Observable = this.tasksWorkedOnOrDoneOrRepeatableFlat$.pipe( withLatestFrom(this.dayStr$), - map(([tasks, dayStr]: [Task[], string]): number => tasks?.length && tasks.reduce((acc, task) => { - if (task.subTaskIds.length || (!task.timeSpentOnDay && !(task.timeSpentOnDay[dayStr] > 0))) { - return acc; - } - const remainingEstimate = task.timeEstimate + (task.timeSpentOnDay[dayStr]) - task.timeSpent; - return (remainingEstimate > 0) - ? acc + remainingEstimate - : acc; - }, 0 - )), + map( + ([tasks, dayStr]: [Task[], string]): number => + tasks?.length && + tasks.reduce((acc, task) => { + if ( + task.subTaskIds.length || + (!task.timeSpentOnDay && !(task.timeSpentOnDay[dayStr] > 0)) + ) { + return acc; + } + const remainingEstimate = + task.timeEstimate + task.timeSpentOnDay[dayStr] - task.timeSpent; + return remainingEstimate > 0 ? acc + remainingEstimate : acc; + }, 0), + ), ); timeWorked$: Observable = this.tasksWorkedOnOrDoneOrRepeatableFlat$.pipe( withLatestFrom(this.dayStr$), - map(([tasks, dayStr]: [Task[], string]): number => tasks?.length && tasks.reduce((acc, task) => { - if (task.subTaskIds.length) { - return acc; - } - return acc + ( - (task.timeSpentOnDay && +task.timeSpentOnDay[dayStr]) - ? +task.timeSpentOnDay[dayStr] - : 0 - ); - }, 0 - )), + map( + ([tasks, dayStr]: [Task[], string]): number => + tasks?.length && + tasks.reduce((acc, task) => { + if (task.subTaskIds.length) { + return acc; + } + return ( + acc + + (task.timeSpentOnDay && +task.timeSpentOnDay[dayStr] + ? +task.timeSpentOnDay[dayStr] + : 0) + ); + }, 0), + ), ); - started$: Observable = this.dayStr$.pipe(switchMap((dayStr) => this.workContextService.getWorkStart$(dayStr))); - end$: Observable = this.dayStr$.pipe(switchMap((dayStr) => this.workContextService.getWorkEnd$(dayStr))); + started$: Observable = this.dayStr$.pipe( + switchMap((dayStr) => this.workContextService.getWorkStart$(dayStr)), + ); + end$: Observable = this.dayStr$.pipe( + switchMap((dayStr) => this.workContextService.getWorkEnd$(dayStr)), + ); - breakTime$: Observable = this.dayStr$.pipe(switchMap((dayStr) => this.workContextService.getBreakTime$(dayStr))); - breakNr$: Observable = this.dayStr$.pipe(switchMap((dayStr) => this.workContextService.getBreakNr$(dayStr))); + breakTime$: Observable = this.dayStr$.pipe( + switchMap((dayStr) => this.workContextService.getBreakTime$(dayStr)), + ); + breakNr$: Observable = this.dayStr$.pipe( + switchMap((dayStr) => this.workContextService.getBreakNr$(dayStr)), + ); - isBreakTrackingSupport$: Observable = this.configService.idle$.pipe(map(cfg => cfg && cfg.isEnableIdleTimeTracking)); + isBreakTrackingSupport$: Observable = this.configService.idle$.pipe( + map((cfg) => cfg && cfg.isEnableIdleTimeTracking), + ); private _successAnimationTimeout?: number; @@ -131,24 +167,29 @@ export class DailySummaryComponent implements OnInit, OnDestroy { this._taskService.setSelectedId(null); const todayStart = new Date(); todayStart.setHours(0, 0, 0, 0); - this.isIncludeYesterday = (Date.now() - todayStart.getTime()) <= MAGIC_YESTERDAY_MARGIN; + this.isIncludeYesterday = Date.now() - todayStart.getTime() <= MAGIC_YESTERDAY_MARGIN; } ngOnInit() { // we need to wait, otherwise data would get overwritten - this._subs.add(this._taskService.currentTaskId$.pipe( - filter(id => !!id), - take(1), - ).subscribe(() => { - this._taskService.setCurrentId(null); - })); + this._subs.add( + this._taskService.currentTaskId$ + .pipe( + filter((id) => !!id), + take(1), + ) + .subscribe(() => { + this._taskService.setCurrentId(null); + }), + ); - this._subs.add(this._activatedRoute.paramMap.subscribe((s: any) => { + this._subs.add( + this._activatedRoute.paramMap.subscribe((s: any) => { if (s && s.params.dayStr) { this.isForToday = false; this.dayStr = s.params.dayStr; } - }) + }), ); } @@ -170,18 +211,22 @@ export class DailySummaryComponent implements OnInit, OnDestroy { this._taskService.moveToArchive(doneTasks); if (IS_ELECTRON && this.isForToday) { - this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - okTxt: T.PDS.D_CONFIRM_APP_CLOSE.OK, - cancelTxt: T.PDS.D_CONFIRM_APP_CLOSE.CANCEL, - message: T.PDS.D_CONFIRM_APP_CLOSE.MSG, - } - }).afterClosed() + this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + okTxt: T.PDS.D_CONFIRM_APP_CLOSE.OK, + cancelTxt: T.PDS.D_CONFIRM_APP_CLOSE.CANCEL, + message: T.PDS.D_CONFIRM_APP_CLOSE.MSG, + }, + }) + .afterClosed() .subscribe((isConfirm: boolean) => { if (isConfirm) { this._finishDayForGood(() => { - (this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.SHUTDOWN_NOW); + (this._electronService.ipcRenderer as typeof ipcRenderer).send( + IPC.SHUTDOWN_NOW, + ); }); } else if (isConfirm === false) { this._finishDayForGood(() => { @@ -235,7 +280,6 @@ export class DailySummaryComponent implements OnInit, OnDestroy { } private _getDailySummaryTasksFlat$(dayStr: string): Observable { - // TODO make more performant!! const _isWorkedOnOrDoneToday = (() => { if (this.isIncludeYesterday) { @@ -244,54 +288,68 @@ export class DailySummaryComponent implements OnInit, OnDestroy { const yesterdayStr = getWorklogStr(yesterday); return (t: Task) => - (t.timeSpentOnDay && t.timeSpentOnDay[dayStr] && t.timeSpentOnDay[dayStr] > 0) - || (t.timeSpentOnDay && t.timeSpentOnDay[yesterdayStr] && t.timeSpentOnDay[yesterdayStr] > 0) - || (t.isDone && t.doneOn && (isToday(t.doneOn) || isYesterday(t.doneOn))); + (t.timeSpentOnDay && + t.timeSpentOnDay[dayStr] && + t.timeSpentOnDay[dayStr] > 0) || + (t.timeSpentOnDay && + t.timeSpentOnDay[yesterdayStr] && + t.timeSpentOnDay[yesterdayStr] > 0) || + (t.isDone && t.doneOn && (isToday(t.doneOn) || isYesterday(t.doneOn))); } else { - return (t: Task) => (t.timeSpentOnDay && t.timeSpentOnDay[dayStr] && t.timeSpentOnDay[dayStr] > 0) - || (t.isDone && t.doneOn && isToday((t.doneOn))); + return (t: Task) => + (t.timeSpentOnDay && + t.timeSpentOnDay[dayStr] && + t.timeSpentOnDay[dayStr] > 0) || + (t.isDone && t.doneOn && isToday(t.doneOn)); } })(); - const _mapEntities = ([taskState, {activeType, activeId}]: [EntityState, { - activeId: string; - activeType: WorkContextType; - }]): TaskWithSubTasks[] => { - const ids = taskState && taskState.ids as string[] || []; - const archiveTasksI = ids.map(id => taskState.entities[id]); + const _mapEntities = ([taskState, { activeType, activeId }]: [ + EntityState, + { + activeId: string; + activeType: WorkContextType; + }, + ]): TaskWithSubTasks[] => { + const ids = (taskState && (taskState.ids as string[])) || []; + const archiveTasksI = ids.map((id) => taskState.entities[id]); let filteredTasks; if (activeId === TODAY_TAG.id) { filteredTasks = archiveTasksI as Task[]; } else if (activeType === WorkContextType.PROJECT) { filteredTasks = archiveTasksI.filter( - (task) => ((task as Task).projectId === activeId) + (task) => (task as Task).projectId === activeId, ) as Task[]; } else { - filteredTasks = archiveTasksI.filter( - (task) => !!(task as Task).parentId - ? (taskState.entities[(task as Task).parentId as string] as Task).tagIds.includes(activeId) - : (task as Task).tagIds.includes(activeId) + filteredTasks = archiveTasksI.filter((task) => + !!(task as Task).parentId + ? (taskState.entities[ + (task as Task).parentId as string + ] as Task).tagIds.includes(activeId) + : (task as Task).tagIds.includes(activeId), ) as Task[]; } // return filteredTasks; // to order sub tasks after their parents return filteredTasks - .filter(task => !task.parentId) - .map(task => task.subTaskIds.length - ? ({ - ...task, - subTasks: task.subTaskIds - .map(tid => taskState.entities[tid]) - .filter(t => t) - }) - : task) as TaskWithSubTasks[]; + .filter((task) => !task.parentId) + .map((task) => + task.subTaskIds.length + ? { + ...task, + subTasks: task.subTaskIds + .map((tid) => taskState.entities[tid]) + .filter((t) => t), + } + : task, + ) as TaskWithSubTasks[]; }; const _mapFilterToFlatToday = (tasks: TaskWithSubTasks[]): Task[] => { let flatTasks: Task[] = []; tasks.forEach((pt: TaskWithSubTasks) => { if (pt.subTasks && pt.subTasks.length) { - const subTasks = pt.subTasks.filter(st => _isWorkedOnOrDoneToday(st)); + const subTasks = pt.subTasks.filter((st) => _isWorkedOnOrDoneToday(st)); if (subTasks.length) { flatTasks.push(pt); flatTasks = flatTasks.concat(subTasks); @@ -307,7 +365,7 @@ export class DailySummaryComponent implements OnInit, OnDestroy { let flatTasks: Task[] = []; tasks.forEach((pt: TaskWithSubTasks) => { if (pt.subTasks && pt.subTasks.length) { - const subTasks = pt.subTasks.filter(st => _isWorkedOnOrDoneToday(st)); + const subTasks = pt.subTasks.filter((st) => _isWorkedOnOrDoneToday(st)); if (subTasks.length) { flatTasks.push(pt); flatTasks = flatTasks.concat(subTasks); @@ -324,24 +382,23 @@ export class DailySummaryComponent implements OnInit, OnDestroy { this._worklogService.archiveUpdateManualTrigger$.pipe( // hacky wait for save delay(70), - switchMap(() => this._persistenceService.taskArchive.loadState()) - ) + switchMap(() => this._persistenceService.taskArchive.loadState()), + ), ).pipe( withLatestFrom(this.workContextService.activeWorkContextTypeAndId$), map(_mapEntities), map(_mapFilterToFlatToday), ); - const todayTasks: Observable = this._taskService.taskFeatureState$.pipe( + const todayTasks: Observable< + TaskWithSubTasks[] + > = this._taskService.taskFeatureState$.pipe( withLatestFrom(this.workContextService.activeWorkContextTypeAndId$), map(_mapEntities), map(_mapFilterToFlatOrRepeatToday), ); - return combineLatest([ - todayTasks, - archiveTasks, - ]).pipe( + return combineLatest([todayTasks, archiveTasks]).pipe( map(([t1, t2]) => t1.concat(t2)), ); } diff --git a/src/app/pages/daily-summary/daily-summary.module.ts b/src/app/pages/daily-summary/daily-summary.module.ts index 9db9f289e..1c17aceb2 100644 --- a/src/app/pages/daily-summary/daily-summary.module.ts +++ b/src/app/pages/daily-summary/daily-summary.module.ts @@ -21,10 +21,6 @@ import { BetterDrawerModule } from '../../ui/better-drawer/better-drawer.module' TasksModule, BetterDrawerModule, ], - declarations: [ - DailySummaryComponent, - PlanTasksTomorrowComponent, - ], + declarations: [DailySummaryComponent, PlanTasksTomorrowComponent], }) -export class DailySummaryModule { -} +export class DailySummaryModule {} diff --git a/src/app/pages/daily-summary/plan-tasks-tomorrow/plan-tasks-tomorrow.component.ts b/src/app/pages/daily-summary/plan-tasks-tomorrow/plan-tasks-tomorrow.component.ts index f8f880763..840809d83 100644 --- a/src/app/pages/daily-summary/plan-tasks-tomorrow/plan-tasks-tomorrow.component.ts +++ b/src/app/pages/daily-summary/plan-tasks-tomorrow/plan-tasks-tomorrow.component.ts @@ -5,12 +5,8 @@ import { WorkContextService } from '../../../features/work-context/work-context. selector: 'plan-tasks-tomorrow', templateUrl: './plan-tasks-tomorrow.component.html', styleUrls: ['./plan-tasks-tomorrow.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class PlanTasksTomorrowComponent { - - constructor( - public workContextService: WorkContextService, - ) { - } + constructor(public workContextService: WorkContextService) {} } diff --git a/src/app/pages/metric-page/metric-page.component.ts b/src/app/pages/metric-page/metric-page.component.ts index d3db4e947..2039b6a14 100644 --- a/src/app/pages/metric-page/metric-page.component.ts +++ b/src/app/pages/metric-page/metric-page.component.ts @@ -5,11 +5,10 @@ import { T } from '../../t.const'; selector: 'metric-page', templateUrl: './metric-page.component.html', styleUrls: ['./metric-page.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class MetricPageComponent { T: typeof T = T; - constructor() { - } + constructor() {} } diff --git a/src/app/pages/metric-page/metric-page.module.ts b/src/app/pages/metric-page/metric-page.module.ts index 5f006e301..445903646 100644 --- a/src/app/pages/metric-page/metric-page.module.ts +++ b/src/app/pages/metric-page/metric-page.module.ts @@ -6,11 +6,6 @@ import { MetricModule } from '../../features/metric/metric.module'; @NgModule({ declarations: [MetricPageComponent], - imports: [ - CommonModule, - UiModule, - MetricModule, - ] + imports: [CommonModule, UiModule, MetricModule], }) -export class MetricPageModule { -} +export class MetricPageModule {} diff --git a/src/app/pages/pages.module.ts b/src/app/pages/pages.module.ts index 9773f31a6..fe2fdd2c7 100644 --- a/src/app/pages/pages.module.ts +++ b/src/app/pages/pages.module.ts @@ -25,7 +25,6 @@ import { TagSettingsPageModule } from './tag-settings-page/tag-settings-page.mod TagTaskPageModule, TagSettingsPageModule, ], - declarations: [] + declarations: [], }) -export class PagesModule { -} +export class PagesModule {} diff --git a/src/app/pages/project-overview-page/project-overview-page.component.ts b/src/app/pages/project-overview-page/project-overview-page.component.ts index e630008dd..08a840390 100644 --- a/src/app/pages/project-overview-page/project-overview-page.component.ts +++ b/src/app/pages/project-overview-page/project-overview-page.component.ts @@ -34,8 +34,7 @@ export class ProjectOverviewPageComponent implements OnInit, OnDestroy { private readonly _dragulaService: DragulaService, private readonly _snackService: SnackService, private readonly _persistenceService: PersistenceService, - ) { - } + ) {} openCreateDialog() { this._matDialog.open(DialogCreateProjectComponent, { @@ -44,17 +43,17 @@ export class ProjectOverviewPageComponent implements OnInit, OnDestroy { } ngOnInit() { - this._subs.add(this._dragulaService.dropModel('PROJECTS').pipe( - withLatestFrom(this.projectService.archived$), - ).subscribe(([params, archived]: any) => { - const {targetModel} = params; - const targetNewIds = targetModel.map((project: Project) => project.id); + this._subs.add( + this._dragulaService + .dropModel('PROJECTS') + .pipe(withLatestFrom(this.projectService.archived$)) + .subscribe(([params, archived]: any) => { + const { targetModel } = params; + const targetNewIds = targetModel.map((project: Project) => project.id); - const archivedIds = archived - ? archived.map((p: Project) => p.id) - : []; - this.projectService.updateOrder([...targetNewIds, ...archivedIds]); - }) + const archivedIds = archived ? archived.map((p: Project) => p.id) : []; + this.projectService.updateOrder([...targetNewIds, ...archivedIds]); + }), ); } @@ -63,7 +62,9 @@ export class ProjectOverviewPageComponent implements OnInit, OnDestroy { } async export(projectId: string, projectTitle: string) { - const data: ExportedProject = await this._persistenceService.loadCompleteProject(projectId); + const data: ExportedProject = await this._persistenceService.loadCompleteProject( + projectId, + ); console.log(data); const dataString = JSON.stringify(data); download(`${projectTitle}.json`, dataString); @@ -83,7 +84,7 @@ export class ProjectOverviewPageComponent implements OnInit, OnDestroy { this.projectService.importCompleteProject(project); } catch (e) { console.error(e); - this._snackService.open({type: 'ERROR', msg: T.PP.S_INVALID_JSON}); + this._snackService.open({ type: 'ERROR', msg: T.PP.S_INVALID_JSON }); } }; reader.readAsText(file); @@ -97,13 +98,15 @@ export class ProjectOverviewPageComponent implements OnInit, OnDestroy { } archive(projectId: string) { - this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - okTxt: T.PP.D_CONFIRM_ARCHIVE.OK, - message: T.PP.D_CONFIRM_ARCHIVE.MSG, - } - }).afterClosed() + this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + okTxt: T.PP.D_CONFIRM_ARCHIVE.OK, + message: T.PP.D_CONFIRM_ARCHIVE.MSG, + }, + }) + .afterClosed() .subscribe((isConfirm: boolean) => { if (isConfirm) { this.projectService.archive(projectId); @@ -112,13 +115,15 @@ export class ProjectOverviewPageComponent implements OnInit, OnDestroy { } unarchive(projectId: string) { - this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - okTxt: T.PP.D_CONFIRM_UNARCHIVE.OK, - message: T.PP.D_CONFIRM_UNARCHIVE.MSG, - } - }).afterClosed() + this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + okTxt: T.PP.D_CONFIRM_UNARCHIVE.OK, + message: T.PP.D_CONFIRM_UNARCHIVE.MSG, + }, + }) + .afterClosed() .subscribe((isConfirm: boolean) => { if (isConfirm) { this.projectService.unarchive(projectId); @@ -127,13 +132,15 @@ export class ProjectOverviewPageComponent implements OnInit, OnDestroy { } remove(projectId: string) { - this._matDialog.open(DialogConfirmComponent, { - restoreFocus: true, - data: { - okTxt: T.PP.D_CONFIRM_DELETE.OK, - message: T.PP.D_CONFIRM_DELETE.MSG, - } - }).afterClosed() + this._matDialog + .open(DialogConfirmComponent, { + restoreFocus: true, + data: { + okTxt: T.PP.D_CONFIRM_DELETE.OK, + message: T.PP.D_CONFIRM_DELETE.MSG, + }, + }) + .afterClosed() .subscribe((isConfirm: boolean) => { if (isConfirm) { this.projectService.remove(projectId); @@ -147,9 +154,7 @@ export class ProjectOverviewPageComponent implements OnInit, OnDestroy { getThemeColor(color: string): { [key: string]: string } { const standardColor = (THEME_COLOR_MAP as any)[color]; - const colorToUse = (standardColor) - ? standardColor - : color; - return {background: colorToUse}; + const colorToUse = standardColor ? standardColor : color; + return { background: colorToUse }; } } diff --git a/src/app/pages/project-overview-page/project-overview-page.module.ts b/src/app/pages/project-overview-page/project-overview-page.module.ts index 043ac1988..b742ee28e 100644 --- a/src/app/pages/project-overview-page/project-overview-page.module.ts +++ b/src/app/pages/project-overview-page/project-overview-page.module.ts @@ -5,12 +5,7 @@ import { ProjectModule } from '../../features/project/project.module'; import { UiModule } from '../../ui/ui.module'; @NgModule({ - imports: [ - CommonModule, - UiModule, - ProjectModule - ], + imports: [CommonModule, UiModule, ProjectModule], declarations: [ProjectOverviewPageComponent], }) -export class ProjectOverviewPageModule { -} +export class ProjectOverviewPageModule {} diff --git a/src/app/pages/project-settings-page/project-settings-page.component.ts b/src/app/pages/project-settings-page/project-settings-page.component.ts index b8e2362c9..f8ae2c2af 100644 --- a/src/app/pages/project-settings-page/project-settings-page.component.ts +++ b/src/app/pages/project-settings-page/project-settings-page.component.ts @@ -1,8 +1,22 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, +} from '@angular/core'; import { T } from '../../t.const'; -import { ConfigFormConfig, ConfigFormSection, GlobalConfigSectionKey } from '../../features/config/global-config.model'; +import { + ConfigFormConfig, + ConfigFormSection, + GlobalConfigSectionKey, +} from '../../features/config/global-config.model'; import { Project, ProjectCfgFormKey } from '../../features/project/project.model'; -import { IssueIntegrationCfg, IssueIntegrationCfgs, IssueProviderKey } from '../../features/issue/issue.model'; +import { + IssueIntegrationCfg, + IssueIntegrationCfgs, + IssueProviderKey, +} from '../../features/issue/issue.model'; import { Subscription } from 'rxjs'; import { ProjectService } from '../../features/project/project.service'; import { BASIC_PROJECT_CONFIG_FORM_CONFIG } from '../../features/project/project-form-cfg.const'; @@ -11,7 +25,10 @@ import { GLOBAL_CONFIG_FORM_CONFIG } from '../../features/config/global-config-f import { IS_ELECTRON } from '../../app.constants'; import { DEFAULT_JIRA_CFG } from '../../features/issue/providers/jira/jira.const'; import { DEFAULT_GITHUB_CFG } from '../../features/issue/providers/github/github.const'; -import { WorkContextAdvancedCfg, WorkContextThemeCfg } from '../../features/work-context/work-context.model'; +import { + WorkContextAdvancedCfg, + WorkContextThemeCfg, +} from '../../features/work-context/work-context.model'; import { WORK_CONTEXT_THEME_CONFIG_FORM_CONFIG } from '../../features/work-context/work-context.const'; import { WorkContextService } from '../../features/work-context/work-context.service'; import { DEFAULT_GITLAB_CFG } from 'src/app/features/issue/providers/gitlab/gitlab.const'; @@ -20,7 +37,7 @@ import { DEFAULT_GITLAB_CFG } from 'src/app/features/issue/providers/gitlab/gitl selector: 'project-settings', templateUrl: './project-settings-page.component.html', styleUrls: ['./project-settings-page.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectSettingsPageComponent implements OnInit, OnDestroy { T: typeof T = T; @@ -45,35 +62,39 @@ export class ProjectSettingsPageComponent implements OnInit, OnDestroy { this.projectThemeSettingsFormCfg = WORK_CONTEXT_THEME_CONFIG_FORM_CONFIG; this.issueIntegrationFormCfg = ISSUE_PROVIDER_FORM_CFGS; this.basicFormCfg = BASIC_PROJECT_CONFIG_FORM_CONFIG; - this.globalConfigFormCfg = GLOBAL_CONFIG_FORM_CONFIG.filter((cfg) => IS_ELECTRON || !cfg.isElectronOnly); + this.globalConfigFormCfg = GLOBAL_CONFIG_FORM_CONFIG.filter( + (cfg) => IS_ELECTRON || !cfg.isElectronOnly, + ); } ngOnInit() { - this._subs.add(this.projectService.currentProject$.subscribe((project: Project | null) => { - if (!project) { - throw new Error(); - } + this._subs.add( + this.projectService.currentProject$.subscribe((project: Project | null) => { + if (!project) { + throw new Error(); + } - this.currentProject = project as Project; - this.projectCfg = project.advancedCfg; - this.currentProjectTheme = project.theme; + this.currentProject = project as Project; + this.projectCfg = project.advancedCfg; + this.currentProjectTheme = project.theme; - // in case there are new ones... - this.issueIntegrationCfgs = {...project.issueIntegrationCfgs}; + // in case there are new ones... + this.issueIntegrationCfgs = { ...project.issueIntegrationCfgs }; - // Unfortunately needed, to make sure we have no empty configs - // TODO maybe think of a better solution for the defaults - if (!this.issueIntegrationCfgs.JIRA) { - this.issueIntegrationCfgs.JIRA = DEFAULT_JIRA_CFG; - } - if (!this.issueIntegrationCfgs.GITHUB) { - this.issueIntegrationCfgs.GITHUB = DEFAULT_GITHUB_CFG; - } - if (!this.issueIntegrationCfgs.GITLAB) { - this.issueIntegrationCfgs.GITLAB = DEFAULT_GITLAB_CFG; - } - this._cd.detectChanges(); - })); + // Unfortunately needed, to make sure we have no empty configs + // TODO maybe think of a better solution for the defaults + if (!this.issueIntegrationCfgs.JIRA) { + this.issueIntegrationCfgs.JIRA = DEFAULT_JIRA_CFG; + } + if (!this.issueIntegrationCfgs.GITHUB) { + this.issueIntegrationCfgs.GITHUB = DEFAULT_GITHUB_CFG; + } + if (!this.issueIntegrationCfgs.GITLAB) { + this.issueIntegrationCfgs.GITLAB = DEFAULT_GITLAB_CFG; + } + this._cd.detectChanges(); + }), + ); } ngOnDestroy() { @@ -84,19 +105,25 @@ export class ProjectSettingsPageComponent implements OnInit, OnDestroy { return section.key; } - saveProjectThemCfg($event: { sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; config: WorkContextThemeCfg }) { + saveProjectThemCfg($event: { + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; + config: WorkContextThemeCfg; + }) { if (!$event.config || !this.currentProject) { throw new Error('Not enough data'); } else { this.projectService.update(this.currentProject.id, { theme: { ...$event.config, - } + }, }); } } - saveBasicSettings($event: { sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; config: Project }) { + saveBasicSettings($event: { + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; + config: Project; + }) { if (!$event.config || !this.currentProject) { throw new Error('Not enough data'); } else { @@ -106,15 +133,23 @@ export class ProjectSettingsPageComponent implements OnInit, OnDestroy { } } - saveIssueProviderCfg($event: { sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; config: IssueIntegrationCfg }) { + saveIssueProviderCfg($event: { + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey; + config: IssueIntegrationCfg; + }) { if (!$event.config || !this.currentProject) { throw new Error('Not enough data'); } - const {sectionKey, config} = $event; + const { sectionKey, config } = $event; const sectionKeyIN = sectionKey as IssueProviderKey; - this.projectService.updateIssueProviderConfig(this.currentProject.id, sectionKeyIN, { - ...config, - }, true); + this.projectService.updateIssueProviderConfig( + this.currentProject.id, + sectionKeyIN, + { + ...config, + }, + true, + ); } getIssueIntegrationCfg(key: IssueProviderKey): IssueIntegrationCfg { diff --git a/src/app/pages/project-settings-page/project-settings-page.module.ts b/src/app/pages/project-settings-page/project-settings-page.module.ts index fcfa801c0..60c36bd43 100644 --- a/src/app/pages/project-settings-page/project-settings-page.module.ts +++ b/src/app/pages/project-settings-page/project-settings-page.module.ts @@ -7,12 +7,6 @@ import { JiraViewComponentsModule } from '../../features/issue/providers/jira/ji @NgModule({ declarations: [ProjectSettingsPageComponent], - imports: [ - CommonModule, - ConfigModule, - UiModule, - JiraViewComponentsModule, - ] + imports: [CommonModule, ConfigModule, UiModule, JiraViewComponentsModule], }) -export class ProjectSettingsPageModule { -} +export class ProjectSettingsPageModule {} diff --git a/src/app/pages/project-task-page/project-task-page.component.ts b/src/app/pages/project-task-page/project-task-page.component.ts index cb2cc2209..9e998dac3 100644 --- a/src/app/pages/project-task-page/project-task-page.component.ts +++ b/src/app/pages/project-task-page/project-task-page.component.ts @@ -8,9 +8,5 @@ import { WorkContextService } from '../../features/work-context/work-context.ser changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectTaskPageComponent { - constructor( - public workContextService: WorkContextService, - ) { - } - + constructor(public workContextService: WorkContextService) {} } diff --git a/src/app/pages/project-task-page/project-task-page.module.ts b/src/app/pages/project-task-page/project-task-page.module.ts index 6c872fc06..b067dc90b 100644 --- a/src/app/pages/project-task-page/project-task-page.module.ts +++ b/src/app/pages/project-task-page/project-task-page.module.ts @@ -4,11 +4,7 @@ import { ProjectTaskPageComponent } from './project-task-page.component'; import { WorkViewModule } from '../../features/work-view/work-view.module'; @NgModule({ - imports: [ - CommonModule, - WorkViewModule, - ], + imports: [CommonModule, WorkViewModule], declarations: [ProjectTaskPageComponent], }) -export class ProjectTaskPageModule { -} +export class ProjectTaskPageModule {} diff --git a/src/app/pages/schedule-page/schedule-page.component.ts b/src/app/pages/schedule-page/schedule-page.component.ts index b5509d93c..7c2022eb0 100644 --- a/src/app/pages/schedule-page/schedule-page.component.ts +++ b/src/app/pages/schedule-page/schedule-page.component.ts @@ -19,7 +19,7 @@ import { Tag } from '../../features/tag/tag.model'; templateUrl: './schedule-page.component.html', styleUrls: ['./schedule-page.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [standardListAnimation] + animations: [standardListAnimation], }) export class SchedulePageComponent { T: typeof T = T; @@ -32,11 +32,12 @@ export class SchedulePageComponent { private _taskService: TaskService, private _matDialog: MatDialog, private _router: Router, - ) { - } + ) {} startTask(task: TaskWithReminderData) { - if (task.reminderData.workContextId === this._workContextService.activeWorkContextId) { + if ( + task.reminderData.workContextId === this._workContextService.activeWorkContextId + ) { this._startTaskFronCurrentProject(task); } else { this._startTaskFromOtherProject(task); @@ -45,10 +46,13 @@ export class SchedulePageComponent { toggleToday(task: TaskWithReminderData | Task) { if (task.tagIds.includes(TODAY_TAG.id)) { - this._taskService.updateTags(task, task.tagIds.filter(id => id !== TODAY_TAG.id), task.tagIds); + this._taskService.updateTags( + task, + task.tagIds.filter((id) => id !== TODAY_TAG.id), + task.tagIds, + ); } else { this._taskService.updateTags(task, [TODAY_TAG.id, ...task.tagIds], task.tagIds); - } } @@ -56,19 +60,18 @@ export class SchedulePageComponent { if (task.reminderId) { this._taskService.unScheduleTask(task.id, task.reminderId); } - } editReminder(task: TaskWithReminderData) { this._matDialog.open(DialogAddTaskReminderComponent, { restoreFocus: true, - data: {task} as AddTaskReminderInterface + data: { task } as AddTaskReminderInterface, }); } updateTaskTitleIfChanged(isChanged: boolean, newTitle: string, task: Task) { if (isChanged) { - this._taskService.update(task.id, {title: newTitle}); + this._taskService.update(task.id, { title: newTitle }); } // this.focusSelf(); } @@ -91,7 +94,12 @@ export class SchedulePageComponent { } private _startTaskFromOtherProject(task: TaskWithReminderData) { - this._taskService.startTaskFromOtherContext$(task.id, task.reminderData.workContextType, task.reminderData.workContextId) + this._taskService + .startTaskFromOtherContext$( + task.id, + task.reminderData.workContextType, + task.reminderData.workContextId, + ) .pipe(take(1)) .subscribe(() => { this._router.navigate(['/active/tasks']); diff --git a/src/app/pages/schedule-page/schedule-page.module.ts b/src/app/pages/schedule-page/schedule-page.module.ts index 43acf92b1..3197929c6 100644 --- a/src/app/pages/schedule-page/schedule-page.module.ts +++ b/src/app/pages/schedule-page/schedule-page.module.ts @@ -6,12 +6,7 @@ import { TagModule } from '../../features/tag/tag.module'; @NgModule({ declarations: [SchedulePageComponent], - imports: [ - CommonModule, - UiModule, - TagModule, - ], - exports: [SchedulePageComponent] + imports: [CommonModule, UiModule, TagModule], + exports: [SchedulePageComponent], }) -export class SchedulePageModule { -} +export class SchedulePageModule {} diff --git a/src/app/pages/tag-settings-page/tag-settings-page.component.ts b/src/app/pages/tag-settings-page/tag-settings-page.component.ts index ded2b740f..9b530ac40 100644 --- a/src/app/pages/tag-settings-page/tag-settings-page.component.ts +++ b/src/app/pages/tag-settings-page/tag-settings-page.component.ts @@ -1,13 +1,23 @@ -import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + OnDestroy, + OnInit, +} from '@angular/core'; import { T } from '../../t.const'; -import { ConfigFormConfig, ConfigFormSection, GlobalConfigSectionKey } from '../../features/config/global-config.model'; +import { + ConfigFormConfig, + ConfigFormSection, + GlobalConfigSectionKey, +} from '../../features/config/global-config.model'; import { Subscription } from 'rxjs'; import { GLOBAL_CONFIG_FORM_CONFIG } from '../../features/config/global-config-form-config.const'; import { IS_ELECTRON } from '../../app.constants'; import { WorkContext, WorkContextAdvancedCfg, - WorkContextThemeCfg + WorkContextThemeCfg, } from '../../features/work-context/work-context.model'; import { WorkContextService } from '../../features/work-context/work-context.service'; import { Tag, TagCfgFormKey } from '../../features/tag/tag.model'; @@ -20,7 +30,7 @@ import { BASIC_TAG_CONFIG_FORM_CONFIG } from '../../features/tag/tag-form-cfg.co selector: 'project-settings', templateUrl: './tag-settings-page.component.html', styleUrls: ['./tag-settings-page.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class TagSettingsPageComponent implements OnInit, OnDestroy { T: typeof T = T; @@ -42,39 +52,49 @@ export class TagSettingsPageComponent implements OnInit, OnDestroy { // somehow they are only unproblematic if assigned here this.tagThemeSettingsFormCfg = WORK_CONTEXT_THEME_CONFIG_FORM_CONFIG; this.basicFormCfg = BASIC_TAG_CONFIG_FORM_CONFIG; - this.globalConfigFormCfg = GLOBAL_CONFIG_FORM_CONFIG.filter((cfg) => IS_ELECTRON || !cfg.isElectronOnly); + this.globalConfigFormCfg = GLOBAL_CONFIG_FORM_CONFIG.filter( + (cfg) => IS_ELECTRON || !cfg.isElectronOnly, + ); } ngOnInit() { - this._subs.add(this.workContextService.activeWorkContext$.subscribe((ac) => { - this.activeWorkContext = ac; - this.workContextAdvCfg = ac.advancedCfg; - this.currentWorkContextTheme = ac.theme; - this._cd.detectChanges(); - })); + this._subs.add( + this.workContextService.activeWorkContext$.subscribe((ac) => { + this.activeWorkContext = ac; + this.workContextAdvCfg = ac.advancedCfg; + this.currentWorkContextTheme = ac.theme; + this._cd.detectChanges(); + }), + ); } ngOnDestroy() { this._subs.unsubscribe(); } - saveTagThemCfg($event: { sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey | TagCfgFormKey; config: WorkContextThemeCfg }) { + saveTagThemCfg($event: { + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey | TagCfgFormKey; + config: WorkContextThemeCfg; + }) { if (!$event.config || this.activeWorkContext === null) { throw new Error('Not enough data'); } else { this.tagService.updateTag(this.activeWorkContext.id, { theme: { ...$event.config, - } + }, }); } } - saveBasicSettings($event: { sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey | TagCfgFormKey; config: Tag }) { + saveBasicSettings($event: { + sectionKey: GlobalConfigSectionKey | ProjectCfgFormKey | TagCfgFormKey; + config: Tag; + }) { if (!$event.config || this.activeWorkContext === null) { throw new Error('Not enough data'); } else { - const {title, icon, color} = $event.config; + const { title, icon, color } = $event.config; this.tagService.updateTag(this.activeWorkContext.id, { title, icon, diff --git a/src/app/pages/tag-settings-page/tag-settings-page.module.ts b/src/app/pages/tag-settings-page/tag-settings-page.module.ts index bacd35fa9..895484e26 100644 --- a/src/app/pages/tag-settings-page/tag-settings-page.module.ts +++ b/src/app/pages/tag-settings-page/tag-settings-page.module.ts @@ -7,12 +7,6 @@ import { JiraViewComponentsModule } from '../../features/issue/providers/jira/ji @NgModule({ declarations: [TagSettingsPageComponent], - imports: [ - CommonModule, - ConfigModule, - UiModule, - JiraViewComponentsModule, - ] + imports: [CommonModule, ConfigModule, UiModule, JiraViewComponentsModule], }) -export class TagSettingsPageModule { -} +export class TagSettingsPageModule {} diff --git a/src/app/pages/tag-task-page/tag-task-page.component.ts b/src/app/pages/tag-task-page/tag-task-page.component.ts index 2e3d5f934..828aafd80 100644 --- a/src/app/pages/tag-task-page/tag-task-page.component.ts +++ b/src/app/pages/tag-task-page/tag-task-page.component.ts @@ -8,8 +8,5 @@ import { WorkContextService } from '../../features/work-context/work-context.ser changeDetection: ChangeDetectionStrategy.OnPush, }) export class TagTaskPageComponent { - constructor( - public workContextService: WorkContextService, - ) { - } + constructor(public workContextService: WorkContextService) {} } diff --git a/src/app/pages/tag-task-page/tag-task-page.module.ts b/src/app/pages/tag-task-page/tag-task-page.module.ts index 5688f8c29..6a3d47d59 100644 --- a/src/app/pages/tag-task-page/tag-task-page.module.ts +++ b/src/app/pages/tag-task-page/tag-task-page.module.ts @@ -4,13 +4,7 @@ import { TagTaskPageComponent } from './tag-task-page.component'; import { WorkViewModule } from '../../features/work-view/work-view.module'; @NgModule({ - declarations: [ - TagTaskPageComponent, - ], - imports: [ - CommonModule, - WorkViewModule, - ], + declarations: [TagTaskPageComponent], + imports: [CommonModule, WorkViewModule], }) -export class TagTaskPageModule { -} +export class TagTaskPageModule {} diff --git a/src/app/pages/worklog-page/worklog-page.component.ts b/src/app/pages/worklog-page/worklog-page.component.ts index d6f397b26..9cb8d67c8 100644 --- a/src/app/pages/worklog-page/worklog-page.component.ts +++ b/src/app/pages/worklog-page/worklog-page.component.ts @@ -5,11 +5,10 @@ import { T } from '../../t.const'; selector: 'worklog-page', templateUrl: './worklog-page.component.html', styleUrls: ['./worklog-page.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class WorklogPageComponent { T: typeof T = T; - constructor() { - } + constructor() {} } diff --git a/src/app/pages/worklog-page/worklog-page.module.ts b/src/app/pages/worklog-page/worklog-page.module.ts index b38b2fd6c..7c98e7c89 100644 --- a/src/app/pages/worklog-page/worklog-page.module.ts +++ b/src/app/pages/worklog-page/worklog-page.module.ts @@ -6,11 +6,6 @@ import { WorklogModule } from '../../features/worklog/worklog.module'; @NgModule({ declarations: [WorklogPageComponent], - imports: [ - CommonModule, - UiModule, - WorklogModule, - ] + imports: [CommonModule, UiModule, WorklogModule], }) -export class WorklogPageModule { -} +export class WorklogPageModule {} diff --git a/src/app/root-store/index.ts b/src/app/root-store/index.ts index c30d5828f..de86a36af 100644 --- a/src/app/root-store/index.ts +++ b/src/app/root-store/index.ts @@ -6,4 +6,3 @@ export const reducers: ActionReducerMap = { // return state; // } }; - diff --git a/src/app/root-store/meta/all-data-was-loaded.actions.ts b/src/app/root-store/meta/all-data-was-loaded.actions.ts index 62fce1a05..1a1c2d7d6 100644 --- a/src/app/root-store/meta/all-data-was-loaded.actions.ts +++ b/src/app/root-store/meta/all-data-was-loaded.actions.ts @@ -1,5 +1,3 @@ import { createAction } from '@ngrx/store'; -export const allDataWasLoaded = createAction( - '[SP_ALL] All Data was loaded', -); +export const allDataWasLoaded = createAction('[SP_ALL] All Data was loaded'); diff --git a/src/app/root-store/meta/load-all-data.action.ts b/src/app/root-store/meta/load-all-data.action.ts index 80ffbb4af..02f2617e0 100644 --- a/src/app/root-store/meta/load-all-data.action.ts +++ b/src/app/root-store/meta/load-all-data.action.ts @@ -5,4 +5,3 @@ export const loadAllData = createAction( '[SP_ALL] Load(import) all data', props<{ appDataComplete: AppDataComplete; isOmitTokens: boolean }>(), ); - diff --git a/src/app/root-store/meta/undo-task-delete.meta-reducer.ts b/src/app/root-store/meta/undo-task-delete.meta-reducer.ts index 2a75c9193..42fa548cc 100644 --- a/src/app/root-store/meta/undo-task-delete.meta-reducer.ts +++ b/src/app/root-store/meta/undo-task-delete.meta-reducer.ts @@ -2,7 +2,10 @@ import { RootState } from '../root-state'; import { Dictionary } from '@ngrx/entity'; import { Task, TaskWithSubTasks } from '../../features/tasks/task.model'; import { TaskActionTypes } from '../../features/tasks/store/task.actions'; -import { PROJECT_FEATURE_NAME, projectAdapter } from '../../features/project/store/project.reducer'; +import { + PROJECT_FEATURE_NAME, + projectAdapter, +} from '../../features/project/store/project.reducer'; import { TASK_FEATURE_NAME } from '../../features/tasks/store/task.reducer'; import { TAG_FEATURE_NAME, tagAdapter } from '../../features/tag/store/tag.reducer'; import { taskAdapter } from '../../features/tasks/store/task.adapter'; @@ -27,7 +30,6 @@ let U_STORE: UndoTaskDeleteState; export const undoTaskDeleteMetaReducer = (reducer: any): any => { return (state: RootState, action: any) => { - switch (action.type) { case TaskActionTypes.DeleteTask: U_STORE = _createTaskDeleteState(state, action.payload.task); @@ -35,30 +37,35 @@ export const undoTaskDeleteMetaReducer = (reducer: any): any => { case TaskActionTypes.UndoDeleteTask: let updatedState = state; - const tasksToRestore: Task[] = Object.keys(U_STORE.deletedTaskEntities).map( - (id: string) => U_STORE.deletedTaskEntities[id] - ).filter(t => { - if (!t) { - throw new Error('Task Restore Error: Missing task data when restoruii'); - } - return true; - }) as Task[]; + const tasksToRestore: Task[] = Object.keys(U_STORE.deletedTaskEntities) + .map((id: string) => U_STORE.deletedTaskEntities[id]) + .filter((t) => { + if (!t) { + throw new Error('Task Restore Error: Missing task data when restoruii'); + } + return true; + }) as Task[]; updatedState = { ...updatedState, - [TASK_FEATURE_NAME]: taskAdapter.addMany(tasksToRestore, updatedState[TASK_FEATURE_NAME] + [TASK_FEATURE_NAME]: taskAdapter.addMany( + tasksToRestore, + updatedState[TASK_FEATURE_NAME], ), }; if (U_STORE.parentTaskId) { updatedState = { ...updatedState, - [TASK_FEATURE_NAME]: taskAdapter.updateOne({ - id: U_STORE.parentTaskId, - changes: { - subTaskIds: U_STORE.subTaskIds, - } - }, updatedState[TASK_FEATURE_NAME]), + [TASK_FEATURE_NAME]: taskAdapter.updateOne( + { + id: U_STORE.parentTaskId, + changes: { + subTaskIds: U_STORE.subTaskIds, + }, + }, + updatedState[TASK_FEATURE_NAME], + ), }; } @@ -66,42 +73,46 @@ export const undoTaskDeleteMetaReducer = (reducer: any): any => { updatedState = { ...updatedState, [TAG_FEATURE_NAME]: tagAdapter.updateMany( - Object.keys(U_STORE.tagTaskIdMap).map(id => { - if (!U_STORE.tagTaskIdMap) { - throw new Error('Task Restore Error: Missing tagTaskIdMap data for restoring task'); - } - if (!U_STORE.tagTaskIdMap[id]) { - throw new Error('Task Restore Error: Missing tag data for restoring task'); - } - return { - id, - changes: { - taskIds: U_STORE.tagTaskIdMap[id] - } - }; + Object.keys(U_STORE.tagTaskIdMap).map((id) => { + if (!U_STORE.tagTaskIdMap) { + throw new Error( + 'Task Restore Error: Missing tagTaskIdMap data for restoring task', + ); } - ), updatedState[TAG_FEATURE_NAME]), + if (!U_STORE.tagTaskIdMap[id]) { + throw new Error( + 'Task Restore Error: Missing tag data for restoring task', + ); + } + return { + id, + changes: { + taskIds: U_STORE.tagTaskIdMap[id], + }, + }; + }), + updatedState[TAG_FEATURE_NAME], + ), }; } if (U_STORE.projectId) { updatedState = { ...updatedState, - [PROJECT_FEATURE_NAME]: projectAdapter.updateOne({ - id: U_STORE.projectId, - changes: { - ...( - U_STORE.taskIdsForProject - ? {taskIds: U_STORE.taskIdsForProject} - : {} - ), - ...( - U_STORE.taskIdsForProjectBacklog - ? {backlogTaskIds: U_STORE.taskIdsForProjectBacklog} - : {} - ) - } - }, updatedState[PROJECT_FEATURE_NAME]), + [PROJECT_FEATURE_NAME]: projectAdapter.updateOne( + { + id: U_STORE.projectId, + changes: { + ...(U_STORE.taskIdsForProject + ? { taskIds: U_STORE.taskIdsForProject } + : {}), + ...(U_STORE.taskIdsForProjectBacklog + ? { backlogTaskIds: U_STORE.taskIdsForProjectBacklog } + : {}), + }, + }, + updatedState[PROJECT_FEATURE_NAME], + ), }; } @@ -112,7 +123,10 @@ export const undoTaskDeleteMetaReducer = (reducer: any): any => { }; }; -const _createTaskDeleteState = (state: RootState, task: TaskWithSubTasks): UndoTaskDeleteState => { +const _createTaskDeleteState = ( + state: RootState, + task: TaskWithSubTasks, +): UndoTaskDeleteState => { const taskEntities = state[TASK_FEATURE_NAME].entities; const deletedTaskEntities = [task.id, ...task.subTaskIds].reduce((acc, id) => { return { @@ -132,15 +146,20 @@ const _createTaskDeleteState = (state: RootState, task: TaskWithSubTasks): UndoT }; } else { // PROJECT CASE - const project: (Project | undefined) = state[PROJECT_FEATURE_NAME].entities[task.projectId as string]; - const isProjectTask = (task.projectId !== null && project !== undefined); + const project: Project | undefined = + state[PROJECT_FEATURE_NAME].entities[task.projectId as string]; + const isProjectTask = task.projectId !== null && project !== undefined; let taskIdsForProjectBacklog; let taskIdsForProject; if (isProjectTask) { taskIdsForProjectBacklog = (project as Project).backlogTaskIds; taskIdsForProject = (project as Project).taskIds; - if (!taskIdsForProject || !taskIdsForProjectBacklog || (!taskIdsForProjectBacklog.length && !taskIdsForProject.length)) { + if ( + !taskIdsForProject || + !taskIdsForProjectBacklog || + (!taskIdsForProjectBacklog.length && !taskIdsForProject.length) + ) { console.log('------ERR_ADDITIONAL_INFO------'); console.log('project', project); console.log('taskIdsForProject', taskIdsForProject); @@ -150,7 +169,7 @@ const _createTaskDeleteState = (state: RootState, task: TaskWithSubTasks): UndoT } const tagState = state[TAG_FEATURE_NAME]; - const tagTaskIdMap = (task.tagIds).reduce((acc, id) => { + const tagTaskIdMap = task.tagIds.reduce((acc, id) => { const tag = tagState.entities[id]; if (!tag) { console.log('------ERR_ADDITIONAL_INFO------'); @@ -175,8 +194,7 @@ const _createTaskDeleteState = (state: RootState, task: TaskWithSubTasks): UndoT taskIdsForProjectBacklog, taskIdsForProject, tagTaskIdMap, - deletedTaskEntities + deletedTaskEntities, }; } }; - diff --git a/src/app/root-store/root-state.ts b/src/app/root-store/root-state.ts index cb86292ca..8c498356b 100644 --- a/src/app/root-store/root-state.ts +++ b/src/app/root-store/root-state.ts @@ -1,8 +1,14 @@ import { TASK_FEATURE_NAME } from '../features/tasks/store/task.reducer'; import { TaskState } from '../features/tasks/task.model'; -import { PROJECT_FEATURE_NAME, ProjectState } from '../features/project/store/project.reducer'; +import { + PROJECT_FEATURE_NAME, + ProjectState, +} from '../features/project/store/project.reducer'; import { NOTE_FEATURE_NAME, NoteState } from '../features/note/store/note.reducer'; -import { BOOKMARK_FEATURE_NAME, BookmarkState } from '../features/bookmark/store/bookmark.reducer'; +import { + BOOKMARK_FEATURE_NAME, + BookmarkState, +} from '../features/bookmark/store/bookmark.reducer'; import { LAYOUT_FEATURE_NAME, LayoutState } from '../core-ui/layout/store/layout.reducer'; import { CONFIG_FEATURE_NAME } from '../features/config/store/global-config.reducer'; import { GlobalConfigState } from '../features/config/global-config.model'; diff --git a/src/app/root-store/root-store.module.ts b/src/app/root-store/root-store.module.ts index b268b8b11..88b9e1bdd 100644 --- a/src/app/root-store/root-store.module.ts +++ b/src/app/root-store/root-store.module.ts @@ -2,7 +2,6 @@ import { NgModule } from '@angular/core'; @NgModule({ imports: [], - declarations: [] + declarations: [], }) -export class RootStoreModule { -} +export class RootStoreModule {} diff --git a/src/app/t.const.ts b/src/app/t.const.ts index 3528337b8..14c9a3934 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -3,35 +3,35 @@ const T = { B_INSTALL: { IGNORE: 'APP.B_INSTALL.IGNORE', INSTALL: 'APP.B_INSTALL.INSTALL', - MSG: 'APP.B_INSTALL.MSG' + MSG: 'APP.B_INSTALL.MSG', }, B_OFFLINE: 'APP.B_OFFLINE', D_INITIAL: { - TITLE: 'APP.D_INITIAL.TITLE' + TITLE: 'APP.D_INITIAL.TITLE', }, UPDATE_MAIN_MODEL: 'APP.UPDATE_MAIN_MODEL', UPDATE_MAIN_MODEL_NO_UPDATE: 'APP.UPDATE_MAIN_MODEL_NO_UPDATE', - UPDATE_WEB_APP: 'APP.UPDATE_WEB_APP' + UPDATE_WEB_APP: 'APP.UPDATE_WEB_APP', }, BL: { - NO_TASKS: 'BL.NO_TASKS' + NO_TASKS: 'BL.NO_TASKS', }, CONFIRM: { AUTO_FIX: 'CONFIRM.AUTO_FIX', DELETE_STRAY_BACKUP: 'CONFIRM.DELETE_STRAY_BACKUP', RESTORE_FILE_BACKUP: 'CONFIRM.RESTORE_FILE_BACKUP', - RESTORE_STRAY_BACKUP: 'CONFIRM.RESTORE_STRAY_BACKUP' + RESTORE_STRAY_BACKUP: 'CONFIRM.RESTORE_STRAY_BACKUP', }, DATETIME_INPUT: { IN: 'DATETIME_INPUT.IN', - TOMORROW: 'DATETIME_INPUT.TOMORROW' + TOMORROW: 'DATETIME_INPUT.TOMORROW', }, DATETIME_SCHEDULE: { LATER_TODAY: 'DATETIME_SCHEDULE.LATER_TODAY', NEXT_WEEK: 'DATETIME_SCHEDULE.NEXT_WEEK', PLACEHOLDER: 'DATETIME_SCHEDULE.PLACEHOLDER', PRESS_ENTER_AGAIN: 'DATETIME_SCHEDULE.PRESS_ENTER_AGAIN', - TOMORROW: 'DATETIME_SCHEDULE.TOMORROW' + TOMORROW: 'DATETIME_SCHEDULE.TOMORROW', }, F: { ATTACHMENT: { @@ -41,22 +41,22 @@ const T = { LABELS: { FILE: 'F.ATTACHMENT.DIALOG_EDIT.LABELS.FILE', IMG: 'F.ATTACHMENT.DIALOG_EDIT.LABELS.IMG', - LINK: 'F.ATTACHMENT.DIALOG_EDIT.LABELS.LINK' + LINK: 'F.ATTACHMENT.DIALOG_EDIT.LABELS.LINK', }, SELECT_TYPE: 'F.ATTACHMENT.DIALOG_EDIT.SELECT_TYPE', TYPES: { FILE: 'F.ATTACHMENT.DIALOG_EDIT.TYPES.FILE', IMG: 'F.ATTACHMENT.DIALOG_EDIT.TYPES.IMG', - LINK: 'F.ATTACHMENT.DIALOG_EDIT.TYPES.LINK' - } - } + LINK: 'F.ATTACHMENT.DIALOG_EDIT.TYPES.LINK', + }, + }, }, BOOKMARK: { BAR: { ADD: 'F.BOOKMARK.BAR.ADD', DROP: 'F.BOOKMARK.BAR.DROP', EDIT: 'F.BOOKMARK.BAR.EDIT', - NO_BOOKMARKS: 'F.BOOKMARK.BAR.NO_BOOKMARKS' + NO_BOOKMARKS: 'F.BOOKMARK.BAR.NO_BOOKMARKS', }, DIALOG_EDIT: { ADD_BOOKMARK: 'F.BOOKMARK.DIALOG_EDIT.ADD_BOOKMARK', @@ -65,7 +65,7 @@ const T = { COMMAND: 'F.BOOKMARK.DIALOG_EDIT.LABELS.COMMAND', FILE: 'F.BOOKMARK.DIALOG_EDIT.LABELS.FILE', IMG: 'F.BOOKMARK.DIALOG_EDIT.LABELS.IMG', - LINK: 'F.BOOKMARK.DIALOG_EDIT.LABELS.LINK' + LINK: 'F.BOOKMARK.DIALOG_EDIT.LABELS.LINK', }, SELECT_ICON: 'F.BOOKMARK.DIALOG_EDIT.SELECT_ICON', SELECT_TYPE: 'F.BOOKMARK.DIALOG_EDIT.SELECT_TYPE', @@ -73,13 +73,13 @@ const T = { COMMAND: 'F.BOOKMARK.DIALOG_EDIT.TYPES.COMMAND', FILE: 'F.BOOKMARK.DIALOG_EDIT.TYPES.FILE', IMG: 'F.BOOKMARK.DIALOG_EDIT.TYPES.IMG', - LINK: 'F.BOOKMARK.DIALOG_EDIT.TYPES.LINK' - } - } + LINK: 'F.BOOKMARK.DIALOG_EDIT.TYPES.LINK', + }, + }, }, CALDAV: { DIALOG_INITIAL: { - TITLE: 'F.CALDAV.DIALOG_INITIAL.TITLE' + TITLE: 'F.CALDAV.DIALOG_INITIAL.TITLE', }, FORM: { CALDAV_URL: 'F.CALDAV.FORM.CALDAV_URL', @@ -90,18 +90,18 @@ const T = { IS_AUTO_ADD_TO_BACKLOG: 'F.CALDAV.FORM.IS_AUTO_ADD_TO_BACKLOG', IS_AUTO_POLL: 'F.CALDAV.FORM.IS_AUTO_POLL', IS_SEARCH_ISSUES_FROM_CALDAV: 'F.CALDAV.FORM.IS_SEARCH_ISSUES_FROM_CALDAV', - IS_TRANSITION_ISSUES_ENABLED: 'F.CALDAV.FORM.IS_TRANSITION_ISSUES_ENABLED' + IS_TRANSITION_ISSUES_ENABLED: 'F.CALDAV.FORM.IS_TRANSITION_ISSUES_ENABLED', }, FORM_SECTION: { HELP: 'F.CALDAV.FORM_SECTION.HELP', - TITLE: 'F.CALDAV.FORM_SECTION.TITLE' + TITLE: 'F.CALDAV.FORM_SECTION.TITLE', }, ISSUE_CONTENT: { DESCRIPTION: 'F.CALDAV.ISSUE_CONTENT.DESCRIPTION', LABELS: 'F.CALDAV.ISSUE_CONTENT.LABELS', MARK_AS_CHECKED: 'F.CALDAV.ISSUE_CONTENT.MARK_AS_CHECKED', STATUS: 'F.CALDAV.ISSUE_CONTENT.STATUS', - SUMMARY: 'F.CALDAV.ISSUE_CONTENT.SUMMARY' + SUMMARY: 'F.CALDAV.ISSUE_CONTENT.SUMMARY', }, S: { CALENDAR_NOT_FOUND: 'F.CALDAV.S.CALENDAR_NOT_FOUND', @@ -113,13 +113,13 @@ const T = { ISSUE_NOT_FOUND: 'F.CALDAV.S.ISSUE_NOT_FOUND', ISSUE_NO_UPDATE_REQUIRED: 'F.CALDAV.S.ISSUE_NO_UPDATE_REQUIRED', ISSUE_UPDATE: 'F.CALDAV.S.ISSUE_UPDATE', - POLLING: 'F.CALDAV.S.POLLING' - } + POLLING: 'F.CALDAV.S.POLLING', + }, }, CONFIG: { S: { - UPDATE_SECTION: 'F.CONFIG.S.UPDATE_SECTION' - } + UPDATE_SECTION: 'F.CONFIG.S.UPDATE_SECTION', + }, }, DROPBOX: { S: { @@ -127,12 +127,12 @@ const T = { ACCESS_TOKEN_GENERATED: 'F.DROPBOX.S.ACCESS_TOKEN_GENERATED', AUTH_ERROR: 'F.DROPBOX.S.AUTH_ERROR', OFFLINE: 'F.DROPBOX.S.OFFLINE', - SYNC_ERROR: 'F.DROPBOX.S.SYNC_ERROR' - } + SYNC_ERROR: 'F.DROPBOX.S.SYNC_ERROR', + }, }, GITHUB: { DIALOG_INITIAL: { - TITLE: 'F.GITHUB.DIALOG_INITIAL.TITLE' + TITLE: 'F.GITHUB.DIALOG_INITIAL.TITLE', }, FORM: { FILTER_USER: 'F.GITHUB.FORM.FILTER_USER', @@ -141,11 +141,11 @@ const T = { IS_SEARCH_ISSUES_FROM_GITHUB: 'F.GITHUB.FORM.IS_SEARCH_ISSUES_FROM_GITHUB', REPO: 'F.GITHUB.FORM.REPO', TOKEN: 'F.GITHUB.FORM.TOKEN', - TOKEN_DESCRIPTION: 'F.GITHUB.FORM.TOKEN_DESCRIPTION' + TOKEN_DESCRIPTION: 'F.GITHUB.FORM.TOKEN_DESCRIPTION', }, FORM_SECTION: { HELP: 'F.GITHUB.FORM_SECTION.HELP', - TITLE: 'F.GITHUB.FORM_SECTION.TITLE' + TITLE: 'F.GITHUB.FORM_SECTION.TITLE', }, ISSUE_CONTENT: { ASSIGNEE: 'F.GITHUB.ISSUE_CONTENT.ASSIGNEE', @@ -155,7 +155,7 @@ const T = { MARK_AS_CHECKED: 'F.GITHUB.ISSUE_CONTENT.MARK_AS_CHECKED', STATUS: 'F.GITHUB.ISSUE_CONTENT.STATUS', SUMMARY: 'F.GITHUB.ISSUE_CONTENT.SUMMARY', - WRITE_A_COMMENT: 'F.GITHUB.ISSUE_CONTENT.WRITE_A_COMMENT' + WRITE_A_COMMENT: 'F.GITHUB.ISSUE_CONTENT.WRITE_A_COMMENT', }, S: { ERR_NETWORK: 'F.GITHUB.S.ERR_NETWORK', @@ -170,12 +170,12 @@ const T = { MISSING_ISSUE_DATA: 'F.GITHUB.S.MISSING_ISSUE_DATA', NEW_COMMENT: 'F.GITHUB.S.NEW_COMMENT', POLLING: 'F.GITHUB.S.POLLING', - SHOW_ISSUE_BTN: 'F.GITHUB.S.SHOW_ISSUE_BTN' - } + SHOW_ISSUE_BTN: 'F.GITHUB.S.SHOW_ISSUE_BTN', + }, }, GITLAB: { DIALOG_INITIAL: { - TITLE: 'F.GITLAB.DIALOG_INITIAL.TITLE' + TITLE: 'F.GITLAB.DIALOG_INITIAL.TITLE', }, FORM: { FILTER_USER: 'F.GITLAB.FORM.FILTER_USER', @@ -184,11 +184,11 @@ const T = { IS_SEARCH_ISSUES_FROM_GITLAB: 'F.GITLAB.FORM.IS_SEARCH_ISSUES_FROM_GITLAB', PROJECT: 'F.GITLAB.FORM.PROJECT', GITLAB_BASE_URL: 'F.GITLAB.FORM.GITLAB_BASE_URL', - TOKEN: 'F.GITLAB.FORM.TOKEN' + TOKEN: 'F.GITLAB.FORM.TOKEN', }, FORM_SECTION: { HELP: 'F.GITLAB.FORM_SECTION.HELP', - TITLE: 'F.GITLAB.FORM_SECTION.TITLE' + TITLE: 'F.GITLAB.FORM_SECTION.TITLE', }, ISSUE_CONTENT: { ASSIGNEE: 'F.GITLAB.ISSUE_CONTENT.ASSIGNEE', @@ -198,7 +198,7 @@ const T = { MARK_AS_CHECKED: 'F.GITLAB.ISSUE_CONTENT.MARK_AS_CHECKED', STATUS: 'F.GITLAB.ISSUE_CONTENT.STATUS', SUMMARY: 'F.GITLAB.ISSUE_CONTENT.SUMMARY', - WRITE_A_COMMENT: 'F.GITLAB.ISSUE_CONTENT.WRITE_A_COMMENT' + WRITE_A_COMMENT: 'F.GITLAB.ISSUE_CONTENT.WRITE_A_COMMENT', }, S: { ERR_NETWORK: 'F.GITLAB.S.ERR_NETWORK', @@ -213,33 +213,34 @@ const T = { MISSING_ISSUE_DATA: 'F.GITLAB.S.MISSING_ISSUE_DATA', NEW_COMMENT: 'F.GITLAB.S.NEW_COMMENT', POLLING: 'F.GITLAB.S.POLLING', - SHOW_ISSUE_BTN: 'F.GITLAB.S.SHOW_ISSUE_BTN' - } + SHOW_ISSUE_BTN: 'F.GITLAB.S.SHOW_ISSUE_BTN', + }, }, GOOGLE: { BANNER: { - AUTH_FAIL: 'F.GOOGLE.BANNER.AUTH_FAIL' + AUTH_FAIL: 'F.GOOGLE.BANNER.AUTH_FAIL', }, DIALOG: { CREATE_SYNC_FILE: 'F.GOOGLE.DIALOG.CREATE_SYNC_FILE', - USE_EXISTING_SYNC_FILE: 'F.GOOGLE.DIALOG.USE_EXISTING_SYNC_FILE' + USE_EXISTING_SYNC_FILE: 'F.GOOGLE.DIALOG.USE_EXISTING_SYNC_FILE', }, S: { - MULTIPLE_SYNC_FILES_WITH_SAME_NAME: 'F.GOOGLE.S.MULTIPLE_SYNC_FILES_WITH_SAME_NAME', + MULTIPLE_SYNC_FILES_WITH_SAME_NAME: + 'F.GOOGLE.S.MULTIPLE_SYNC_FILES_WITH_SAME_NAME', SYNC_FILE_CREATION_ERROR: 'F.GOOGLE.S.SYNC_FILE_CREATION_ERROR', - UPDATED_SYNC_FILE_NAME: 'F.GOOGLE.S.UPDATED_SYNC_FILE_NAME' + UPDATED_SYNC_FILE_NAME: 'F.GOOGLE.S.UPDATED_SYNC_FILE_NAME', }, S_API: { ERR: 'F.GOOGLE.S_API.ERR', ERR_NO_FILE_ID: 'F.GOOGLE.S_API.ERR_NO_FILE_ID', ERR_NO_FILE_NAME: 'F.GOOGLE.S_API.ERR_NO_FILE_NAME', - SUCCESS_LOGIN: 'F.GOOGLE.S_API.SUCCESS_LOGIN' - } + SUCCESS_LOGIN: 'F.GOOGLE.S_API.SUCCESS_LOGIN', + }, }, JIRA: { BANNER: { BLOCK_ACCESS_MSG: 'F.JIRA.BANNER.BLOCK_ACCESS_MSG', - BLOCK_ACCESS_UNBLOCK: 'F.JIRA.BANNER.BLOCK_ACCESS_UNBLOCK' + BLOCK_ACCESS_UNBLOCK: 'F.JIRA.BANNER.BLOCK_ACCESS_UNBLOCK', }, CFG_CMP: { ALWAYS_ASK: 'F.JIRA.CFG_CMP.ALWAYS_ASK', @@ -253,21 +254,21 @@ const T = { MAP_CUSTOM_FIELDS_INFO: 'F.JIRA.CFG_CMP.MAP_CUSTOM_FIELDS_INFO', OPEN: 'F.JIRA.CFG_CMP.OPEN', SELECT_ISSUE_FOR_TRANSITIONS: 'F.JIRA.CFG_CMP.SELECT_ISSUE_FOR_TRANSITIONS', - STORY_POINTS: 'F.JIRA.CFG_CMP.STORY_POINTS' + STORY_POINTS: 'F.JIRA.CFG_CMP.STORY_POINTS', }, DIALOG_CONFIRM_ASSIGNMENT: { MSG: 'F.JIRA.DIALOG_CONFIRM_ASSIGNMENT.MSG', - OK: 'F.JIRA.DIALOG_CONFIRM_ASSIGNMENT.OK' + OK: 'F.JIRA.DIALOG_CONFIRM_ASSIGNMENT.OK', }, DIALOG_INITIAL: { - TITLE: 'F.JIRA.DIALOG_INITIAL.TITLE' + TITLE: 'F.JIRA.DIALOG_INITIAL.TITLE', }, DIALOG_TRANSITION: { CHOOSE_STATUS: 'F.JIRA.DIALOG_TRANSITION.CHOOSE_STATUS', CURRENT_ASSIGNEE: 'F.JIRA.DIALOG_TRANSITION.CURRENT_ASSIGNEE', CURRENT_STATUS: 'F.JIRA.DIALOG_TRANSITION.CURRENT_STATUS', TITLE: 'F.JIRA.DIALOG_TRANSITION.TITLE', - UPDATE_STATUS: 'F.JIRA.DIALOG_TRANSITION.UPDATE_STATUS' + UPDATE_STATUS: 'F.JIRA.DIALOG_TRANSITION.UPDATE_STATUS', }, DIALOG_WORKLOG: { CURRENTLY_LOGGED: 'F.JIRA.DIALOG_WORKLOG.CURRENTLY_LOGGED', @@ -276,29 +277,31 @@ const T = { STARTED: 'F.JIRA.DIALOG_WORKLOG.STARTED', SUBMIT_WORKLOG_FOR: 'F.JIRA.DIALOG_WORKLOG.SUBMIT_WORKLOG_FOR', TIME_SPENT: 'F.JIRA.DIALOG_WORKLOG.TIME_SPENT', - TITLE: 'F.JIRA.DIALOG_WORKLOG.TITLE' + TITLE: 'F.JIRA.DIALOG_WORKLOG.TITLE', }, FORM: { IS_AUTO_ADD_TO_BACKLOG: 'F.JIRA.FORM.IS_AUTO_ADD_TO_BACKLOG', IS_AUTO_POLL: 'F.JIRA.FORM.IS_AUTO_POLL', IS_SEARCH_ISSUES_FROM_GITHUB: 'F.JIRA.FORM.IS_SEARCH_ISSUES_FROM_GITHUB', - REPO: 'F.JIRA.FORM.REPO' + REPO: 'F.JIRA.FORM.REPO', }, FORM_ADV: { AUTO_ADD_BACKLOG_JQL_QUERY: 'F.JIRA.FORM_ADV.AUTO_ADD_BACKLOG_JQL_QUERY', - IS_ADD_WORKLOG_ON_SUB_TASK_DONE: 'F.JIRA.FORM_ADV.IS_ADD_WORKLOG_ON_SUB_TASK_DONE', + IS_ADD_WORKLOG_ON_SUB_TASK_DONE: + 'F.JIRA.FORM_ADV.IS_ADD_WORKLOG_ON_SUB_TASK_DONE', IS_AUTO_ADD_TO_BACKLOG: 'F.JIRA.FORM_ADV.IS_AUTO_ADD_TO_BACKLOG', IS_AUTO_POLL_TICKETS: 'F.JIRA.FORM_ADV.IS_AUTO_POLL_TICKETS', - IS_CHECK_TO_RE_ASSIGN_TICKET_ON_TASK_START: 'F.JIRA.FORM_ADV.IS_CHECK_TO_RE_ASSIGN_TICKET_ON_TASK_START', + IS_CHECK_TO_RE_ASSIGN_TICKET_ON_TASK_START: + 'F.JIRA.FORM_ADV.IS_CHECK_TO_RE_ASSIGN_TICKET_ON_TASK_START', IS_WORKLOG_ENABLED: 'F.JIRA.FORM_ADV.IS_WORKLOG_ENABLED', - SEARCH_JQL_QUERY: 'F.JIRA.FORM_ADV.SEARCH_JQL_QUERY' + SEARCH_JQL_QUERY: 'F.JIRA.FORM_ADV.SEARCH_JQL_QUERY', }, FORM_CRED: { ALLOW_SELF_SIGNED: 'F.JIRA.FORM_CRED.ALLOW_SELF_SIGNED', HOST: 'F.JIRA.FORM_CRED.HOST', PASSWORD: 'F.JIRA.FORM_CRED.PASSWORD', USER_NAME: 'F.JIRA.FORM_CRED.USER_NAME', - WONKY_COOKIE_MODE: 'F.JIRA.FORM_CRED.WONKY_COOKIE_MODE' + WONKY_COOKIE_MODE: 'F.JIRA.FORM_CRED.WONKY_COOKIE_MODE', }, FORM_SECTION: { ADV_CFG: 'F.JIRA.FORM_SECTION.ADV_CFG', @@ -314,8 +317,8 @@ const T = { P2_1: 'F.JIRA.FORM_SECTION.HELP_ARR.P2_1', P2_2: 'F.JIRA.FORM_SECTION.HELP_ARR.P2_2', P2_3: 'F.JIRA.FORM_SECTION.HELP_ARR.P2_3', - P3_1: 'F.JIRA.FORM_SECTION.HELP_ARR.P3_1' - } + P3_1: 'F.JIRA.FORM_SECTION.HELP_ARR.P3_1', + }, }, ISSUE_CONTENT: { ASSIGNEE: 'F.JIRA.ISSUE_CONTENT.ASSIGNEE', @@ -332,7 +335,7 @@ const T = { STORY_POINTS: 'F.JIRA.ISSUE_CONTENT.STORY_POINTS', SUMMARY: 'F.JIRA.ISSUE_CONTENT.SUMMARY', WORKLOG: 'F.JIRA.ISSUE_CONTENT.WORKLOG', - WRITE_A_COMMENT: 'F.JIRA.ISSUE_CONTENT.WRITE_A_COMMENT' + WRITE_A_COMMENT: 'F.JIRA.ISSUE_CONTENT.WRITE_A_COMMENT', }, S: { ADDED_WORKLOG_FOR: 'F.JIRA.S.ADDED_WORKLOG_FOR', @@ -351,19 +354,19 @@ const T = { TRANSITION: 'F.JIRA.S.TRANSITION', TRANSITIONS_LOADED: 'F.JIRA.S.TRANSITIONS_LOADED', TRANSITION_SUCCESS: 'F.JIRA.S.TRANSITION_SUCCESS', - UNABLE_TO_REASSIGN: 'F.JIRA.S.UNABLE_TO_REASSIGN' + UNABLE_TO_REASSIGN: 'F.JIRA.S.UNABLE_TO_REASSIGN', }, STEPPER: { CREDENTIALS: 'F.JIRA.STEPPER.CREDENTIALS', DONE: 'F.JIRA.STEPPER.DONE', LOGIN_SUCCESS: 'F.JIRA.STEPPER.LOGIN_SUCCESS', TEST_CREDENTIALS: 'F.JIRA.STEPPER.TEST_CREDENTIALS', - WELCOME_USER: 'F.JIRA.STEPPER.WELCOME_USER' - } + WELCOME_USER: 'F.JIRA.STEPPER.WELCOME_USER', + }, }, METRIC: { BANNER: { - CHECK: 'F.METRIC.BANNER.CHECK' + CHECK: 'F.METRIC.BANNER.CHECK', }, CMP: { AVG_BREAKS_PER_DAY: 'F.METRIC.CMP.AVG_BREAKS_PER_DAY', @@ -380,7 +383,7 @@ const T = { OBSTRUCTION_SELECTION_COUNT: 'F.METRIC.CMP.OBSTRUCTION_SELECTION_COUNT', TASKS_DONE_CREATED: 'F.METRIC.CMP.TASKS_DONE_CREATED', TIME_ESTIMATED: 'F.METRIC.CMP.TIME_ESTIMATED', - TIME_SPENT: 'F.METRIC.CMP.TIME_SPENT' + TIME_SPENT: 'F.METRIC.CMP.TIME_SPENT', }, EVAL_FORM: { ADD_NOTE_FOR_TOMORROW: 'F.METRIC.EVAL_FORM.ADD_NOTE_FOR_TOMORROW', @@ -397,44 +400,44 @@ const T = { NOTES: 'F.METRIC.EVAL_FORM.NOTES', OBSTRUCTIONS: 'F.METRIC.EVAL_FORM.OBSTRUCTIONS', PRODUCTIVITY: 'F.METRIC.EVAL_FORM.PRODUCTIVITY', - PRODUCTIVITY_HINT: 'F.METRIC.EVAL_FORM.PRODUCTIVITY_HINT' + PRODUCTIVITY_HINT: 'F.METRIC.EVAL_FORM.PRODUCTIVITY_HINT', }, S: { - SAVE_METRIC: 'F.METRIC.S.SAVE_METRIC' - } + SAVE_METRIC: 'F.METRIC.S.SAVE_METRIC', + }, }, NOTE: { ADD_REMINDER: 'F.NOTE.ADD_REMINDER', D_ADD: { DATETIME_LABEL: 'F.NOTE.D_ADD.DATETIME_LABEL', - NOTE_LABEL: 'F.NOTE.D_ADD.NOTE_LABEL' + NOTE_LABEL: 'F.NOTE.D_ADD.NOTE_LABEL', }, D_ADD_REMINDER: { E_ENTER_TITLE: 'F.NOTE.D_ADD_REMINDER.E_ENTER_TITLE', L_DATETIME: 'F.NOTE.D_ADD_REMINDER.L_DATETIME', - L_TITLE: 'F.NOTE.D_ADD_REMINDER.L_TITLE' + L_TITLE: 'F.NOTE.D_ADD_REMINDER.L_TITLE', }, D_VIEW_REMINDER: { SNOOZE: 'F.NOTE.D_VIEW_REMINDER.SNOOZE', - TITLE: 'F.NOTE.D_VIEW_REMINDER.TITLE' + TITLE: 'F.NOTE.D_VIEW_REMINDER.TITLE', }, EDIT_FULLSCREEN: 'F.NOTE.EDIT_FULLSCREEN', EDIT_REMINDER: 'F.NOTE.EDIT_REMINDER', NOTES_CMP: { ADD_BTN: 'F.NOTE.NOTES_CMP.ADD_BTN', - DROP_TO_ADD: 'F.NOTE.NOTES_CMP.DROP_TO_ADD' + DROP_TO_ADD: 'F.NOTE.NOTES_CMP.DROP_TO_ADD', }, NOTE_CMP: { DISABLE_PARSE: 'F.NOTE.NOTE_CMP.DISABLE_PARSE', - ENABLE_PARSE: 'F.NOTE.NOTE_CMP.ENABLE_PARSE' + ENABLE_PARSE: 'F.NOTE.NOTE_CMP.ENABLE_PARSE', }, REMOVE_REMINDER: 'F.NOTE.REMOVE_REMINDER', S: { ADDED_REMINDER: 'F.NOTE.S.ADDED_REMINDER', DELETED_REMINDER: 'F.NOTE.S.DELETED_REMINDER', - UPDATED_REMINDER: 'F.NOTE.S.UPDATED_REMINDER' + UPDATED_REMINDER: 'F.NOTE.S.UPDATED_REMINDER', }, - UPDATE_REMINDER: 'F.NOTE.UPDATE_REMINDER' + UPDATE_REMINDER: 'F.NOTE.UPDATE_REMINDER', }, POMODORO: { BACK_TO_WORK: 'F.POMODORO.BACK_TO_WORK', @@ -443,12 +446,12 @@ const T = { FINISH_SESSION_X: 'F.POMODORO.FINISH_SESSION_X', NOTIFICATION: { BREAK_X_START: 'F.POMODORO.NOTIFICATION.BREAK_X_START', - SESSION_X_START: 'F.POMODORO.NOTIFICATION.SESSION_X_START' + SESSION_X_START: 'F.POMODORO.NOTIFICATION.SESSION_X_START', }, S: { - SESSION_X_START: 'F.POMODORO.S.SESSION_X_START' + SESSION_X_START: 'F.POMODORO.S.SESSION_X_START', }, - SKIP_BREAK: 'F.POMODORO.SKIP_BREAK' + SKIP_BREAK: 'F.POMODORO.SKIP_BREAK', }, PROCRASTINATION: { BACK_TO_WORK: 'F.PROCRASTINATION.BACK_TO_WORK', @@ -459,7 +462,7 @@ const T = { L3: 'F.PROCRASTINATION.COMP.L3', L4: 'F.PROCRASTINATION.COMP.L4', OUTRO: 'F.PROCRASTINATION.COMP.OUTRO', - TITLE: 'F.PROCRASTINATION.COMP.TITLE' + TITLE: 'F.PROCRASTINATION.COMP.TITLE', }, CUR: { INTRO: 'F.PROCRASTINATION.CUR.INTRO', @@ -468,7 +471,7 @@ const T = { L3: 'F.PROCRASTINATION.CUR.L3', L4: 'F.PROCRASTINATION.CUR.L4', L5: 'F.PROCRASTINATION.CUR.L5', - TITLE: 'F.PROCRASTINATION.CUR.TITLE' + TITLE: 'F.PROCRASTINATION.CUR.TITLE', }, H1: 'F.PROCRASTINATION.H1', P1: 'F.PROCRASTINATION.P1', @@ -479,13 +482,13 @@ const T = { TITLE: 'F.PROCRASTINATION.REFRAME.TITLE', TL1: 'F.PROCRASTINATION.REFRAME.TL1', TL2: 'F.PROCRASTINATION.REFRAME.TL2', - TL3: 'F.PROCRASTINATION.REFRAME.TL3' + TL3: 'F.PROCRASTINATION.REFRAME.TL3', }, SPLIT_UP: { INTRO: 'F.PROCRASTINATION.SPLIT_UP.INTRO', OUTRO: 'F.PROCRASTINATION.SPLIT_UP.OUTRO', - TITLE: 'F.PROCRASTINATION.SPLIT_UP.TITLE' - } + TITLE: 'F.PROCRASTINATION.SPLIT_UP.TITLE', + }, }, PROJECT: { D_CREATE: { @@ -494,11 +497,11 @@ const T = { SETUP_GIT: 'F.PROJECT.D_CREATE.SETUP_GIT', SETUP_GITLAB: 'F.PROJECT.D_CREATE.SETUP_GITLAB', SETUP_JIRA: 'F.PROJECT.D_CREATE.SETUP_JIRA', - SETUP_CALDAV: 'F.PROJECT.D_CREATE.SETUP_CALDAV' + SETUP_CALDAV: 'F.PROJECT.D_CREATE.SETUP_CALDAV', }, FORM_BASIC: { L_TITLE: 'F.PROJECT.FORM_BASIC.L_TITLE', - TITLE: 'F.PROJECT.FORM_BASIC.TITLE' + TITLE: 'F.PROJECT.FORM_BASIC.TITLE', }, FORM_THEME: { D_IS_DARK_THEME: 'F.PROJECT.FORM_THEME.D_IS_DARK_THEME', @@ -512,11 +515,12 @@ const T = { L_HUE_PRIMARY: 'F.PROJECT.FORM_THEME.L_HUE_PRIMARY', L_HUE_WARN: 'F.PROJECT.FORM_THEME.L_HUE_WARN', L_IS_AUTO_CONTRAST: 'F.PROJECT.FORM_THEME.L_IS_AUTO_CONTRAST', - L_IS_DISABLE_BACKGROUND_GRADIENT: 'F.PROJECT.FORM_THEME.L_IS_DISABLE_BACKGROUND_GRADIENT', + L_IS_DISABLE_BACKGROUND_GRADIENT: + 'F.PROJECT.FORM_THEME.L_IS_DISABLE_BACKGROUND_GRADIENT', L_IS_REDUCED_THEME: 'F.PROJECT.FORM_THEME.L_IS_REDUCED_THEME', L_THEME_COLOR: 'F.PROJECT.FORM_THEME.L_THEME_COLOR', L_TITLE: 'F.PROJECT.FORM_THEME.L_TITLE', - TITLE: 'F.PROJECT.FORM_THEME.TITLE' + TITLE: 'F.PROJECT.FORM_THEME.TITLE', }, S: { ARCHIVED: 'F.PROJECT.S.ARCHIVED', @@ -526,20 +530,20 @@ const T = { E_INVALID_FILE: 'F.PROJECT.S.E_INVALID_FILE', ISSUE_PROVIDER_UPDATED: 'F.PROJECT.S.ISSUE_PROVIDER_UPDATED', UNARCHIVED: 'F.PROJECT.S.UNARCHIVED', - UPDATED: 'F.PROJECT.S.UPDATED' - } + UPDATED: 'F.PROJECT.S.UPDATED', + }, }, REMINDER: { - S_REMINDER_ERR: 'F.REMINDER.S_REMINDER_ERR' + S_REMINDER_ERR: 'F.REMINDER.S_REMINDER_ERR', }, SIMPLE_COUNTER: { D_CONFIRM_REMOVE: { MSG: 'F.SIMPLE_COUNTER.D_CONFIRM_REMOVE.MSG', - OK: 'F.SIMPLE_COUNTER.D_CONFIRM_REMOVE.OK' + OK: 'F.SIMPLE_COUNTER.D_CONFIRM_REMOVE.OK', }, D_EDIT: { L_COUNTER: 'F.SIMPLE_COUNTER.D_EDIT.L_COUNTER', - TITLE: 'F.SIMPLE_COUNTER.D_EDIT.TITLE' + TITLE: 'F.SIMPLE_COUNTER.D_EDIT.TITLE', }, FORM: { ADD_NEW: 'F.SIMPLE_COUNTER.FORM.ADD_NEW', @@ -554,8 +558,8 @@ const T = { L_TYPE: 'F.SIMPLE_COUNTER.FORM.L_TYPE', TITLE: 'F.SIMPLE_COUNTER.FORM.TITLE', TYPE_CLICK_COUNTER: 'F.SIMPLE_COUNTER.FORM.TYPE_CLICK_COUNTER', - TYPE_STOPWATCH: 'F.SIMPLE_COUNTER.FORM.TYPE_STOPWATCH' - } + TYPE_STOPWATCH: 'F.SIMPLE_COUNTER.FORM.TYPE_STOPWATCH', + }, }, SYNC: { C: { @@ -564,13 +568,13 @@ const T = { FORCE_UPLOAD: 'F.SYNC.C.FORCE_UPLOAD', FORCE_UPLOAD_AFTER_ERROR: 'F.SYNC.C.FORCE_UPLOAD_AFTER_ERROR', NO_REMOTE_DATA: 'F.SYNC.C.NO_REMOTE_DATA', - TRY_LOAD_REMOTE_AGAIN: 'F.SYNC.C.TRY_LOAD_REMOTE_AGAIN' + TRY_LOAD_REMOTE_AGAIN: 'F.SYNC.C.TRY_LOAD_REMOTE_AGAIN', }, D_AUTH_CODE: { TITLE: 'F.SYNC.D_AUTH_CODE.TITLE', FOLLOW_LINK: 'F.SYNC.D_AUTH_CODE.FOLLOW_LINK', GET_AUTH_CODE: 'F.SYNC.D_AUTH_CODE.GET_AUTH_CODE', - L_AUTH_CODE: 'F.SYNC.D_AUTH_CODE.L_AUTH_CODE' + L_AUTH_CODE: 'F.SYNC.D_AUTH_CODE.L_AUTH_CODE', }, D_CONFLICT: { LAST_CHANGE: 'F.SYNC.D_CONFLICT.LAST_CHANGE', @@ -581,18 +585,18 @@ const T = { TEXT: 'F.SYNC.D_CONFLICT.TEXT', TITLE: 'F.SYNC.D_CONFLICT.TITLE', USE_LOCAL: 'F.SYNC.D_CONFLICT.USE_LOCAL', - USE_REMOTE: 'F.SYNC.D_CONFLICT.USE_REMOTE' + USE_REMOTE: 'F.SYNC.D_CONFLICT.USE_REMOTE', }, FORM: { DROPBOX: { B_GENERATE_TOKEN: 'F.SYNC.FORM.DROPBOX.B_GENERATE_TOKEN', FOLLOW_LINK: 'F.SYNC.FORM.DROPBOX.FOLLOW_LINK', L_ACCESS_TOKEN: 'F.SYNC.FORM.DROPBOX.L_ACCESS_TOKEN', - L_AUTH_CODE: 'F.SYNC.FORM.DROPBOX.L_AUTH_CODE' + L_AUTH_CODE: 'F.SYNC.FORM.DROPBOX.L_AUTH_CODE', }, GOOGLE: { L_IS_COMPRESS_DATA: 'F.SYNC.FORM.GOOGLE.L_IS_COMPRESS_DATA', - L_SYNC_FILE_NAME: 'F.SYNC.FORM.GOOGLE.L_SYNC_FILE_NAME' + L_SYNC_FILE_NAME: 'F.SYNC.FORM.GOOGLE.L_SYNC_FILE_NAME', }, L_ENABLE_SYNCING: 'F.SYNC.FORM.L_ENABLE_SYNCING', L_SYNC_INTERVAL: 'F.SYNC.FORM.L_SYNC_INTERVAL', @@ -603,8 +607,8 @@ const T = { L_BASE_URL: 'F.SYNC.FORM.WEB_DAV.L_BASE_URL', L_PASSWORD: 'F.SYNC.FORM.WEB_DAV.L_PASSWORD', L_SYNC_FILE_PATH: 'F.SYNC.FORM.WEB_DAV.L_SYNC_FILE_PATH', - L_USER_NAME: 'F.SYNC.FORM.WEB_DAV.L_USER_NAME' - } + L_USER_NAME: 'F.SYNC.FORM.WEB_DAV.L_USER_NAME', + }, }, S: { ERROR_FALLBACK_TO_BACKUP: 'F.SYNC.S.ERROR_FALLBACK_TO_BACKUP', @@ -614,31 +618,31 @@ const T = { INCOMPLETE_CFG: 'F.SYNC.S.INCOMPLETE_CFG', SUCCESS: 'F.SYNC.S.SUCCESS', UNKNOWN_ERROR: 'F.SYNC.S.UNKNOWN_ERROR', - UPLOAD_ERROR: 'F.SYNC.S.UPLOAD_ERROR' - } + UPLOAD_ERROR: 'F.SYNC.S.UPLOAD_ERROR', + }, }, TAG: { D_CREATE: { CREATE: 'F.TAG.D_CREATE.CREATE', - EDIT: 'F.TAG.D_CREATE.EDIT' + EDIT: 'F.TAG.D_CREATE.EDIT', }, D_DELETE: { - CONFIRM_MSG: 'F.TAG.D_DELETE.CONFIRM_MSG' + CONFIRM_MSG: 'F.TAG.D_DELETE.CONFIRM_MSG', }, D_EDIT: { ADD: 'F.TAG.D_EDIT.ADD', EDIT: 'F.TAG.D_EDIT.EDIT', - LABEL: 'F.TAG.D_EDIT.LABEL' + LABEL: 'F.TAG.D_EDIT.LABEL', }, FORM_BASIC: { L_COLOR: 'F.TAG.FORM_BASIC.L_COLOR', L_ICON: 'F.TAG.FORM_BASIC.L_ICON', L_TITLE: 'F.TAG.FORM_BASIC.L_TITLE', - TITLE: 'F.TAG.FORM_BASIC.TITLE' + TITLE: 'F.TAG.FORM_BASIC.TITLE', }, S: { - UPDATED: 'F.TAG.S.UPDATED' - } + UPDATED: 'F.TAG.S.UPDATED', + }, }, TASK: { ADDITIONAL_INFO: { @@ -653,7 +657,7 @@ const T = { REPEAT: 'F.TASK.ADDITIONAL_INFO.REPEAT', SCHEDULE_TASK: 'F.TASK.ADDITIONAL_INFO.SCHEDULE_TASK', SUB_TASKS: 'F.TASK.ADDITIONAL_INFO.SUB_TASKS', - TIME: 'F.TASK.ADDITIONAL_INFO.TIME' + TIME: 'F.TASK.ADDITIONAL_INFO.TIME', }, ADD_TASK_BAR: { ADD_EXISTING_TASK: 'F.TASK.ADD_TASK_BAR.ADD_EXISTING_TASK', @@ -662,11 +666,11 @@ const T = { ADD_TASK_TO_BACKLOG: 'F.TASK.ADD_TASK_BAR.ADD_TASK_TO_BACKLOG', CREATE_TASK: 'F.TASK.ADD_TASK_BAR.CREATE_TASK', EXAMPLE: 'F.TASK.ADD_TASK_BAR.EXAMPLE', - START: 'F.TASK.ADD_TASK_BAR.START' + START: 'F.TASK.ADD_TASK_BAR.START', }, B: { ADD_HALF_HOUR: 'F.TASK.B.ADD_HALF_HOUR', - ESTIMATE_EXCEEDED: 'F.TASK.B.ESTIMATE_EXCEEDED' + ESTIMATE_EXCEEDED: 'F.TASK.B.ESTIMATE_EXCEEDED', }, CMP: { ADD_SUB_TASK: 'F.TASK.CMP.ADD_SUB_TASK', @@ -695,14 +699,14 @@ const T = { TOGGLE_SUB_TASK_VISIBILITY: 'F.TASK.CMP.TOGGLE_SUB_TASK_VISIBILITY', TRACK_TIME: 'F.TASK.CMP.TRACK_TIME', TRACK_TIME_STOP: 'F.TASK.CMP.TRACK_TIME_STOP', - UPDATE_ISSUE_DATA: 'F.TASK.CMP.UPDATE_ISSUE_DATA' + UPDATE_ISSUE_DATA: 'F.TASK.CMP.UPDATE_ISSUE_DATA', }, D_REMINDER_ADD: { DATETIME_FOR: 'F.TASK.D_REMINDER_ADD.DATETIME_FOR', EDIT: 'F.TASK.D_REMINDER_ADD.EDIT', MOVE_TO_BACKLOG: 'F.TASK.D_REMINDER_ADD.MOVE_TO_BACKLOG', SCHEDULE: 'F.TASK.D_REMINDER_ADD.SCHEDULE', - UNSCHEDULE: 'F.TASK.D_REMINDER_ADD.UNSCHEDULE' + UNSCHEDULE: 'F.TASK.D_REMINDER_ADD.UNSCHEDULE', }, D_REMINDER_VIEW: { ADD_ALL_TO_TODAY: 'F.TASK.D_REMINDER_VIEW.ADD_ALL_TO_TODAY', @@ -719,7 +723,7 @@ const T = { SNOOZE_ALL: 'F.TASK.D_REMINDER_VIEW.SNOOZE_ALL', SNOOZE_UNTIL_TOMORROW: 'F.TASK.D_REMINDER_VIEW.SNOOZE_UNTIL_TOMORROW', START: 'F.TASK.D_REMINDER_VIEW.START', - SWITCH_CONTEXT_START: 'F.TASK.D_REMINDER_VIEW.SWITCH_CONTEXT_START' + SWITCH_CONTEXT_START: 'F.TASK.D_REMINDER_VIEW.SWITCH_CONTEXT_START', }, D_TIME: { ADD_FOR_OTHER_DAY: 'F.TASK.D_TIME.ADD_FOR_OTHER_DAY', @@ -727,18 +731,18 @@ const T = { ESTIMATE: 'F.TASK.D_TIME.ESTIMATE', TIME_SPENT: 'F.TASK.D_TIME.TIME_SPENT', TIME_SPENT_ON: 'F.TASK.D_TIME.TIME_SPENT_ON', - TITLE: 'F.TASK.D_TIME.TITLE' + TITLE: 'F.TASK.D_TIME.TITLE', }, D_TIME_FOR_DAY: { ADD_ENTRY_FOR: 'F.TASK.D_TIME_FOR_DAY.ADD_ENTRY_FOR', DATE: 'F.TASK.D_TIME_FOR_DAY.DATE', HELP: 'F.TASK.D_TIME_FOR_DAY.HELP', TINE_SPENT: 'F.TASK.D_TIME_FOR_DAY.TINE_SPENT', - TITLE: 'F.TASK.D_TIME_FOR_DAY.TITLE' + TITLE: 'F.TASK.D_TIME_FOR_DAY.TITLE', }, N: { ESTIMATE_EXCEEDED: 'F.TASK.N.ESTIMATE_EXCEEDED', - ESTIMATE_EXCEEDED_BODY: 'F.TASK.N.ESTIMATE_EXCEEDED_BODY' + ESTIMATE_EXCEEDED_BODY: 'F.TASK.N.ESTIMATE_EXCEEDED_BODY', }, S: { DELETED: 'F.TASK.S.DELETED', @@ -749,7 +753,7 @@ const T = { REMINDER_ADDED: 'F.TASK.S.REMINDER_ADDED', REMINDER_DELETED: 'F.TASK.S.REMINDER_DELETED', REMINDER_UPDATED: 'F.TASK.S.REMINDER_UPDATED', - TASK_CREATED: 'F.TASK.S.TASK_CREATED' + TASK_CREATED: 'F.TASK.S.TASK_CREATED', }, SELECT_OR_CREATE: 'F.TASK.SELECT_OR_CREATE', SUMMARY_TABLE: { @@ -757,13 +761,13 @@ const T = { SPENT_TODAY: 'F.TASK.SUMMARY_TABLE.SPENT_TODAY', SPENT_TOTAL: 'F.TASK.SUMMARY_TABLE.SPENT_TOTAL', TASK: 'F.TASK.SUMMARY_TABLE.TASK', - TOGGLE_DONE: 'F.TASK.SUMMARY_TABLE.TOGGLE_DONE' - } + TOGGLE_DONE: 'F.TASK.SUMMARY_TABLE.TOGGLE_DONE', + }, }, TASK_REPEAT: { D_CONFIRM_REMOVE: { MSG: 'F.TASK_REPEAT.D_CONFIRM_REMOVE.MSG', - OK: 'F.TASK_REPEAT.D_CONFIRM_REMOVE.OK' + OK: 'F.TASK_REPEAT.D_CONFIRM_REMOVE.OK', }, D_EDIT: { ADD: 'F.TASK_REPEAT.D_EDIT.ADD', @@ -771,7 +775,7 @@ const T = { HELP1: 'F.TASK_REPEAT.D_EDIT.HELP1', HELP2: 'F.TASK_REPEAT.D_EDIT.HELP2', HELP3: 'F.TASK_REPEAT.D_EDIT.HELP3', - TAG_LABEL: 'F.TASK_REPEAT.D_EDIT.TAG_LABEL' + TAG_LABEL: 'F.TASK_REPEAT.D_EDIT.TAG_LABEL', }, F: { DEFAULT_ESTIMATE: 'F.TASK_REPEAT.F.DEFAULT_ESTIMATE', @@ -783,17 +787,17 @@ const T = { THURSDAY: 'F.TASK_REPEAT.F.THURSDAY', TITLE: 'F.TASK_REPEAT.F.TITLE', TUESDAY: 'F.TASK_REPEAT.F.TUESDAY', - WEDNESDAY: 'F.TASK_REPEAT.F.WEDNESDAY' - } + WEDNESDAY: 'F.TASK_REPEAT.F.WEDNESDAY', + }, }, TIME_TRACKING: { B: { ALREADY_DID: 'F.TIME_TRACKING.B.ALREADY_DID', - SNOOZE: 'F.TIME_TRACKING.B.SNOOZE' + SNOOZE: 'F.TIME_TRACKING.B.SNOOZE', }, B_TTR: { ADD_TO_TASK: 'F.TIME_TRACKING.B_TTR.ADD_TO_TASK', - MSG: 'F.TIME_TRACKING.B_TTR.MSG' + MSG: 'F.TIME_TRACKING.B_TTR.MSG', }, D_IDLE: { BREAK: 'F.TIME_TRACKING.D_IDLE.BREAK', @@ -802,15 +806,15 @@ const T = { SKIP: 'F.TIME_TRACKING.D_IDLE.SKIP', TASK: 'F.TIME_TRACKING.D_IDLE.TASK', TASK_BREAK: 'F.TIME_TRACKING.D_IDLE.TASK_BREAK', - TRACK_TO: 'F.TIME_TRACKING.D_IDLE.TRACK_TO' + TRACK_TO: 'F.TIME_TRACKING.D_IDLE.TRACK_TO', }, D_TRACKING_REMINDER: { CREATE_AND_TRACK: 'F.TIME_TRACKING.D_TRACKING_REMINDER.CREATE_AND_TRACK', IDLE_FOR: 'F.TIME_TRACKING.D_TRACKING_REMINDER.IDLE_FOR', TASK: 'F.TIME_TRACKING.D_TRACKING_REMINDER.TASK', TRACK_TO: 'F.TIME_TRACKING.D_TRACKING_REMINDER.TRACK_TO', - UNTRACKED_TIME: 'F.TIME_TRACKING.D_TRACKING_REMINDER.UNTRACKED_TIME' - } + UNTRACKED_TIME: 'F.TIME_TRACKING.D_TRACKING_REMINDER.UNTRACKED_TIME', + }, }, WORKLOG: { CMP: { @@ -821,7 +825,7 @@ const T = { TASKS: 'F.WORKLOG.CMP.TASKS', TOTAL_TIME: 'F.WORKLOG.CMP.TOTAL_TIME', WEEK_NR: 'F.WORKLOG.CMP.WEEK_NR', - WORKED: 'F.WORKLOG.CMP.WORKED' + WORKED: 'F.WORKLOG.CMP.WORKED', }, D_CONFIRM_RESTORE: 'F.WORKLOG.D_CONFIRM_RESTORE', D_EXPORT_TITLE: 'F.WORKLOG.D_EXPORT_TITLE', @@ -852,7 +856,7 @@ const T = { NOTES: 'F.WORKLOG.EXPORT.O.NOTES', PROJECTS: 'F.WORKLOG.EXPORT.O.PROJECTS', TAGS: 'F.WORKLOG.EXPORT.O.TAGS', - WORKLOG: 'F.WORKLOG.EXPORT.O.WORKLOG' + WORKLOG: 'F.WORKLOG.EXPORT.O.WORKLOG', }, OPTIONS: 'F.WORKLOG.EXPORT.OPTIONS', ROUND_END_TIME_TO: 'F.WORKLOG.EXPORT.ROUND_END_TIME_TO', @@ -860,19 +864,19 @@ const T = { ROUND_TIME_WORKED_TO: 'F.WORKLOG.EXPORT.ROUND_TIME_WORKED_TO', SAVE_TO_FILE: 'F.WORKLOG.EXPORT.SAVE_TO_FILE', SEPARATE_TASKS_BY: 'F.WORKLOG.EXPORT.SEPARATE_TASKS_BY', - SHOW_AS_TEXT: 'F.WORKLOG.EXPORT.SHOW_AS_TEXT' + SHOW_AS_TEXT: 'F.WORKLOG.EXPORT.SHOW_AS_TEXT', }, WEEK: { EXPORT: 'F.WORKLOG.WEEK.EXPORT', NO_DATA: 'F.WORKLOG.WEEK.NO_DATA', - TITLE: 'F.WORKLOG.WEEK.TITLE' - } - } + TITLE: 'F.WORKLOG.WEEK.TITLE', + }, + }, }, FILE_IMEX: { EXPORT_DATA: 'FILE_IMEX.EXPORT_DATA', IMPORT_FROM_FILE: 'FILE_IMEX.IMPORT_FROM_FILE', - S_ERR_INVALID_DATA: 'FILE_IMEX.S_ERR_INVALID_DATA' + S_ERR_INVALID_DATA: 'FILE_IMEX.S_ERR_INVALID_DATA', }, G: { CANCEL: 'G.CANCEL', @@ -898,30 +902,31 @@ const T = { UNDO: 'G.UNDO', UPDATE: 'G.UPDATE', WITHOUT_PROJECT: 'G.WITHOUT_PROJECT', - YESTERDAY: 'G.YESTERDAY' + YESTERDAY: 'G.YESTERDAY', }, GCF: { AUTO_BACKUPS: { HELP: 'GCF.AUTO_BACKUPS.HELP', LABEL_IS_ENABLED: 'GCF.AUTO_BACKUPS.LABEL_IS_ENABLED', LOCATION_INFO: 'GCF.AUTO_BACKUPS.LOCATION_INFO', - TITLE: 'GCF.AUTO_BACKUPS.TITLE' + TITLE: 'GCF.AUTO_BACKUPS.TITLE', }, EVALUATION: { IS_HIDE_EVALUATION_SHEET: 'GCF.EVALUATION.IS_HIDE_EVALUATION_SHEET', - TITLE: 'GCF.EVALUATION.TITLE' + TITLE: 'GCF.EVALUATION.TITLE', }, IDLE: { HELP: 'GCF.IDLE.HELP', IS_ENABLE_IDLE_TIME_TRACKING: 'GCF.IDLE.IS_ENABLE_IDLE_TIME_TRACKING', IS_ONLY_OPEN_IDLE_WHEN_CURRENT_TASK: 'GCF.IDLE.IS_ONLY_OPEN_IDLE_WHEN_CURRENT_TASK', - IS_UN_TRACKED_IDLE_RESETS_BREAK_TIMER: 'GCF.IDLE.IS_UN_TRACKED_IDLE_RESETS_BREAK_TIMER', + IS_UN_TRACKED_IDLE_RESETS_BREAK_TIMER: + 'GCF.IDLE.IS_UN_TRACKED_IDLE_RESETS_BREAK_TIMER', MIN_IDLE_TIME: 'GCF.IDLE.MIN_IDLE_TIME', - TITLE: 'GCF.IDLE.TITLE' + TITLE: 'GCF.IDLE.TITLE', }, IMEX: { HELP: 'GCF.IMEX.HELP', - TITLE: 'GCF.IMEX.TITLE' + TITLE: 'GCF.IMEX.TITLE', }, KEYBOARD: { ADD_NEW_NOTE: 'GCF.KEYBOARD.ADD_NEW_NOTE', @@ -966,7 +971,7 @@ const T = { TOGGLE_SIDE_NAV: 'GCF.KEYBOARD.TOGGLE_SIDE_NAV', ZOOM_DEFAULT: 'GCF.KEYBOARD.ZOOM_DEFAULT', ZOOM_IN: 'GCF.KEYBOARD.ZOOM_IN', - ZOOM_OUT: 'GCF.KEYBOARD.ZOOM_OUT' + ZOOM_OUT: 'GCF.KEYBOARD.ZOOM_OUT', }, LANG: { AR: 'GCF.LANG.AR', @@ -986,7 +991,7 @@ const T = { TITLE: 'GCF.LANG.TITLE', TR: 'GCF.LANG.TR', ZH: 'GCF.LANG.ZH', - ZH_TW: 'GCF.LANG.ZH_TW' + ZH_TW: 'GCF.LANG.ZH_TW', }, MISC: { DEFAULT_PROJECT: 'GCF.MISC.DEFAULT_PROJECT', @@ -1000,11 +1005,12 @@ const T = { IS_DISABLE_INITIAL_DIALOG: 'GCF.MISC.IS_DISABLE_INITIAL_DIALOG', IS_HIDE_NAV: 'GCF.MISC.IS_HIDE_NAV', IS_MINIMIZE_TO_TRAY: 'GCF.MISC.IS_MINIMIZE_TO_TRAY', - IS_NOTIFY_WHEN_TIME_ESTIMATE_EXCEEDED: 'GCF.MISC.IS_NOTIFY_WHEN_TIME_ESTIMATE_EXCEEDED', + IS_NOTIFY_WHEN_TIME_ESTIMATE_EXCEEDED: + 'GCF.MISC.IS_NOTIFY_WHEN_TIME_ESTIMATE_EXCEEDED', IS_TRAY_SHOW_CURRENT_TASK: 'GCF.MISC.IS_TRAY_SHOW_CURRENT_TASK', IS_TURN_OFF_MARKDOWN: 'GCF.MISC.IS_TURN_OFF_MARKDOWN', TASK_NOTES_TPL: 'GCF.MISC.TASK_NOTES_TPL', - TITLE: 'GCF.MISC.TITLE' + TITLE: 'GCF.MISC.TITLE', }, POMODORO: { BREAK_DURATION: 'GCF.POMODORO.BREAK_DURATION', @@ -1018,14 +1024,14 @@ const T = { IS_PLAY_TICK: 'GCF.POMODORO.IS_PLAY_TICK', IS_STOP_TRACKING_ON_BREAK: 'GCF.POMODORO.IS_STOP_TRACKING_ON_BREAK', LONGER_BREAK_DURATION: 'GCF.POMODORO.LONGER_BREAK_DURATION', - TITLE: 'GCF.POMODORO.TITLE' + TITLE: 'GCF.POMODORO.TITLE', }, SOUND: { DONE_SOUND: 'GCF.SOUND.DONE_SOUND', IS_INCREASE_DONE_PITCH: 'GCF.SOUND.IS_INCREASE_DONE_PITCH', IS_PLAY_DONE_SOUND: 'GCF.SOUND.IS_PLAY_DONE_SOUND', TITLE: 'GCF.SOUND.TITLE', - VOLUME: 'GCF.SOUND.VOLUME' + VOLUME: 'GCF.SOUND.VOLUME', }, TAKE_A_BREAK: { HELP: 'GCF.TAKE_A_BREAK.HELP', @@ -1035,23 +1041,24 @@ const T = { MESSAGE: 'GCF.TAKE_A_BREAK.MESSAGE', MIN_WORKING_TIME: 'GCF.TAKE_A_BREAK.MIN_WORKING_TIME', MOTIVATIONAL_IMG: 'GCF.TAKE_A_BREAK.MOTIVATIONAL_IMG', - TITLE: 'GCF.TAKE_A_BREAK.TITLE' + TITLE: 'GCF.TAKE_A_BREAK.TITLE', }, TRACKING_REMINDER: { HELP: 'GCF.TRACKING_REMINDER.HELP', L_IS_ENABLED: 'GCF.TRACKING_REMINDER.L_IS_ENABLED', L_IS_SHOW_ON_MOBILE: 'GCF.TRACKING_REMINDER.L_IS_SHOW_ON_MOBILE', L_MIN_TIME: 'GCF.TRACKING_REMINDER.L_MIN_TIME', - TITLE: 'GCF.TRACKING_REMINDER.TITLE' - } + TITLE: 'GCF.TRACKING_REMINDER.TITLE', + }, }, GLOBAL_SNACK: { COPY_TO_CLIPPBOARD: 'GLOBAL_SNACK.COPY_TO_CLIPPBOARD', ERR_COMPRESSION: 'GLOBAL_SNACK.ERR_COMPRESSION', PERSISTENCE_DISALLOWED: 'GLOBAL_SNACK.PERSISTENCE_DISALLOWED', RUNNING_X: 'GLOBAL_SNACK.RUNNING_X', - SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG: 'GLOBAL_SNACK.SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG', - SHORTCUT_WARN_OPEN_NOTES_FROM_TAG: 'GLOBAL_SNACK.SHORTCUT_WARN_OPEN_NOTES_FROM_TAG' + SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG: + 'GLOBAL_SNACK.SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG', + SHORTCUT_WARN_OPEN_NOTES_FROM_TAG: 'GLOBAL_SNACK.SHORTCUT_WARN_OPEN_NOTES_FROM_TAG', }, GPB: { ASSETS: 'GPB.ASSETS', @@ -1065,7 +1072,7 @@ const T = { JIRA_LOAD_ISSUE: 'GPB.JIRA_LOAD_ISSUE', UNKNOWN: 'GPB.UNKNOWN', WEB_DAV_DOWNLOAD: 'GPB.WEB_DAV_DOWNLOAD', - WEB_DAV_UPLOAD: 'GPB.WEB_DAV_UPLOAD' + WEB_DAV_UPLOAD: 'GPB.WEB_DAV_UPLOAD', }, MH: { ADD_NEW_TASK: 'MH.ADD_NEW_TASK', @@ -1087,7 +1094,7 @@ const T = { TOGGLE_SHOW_BOOKMARKS: 'MH.TOGGLE_SHOW_BOOKMARKS', TOGGLE_SHOW_NOTES: 'MH.TOGGLE_SHOW_NOTES', TOGGLE_TRACK_TIME: 'MH.TOGGLE_TRACK_TIME', - WORKLOG: 'MH.WORKLOG' + WORKLOG: 'MH.WORKLOG', }, PDS: { BACK: 'PDS.BACK', @@ -1097,7 +1104,7 @@ const T = { D_CONFIRM_APP_CLOSE: { CANCEL: 'PDS.D_CONFIRM_APP_CLOSE.CANCEL', MSG: 'PDS.D_CONFIRM_APP_CLOSE.MSG', - OK: 'PDS.D_CONFIRM_APP_CLOSE.OK' + OK: 'PDS.D_CONFIRM_APP_CLOSE.OK', }, EVALUATION: 'PDS.EVALUATION', EXPORT_TASK_LIST: 'PDS.EXPORT_TASK_LIST', @@ -1120,10 +1127,10 @@ const T = { TIME_SPENT_AND_ESTIMATE_LABEL: 'PDS.TIME_SPENT_AND_ESTIMATE_LABEL', TIME_SPENT_ESTIMATE_TITLE: 'PDS.TIME_SPENT_ESTIMATE_TITLE', TODAY: 'PDS.TODAY', - WEEK: 'PDS.WEEK' + WEEK: 'PDS.WEEK', }, PM: { - TITLE: 'PM.TITLE' + TITLE: 'PM.TITLE', }, PP: { ARCHIVED_PROJECTS: 'PP.ARCHIVED_PROJECTS', @@ -1132,15 +1139,15 @@ const T = { DELETE_PROJECT: 'PP.DELETE_PROJECT', D_CONFIRM_ARCHIVE: { MSG: 'PP.D_CONFIRM_ARCHIVE.MSG', - OK: 'PP.D_CONFIRM_ARCHIVE.OK' + OK: 'PP.D_CONFIRM_ARCHIVE.OK', }, D_CONFIRM_DELETE: { MSG: 'PP.D_CONFIRM_DELETE.MSG', - OK: 'PP.D_CONFIRM_DELETE.OK' + OK: 'PP.D_CONFIRM_DELETE.OK', }, D_CONFIRM_UNARCHIVE: { MSG: 'PP.D_CONFIRM_UNARCHIVE.MSG', - OK: 'PP.D_CONFIRM_UNARCHIVE.OK' + OK: 'PP.D_CONFIRM_UNARCHIVE.OK', }, EDIT_PROJECT: 'PP.EDIT_PROJECT', EXPORT_PROJECT: 'PP.EXPORT_PROJECT', @@ -1150,7 +1157,7 @@ const T = { JIRA_CONFIGURED: 'PP.JIRA_CONFIGURED', S_INVALID_JSON: 'PP.S_INVALID_JSON', TITLE: 'PP.TITLE', - UN_ARCHIVE_PROJECT: 'PP.UN_ARCHIVE_PROJECT' + UN_ARCHIVE_PROJECT: 'PP.UN_ARCHIVE_PROJECT', }, PS: { GLOBAL_SETTINGS: 'PS.GLOBAL_SETTINGS', @@ -1161,11 +1168,11 @@ const T = { PROVIDE_FEEDBACK: 'PS.PROVIDE_FEEDBACK', SYNC_EXPORT: 'PS.SYNC_EXPORT', TAG_SETTINGS: 'PS.TAG_SETTINGS', - TOGGLE_DARK_MODE: 'PS.TOGGLE_DARK_MODE' + TOGGLE_DARK_MODE: 'PS.TOGGLE_DARK_MODE', }, SCHEDULE: { NO_SCHEDULED: 'SCHEDULE.NO_SCHEDULED', - START_TASK: 'SCHEDULE.START_TASK' + START_TASK: 'SCHEDULE.START_TASK', }, THEMES: { SELECT_THEME: 'THEMES.SELECT_THEME', @@ -1183,7 +1190,7 @@ const T = { pink: 'THEMES.pink', purple: 'THEMES.purple', teal: 'THEMES.teal', - yellow: 'THEMES.yellow' + yellow: 'THEMES.yellow', }, V: { E_1TO10: 'V.E_1TO10', @@ -1193,7 +1200,7 @@ const T = { E_MIN: 'V.E_MIN', E_MIN_LENGTH: 'V.E_MIN_LENGTH', E_PATTERN: 'V.E_PATTERN', - E_REQUIRED: 'V.E_REQUIRED' + E_REQUIRED: 'V.E_REQUIRED', }, WW: { ADD_MORE: 'WW.ADD_MORE', @@ -1208,7 +1215,7 @@ const T = { RESET_BREAK_TIMER: 'WW.RESET_BREAK_TIMER', TIME_ESTIMATED: 'WW.TIME_ESTIMATED', WITHOUT_BREAK: 'WW.WITHOUT_BREAK', - WORKING_TODAY: 'WW.WORKING_TODAY' - } + WORKING_TODAY: 'WW.WORKING_TODAY', + }, }; export { T }; diff --git a/src/app/ui/animations/animation.const.ts b/src/app/ui/animations/animation.const.ts index d0aa34c5c..3e5ad07ef 100644 --- a/src/app/ui/animations/animation.const.ts +++ b/src/app/ui/animations/animation.const.ts @@ -23,4 +23,3 @@ export const ANI_FASTEST_TIMING = `${TRANSITION_DURATION_XS} ${ANI_STANDARD_TIMI export const ANI_FAST_TIMING = `${TRANSITION_DURATION_M} ${ANI_STANDARD_TIMING_}`; export const ANI_ENTER_FAST_TIMING = `${TRANSITION_DURATION_S} ${ANI_ENTER_TIMING_}`; export const ANI_LEAVE_FAST_TIMING = `${TRANSITION_DURATION_S} ${ANI_LEAVE_TIMING}`; - diff --git a/src/app/ui/animations/blend-in-out.ani.ts b/src/app/ui/animations/blend-in-out.ani.ts index a71161637..e13fe0d27 100644 --- a/src/app/ui/animations/blend-in-out.ani.ts +++ b/src/app/ui/animations/blend-in-out.ani.ts @@ -6,23 +6,28 @@ export const blendInOutAnimation = [ transition(':enter', [ style({ opacity: 0, - transform: 'scaleY(0) translateX(-50%)' + transform: 'scaleY(0) translateX(-50%)', }), - animate(ANI_STANDARD_TIMING, style({ - opacity: 1, - transform: 'scaleY(1) translateX(-50%)' - })) + animate( + ANI_STANDARD_TIMING, + style({ + opacity: 1, + transform: 'scaleY(1) translateX(-50%)', + }), + ), ]), // void => * transition(':leave', [ style({ opacity: 1, - transform: 'scaleY(1) translateX(-50%)' + transform: 'scaleY(1) translateX(-50%)', }), - animate(ANI_STANDARD_TIMING, style({ - opacity: 0, - transform: 'scaleY(0) translateX(-50%)' - })) - ]) - ]) + animate( + ANI_STANDARD_TIMING, + style({ + opacity: 0, + transform: 'scaleY(0) translateX(-50%)', + }), + ), + ]), + ]), ]; - diff --git a/src/app/ui/animations/dynamic-height.ani.ts b/src/app/ui/animations/dynamic-height.ani.ts index af8e1635e..569a87ca5 100644 --- a/src/app/ui/animations/dynamic-height.ani.ts +++ b/src/app/ui/animations/dynamic-height.ani.ts @@ -4,10 +4,10 @@ import { ANI_STANDARD_TIMING } from './animation.const'; export const dynamicHeightAnimation = [ trigger('dynamicHeight', [ transition('void <=> *', []), - transition('* <=> *', [ - style({height: '{{startHeight}}px', opacity: 0}), - animate(ANI_STANDARD_TIMING), - ], {params: {startHeight: 0}}) - ]) + transition( + '* <=> *', + [style({ height: '{{startHeight}}px', opacity: 0 }), animate(ANI_STANDARD_TIMING)], + { params: { startHeight: 0 } }, + ), + ]), ]; - diff --git a/src/app/ui/animations/expand.ani.ts b/src/app/ui/animations/expand.ani.ts index 123224bd0..b4a9a42c8 100644 --- a/src/app/ui/animations/expand.ani.ts +++ b/src/app/ui/animations/expand.ani.ts @@ -4,89 +4,87 @@ import { ANI_ENTER_TIMING, ANI_FAST_TIMING, ANI_LEAVE_TIMING } from './animation export const expandAnimation = [ trigger('expand', [ transition(':enter', [ - style({height: 0, overflow: 'hidden'}), - animate(ANI_ENTER_TIMING, style({height: '*'})) + style({ height: 0, overflow: 'hidden' }), + animate(ANI_ENTER_TIMING, style({ height: '*' })), ]), // void => * transition(':leave', [ - style({overflow: 'hidden'}), - animate(ANI_LEAVE_TIMING, style({height: 0})) - ]) - ]) + style({ overflow: 'hidden' }), + animate(ANI_LEAVE_TIMING, style({ height: 0 })), + ]), + ]), ]; export const expandFastAnimation = [ trigger('expandFast', [ transition(':enter', [ - style({height: 0, overflow: 'hidden'}), - animate(ANI_FAST_TIMING, style({height: '*'})) + style({ height: 0, overflow: 'hidden' }), + animate(ANI_FAST_TIMING, style({ height: '*' })), ]), // void => * transition(':leave', [ - style({overflow: 'hidden'}), - animate(ANI_FAST_TIMING, style({height: 0})) - ]) - ]) + style({ overflow: 'hidden' }), + animate(ANI_FAST_TIMING, style({ height: 0 })), + ]), + ]), ]; export const expandAnimationAllowOverflow = [ trigger('expandAllowOverflow', [ transition(':enter', [ - style({height: 0}), - animate(ANI_ENTER_TIMING, style({height: '*'})) + style({ height: 0 }), + animate(ANI_ENTER_TIMING, style({ height: '*' })), ]), // void => * - transition(':leave', [ - animate(ANI_LEAVE_TIMING, style({height: 0})) - ]) - ]) + transition(':leave', [animate(ANI_LEAVE_TIMING, style({ height: 0 }))]), + ]), ]; export const expandFadeAnimation = [ trigger('expandFade', [ transition(':enter', [ - style({height: 0, opacity: 0, overflow: 'hidden'}), - animate(ANI_ENTER_TIMING, style({height: '*', opacity: 1})) + style({ height: 0, opacity: 0, overflow: 'hidden' }), + animate(ANI_ENTER_TIMING, style({ height: '*', opacity: 1 })), ]), // void => * transition(':leave', [ - style({overflow: 'hidden', opacity: 1}), - animate(ANI_LEAVE_TIMING, style({height: 0, opacity: 0})) - ]) - ]) + style({ overflow: 'hidden', opacity: 1 }), + animate(ANI_LEAVE_TIMING, style({ height: 0, opacity: 0 })), + ]), + ]), ]; export const expandFadeInOnlyAnimation = [ trigger('expandFadeInOnly', [ transition(':enter', [ - style({height: 0, opacity: 0, overflow: 'hidden'}), - animate(ANI_ENTER_TIMING, style({height: '*', opacity: 1})) + style({ height: 0, opacity: 0, overflow: 'hidden' }), + animate(ANI_ENTER_TIMING, style({ height: '*', opacity: 1 })), ]), // void => * // transition(':leave', [ // style({overflow: 'hidden', opacity: 1}), // animate(ANI_LEAVE_TIMING, style({height: 0, opacity: 0})) // ]) - ]) + ]), ]; export const expandFadeFastAnimation = [ trigger('expandFadeFast', [ transition(':enter', [ - style({height: 0, opacity: 0, overflow: 'hidden'}), - animate(ANI_FAST_TIMING, style({height: '*', opacity: 1})) + style({ height: 0, opacity: 0, overflow: 'hidden' }), + animate(ANI_FAST_TIMING, style({ height: '*', opacity: 1 })), ]), // void => * transition(':leave', [ - style({overflow: 'hidden', opacity: 1}), - animate(ANI_FAST_TIMING, style({height: 0, opacity: 0})) - ]) - ]) + style({ overflow: 'hidden', opacity: 1 }), + animate(ANI_FAST_TIMING, style({ height: 0, opacity: 0 })), + ]), + ]), ]; export const expandFadeHorizontalAnimation = [ trigger('expandFadeHorizontal', [ transition(':enter', [ - style({width: 0, opacity: 0, overflow: 'hidden'}), - animate(ANI_ENTER_TIMING, style({width: '*', opacity: 1})) + style({ width: 0, opacity: 0, overflow: 'hidden' }), + animate(ANI_ENTER_TIMING, style({ width: '*', opacity: 1 })), ]), // void => * transition(':leave', [ - style({overflow: 'hidden', opacity: 1}), - animate(ANI_LEAVE_TIMING, style({width: 0, opacity: 0})) - ]) - ]) + style({ overflow: 'hidden', opacity: 1 }), + animate(ANI_LEAVE_TIMING, style({ width: 0, opacity: 0 })), + ]), + ]), ]; diff --git a/src/app/ui/animations/fade.ani.ts b/src/app/ui/animations/fade.ani.ts index e1e10c6e2..b9a818b80 100644 --- a/src/app/ui/animations/fade.ani.ts +++ b/src/app/ui/animations/fade.ani.ts @@ -4,27 +4,27 @@ import { ANI_ENTER_TIMING, ANI_LEAVE_TIMING } from './animation.const'; export const fadeAnimation = [ trigger('fade', [ transition(':enter', [ - style({opacity: 0}), - animate(ANI_ENTER_TIMING, style({opacity: '*'})) + style({ opacity: 0 }), + animate(ANI_ENTER_TIMING, style({ opacity: '*' })), ]), // void => * transition(':leave', [ - style({opacity: '*'}), - animate(ANI_LEAVE_TIMING, style({opacity: 0})) - ]) - ]) + style({ opacity: '*' }), + animate(ANI_LEAVE_TIMING, style({ opacity: 0 })), + ]), + ]), ]; export const fadeInOutBottomAnimation = [ trigger('fadeInOutBottom', [ transition(':enter', [ - style({opacity: 0, transform: 'translateY(100%)'}), - animate(ANI_ENTER_TIMING, style({opacity: '*', transform: 'translateY(0)'})) + style({ opacity: 0, transform: 'translateY(100%)' }), + animate(ANI_ENTER_TIMING, style({ opacity: '*', transform: 'translateY(0)' })), ]), // void => * transition(':leave', [ - style({opacity: '*', transform: 'translateY(0)'}), - animate(ANI_LEAVE_TIMING, style({opacity: 0, transform: 'translateY(100%)'})) - ]) - ]) + style({ opacity: '*', transform: 'translateY(0)' }), + animate(ANI_LEAVE_TIMING, style({ opacity: 0, transform: 'translateY(100%)' })), + ]), + ]), ]; export const fadeOutAnimation = [ @@ -34,8 +34,8 @@ export const fadeOutAnimation = [ // animate(ANI_ENTER_TIMING, style({opacity: '*', transform: 'translateY(0)'})) // ]), // void => * transition(':leave', [ - style({opacity: '*'}), - animate(ANI_LEAVE_TIMING, style({opacity: 0})) - ]) - ]) + style({ opacity: '*' }), + animate(ANI_LEAVE_TIMING, style({ opacity: 0 })), + ]), + ]), ]; diff --git a/src/app/ui/animations/noop.ani.ts b/src/app/ui/animations/noop.ani.ts index 94fb926b3..1015fc3cc 100644 --- a/src/app/ui/animations/noop.ani.ts +++ b/src/app/ui/animations/noop.ani.ts @@ -1,9 +1,4 @@ import { transition, trigger } from '@angular/animations'; // use on parent to skip initial animation -export const noopAnimation = trigger( - 'noop', - [ - transition(':enter', []) - ] -); +export const noopAnimation = trigger('noop', [transition(':enter', [])]); diff --git a/src/app/ui/animations/slide.ani.ts b/src/app/ui/animations/slide.ani.ts index 51c5cdf04..89785d085 100644 --- a/src/app/ui/animations/slide.ani.ts +++ b/src/app/ui/animations/slide.ani.ts @@ -4,14 +4,14 @@ import { ANI_ENTER_TIMING, ANI_LEAVE_TIMING } from './animation.const'; export const slideAnimation = [ trigger('slide', [ transition(':enter', [ - style({marginTop: '-{{elHeight}}px', opacity: 0}), - animate(ANI_ENTER_TIMING, style({marginTop: '*', opacity: 1})) + style({ marginTop: '-{{elHeight}}px', opacity: 0 }), + animate(ANI_ENTER_TIMING, style({ marginTop: '*', opacity: 1 })), ]), // void => * transition(':leave', [ - style({marginTop: 0, opacity: 1}), - animate(ANI_LEAVE_TIMING, style({marginTop: '-{{elHeight}}px', opacity: 0})) - ]) - ]) + style({ marginTop: 0, opacity: 1 }), + animate(ANI_LEAVE_TIMING, style({ marginTop: '-{{elHeight}}px', opacity: 0 })), + ]), + ]), ]; // export const slideAnimation = [ diff --git a/src/app/ui/animations/standard-list.ani.ts b/src/app/ui/animations/standard-list.ani.ts index 03fb8c1d1..80dd2540c 100644 --- a/src/app/ui/animations/standard-list.ani.ts +++ b/src/app/ui/animations/standard-list.ani.ts @@ -1,31 +1,50 @@ -import { animate, keyframes, query, stagger, style, transition, trigger } from '@angular/animations'; +import { + animate, + keyframes, + query, + stagger, + style, + transition, + trigger, +} from '@angular/animations'; import { ANI_FAST_TIMING } from './animation.const'; const ANI = [ - query(':enter', style({opacity: 0, height: 0}), {optional: true}), + query(':enter', style({ opacity: 0, height: 0 }), { optional: true }), - query(':enter', stagger('40ms', [ - animate(ANI_FAST_TIMING, keyframes([ - style({opacity: 0, height: 0, transform: 'scale(0)', offset: 0}), - style({opacity: 1, height: '*', transform: 'scale(1)', offset: 0.99}), - style({height: 'auto', offset: 1.0}), - ]))]), {optional: true} + query( + ':enter', + stagger('40ms', [ + animate( + ANI_FAST_TIMING, + keyframes([ + style({ opacity: 0, height: 0, transform: 'scale(0)', offset: 0 }), + style({ opacity: 1, height: '*', transform: 'scale(1)', offset: 0.99 }), + style({ height: 'auto', offset: 1.0 }), + ]), + ), + ]), + { optional: true }, ), query( - ':leave', stagger('-40ms', [ - style({transform: 'scale(1)', opacity: 1, height: '*'}), - animate(ANI_FAST_TIMING, style({transform: 'scale(0)', height: 0})) - ], - ), {optional: true}), + ':leave', + stagger('-40ms', [ + style({ transform: 'scale(1)', opacity: 1, height: '*' }), + animate(ANI_FAST_TIMING, style({ transform: 'scale(0)', height: 0 })), + ]), + { optional: true }, + ), - query('.gu-transit', style({ + query( + '.gu-transit', + style({ display: 'none', opacity: 0, height: 0, - visibility: 'hidden' + visibility: 'hidden', }), - {optional: true} + { optional: true }, ), ]; diff --git a/src/app/ui/animations/swirl-in-out.ani.ts b/src/app/ui/animations/swirl-in-out.ani.ts index 11ba78307..92b6d7924 100644 --- a/src/app/ui/animations/swirl-in-out.ani.ts +++ b/src/app/ui/animations/swirl-in-out.ani.ts @@ -6,19 +6,24 @@ const TIMING = `${TRANSITION_DURATION_M} linear`; export const swirlAnimation = [ trigger('swirl', [ transition(':enter', [ - animate(TIMING, keyframes([ - style({transform: 'scale(0.5) rotate(-180deg)'}), - style({transform: 'scale(1) rotate(-90deg)'}), - style({transform: 'scale(1) rotate(0deg)'}), - ])) + animate( + TIMING, + keyframes([ + style({ transform: 'scale(0.5) rotate(-180deg)' }), + style({ transform: 'scale(1) rotate(-90deg)' }), + style({ transform: 'scale(1) rotate(0deg)' }), + ]), + ), ]), // void => * transition(':leave', [ - animate(TIMING, keyframes([ - style({transform: 'scale(0.5) rotate(-180deg)'}), - style({transform: 'scale(1) rotate(-90deg)'}), - style({transform: 'scale(1) rotate(0deg)'}), - ])) - ]) - ]) + animate( + TIMING, + keyframes([ + style({ transform: 'scale(0.5) rotate(-180deg)' }), + style({ transform: 'scale(1) rotate(-90deg)' }), + style({ transform: 'scale(1) rotate(0deg)' }), + ]), + ), + ]), + ]), ]; - diff --git a/src/app/ui/animations/warp-route.ts b/src/app/ui/animations/warp-route.ts index 2ad5e6e4d..71009d8e6 100644 --- a/src/app/ui/animations/warp-route.ts +++ b/src/app/ui/animations/warp-route.ts @@ -1,56 +1,65 @@ -import { animate, animateChild, group, query, style, transition, trigger } from '@angular/animations'; +import { + animate, + animateChild, + group, + query, + style, + transition, + trigger, +} from '@angular/animations'; import { ANI_ENTER_TIMING } from './animation.const'; -export const warpRouteAnimation = - trigger('warpRoute', [ - // transition('* <=> *', [ - // group([ - // query( - // ':enter', - // [ - // style({ - // opacity: 0, - // transform: 'translateY(9rem) rotate(-10deg)' - // }), - // animate( - // '0.35s cubic-bezier(0, 1.8, 1, 1.8)', - // style({opacity: 1, transform: 'translateY(0) rotate(0)'}) - // ), - // animateChild() - // ], - // {optional: true} - // ), - // query( - // ':leave', - // [animate('0.35s', style({opacity: 0})), animateChild()], - // {optional: true} - // ) - // ]) - // ]) +export const warpRouteAnimation = trigger('warpRoute', [ + // transition('* <=> *', [ + // group([ + // query( + // ':enter', + // [ + // style({ + // opacity: 0, + // transform: 'translateY(9rem) rotate(-10deg)' + // }), + // animate( + // '0.35s cubic-bezier(0, 1.8, 1, 1.8)', + // style({opacity: 1, transform: 'translateY(0) rotate(0)'}) + // ), + // animateChild() + // ], + // {optional: true} + // ), + // query( + // ':leave', + // [animate('0.35s', style({opacity: 0})), animateChild()], + // {optional: true} + // ) + // ]) + // ]) - transition('* <=> *', [ - - /* 1 */ query(':enter, :leave', style({position: 'absolute', width: '100%', minHeight: '100%', height: '100%'}) - , {optional: true}), - group([ - query( - ':leave', - [ - style({opacity: 1, transform: 'scale(1)'}), - animate(ANI_ENTER_TIMING, style({opacity: 0, transform: 'scale(1.1)'})), - animateChild() - ], - {optional: true}, - ), - query( - ':enter', - [ - style({opacity: 0, transform: 'scale(1.2)'}), - animate(ANI_ENTER_TIMING, style({opacity: 1, transform: 'scale(1)'})), - animateChild() - ], - {optional: true}, - ), - ]) - ]) - ]); + transition('* <=> *', [ + /* 1 */ query( + ':enter, :leave', + style({ position: 'absolute', width: '100%', minHeight: '100%', height: '100%' }), + { optional: true }, + ), + group([ + query( + ':leave', + [ + style({ opacity: 1, transform: 'scale(1)' }), + animate(ANI_ENTER_TIMING, style({ opacity: 0, transform: 'scale(1.1)' })), + animateChild(), + ], + { optional: true }, + ), + query( + ':enter', + [ + style({ opacity: 0, transform: 'scale(1.2)' }), + animate(ANI_ENTER_TIMING, style({ opacity: 1, transform: 'scale(1)' })), + animateChild(), + ], + { optional: true }, + ), + ]), + ]), +]); diff --git a/src/app/ui/animations/work-view-project-change.ani.ts b/src/app/ui/animations/work-view-project-change.ani.ts index 9c37b7316..23fb65d6e 100644 --- a/src/app/ui/animations/work-view-project-change.ani.ts +++ b/src/app/ui/animations/work-view-project-change.ani.ts @@ -4,14 +4,14 @@ import { ANI_ENTER_TIMING } from './animation.const'; export const workViewProjectChangeAnimation = [ trigger('projectChange', [ transition(':enter', [ - style({opacity: 0, transform: 'scale(1.2)'}), - animate(ANI_ENTER_TIMING, style({opacity: 1, transform: 'scale(1)'})), - animateChild() + style({ opacity: 0, transform: 'scale(1.2)' }), + animate(ANI_ENTER_TIMING, style({ opacity: 1, transform: 'scale(1)' })), + animateChild(), ]), // void => * transition(':leave', [ - style({opacity: 1, transform: 'scale(1)'}), - animate(ANI_ENTER_TIMING, style({opacity: 0, transform: 'scale(1.1)'})), + style({ opacity: 1, transform: 'scale(1)' }), + animate(ANI_ENTER_TIMING, style({ opacity: 0, transform: 'scale(1.1)' })), animateChild(), ]), - ]) + ]), ]; diff --git a/src/app/ui/better-drawer/better-drawer-container/better-drawer-container.component.spec.ts b/src/app/ui/better-drawer/better-drawer-container/better-drawer-container.component.spec.ts index 942a0e436..d3fc948ee 100644 --- a/src/app/ui/better-drawer/better-drawer-container/better-drawer-container.component.spec.ts +++ b/src/app/ui/better-drawer/better-drawer-container/better-drawer-container.component.spec.ts @@ -6,12 +6,13 @@ describe('BetterDrawerContainerComponent', () => { let component: BetterDrawerContainerComponent; let fixture: ComponentFixture; - beforeEach(waitForAsync(() => { - TestBed.configureTestingModule({ - declarations: [BetterDrawerContainerComponent] - }) - .compileComponents(); - })); + beforeEach( + waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [BetterDrawerContainerComponent], + }).compileComponents(); + }), + ); beforeEach(() => { fixture = TestBed.createComponent(BetterDrawerContainerComponent); diff --git a/src/app/ui/better-drawer/better-drawer-container/better-drawer-container.component.ts b/src/app/ui/better-drawer/better-drawer-container/better-drawer-container.component.ts index fd8da34f9..6cca86f27 100644 --- a/src/app/ui/better-drawer/better-drawer-container/better-drawer-container.component.ts +++ b/src/app/ui/better-drawer/better-drawer-container/better-drawer-container.component.ts @@ -9,7 +9,7 @@ import { OnDestroy, OnInit, Output, - ViewChild + ViewChild, } from '@angular/core'; import { fadeAnimation } from '../../animations/fade.ani'; import { DomSanitizer, SafeStyle } from '@angular/platform-browser'; @@ -27,9 +27,10 @@ const VERY_SMALL_CONTAINER_WIDTH = 450; templateUrl: './better-drawer-container.component.html', styleUrls: ['./better-drawer-container.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [fadeAnimation] + animations: [fadeAnimation], }) -export class BetterDrawerContainerComponent implements OnInit, AfterContentInit, OnDestroy { +export class BetterDrawerContainerComponent + implements OnInit, AfterContentInit, OnDestroy { @Input() sideWidth: number = 0; @Output() wasClosed: EventEmitter = new EventEmitter(); contentEl$: ReplaySubject = new ReplaySubject(1); @@ -39,21 +40,17 @@ export class BetterDrawerContainerComponent implements OnInit, AfterContentInit, share(), ); isSmallMainContainer$: Observable = this.containerWidth$.pipe( - map(v => v < SMALL_CONTAINER_WIDTH), + map((v) => v < SMALL_CONTAINER_WIDTH), distinctUntilChanged(), ); isVerySmallMainContainer$: Observable = this.containerWidth$.pipe( - map(v => v < VERY_SMALL_CONTAINER_WIDTH), + map((v) => v < VERY_SMALL_CONTAINER_WIDTH), distinctUntilChanged(), ); sideStyle: SafeStyle = ''; private _subs: Subscription = new Subscription(); - constructor( - private _elementRef: ElementRef, - private _domSanitizer: DomSanitizer, - ) { - } + constructor(private _elementRef: ElementRef, private _domSanitizer: DomSanitizer) {} @HostBinding('class.isOpen') get isOpenGet() { return this._isOpen; @@ -63,7 +60,7 @@ export class BetterDrawerContainerComponent implements OnInit, AfterContentInit, return this._isOver; } - @ViewChild('contentElRef', {read: ElementRef}) set setContentElRef(ref: ElementRef) { + @ViewChild('contentElRef', { read: ElementRef }) set setContentElRef(ref: ElementRef) { this.contentEl$.next(ref.nativeElement); } @@ -87,20 +84,24 @@ export class BetterDrawerContainerComponent implements OnInit, AfterContentInit, ngAfterContentInit(): void { const containerEl = this._elementRef.nativeElement; - this._subs.add(this.isSmallMainContainer$.subscribe(v => { - if (v) { - containerEl.classList.add(MainContainerClass.isSmallMainContainer); - } else { - containerEl.classList.remove(MainContainerClass.isSmallMainContainer); - } - })); - this._subs.add(this.isVerySmallMainContainer$.subscribe(v => { - if (v) { - containerEl.classList.add(MainContainerClass.isVerySmallMainContainer); - } else { - containerEl.classList.remove(MainContainerClass.isVerySmallMainContainer); - } - })); + this._subs.add( + this.isSmallMainContainer$.subscribe((v) => { + if (v) { + containerEl.classList.add(MainContainerClass.isSmallMainContainer); + } else { + containerEl.classList.remove(MainContainerClass.isSmallMainContainer); + } + }), + ); + this._subs.add( + this.isVerySmallMainContainer$.subscribe((v) => { + if (v) { + containerEl.classList.add(MainContainerClass.isVerySmallMainContainer); + } else { + containerEl.classList.remove(MainContainerClass.isVerySmallMainContainer); + } + }), + ); } ngOnDestroy(): void { @@ -121,14 +122,13 @@ export class BetterDrawerContainerComponent implements OnInit, AfterContentInit, private _updateStyle() { const widthStyle = ` width: ${this.sideWidth}%;`; - const style = (this.isOverGet) - ? (this.isOpenGet) + const style = this.isOverGet + ? this.isOpenGet ? 'transform: translateX(0);' : 'transform: translateX(100%);' - : (this.isOpenGet) - ? `margin-right: 0; ${widthStyle}` - : `margin-right: ${-1 * this.sideWidth}%; ${widthStyle}` - ; + : this.isOpenGet + ? `margin-right: 0; ${widthStyle}` + : `margin-right: ${-1 * this.sideWidth}%; ${widthStyle}`; this.sideStyle = this._domSanitizer.bypassSecurityTrustStyle(style); } } diff --git a/src/app/ui/better-drawer/better-drawer.module.ts b/src/app/ui/better-drawer/better-drawer.module.ts index 8d8e649da..c4eb8ccc1 100644 --- a/src/app/ui/better-drawer/better-drawer.module.ts +++ b/src/app/ui/better-drawer/better-drawer.module.ts @@ -3,15 +3,8 @@ import { CommonModule } from '@angular/common'; import { BetterDrawerContainerComponent } from './better-drawer-container/better-drawer-container.component'; @NgModule({ - declarations: [ - BetterDrawerContainerComponent, - ], - exports: [ - BetterDrawerContainerComponent - ], - imports: [ - CommonModule - ], + declarations: [BetterDrawerContainerComponent], + exports: [BetterDrawerContainerComponent], + imports: [CommonModule], }) -export class BetterDrawerModule { -} +export class BetterDrawerModule {} diff --git a/src/app/ui/chip-list-input/chip-list-input.component.ts b/src/app/ui/chip-list-input/chip-list-input.component.ts index f34d5a24a..dc4abc9d1 100644 --- a/src/app/ui/chip-list-input/chip-list-input.component.ts +++ b/src/app/ui/chip-list-input/chip-list-input.component.ts @@ -1,7 +1,18 @@ -import { ChangeDetectionStrategy, Component, ElementRef, EventEmitter, Input, Output, ViewChild } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + Output, + ViewChild, +} from '@angular/core'; import { FormControl } from '@angular/forms'; import { Observable } from 'rxjs'; -import { MatAutocomplete, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { + MatAutocomplete, + MatAutocompleteSelectedEvent, +} from '@angular/material/autocomplete'; import { MatChipInputEvent } from '@angular/material/chips'; import { map, startWith } from 'rxjs/operators'; import { COMMA, ENTER } from '@angular/cdk/keycodes'; @@ -18,7 +29,7 @@ interface Suggestion { selector: 'chip-list-input', templateUrl: './chip-list-input.component.html', styleUrls: ['./chip-list-input.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ChipListInputComponent { T: typeof T = T; @@ -39,20 +50,22 @@ export class ChipListInputComponent { modelItems: Suggestion[] = []; inputCtrl: FormControl = new FormControl(); separatorKeysCodes: number[] = [ENTER, COMMA]; - @ViewChild('inputElRef', {static: true}) inputEl?: ElementRef; - @ViewChild('autoElRef', {static: true}) matAutocomplete?: MatAutocomplete; + @ViewChild('inputElRef', { static: true }) inputEl?: ElementRef; + @ViewChild('autoElRef', { static: true }) matAutocomplete?: MatAutocomplete; private _modelIds: string[] = []; filteredSuggestions: Observable = this.inputCtrl.valueChanges.pipe( startWith(''), - map((val: string | null) => (val !== null) - ? this._filter(val) - : this.suggestionsIn.filter(suggestion => !this._modelIds.includes(suggestion.id)) - ) + map((val: string | null) => + val !== null + ? this._filter(val) + : this.suggestionsIn.filter( + (suggestion) => !this._modelIds.includes(suggestion.id), + ), + ), ); - constructor() { - } + constructor() {} @Input() set suggestions(val: Suggestion[]) { this.suggestionsIn = val.sort((a, b) => a.title.localeCompare(b.title)); @@ -105,13 +118,15 @@ export class ChipListInputComponent { } private _updateModelItems(modelIds: string[]) { - this.modelItems = (this.suggestionsIn.length) - ? modelIds.map(id => this.suggestionsIn.find(suggestion => suggestion.id === id)) as Suggestion[] + this.modelItems = this.suggestionsIn.length + ? (modelIds.map((id) => + this.suggestionsIn.find((suggestion) => suggestion.id === id), + ) as Suggestion[]) : []; } private _getExistingSuggestionByTitle(v: string) { - return this.suggestionsIn.find(suggestion => suggestion.title === v); + return this.suggestionsIn.find((suggestion) => suggestion.title === v); } private _add(id: string) { @@ -137,8 +152,9 @@ export class ChipListInputComponent { const filterValue = val.toLowerCase(); return this.suggestionsIn.filter( - suggestion => suggestion.title.toLowerCase().indexOf(filterValue) === 0 - && !this._modelIds.includes(suggestion.id) + (suggestion) => + suggestion.title.toLowerCase().indexOf(filterValue) === 0 && + !this._modelIds.includes(suggestion.id), ); } } diff --git a/src/app/ui/collapsible/collapsible.component.ts b/src/app/ui/collapsible/collapsible.component.ts index b6c607a2e..532b4ddd0 100644 --- a/src/app/ui/collapsible/collapsible.component.ts +++ b/src/app/ui/collapsible/collapsible.component.ts @@ -1,4 +1,11 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, HostBinding, Input, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + HostBinding, + Input, + Output, +} from '@angular/core'; import { expandAnimation } from '../animations/expand.ani'; @Component({ @@ -6,7 +13,7 @@ import { expandAnimation } from '../animations/expand.ani'; templateUrl: './collapsible.component.html', styleUrls: ['./collapsible.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandAnimation] + animations: [expandAnimation], }) export class CollapsibleComponent { @Input() title?: string; @@ -19,8 +26,7 @@ export class CollapsibleComponent { @Output() isExpandedChange: EventEmitter = new EventEmitter(); - constructor() { - } + constructor() {} toggleExpand() { this.isExpanded = !this.isExpanded; diff --git a/src/app/ui/datetime-input/datetime-input.component.ts b/src/app/ui/datetime-input/datetime-input.component.ts index dbe177d96..38725e51f 100644 --- a/src/app/ui/datetime-input/datetime-input.component.ts +++ b/src/app/ui/datetime-input/datetime-input.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; import { T } from '../../t.const'; import { LS_LAST_REMINDER_DATE } from '../../core/persistence/ls-keys.const'; import { timestampToDatetimeInputString } from '../../util/timestamp-to-datetime-input-string'; @@ -7,7 +13,7 @@ import { timestampToDatetimeInputString } from '../../util/timestamp-to-datetime selector: 'datetime-input', templateUrl: './datetime-input.component.html', styleUrls: ['./datetime-input.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DatetimeInputComponent { @Input() name: string | undefined; @@ -94,5 +100,4 @@ export class DatetimeInputComponent { } return timestampToDatetimeInputString(dateTime); } - } diff --git a/src/app/ui/dialog-confirm/dialog-confirm.component.ts b/src/app/ui/dialog-confirm/dialog-confirm.component.ts index a7f6065bb..d2f1338b5 100644 --- a/src/app/ui/dialog-confirm/dialog-confirm.component.ts +++ b/src/app/ui/dialog-confirm/dialog-confirm.component.ts @@ -6,16 +6,15 @@ import { T } from '../../t.const'; selector: 'dialog-confirm', templateUrl: './dialog-confirm.component.html', styleUrls: ['./dialog-confirm.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogConfirmComponent { T: typeof T = T; constructor( private _matDialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any - ) { - } + @Inject(MAT_DIALOG_DATA) public data: any, + ) {} close(res: any) { this._matDialogRef.close(res); diff --git a/src/app/ui/dialog-fullscreen-markdown/dialog-fullscreen-markdown.component.ts b/src/app/ui/dialog-fullscreen-markdown/dialog-fullscreen-markdown.component.ts index 08731b214..89edf9f48 100644 --- a/src/app/ui/dialog-fullscreen-markdown/dialog-fullscreen-markdown.component.ts +++ b/src/app/ui/dialog-fullscreen-markdown/dialog-fullscreen-markdown.component.ts @@ -8,7 +8,7 @@ import { ESCAPE } from '@angular/cdk/keycodes'; selector: 'dialog-fullscreen-markdown', templateUrl: './dialog-fullscreen-markdown.component.html', styleUrls: ['./dialog-fullscreen-markdown.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogFullscreenMarkdownComponent implements OnDestroy { T: typeof T = T; @@ -17,16 +17,18 @@ export class DialogFullscreenMarkdownComponent implements OnDestroy { constructor( private _matDialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any + @Inject(MAT_DIALOG_DATA) public data: any, ) { // we want to save as default _matDialogRef.disableClose = true; - this._subs.add(_matDialogRef.keydownEvents().subscribe(e => { - if ((e as any).keyCode === ESCAPE) { - e.preventDefault(); - this.close(); - } - })); + this._subs.add( + _matDialogRef.keydownEvents().subscribe((e) => { + if ((e as any).keyCode === ESCAPE) { + e.preventDefault(); + this.close(); + } + }), + ); } ngOnDestroy() { diff --git a/src/app/ui/dialog-prompt/dialog-prompt.component.ts b/src/app/ui/dialog-prompt/dialog-prompt.component.ts index a295d203b..32d527ebf 100644 --- a/src/app/ui/dialog-prompt/dialog-prompt.component.ts +++ b/src/app/ui/dialog-prompt/dialog-prompt.component.ts @@ -6,7 +6,7 @@ import { T } from '../../t.const'; selector: 'dialog-prompt', templateUrl: './dialog-prompt.component.html', styleUrls: ['./dialog-prompt.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class DialogPromptComponent { T: typeof T = T; @@ -14,9 +14,8 @@ export class DialogPromptComponent { constructor( private _matDialogRef: MatDialogRef, - @Inject(MAT_DIALOG_DATA) public data: any - ) { - } + @Inject(MAT_DIALOG_DATA) public data: any, + ) {} close(isSave: boolean) { if (isSave) { diff --git a/src/app/ui/duration/duration-from-string.pipe.ts b/src/app/ui/duration/duration-from-string.pipe.ts index 5c0dbef60..99ea8167e 100644 --- a/src/app/ui/duration/duration-from-string.pipe.ts +++ b/src/app/ui/duration/duration-from-string.pipe.ts @@ -3,7 +3,7 @@ import * as moment from 'moment'; import { stringToMs } from './string-to-ms.pipe'; @Pipe({ - name: 'durationFromString' + name: 'durationFromString', }) export class DurationFromStringPipe implements PipeTransform { transform: (value: any, ...args: any[]) => any = durationFromString; diff --git a/src/app/ui/duration/duration-to-string.pipe.ts b/src/app/ui/duration/duration-to-string.pipe.ts index d04ccf842..602fa2ae9 100644 --- a/src/app/ui/duration/duration-to-string.pipe.ts +++ b/src/app/ui/duration/duration-to-string.pipe.ts @@ -1,7 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ - name: 'durationToString' + name: 'durationToString', }) export class DurationToStringPipe implements PipeTransform { transform: (value: any, ...args: any[]) => any = durationToString; @@ -14,13 +14,12 @@ export const durationToString = (momentDuration: any, args?: any): any => { if (md) { // if moment duration object if (md.duration || md._milliseconds) { - - const dd = md.duration && md.duration()._data || md._data; + const dd = (md.duration && md.duration()._data) || md._data; val = ''; - val += parseInt(dd.days, 10) > 0 && (dd.days + 'd ') || ''; - val += parseInt(dd.hours, 10) > 0 && (dd.hours + 'h ') || ''; - val += parseInt(dd.minutes, 10) > 0 && (dd.minutes + 'm ') || ''; - val += parseInt(dd.seconds, 10) > 0 && (dd.seconds + 's ') || ''; + val += (parseInt(dd.days, 10) > 0 && dd.days + 'd ') || ''; + val += (parseInt(dd.hours, 10) > 0 && dd.hours + 'h ') || ''; + val += (parseInt(dd.minutes, 10) > 0 && dd.minutes + 'm ') || ''; + val += (parseInt(dd.seconds, 10) > 0 && dd.seconds + 's ') || ''; val = val.trim(); // if moment duration string diff --git a/src/app/ui/duration/input-duration-formly/input-duration-formly.component.ts b/src/app/ui/duration/input-duration-formly/input-duration-formly.component.ts index 1895f3a51..b3e26669e 100644 --- a/src/app/ui/duration/input-duration-formly/input-duration-formly.component.ts +++ b/src/app/ui/duration/input-duration-formly/input-duration-formly.component.ts @@ -5,7 +5,7 @@ import { FieldType } from '@ngx-formly/material'; selector: 'input-duration-formly', templateUrl: './input-duration-formly.component.html', styleUrls: ['./input-duration-formly.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class InputDurationFormlyComponent extends FieldType { // @ViewChild(MatInput, {static: true}) formFieldControl?: MatInput; diff --git a/src/app/ui/duration/input-duration-slider/dot.ani.ts b/src/app/ui/duration/input-duration-slider/dot.ani.ts index d63911208..5857050a7 100644 --- a/src/app/ui/duration/input-duration-slider/dot.ani.ts +++ b/src/app/ui/duration/input-duration-slider/dot.ani.ts @@ -4,14 +4,14 @@ import { ANI_ENTER_TIMING, ANI_LEAVE_TIMING } from '../../animations/animation.c export const dotAnimation = [ trigger('dot', [ transition(':enter', [ - style({opacity: 0, transform: 'translate(0)'}), - animate(ANI_ENTER_TIMING, style({opacity: 1, transform: '*'})) + style({ opacity: 0, transform: 'translate(0)' }), + animate(ANI_ENTER_TIMING, style({ opacity: 1, transform: '*' })), ]), // void => * transition(':leave', [ - style({opacity: 1, transform: '*'}), - animate(ANI_LEAVE_TIMING, style({opacity: 0, transform: 'translate(0)'})) - ]) - ]) + style({ opacity: 1, transform: '*' }), + animate(ANI_LEAVE_TIMING, style({ opacity: 0, transform: 'translate(0)' })), + ]), + ]), ]; // animation: $transition-duration-l ani-circle-reveal $ani-enter-timing; diff --git a/src/app/ui/duration/input-duration-slider/input-duration-slider.component.ts b/src/app/ui/duration/input-duration-slider/input-duration-slider.component.ts index 3c136ba58..9560621ea 100644 --- a/src/app/ui/duration/input-duration-slider/input-duration-slider.component.ts +++ b/src/app/ui/duration/input-duration-slider/input-duration-slider.component.ts @@ -8,7 +8,7 @@ import { OnDestroy, OnInit, Output, - ViewChild + ViewChild, } from '@angular/core'; import * as shortid from 'shortid'; import * as moment from 'moment'; @@ -33,15 +33,12 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy { endHandler?: () => void; moveHandler?: (ev: any) => void; - @ViewChild('circleEl', {static: true}) circleEl?: ElementRef; + @ViewChild('circleEl', { static: true }) circleEl?: ElementRef; @Input() label: string = ''; @Output() modelChange: EventEmitter = new EventEmitter(); - constructor( - private _el: ElementRef, - private _cd: ChangeDetectorRef, - ) { + constructor(private _el: ElementRef, private _cd: ChangeDetectorRef) { this.el = this._el.nativeElement; } @@ -56,7 +53,6 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy { ngOnInit() { this.startHandler = (ev) => { - if (!this.endHandler || !this.moveHandler || !this.circleEl) { throw new Error(); } @@ -77,9 +73,10 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy { }; this.moveHandler = (ev) => { - if (ev.type === 'click' && - (ev.target.tagName === 'LABEL' || - ev.target.tagName === 'INPUT')) { + if ( + ev.type === 'click' && + (ev.target.tagName === 'LABEL' || ev.target.tagName === 'INPUT') + ) { return; } if (!this.endHandler || !this.moveHandler || !this.circleEl) { @@ -176,9 +173,9 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy { let minutesFromDegrees; // NOTE: values are negative for the last quadrant if (degrees >= 0) { - minutesFromDegrees = (degrees / 360 * 60); + minutesFromDegrees = (degrees / 360) * 60; } else { - minutesFromDegrees = ((degrees + 360) / 360 * 60); + minutesFromDegrees = ((degrees + 360) / 360) * 60; } minutesFromDegrees = Math.round(minutesFromDegrees / 5) * 5; @@ -187,15 +184,19 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy { minutesFromDegrees = 0; } - let hours = Math.floor(moment.duration({ - milliseconds: this._model - }).asHours()); + let hours = Math.floor( + moment + .duration({ + milliseconds: this._model, + }) + .asHours(), + ); const minuteDelta = minutesFromDegrees - this.minutesBefore; if (minuteDelta > THRESHOLD) { hours--; - } else if ((-1 * minuteDelta) > THRESHOLD) { + } else if (-1 * minuteDelta > THRESHOLD) { hours++; } @@ -209,10 +210,12 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy { this.minutesBefore = minutesFromDegrees; this.setDots(hours); - this._model = moment.duration({ - hours, - minutes: minutesFromDegrees - }).asMilliseconds(); + this._model = moment + .duration({ + hours, + minutes: minutesFromDegrees, + }) + .asMilliseconds(); this.modelChange.emit(this._model); this._cd.detectChanges(); @@ -227,12 +230,12 @@ export class InputDurationSliderComponent implements OnInit, OnDestroy { setRotationFromValue(val: number = this._model) { console.log(val); const momentVal = moment.duration({ - milliseconds: val + milliseconds: val, }); const minutes = momentVal.minutes(); this.setDots(Math.floor(momentVal.asHours())); - const degrees = minutes * 360 / 60; + const degrees = (minutes * 360) / 60; this.minutesBefore = minutes; this.setCircleRotation(degrees); this._cd.detectChanges(); diff --git a/src/app/ui/duration/input-duration.directive.ts b/src/app/ui/duration/input-duration.directive.ts index 8b0cbf507..d4b4c774e 100644 --- a/src/app/ui/duration/input-duration.directive.ts +++ b/src/app/ui/duration/input-duration.directive.ts @@ -6,7 +6,7 @@ import { forwardRef, HostListener, Input, - Renderer2 + Renderer2, } from '@angular/core'; import { AbstractControl, @@ -21,19 +21,18 @@ import { import { StringToMsPipe } from './string-to-ms.pipe'; import { MsToStringPipe } from './ms-to-string.pipe'; -const noop = () => { -}; +const noop = () => {}; /* eslint-disable */ export const INPUT_DURATION_VALUE_ACCESSOR: any = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => InputDurationDirective), - multi: true + multi: true, }; export const INPUT_DURATION_VALIDATORS: any = { provide: NG_VALIDATORS, useExisting: forwardRef(() => InputDurationDirective), - multi: true + multi: true, }; /* eslint-enable */ @@ -48,8 +47,8 @@ export const INPUT_DURATION_VALIDATORS: any = { INPUT_DURATION_VALIDATORS, ], }) - -export class InputDurationDirective implements ControlValueAccessor, Validator, AfterViewChecked { +export class InputDurationDirective + implements ControlValueAccessor, Validator, AfterViewChecked { @Input() isAllowSeconds: boolean = false; // by the Control Value Accessor @@ -70,8 +69,8 @@ export class InputDurationDirective implements ControlValueAccessor, Validato private _elementRef: ElementRef, private _stringToMs: StringToMsPipe, private _msToString: MsToStringPipe, - private _renderer: Renderer2) { - } + private _renderer: Renderer2, + ) {} private _value: string | undefined; @@ -124,7 +123,7 @@ export class InputDurationDirective implements ControlValueAccessor, Validato // ControlValueAccessor: Validator validate(c: AbstractControl): ValidationErrors | null { - return (this._validator !== null && this._validator !== undefined) + return this._validator !== null && this._validator !== undefined ? this._validator(c) : null; } @@ -146,6 +145,6 @@ export class InputDurationDirective implements ControlValueAccessor, Validato } return this._value ? null - : {inputDurationParse: {text: this._elementRef.nativeElement.value}}; + : { inputDurationParse: { text: this._elementRef.nativeElement.value } }; } } diff --git a/src/app/ui/duration/ms-to-clock-string.pipe.ts b/src/app/ui/duration/ms-to-clock-string.pipe.ts index a3f0a4423..ca3a4372b 100644 --- a/src/app/ui/duration/ms-to-clock-string.pipe.ts +++ b/src/app/ui/duration/ms-to-clock-string.pipe.ts @@ -1,18 +1,24 @@ import { Pipe, PipeTransform } from '@angular/core'; import * as moment from 'moment'; -export const msToClockString = (value: any, showSeconds?: boolean, isHideEmptyPlaceholder?: boolean): string => { +export const msToClockString = ( + value: any, + showSeconds?: boolean, + isHideEmptyPlaceholder?: boolean, +): string => { const md = moment.duration(value); let hours = 0; if (+md.days() > 0) { - hours = (md.days() * 24); + hours = md.days() * 24; } if (+md.hours() > 0) { hours += md.hours(); } - const parsed = hours + ':' - + ('00' + +md.minutes()).slice(-2) - + (showSeconds ? ('00' + +md.seconds()).slice(-2) : ''); + const parsed = + hours + + ':' + + ('00' + +md.minutes()).slice(-2) + + (showSeconds ? ('00' + +md.seconds()).slice(-2) : ''); if (!isHideEmptyPlaceholder && parsed.trim() === '0:00') { return '-'; @@ -22,9 +28,8 @@ export const msToClockString = (value: any, showSeconds?: boolean, isHideEmptyPl }; @Pipe({ - name: 'msToClockString' + name: 'msToClockString', }) export class MsToClockStringPipe implements PipeTransform { transform: (value: any, ...args: any[]) => any = msToClockString; } - diff --git a/src/app/ui/duration/ms-to-minute-clock-string.pipe.ts b/src/app/ui/duration/ms-to-minute-clock-string.pipe.ts index ddb5932b1..cd8f70668 100644 --- a/src/app/ui/duration/ms-to-minute-clock-string.pipe.ts +++ b/src/app/ui/duration/ms-to-minute-clock-string.pipe.ts @@ -6,7 +6,7 @@ export const msToMinuteClockString = (value: any): string => { let hours = 0; let minutes = 0; if (+md.days() > 0) { - hours = (md.days() * 24); + hours = md.days() * 24; } if (+md.hours() > 0) { hours += md.hours(); @@ -17,16 +17,14 @@ export const msToMinuteClockString = (value: any): string => { minutes += +md.minutes(); } - const parsed = minutes + ':' - + ('00' + +md.seconds()).slice(-2); + const parsed = minutes + ':' + ('00' + +md.seconds()).slice(-2); return parsed.trim(); }; @Pipe({ - name: 'msToMinuteClockString' + name: 'msToMinuteClockString', }) export class MsToMinuteClockStringPipe implements PipeTransform { transform: (value: any, ...args: any[]) => any = msToMinuteClockString; } - diff --git a/src/app/ui/duration/ms-to-string$.pipe.ts b/src/app/ui/duration/ms-to-string$.pipe.ts index 48fb74ee3..c6ae1ac92 100644 --- a/src/app/ui/duration/ms-to-string$.pipe.ts +++ b/src/app/ui/duration/ms-to-string$.pipe.ts @@ -4,15 +4,16 @@ import { map } from 'rxjs/operators'; import { msToString } from './ms-to-string.pipe'; @Pipe({ - name: 'msToString$' + name: 'msToString$', }) export class MsToStringPipe$ implements PipeTransform { - transform(value$: Observable | undefined, showSeconds?: boolean): any { if (value$) { - value$.pipe(map(value => { - return msToString(value, showSeconds); - })); + value$.pipe( + map((value) => { + return msToString(value, showSeconds); + }), + ); } } } diff --git a/src/app/ui/duration/ms-to-string.pipe.ts b/src/app/ui/duration/ms-to-string.pipe.ts index 5c4a11094..eb96737ac 100644 --- a/src/app/ui/duration/ms-to-string.pipe.ts +++ b/src/app/ui/duration/ms-to-string.pipe.ts @@ -4,16 +4,20 @@ const S = 1000; const M = S * 60; const H = M * 60; -export const msToString = (value: any, isShowSeconds?: boolean, isHideEmptyPlaceholder?: boolean): string => { +export const msToString = ( + value: any, + isShowSeconds?: boolean, + isHideEmptyPlaceholder?: boolean, +): string => { const hours = Math.floor(value / H); const minutes = Math.floor((value - hours * H) / M); - const seconds = isShowSeconds ? Math.floor((value - (hours * H) - (minutes * M)) / S) : 0; + const seconds = isShowSeconds ? Math.floor((value - hours * H - minutes * M) / S) : 0; const parsed = // ((+md.days() > 0) ? (md.days() + 'd ') : '') - ((hours > 0) ? (hours + 'h ') : '') - + ((minutes > 0) ? (minutes + 'm ') : '') - + (isShowSeconds && (seconds > 0) ? (seconds + 's ') : ''); + (hours > 0 ? hours + 'h ' : '') + + (minutes > 0 ? minutes + 'm ' : '') + + (isShowSeconds && seconds > 0 ? seconds + 's ' : ''); if (!isHideEmptyPlaceholder && parsed.trim() === '') { return '-'; @@ -23,9 +27,12 @@ export const msToString = (value: any, isShowSeconds?: boolean, isHideEmptyPlace }; @Pipe({ - name: 'msToString' + name: 'msToString', }) export class MsToStringPipe implements PipeTransform { - transform: (value: any, isShowSeconds?: boolean, isHideEmptyPlaceholder?: boolean) => string = msToString; + transform: ( + value: any, + isShowSeconds?: boolean, + isHideEmptyPlaceholder?: boolean, + ) => string = msToString; } - diff --git a/src/app/ui/duration/string-to-ms.pipe.ts b/src/app/ui/duration/string-to-ms.pipe.ts index a2cf6285e..f55fa9d12 100644 --- a/src/app/ui/duration/string-to-ms.pipe.ts +++ b/src/app/ui/duration/string-to-ms.pipe.ts @@ -32,23 +32,25 @@ export const stringToMs = (strValue: string, args?: any): number => { } }); - if (typeof s === 'number' || typeof m === 'number' || typeof h === 'number' || typeof d === 'number') { - s = (typeof s === 'number' && !isNaN(s)) ? s : 0; - m = (typeof m === 'number' && !isNaN(m)) ? m : 0; - h = (typeof h === 'number' && !isNaN(h)) ? h : 0; - d = (typeof d === 'number' && !isNaN(d)) ? d : 0; + if ( + typeof s === 'number' || + typeof m === 'number' || + typeof h === 'number' || + typeof d === 'number' + ) { + s = typeof s === 'number' && !isNaN(s) ? s : 0; + m = typeof m === 'number' && !isNaN(m) ? m : 0; + h = typeof h === 'number' && !isNaN(h) ? h : 0; + d = typeof d === 'number' && !isNaN(d) ? d : 0; - return +(s * 1000) - + (m * 1000 * 60) - + (h * 1000 * 60 * 60) - + (d * 1000 * 60 * 60 * 24); + return +(s * 1000) + m * 1000 * 60 + h * 1000 * 60 * 60 + d * 1000 * 60 * 60 * 24; } else { return 0; } }; @Pipe({ - name: 'stringToMs' + name: 'stringToMs', }) export class StringToMsPipe implements PipeTransform { transform: (value: any, ...args: any[]) => any = stringToMs; diff --git a/src/app/ui/edit-on-click/content-editable-on-click.directive.ts b/src/app/ui/edit-on-click/content-editable-on-click.directive.ts index 6dc912c89..bb64a71ed 100644 --- a/src/app/ui/edit-on-click/content-editable-on-click.directive.ts +++ b/src/app/ui/edit-on-click/content-editable-on-click.directive.ts @@ -1,4 +1,4 @@ -import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output, } from '@angular/core'; // HELPER // ----------------------------------- @@ -8,7 +8,12 @@ import { Directive, ElementRef, EventEmitter, Input, OnDestroy, OnInit, Output } }) export class ContentEditableOnClickDirective implements OnInit, OnDestroy { @Input() isResetAfterEdit: boolean = false; - @Output() editFinished: EventEmitter<{ isChanged: boolean; newVal: string; $taskEl: HTMLElement | null; event: Event }> = new EventEmitter(); + @Output() editFinished: EventEmitter<{ + isChanged: boolean; + newVal: string; + $taskEl: HTMLElement | null; + event: Event; + }> = new EventEmitter(); private _lastDomValue: string | undefined; private _lastOutsideVal: string | undefined; private readonly _el: HTMLElement; @@ -30,7 +35,7 @@ export class ContentEditableOnClickDirective implements OnInit, OnDestroy { ngOnInit() { const el = this._el; - if ((el.getAttribute('contenteditable')) === null) { + if (el.getAttribute('contenteditable') === null) { el.setAttribute('contenteditable', 'true'); } @@ -85,7 +90,7 @@ export class ContentEditableOnClickDirective implements OnInit, OnDestroy { }); el.onpaste = (ev: ClipboardEvent) => { - const data = (ev.clipboardData !== null) && ev.clipboardData.getData('text/plain'); + const data = ev.clipboardData !== null && ev.clipboardData.getData('text/plain'); if (data && data.length) { ev.stopPropagation(); ev.preventDefault(); @@ -112,7 +117,7 @@ export class ContentEditableOnClickDirective implements OnInit, OnDestroy { } const curVal = this._el.innerText; - const isChanged = (this._lastDomValue !== curVal); + const isChanged = this._lastDomValue !== curVal; this._lastDomValue = curVal; this.editFinished.emit({ @@ -160,9 +165,10 @@ export class ContentEditableOnClickDirective implements OnInit, OnDestroy { } private _removeTags(str: string) { - return str.replace(/<\/?[^`]+?\/?>/gmi, '\n') // replace all tags - .replace(/\n/gmi, '') // replace line breaks - .replace(/ /gmi, '') // replace line breaks + return str + .replace(/<\/?[^`]+?\/?>/gim, '\n') // replace all tags + .replace(/\n/gim, '') // replace line breaks + .replace(/ /gim, '') // replace line breaks .replace(' ', '') // replace line breaks again because sometimes it doesn't work .trim(); } diff --git a/src/app/ui/enlarge-img/enlarge-img.directive.ts b/src/app/ui/enlarge-img/enlarge-img.directive.ts index dec635f19..19ae6e44d 100644 --- a/src/app/ui/enlarge-img/enlarge-img.directive.ts +++ b/src/app/ui/enlarge-img/enlarge-img.directive.ts @@ -4,7 +4,7 @@ import { getCoords } from './get-coords'; const LARGE_IMG_ID = 'enlarged-img'; @Directive({ - selector: '[enlargeImg]' + selector: '[enlargeImg]', }) export class EnlargeImgDirective { imageEl: HTMLElement; @@ -17,15 +17,12 @@ export class EnlargeImgDirective { @Input() enlargeImg?: string; - constructor( - private _renderer: Renderer2, - private _el: ElementRef - ) { + constructor(private _renderer: Renderer2, private _el: ElementRef) { this.imageEl = this._el.nativeElement; } @HostListener('click', ['$event']) onClick() { - this.isImg = (this.imageEl.tagName.toLowerCase() === 'img'); + this.isImg = this.imageEl.tagName.toLowerCase() === 'img'; if (this.isImg || this.enlargeImg) { this._showImg(); @@ -57,11 +54,15 @@ export class EnlargeImgDirective { const startLeft = origImgCoords.left - newImageCoords.left; const startTop = origImgCoords.top - newImageCoords.top; - this._renderer.setStyle(this.newImageEl, 'transform', `translate3d(${startLeft}px, ${startTop}px, 0) scale(${scale})`); + this._renderer.setStyle( + this.newImageEl, + 'transform', + `translate3d(${startLeft}px, ${startTop}px, 0) scale(${scale})`, + ); } private _showImg() { - const src = this.enlargeImg || this.imageEl.getAttribute('src') as string; + const src = this.enlargeImg || (this.imageEl.getAttribute('src') as string); const img = new Image(); img.src = src; @@ -69,15 +70,23 @@ export class EnlargeImgDirective { this._setOriginCoordsForImageAni(); this._waitForImgRender().then(() => { this._renderer.addClass(this.enlargedImgWrapperEl, 'ani-enter'); - this._renderer.setStyle(this.newImageEl, 'transform', `translate3d(0, 0, 0) scale(1)`); + this._renderer.setStyle( + this.newImageEl, + 'transform', + `translate3d(0, 0, 0) scale(1)`, + ); }); }; img.onerror = () => { this._hideImg(); }; - this.enlargedImgWrapperEl = this._htmlToElement(`
`); - this.newImageEl = this._htmlToElement(``); + this.enlargedImgWrapperEl = this._htmlToElement( + `
`, + ); + this.newImageEl = this._htmlToElement( + ``, + ); this._renderer.appendChild(this.enlargedImgWrapperEl, this.newImageEl); this._renderer.appendChild(this.lightboxParentEl, this.enlargedImgWrapperEl); this.zoomMode = 0; @@ -115,7 +124,11 @@ export class EnlargeImgDirective { throw new Error(); } this._renderer.addClass(this.enlargedImgWrapperEl, 'isZoomed'); - this._renderer.setStyle((this.newImageEl as HTMLElement), 'transform', `scale(2) translate3d(-25%, -25%, 0)`); + this._renderer.setStyle( + this.newImageEl as HTMLElement, + 'transform', + `scale(2) translate3d(-25%, -25%, 0)`, + ); this.zoomMoveHandler = this._zoom.bind(this); this.enlargedImgWrapperEl.addEventListener('mousemove', this.zoomMoveHandler as any); } @@ -126,7 +139,11 @@ export class EnlargeImgDirective { } this.enlargedImgWrapperEl.removeEventListener('mousemove', this.zoomMoveHandler); this._renderer.removeClass(this.enlargedImgWrapperEl, 'isZoomed'); - this._renderer.setStyle((this.newImageEl as HTMLElement), 'transform', `scale(1) translate3d(0, 0, 0)`); + this._renderer.setStyle( + this.newImageEl as HTMLElement, + 'transform', + `scale(1) translate3d(0, 0, 0)`, + ); } private _htmlToElement(html: string): HTMLElement { @@ -145,14 +162,18 @@ export class EnlargeImgDirective { const zoomer = this.enlargedImgWrapperEl; const extra = 1.1; const magicSpace = 5; - const x = (offsetX / zoomer.offsetWidth * 100 - magicSpace) * -0.5 * extra; - const y = (offsetY / zoomer.offsetHeight * 100 - magicSpace) * -0.5 * extra; - this._renderer.setStyle((this.newImageEl as HTMLElement), 'transform', `scale(2) translate3d(${x}%, ${y}%, 0)`); + const x = ((offsetX / zoomer.offsetWidth) * 100 - magicSpace) * -0.5 * extra; + const y = ((offsetY / zoomer.offsetHeight) * 100 - magicSpace) * -0.5 * extra; + this._renderer.setStyle( + this.newImageEl as HTMLElement, + 'transform', + `scale(2) translate3d(${x}%, ${y}%, 0)`, + ); } private _waitForImgRender() { function rafAsync() { - return new Promise(resolve => { + return new Promise((resolve) => { requestAnimationFrame(resolve); }); } diff --git a/src/app/ui/enlarge-img/get-coords.ts b/src/app/ui/enlarge-img/get-coords.ts index d3324160c..72dd75bd5 100644 --- a/src/app/ui/enlarge-img/get-coords.ts +++ b/src/app/ui/enlarge-img/get-coords.ts @@ -1,4 +1,5 @@ -export const getCoords = (elem: HTMLElement) => { // crossbrowser version +export const getCoords = (elem: HTMLElement) => { + // crossbrowser version const box = elem.getBoundingClientRect(); const body = document.body; @@ -13,5 +14,5 @@ export const getCoords = (elem: HTMLElement) => { // crossbrowser version const top = box.top + scrollTop - clientTop; const left = box.left + scrollLeft - clientLeft; - return {top: Math.round(top), left: Math.round(left)}; + return { top: Math.round(top), left: Math.round(left) }; }; diff --git a/src/app/ui/formly-translate-extension/formly-translate-extension.ts b/src/app/ui/formly-translate-extension/formly-translate-extension.ts index dc9b72567..5209591e0 100644 --- a/src/app/ui/formly-translate-extension/formly-translate-extension.ts +++ b/src/app/ui/formly-translate-extension/formly-translate-extension.ts @@ -4,28 +4,25 @@ import { map } from 'rxjs/operators'; import { T } from '../../t.const'; export class TranslateExtension { - constructor(private translate: TranslateService) { - } + constructor(private translate: TranslateService) {} prePopulate(field: FormlyFieldConfig) { const to = field.templateOptions || {}; if (Array.isArray(to.options)) { const options = to.options; - to.options = this.translate.stream(options.map(o => o.label)).pipe( - map(labels => options.map(o => ({...o, label: labels[o.label]}))) - ); + to.options = this.translate + .stream(options.map((o) => o.label)) + .pipe(map((labels) => options.map((o) => ({ ...o, label: labels[o.label] })))); } field.expressionProperties = { ...(field.expressionProperties || {}), - ...(to.label - ? {'templateOptions.label': this.translate.stream(to.label)} - : {}), + ...(to.label ? { 'templateOptions.label': this.translate.stream(to.label) } : {}), ...(to.description - ? {'templateOptions.description': this.translate.stream(to.description)} + ? { 'templateOptions.description': this.translate.stream(to.description) } : {}), ...(to.placeholder - ? {'templateOptions.placeholder': this.translate.stream(to.placeholder)} + ? { 'templateOptions.placeholder': this.translate.stream(to.placeholder) } : {}), }; } @@ -33,30 +30,42 @@ export class TranslateExtension { export function registerTranslateExtension(translate: TranslateService): ConfigOption { return { - extensions: [{ - name: 'translate', - extension: new TranslateExtension(translate) - }], + extensions: [ + { + name: 'translate', + extension: new TranslateExtension(translate), + }, + ], validationMessages: [ - {name: 'required', message: () => translate.stream(T.V.E_REQUIRED)}, + { name: 'required', message: () => translate.stream(T.V.E_REQUIRED) }, { name: 'minlength', message: (err, field: FormlyFieldConfig) => - translate.stream(T.V.E_MIN_LENGTH, {val: field.templateOptions ? field.templateOptions.minLength : null}) + translate.stream(T.V.E_MIN_LENGTH, { + val: field.templateOptions ? field.templateOptions.minLength : null, + }), }, { name: 'maxlength', message: (err, field: FormlyFieldConfig) => - translate.stream(T.V.E_MAX_LENGTH, {val: field.templateOptions ? field.templateOptions.maxLength : null}) + translate.stream(T.V.E_MAX_LENGTH, { + val: field.templateOptions ? field.templateOptions.maxLength : null, + }), }, { name: 'min', - message: (err, field) => translate.stream(T.V.E_MIN, {val: field.templateOptions ? field.templateOptions.min : null}) + message: (err, field) => + translate.stream(T.V.E_MIN, { + val: field.templateOptions ? field.templateOptions.min : null, + }), }, { name: 'max', - message: (err, field) => translate.stream(T.V.E_MAX, {val: field.templateOptions ? field.templateOptions.max : null}) + message: (err, field) => + translate.stream(T.V.E_MAX, { + val: field.templateOptions ? field.templateOptions.max : null, + }), }, - ] + ], }; } diff --git a/src/app/ui/formly-translated-template/formly-translated-template.component.ts b/src/app/ui/formly-translated-template/formly-translated-template.component.ts index db1e4b737..2d74e3168 100644 --- a/src/app/ui/formly-translated-template/formly-translated-template.component.ts +++ b/src/app/ui/formly-translated-template/formly-translated-template.component.ts @@ -1,4 +1,11 @@ -import { ChangeDetectionStrategy, Component, ElementRef, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + OnDestroy, + OnInit, + ViewChild, +} from '@angular/core'; import { FieldType } from '@ngx-formly/core'; import { Subscription } from 'rxjs'; import { TranslateService } from '@ngx-translate/core'; @@ -7,18 +14,17 @@ import { TranslateService } from '@ngx-translate/core'; selector: 'formly-translated-template', templateUrl: './formly-translated-template.component.html', styleUrls: ['./formly-translated-template.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) -export class FormlyTranslatedTemplateComponent extends FieldType implements OnInit, OnDestroy { - - @ViewChild('tplWrapper', {static: true}) tplWrapper?: ElementRef; +export class FormlyTranslatedTemplateComponent + extends FieldType + implements OnInit, OnDestroy { + @ViewChild('tplWrapper', { static: true }) tplWrapper?: ElementRef; private _el?: HTMLElement; private _subs: Subscription = new Subscription(); - constructor( - private _translateService: TranslateService, - ) { + constructor(private _translateService: TranslateService) { super(); } @@ -34,9 +40,11 @@ export class FormlyTranslatedTemplateComponent extends FieldType implements OnIn return; } - this._subs.add(this._translateService.stream(translationId).subscribe((translationString) => { - (this._el as HTMLElement).innerHTML = translationString; - })); + this._subs.add( + this._translateService.stream(translationId).subscribe((translationString) => { + (this._el as HTMLElement).innerHTML = translationString; + }), + ); } ngOnDestroy(): void { diff --git a/src/app/ui/help-section/help-section.component.ts b/src/app/ui/help-section/help-section.component.ts index 7df54dbe0..da163c680 100644 --- a/src/app/ui/help-section/help-section.component.ts +++ b/src/app/ui/help-section/help-section.component.ts @@ -6,12 +6,10 @@ import { expandFadeAnimation } from '../animations/expand.ani'; templateUrl: './help-section.component.html', styleUrls: ['./help-section.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [expandFadeAnimation] + animations: [expandFadeAnimation], }) export class HelpSectionComponent { @Input() isShowHelp: boolean = false; - constructor() { - } - + constructor() {} } diff --git a/src/app/ui/inline-input/inline-input.component.ts b/src/app/ui/inline-input/inline-input.component.ts index 21006d5e6..2b908f196 100644 --- a/src/app/ui/inline-input/inline-input.component.ts +++ b/src/app/ui/inline-input/inline-input.component.ts @@ -7,14 +7,14 @@ import { HostBinding, Input, Output, - ViewChild + ViewChild, } from '@angular/core'; @Component({ selector: 'inline-input', templateUrl: './inline-input.component.html', styleUrls: ['./inline-input.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class InlineInputComponent implements AfterViewInit { @Input() type: 'text' | 'duration' = 'text'; @@ -30,19 +30,20 @@ export class InlineInputComponent implements AfterViewInit { activeInputEl?: HTMLInputElement; - constructor() { - } + constructor() {} ngAfterViewInit() { - this.activeInputEl = (this.type === 'duration') - ? (this.inputElDuration as ElementRef).nativeElement - : (this.inputEl as ElementRef).nativeElement; + this.activeInputEl = + this.type === 'duration' + ? (this.inputElDuration as ElementRef).nativeElement + : (this.inputEl as ElementRef).nativeElement; } focusInput() { - this.activeInputEl = (this.type === 'duration') - ? (this.inputElDuration as ElementRef).nativeElement - : (this.inputEl as ElementRef).nativeElement; + this.activeInputEl = + this.type === 'duration' + ? (this.inputElDuration as ElementRef).nativeElement + : (this.inputEl as ElementRef).nativeElement; this.isFocused = true; (this.activeInputEl as HTMLElement).focus(); @@ -55,7 +56,10 @@ export class InlineInputComponent implements AfterViewInit { blur() { this.isFocused = false; - if ((this.newValue || this.newValue === '' || this.newValue === 0) && this.newValue !== this.value) { + if ( + (this.newValue || this.newValue === '' || this.newValue === 0) && + this.newValue !== this.value + ) { this.changed.emit(this.newValue); } } diff --git a/src/app/ui/inline-markdown/inline-markdown.component.ts b/src/app/ui/inline-markdown/inline-markdown.component.ts index 04dead701..b9f3e5726 100644 --- a/src/app/ui/inline-markdown/inline-markdown.component.ts +++ b/src/app/ui/inline-markdown/inline-markdown.component.ts @@ -9,7 +9,7 @@ import { OnDestroy, OnInit, Output, - ViewChild + ViewChild, } from '@angular/core'; import { fadeAnimation } from '../animations/fade.ani'; import { MarkdownComponent } from 'ngx-markdown'; @@ -29,7 +29,7 @@ const HIDE_OVERFLOW_TIMEOUT_DURATION = 300; templateUrl: './inline-markdown.component.html', styleUrls: ['./inline-markdown.component.scss'], changeDetection: ChangeDetectionStrategy.OnPush, - animations: [fadeAnimation] + animations: [fadeAnimation], }) export class InlineMarkdownComponent implements OnInit, OnDestroy { @Input() isLock: boolean = false; @@ -39,7 +39,7 @@ export class InlineMarkdownComponent implements OnInit, OnDestroy { @Output() focused: EventEmitter = new EventEmitter(); @Output() blurred: EventEmitter = new EventEmitter(); @Output() keyboardUnToggle: EventEmitter = new EventEmitter(); - @ViewChild('wrapperEl', {static: true}) wrapperEl: ElementRef | undefined; + @ViewChild('wrapperEl', { static: true }) wrapperEl: ElementRef | undefined; @ViewChild('textareaEl') textareaEl: ElementRef | undefined; @ViewChild('previewEl') previewEl: MarkdownComponent | undefined; @@ -48,7 +48,7 @@ export class InlineMarkdownComponent implements OnInit, OnDestroy { modelCopy: string | undefined; isTurnOffMarkdownParsing$: Observable = this._globalConfigService.misc$.pipe( - map(cfg => cfg.isTurnOffMarkdown), + map((cfg) => cfg.isTurnOffMarkdown), startWith(false), ); private _hideOverFlowTimeout: number | undefined; @@ -109,7 +109,7 @@ export class InlineMarkdownComponent implements OnInit, OnDestroy { keypressHandler(ev: KeyboardEvent) { this.resizeTextareaToFit(); - if (ev.key === 'Enter' && ev.ctrlKey || ev.code === 'Escape') { + if ((ev.key === 'Enter' && ev.ctrlKey) || ev.code === 'Escape') { this.untoggleShowEdit(); this.keyboardUnToggle.emit(ev); } @@ -157,24 +157,29 @@ export class InlineMarkdownComponent implements OnInit, OnDestroy { throw new Error('Wrapper el not visible'); } this.textareaEl.nativeElement.style.height = 'auto'; - this.textareaEl.nativeElement.style.height = this.textareaEl.nativeElement.scrollHeight + 'px'; - this.wrapperEl.nativeElement.style.height = this.textareaEl.nativeElement.offsetHeight + 'px'; + this.textareaEl.nativeElement.style.height = + this.textareaEl.nativeElement.scrollHeight + 'px'; + this.wrapperEl.nativeElement.style.height = + this.textareaEl.nativeElement.offsetHeight + 'px'; } openFullScreen() { - this._matDialog.open(DialogFullscreenMarkdownComponent, { - minWidth: '100vw', - height: '100vh', - restoreFocus: true, - data: { - content: this.modelCopy - } - }).afterClosed().subscribe((res) => { - if (typeof res === 'string') { - this.modelCopy = res; - this.changed.emit(res); - } - }); + this._matDialog + .open(DialogFullscreenMarkdownComponent, { + minWidth: '100vw', + height: '100vh', + restoreFocus: true, + data: { + content: this.modelCopy, + }, + }) + .afterClosed() + .subscribe((res) => { + if (typeof res === 'string') { + this.modelCopy = res; + this.changed.emit(res); + } + }); } resizeParsedToFit() { @@ -192,7 +197,8 @@ export class InlineMarkdownComponent implements OnInit, OnDestroy { } this.previewEl.element.nativeElement.style.height = 'auto'; // NOTE: somehow this pixel seem to help - this.wrapperEl.nativeElement.style.height = this.previewEl.element.nativeElement.offsetHeight + 'px'; + this.wrapperEl.nativeElement.style.height = + this.previewEl.element.nativeElement.offsetHeight + 'px'; this.previewEl.element.nativeElement.style.height = ''; }); } diff --git a/src/app/ui/longpress/longpress.component.ts b/src/app/ui/longpress/longpress.component.ts index c9b458afb..a8ba6788b 100644 --- a/src/app/ui/longpress/longpress.component.ts +++ b/src/app/ui/longpress/longpress.component.ts @@ -1,7 +1,14 @@ -import { Directive, EventEmitter, HostListener, Input, OnDestroy, Output } from '@angular/core'; +import { + Directive, + EventEmitter, + HostListener, + Input, + OnDestroy, + Output, +} from '@angular/core'; @Directive({ - selector: '[longPress]' + selector: '[longPress]', }) export class LongPressDirective implements OnDestroy { @Input() longPressDuration: number = 400; diff --git a/src/app/ui/material-icons.const.ts b/src/app/ui/material-icons.const.ts index d80dbbc37..62f4a481a 100644 --- a/src/app/ui/material-icons.const.ts +++ b/src/app/ui/material-icons.const.ts @@ -971,5 +971,5 @@ export const MATERIAL_ICONS: string[] = [ 'youtube_searched_for', 'zoom_in', 'zoom_out', - 'zoom_out_map' + 'zoom_out_map', ]; diff --git a/src/app/ui/owl-wrapper/owl-wrapper.component.ts b/src/app/ui/owl-wrapper/owl-wrapper.component.ts index 80e40ea61..147d21c9a 100644 --- a/src/app/ui/owl-wrapper/owl-wrapper.component.ts +++ b/src/app/ui/owl-wrapper/owl-wrapper.component.ts @@ -1,4 +1,10 @@ -import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; import { Observable } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; import { GlobalConfigService } from 'src/app/features/config/global-config.service'; @@ -8,7 +14,7 @@ import { T } from 'src/app/t.const'; selector: 'owl-wrapper', templateUrl: './owl-wrapper.component.html', styleUrls: ['./owl-wrapper.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class OwlWrapperComponent { @Input() now: Date = new Date(); @@ -37,12 +43,11 @@ export class OwlWrapperComponent { ]; firstDayOfWeek$: Observable = this._globalConfigService.misc$.pipe( - map(cfg => cfg.firstDayOfWeek), + map((cfg) => cfg.firstDayOfWeek), startWith(0), ); - constructor(private _globalConfigService: GlobalConfigService) { - } + constructor(private _globalConfigService: GlobalConfigService) {} @Input('dateTime') set dateTimeSet(v: number) { diff --git a/src/app/ui/pipes/humanize-timestamp.pipe.ts b/src/app/ui/pipes/humanize-timestamp.pipe.ts index f2e17ad36..4b927d22d 100644 --- a/src/app/ui/pipes/humanize-timestamp.pipe.ts +++ b/src/app/ui/pipes/humanize-timestamp.pipe.ts @@ -2,7 +2,7 @@ import { Pipe, PipeTransform } from '@angular/core'; import * as moment from 'moment'; @Pipe({ - name: 'humanizeTimestamp' + name: 'humanizeTimestamp', }) export class HumanizeTimestampPipe implements PipeTransform { transform(value: any): any { diff --git a/src/app/ui/pipes/jira-to-markdown.pipe.ts b/src/app/ui/pipes/jira-to-markdown.pipe.ts index 2dbee82c3..f2f3b3aaa 100644 --- a/src/app/ui/pipes/jira-to-markdown.pipe.ts +++ b/src/app/ui/pipes/jira-to-markdown.pipe.ts @@ -3,13 +3,10 @@ import { Pipe, PipeTransform } from '@angular/core'; import * as j2m from 'jira2md'; @Pipe({ - name: 'jiraToMarkdown' + name: 'jiraToMarkdown', }) export class JiraToMarkdownPipe implements PipeTransform { - transform(value: string): string { - return (value) - ? j2m.to_markdown(value) - : value; + return value ? j2m.to_markdown(value) : value; } } diff --git a/src/app/ui/pipes/keys.pipe.ts b/src/app/ui/pipes/keys.pipe.ts index 8bc48082f..e1ae1f2b2 100644 --- a/src/app/ui/pipes/keys.pipe.ts +++ b/src/app/ui/pipes/keys.pipe.ts @@ -5,7 +5,6 @@ import { Pipe, PipeTransform } from '@angular/core'; pure: false, }) export class KeysPipe implements PipeTransform { - transform(value: any, sort: any, filterOutKeys?: any): any { if (value === Object(value)) { const keys = Object.keys(value); @@ -19,7 +18,6 @@ export class KeysPipe implements PipeTransform { if (sort) { keys.sort(); - } if (sort === 'reverse') { diff --git a/src/app/ui/pipes/moment-format.pipe.ts b/src/app/ui/pipes/moment-format.pipe.ts index e3c0ce6b5..fb77200a3 100644 --- a/src/app/ui/pipes/moment-format.pipe.ts +++ b/src/app/ui/pipes/moment-format.pipe.ts @@ -2,10 +2,9 @@ import { Pipe, PipeTransform } from '@angular/core'; import * as moment from 'moment'; @Pipe({ - name: 'momentFormat' + name: 'momentFormat', }) export class MomentFormatPipe implements PipeTransform { - transform(value: any, args: any): any { if (value && args) { return moment(value).format(args); diff --git a/src/app/ui/pipes/number-to-month.pipe.ts b/src/app/ui/pipes/number-to-month.pipe.ts index 0753e5d3b..71d65d485 100644 --- a/src/app/ui/pipes/number-to-month.pipe.ts +++ b/src/app/ui/pipes/number-to-month.pipe.ts @@ -12,11 +12,11 @@ const MAP = [ 'September', 'October', 'November', - 'December' + 'December', ]; @Pipe({ - name: 'numberToMonth' + name: 'numberToMonth', }) export class NumberToMonthPipe implements PipeTransform { transform(value: any, args?: any): any { diff --git a/src/app/ui/pipes/sort.pipe.ts b/src/app/ui/pipes/sort.pipe.ts index e93289d33..3c749e586 100644 --- a/src/app/ui/pipes/sort.pipe.ts +++ b/src/app/ui/pipes/sort.pipe.ts @@ -1,7 +1,7 @@ import { Pipe, PipeTransform } from '@angular/core'; @Pipe({ - name: 'sort' + name: 'sort', }) export class SortPipe implements PipeTransform { transform(array: any[], field: string, reverse: boolean = false): any[] { diff --git a/src/app/ui/pipes/to-array.pipe.ts b/src/app/ui/pipes/to-array.pipe.ts index 25ee99eff..c3cb5e8cc 100644 --- a/src/app/ui/pipes/to-array.pipe.ts +++ b/src/app/ui/pipes/to-array.pipe.ts @@ -5,7 +5,6 @@ import { Pipe, PipeTransform } from '@angular/core'; pure: false, }) export class ToArrayPipe implements PipeTransform { - transform(obj: any, filterOutKeys?: any): any { if (obj === Object(obj)) { const keys = Object.keys(obj); @@ -20,7 +19,7 @@ export class ToArrayPipe implements PipeTransform { keys.forEach((key) => { newArray.push({ key, - value: obj[key] + value: obj[key], }); }); @@ -29,5 +28,4 @@ export class ToArrayPipe implements PipeTransform { return null; } - } diff --git a/src/app/ui/progress-bar/progress-bar.component.ts b/src/app/ui/progress-bar/progress-bar.component.ts index 57d355f70..ec25fe93b 100644 --- a/src/app/ui/progress-bar/progress-bar.component.ts +++ b/src/app/ui/progress-bar/progress-bar.component.ts @@ -1,16 +1,21 @@ -import { ChangeDetectionStrategy, Component, ElementRef, HostBinding, Input } from '@angular/core'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + HostBinding, + Input, +} from '@angular/core'; @Component({ selector: 'progress-bar', template: '', styleUrls: ['./progress-bar.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProgressBarComponent { @HostBinding('class') @Input() cssClass: string = 'bg-primary'; - constructor(private _elRef: ElementRef) { - } + constructor(private _elRef: ElementRef) {} @Input() set progress(_value: number) { let val; diff --git a/src/app/ui/simple-download/simple-download.directive.ts b/src/app/ui/simple-download/simple-download.directive.ts index 7b3d9ac4d..40d948901 100644 --- a/src/app/ui/simple-download/simple-download.directive.ts +++ b/src/app/ui/simple-download/simple-download.directive.ts @@ -2,14 +2,13 @@ import { Directive, ElementRef, HostListener, Input } from '@angular/core'; import { download } from '../../util/download'; @Directive({ - selector: '[simpleDownload]' + selector: '[simpleDownload]', }) export class SimpleDownloadDirective { @Input() simpleDownload?: string; @Input() simpleDownloadData?: string; - constructor(private _el: ElementRef) { - } + constructor(private _el: ElementRef) {} @HostListener('click') onClick() { if (!this._el.nativeElement.getAttribute('download')) { diff --git a/src/app/ui/theme-select/theme-select.component.ts b/src/app/ui/theme-select/theme-select.component.ts index c697dd005..216c9762b 100644 --- a/src/app/ui/theme-select/theme-select.component.ts +++ b/src/app/ui/theme-select/theme-select.component.ts @@ -6,7 +6,7 @@ import { T } from '../../t.const'; selector: 'theme-select', templateUrl: './theme-select.component.html', styleUrls: ['./theme-select.component.scss'], - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class ThemeSelectComponent { T: typeof T = T; diff --git a/src/app/ui/ui.module.ts b/src/app/ui/ui.module.ts index 822e47a66..c847008bf 100644 --- a/src/app/ui/ui.module.ts +++ b/src/app/ui/ui.module.ts @@ -11,7 +11,11 @@ import { MatButtonModule } from '@angular/material/button'; import { MatCardModule } from '@angular/material/card'; import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatChipsModule } from '@angular/material/chips'; -import { MatNativeDateModule, MatOptionModule, MatRippleModule } from '@angular/material/core'; +import { + MatNativeDateModule, + MatOptionModule, + MatRippleModule, +} from '@angular/material/core'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDialogModule } from '@angular/material/dialog'; import { MatExpansionModule } from '@angular/material/expansion'; @@ -57,7 +61,10 @@ import { MomentFormatPipe } from './pipes/moment-format.pipe'; import { InlineInputComponent } from './inline-input/inline-input.component'; import { ChipListInputComponent } from './chip-list-input/chip-list-input.component'; import { ValidationModule } from './validation/validation.module'; -import { OwlDateTimeModule, OwlNativeDateTimeModule } from 'ngx-date-time-picker-schedule'; +import { + OwlDateTimeModule, + OwlNativeDateTimeModule, +} from 'ngx-date-time-picker-schedule'; import { FullPageSpinnerComponent } from './full-page-spinner/full-page-spinner.component'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { FormlyMaterialModule } from '@ngx-formly/material'; @@ -163,17 +170,20 @@ const OTHER_3RD_PARTY_MODS_WITHOUT_CFG = [ FormsModule, ReactiveFormsModule, FormlyModule.forChild({ - types: [{ - name: 'duration', - component: InputDurationFormlyComponent, - extends: 'input', - wrappers: ['form-field'], - }, { - name: 'tpl', - component: FormlyTranslatedTemplateComponent, - }], + types: [ + { + name: 'duration', + component: InputDurationFormlyComponent, + extends: 'input', + wrappers: ['form-field'], + }, + { + name: 'tpl', + component: FormlyTranslatedTemplateComponent, + }, + ], extras: { - immutable: true + immutable: true, }, }), FormlyMatToggleModule, @@ -187,10 +197,7 @@ const OTHER_3RD_PARTY_MODS_WITHOUT_CFG = [ ValidationModule, BetterDrawerModule, ], - declarations: [ - ...COMPONENT_AND_PIPES, - OwlWrapperComponent, - ], + declarations: [...COMPONENT_AND_PIPES, OwlWrapperComponent], exports: [ ...COMPONENT_AND_PIPES, ...MAT_MODULES, @@ -205,8 +212,8 @@ const OTHER_3RD_PARTY_MODS_WITHOUT_CFG = [ OwlWrapperComponent, ], providers: [ - {provide: ErrorHandler, useClass: GlobalErrorHandler}, - {provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig}, + { provide: ErrorHandler, useClass: GlobalErrorHandler }, + { provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig }, { provide: FORMLY_CONFIG, multi: true, @@ -216,9 +223,7 @@ const OTHER_3RD_PARTY_MODS_WITHOUT_CFG = [ ], }) export class UiModule { - constructor( - private _markdownService: MarkdownService, - ) { + constructor(private _markdownService: MarkdownService) { const linkRenderer = this._markdownService.renderer.link; this._markdownService.renderer.link = (href, title, text) => { const html = linkRenderer.call(this._markdownService.renderer, href, title, text); diff --git a/src/app/ui/validation/max.directive.ts b/src/app/ui/validation/max.directive.ts index 5b13991aa..3beb38e12 100644 --- a/src/app/ui/validation/max.directive.ts +++ b/src/app/ui/validation/max.directive.ts @@ -1,4 +1,11 @@ -import { Directive, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + Directive, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular/forms'; import { maxValidator } from './max.validator'; @@ -6,18 +13,18 @@ import { maxValidator } from './max.validator'; const MAX_VALIDATOR: any = { provide: NG_VALIDATORS, useExisting: forwardRef(() => MaxDirective), - multi: true + multi: true, }; @Directive({ selector: '[max][formControlName],[max][formControl],[max][ngModel]', - providers: [MAX_VALIDATOR] + providers: [MAX_VALIDATOR], }) export class MaxDirective implements Validator, OnInit, OnChanges { @Input() max?: number; private _validator?: ValidatorFn; - private _onChange?: (() => void); + private _onChange?: () => void; ngOnInit() { if (typeof this.max === 'number') { @@ -36,7 +43,7 @@ export class MaxDirective implements Validator, OnInit, OnChanges { } } - validate(c: AbstractControl): ({ [key: string]: any }) | null { + validate(c: AbstractControl): { [key: string]: any } | null { if (this._validator) { return this._validator(c) as { [key: string]: any }; } diff --git a/src/app/ui/validation/max.validator.ts b/src/app/ui/validation/max.validator.ts index a2cc06722..54b97384f 100644 --- a/src/app/ui/validation/max.validator.ts +++ b/src/app/ui/validation/max.validator.ts @@ -7,8 +7,6 @@ export const maxValidator = (max: number): ValidatorFn => { } const v: number = +control.value; - return v <= +max - ? null - : {actualValue: v, requiredValue: +max, max: true}; + return v <= +max ? null : { actualValue: v, requiredValue: +max, max: true }; }; }; diff --git a/src/app/ui/validation/min.directive.ts b/src/app/ui/validation/min.directive.ts index 719d55080..11a9b0d91 100644 --- a/src/app/ui/validation/min.directive.ts +++ b/src/app/ui/validation/min.directive.ts @@ -1,4 +1,11 @@ -import { Directive, forwardRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { + Directive, + forwardRef, + Input, + OnChanges, + OnInit, + SimpleChanges, +} from '@angular/core'; import { AbstractControl, NG_VALIDATORS, Validator, ValidatorFn } from '@angular/forms'; import { minValidator } from './min.validator'; @@ -6,18 +13,18 @@ import { minValidator } from './min.validator'; const MIN_VALIDATOR: any = { provide: NG_VALIDATORS, useExisting: forwardRef(() => MinDirective), - multi: true + multi: true, }; @Directive({ selector: '[min][formControlName],[min][formControl],[min][ngModel]', - providers: [MIN_VALIDATOR] + providers: [MIN_VALIDATOR], }) export class MinDirective implements Validator, OnInit, OnChanges { @Input() min?: number; private _validator?: ValidatorFn; - private _onChange?: (() => void); + private _onChange?: () => void; ngOnInit() { if (typeof this.min === 'number') { @@ -36,7 +43,7 @@ export class MinDirective implements Validator, OnInit, OnChanges { } } - validate(c: AbstractControl): ({ [key: string]: any }) | null { + validate(c: AbstractControl): { [key: string]: any } | null { if (this._validator) { return this._validator(c) as { [key: string]: any }; } diff --git a/src/app/ui/validation/min.validator.ts b/src/app/ui/validation/min.validator.ts index 3b6b9c057..853003429 100644 --- a/src/app/ui/validation/min.validator.ts +++ b/src/app/ui/validation/min.validator.ts @@ -7,8 +7,6 @@ export const minValidator = (min: number): ValidatorFn => { } const v: number = +control.value; - return v >= +min - ? null - : {actualValue: v, requiredValue: +min, min: true}; + return v >= +min ? null : { actualValue: v, requiredValue: +min, min: true }; }; }; diff --git a/src/app/ui/validation/validation.module.ts b/src/app/ui/validation/validation.module.ts index e5a150a1a..631bcb419 100644 --- a/src/app/ui/validation/validation.module.ts +++ b/src/app/ui/validation/validation.module.ts @@ -4,17 +4,8 @@ import { MinDirective } from './min.directive'; import { MaxDirective } from './max.directive'; @NgModule({ - imports: [ - CommonModule, - ], - declarations: [ - MinDirective, - MaxDirective, - ], - exports: [ - MinDirective, - MaxDirective, - ] + imports: [CommonModule], + declarations: [MinDirective, MaxDirective], + exports: [MinDirective, MaxDirective], }) -export class ValidationModule { -} +export class ValidationModule {} diff --git a/src/app/util/action-logger.ts b/src/app/util/action-logger.ts index a164909dd..f7cdc4746 100644 --- a/src/app/util/action-logger.ts +++ b/src/app/util/action-logger.ts @@ -1,13 +1,14 @@ import { loadFromRealLs, saveToRealLs } from '../core/persistence/local-storage'; -import { LS_ACTION_BEFORE_LAST_ERROR_LOG, LS_ACTION_LOG } from '../core/persistence/ls-keys.const'; +import { + LS_ACTION_BEFORE_LAST_ERROR_LOG, + LS_ACTION_LOG, +} from '../core/persistence/ls-keys.const'; const NUMBER_OF_ACTIONS_TO_SAVE = 30; const getActionLog = (): string[] => { const current = loadFromRealLs(LS_ACTION_LOG); - return Array.isArray(current) - ? current - : []; + return Array.isArray(current) ? current : []; }; export const actionLogger = (action: any) => { @@ -32,7 +33,5 @@ export const saveBeforeLastErrorActionLog = () => { export const getBeforeLastErrorActionLog = (): string[] => { const current = loadFromRealLs(LS_ACTION_BEFORE_LAST_ERROR_LOG); - return Array.isArray(current) - ? current - : []; + return Array.isArray(current) ? current : []; }; diff --git a/src/app/util/app-data-mock.ts b/src/app/util/app-data-mock.ts index 1f2a73462..71a2caf21 100644 --- a/src/app/util/app-data-mock.ts +++ b/src/app/util/app-data-mock.ts @@ -6,7 +6,7 @@ import { createEmptyEntity } from './create-empty-entity'; export const createAppDataCompleteMock = (): AppDataComplete => ({ project: { ...createEmptyEntity(), - [MODEL_VERSION_KEY]: 5 + [MODEL_VERSION_KEY]: 5, }, archivedProjects: {}, globalConfig: DEFAULT_GLOBAL_CONFIG, @@ -23,7 +23,7 @@ export const createAppDataCompleteMock = (): AppDataComplete => ({ tag: createEmptyEntity(), simpleCounter: { ...createEmptyEntity(), - ids: [] + ids: [], }, taskArchive: createEmptyEntity(), taskRepeatCfg: createEmptyEntity(), diff --git a/src/app/util/array-move.ts b/src/app/util/array-move.ts index 8df965ed2..cebcc955e 100644 --- a/src/app/util/array-move.ts +++ b/src/app/util/array-move.ts @@ -1,4 +1,9 @@ -export const arrayMove = (arrIN: T[], from: number, to: number, on: number = 1): T[] => { +export const arrayMove = ( + arrIN: T[], + from: number, + to: number, + on: number = 1, +): T[] => { const arr = arrIN.slice(0); arr.splice(to, 0, ...arr.splice(from, on)); return arr; diff --git a/src/app/util/array-to-dictionary.ts b/src/app/util/array-to-dictionary.ts index 8a422ec41..f97017a20 100644 --- a/src/app/util/array-to-dictionary.ts +++ b/src/app/util/array-to-dictionary.ts @@ -1,8 +1,11 @@ import { Dictionary } from '@ngrx/entity'; export const arrayToDictionary = (arr: T[]): Dictionary => { - return arr.reduce((acc: any, sc): Dictionary => ({ - ...acc, - [(sc as any).id]: sc, - }), {}); + return arr.reduce( + (acc: any, sc): Dictionary => ({ + ...acc, + [(sc as any).id]: sc, + }), + {}, + ); }; diff --git a/src/app/util/check-fix-entity-state-consistency.ts b/src/app/util/check-fix-entity-state-consistency.ts index a6a9caa6a..d901af5aa 100644 --- a/src/app/util/check-fix-entity-state-consistency.ts +++ b/src/app/util/check-fix-entity-state-consistency.ts @@ -21,11 +21,13 @@ export const checkFixEntityStateConsistency = (data: any, additionalStr = ''): a }; export const isEntityStateConsistent = (data: any, additionalStr = ''): boolean => { - if (!data - || !data.entities - || !data.ids - || Object.keys(data.entities).length !== data.ids.length - || !arrayEquals(Object.keys(data.entities).sort(), [...data.ids].sort())) { + if ( + !data || + !data.entities || + !data.ids || + Object.keys(data.entities).length !== data.ids.length || + !arrayEquals(Object.keys(data.entities).sort(), [...data.ids].sort()) + ) { console.log(data); devError(`Inconsistent entity state "${additionalStr}"`); return false; diff --git a/src/app/util/check-key-combo.spec.ts b/src/app/util/check-key-combo.spec.ts index fe776de30..2b2ae4fd8 100644 --- a/src/app/util/check-key-combo.spec.ts +++ b/src/app/util/check-key-combo.spec.ts @@ -5,7 +5,7 @@ describe('checkKeyCombo', () => { const ev: Partial = { key: 'A', ctrlKey: true, - shiftKey: true + shiftKey: true, }; expect(checkKeyCombo(ev as any, 'Ctrl+Shift+A')).toBe(true); }); @@ -14,19 +14,19 @@ describe('checkKeyCombo', () => { const ev: Partial = { key: 'A', ctrlKey: true, - shiftKey: true + shiftKey: true, }; const comboToCheck = 'Ctrl+Shift+A'; - expect(checkKeyCombo({...ev, ctrlKey: false} as any, comboToCheck)).toBe(false); - expect(checkKeyCombo({...ev, shiftKey: false} as any, comboToCheck)).toBe(false); - expect(checkKeyCombo({...ev, key: 'B'} as any, comboToCheck)).toBe(false); + expect(checkKeyCombo({ ...ev, ctrlKey: false } as any, comboToCheck)).toBe(false); + expect(checkKeyCombo({ ...ev, shiftKey: false } as any, comboToCheck)).toBe(false); + expect(checkKeyCombo({ ...ev, key: 'B' } as any, comboToCheck)).toBe(false); }); it('should not throw for undefined', () => { const ev: Partial = { key: 'A', ctrlKey: true, - shiftKey: true + shiftKey: true, }; expect((checkKeyCombo as any)(ev as any, undefined)).toBe(false); }); diff --git a/src/app/util/check-key-combo.ts b/src/app/util/check-key-combo.ts index 564802925..eccd3f688 100644 --- a/src/app/util/check-key-combo.ts +++ b/src/app/util/check-key-combo.ts @@ -1,4 +1,7 @@ -const isSpecialKeyExactlyRight = (isKeyRequired: boolean, isKeyPressed: boolean): boolean => { +const isSpecialKeyExactlyRight = ( + isKeyRequired: boolean, + isKeyPressed: boolean, +): boolean => { return (isKeyRequired && isKeyPressed) || (!isKeyRequired && !isKeyPressed); }; @@ -12,11 +15,13 @@ export const checkKeyCombo = (ev: KeyboardEvent, comboToTest: string | null): bo sk.splice(-1, 1); - return isSpecialKeyExactlyRight(sk.includes('Ctrl'), ev.ctrlKey) - && isSpecialKeyExactlyRight(sk.includes('Alt'), ev.altKey) - && isSpecialKeyExactlyRight(sk.includes('Meta'), ev.metaKey) - && (!(sk.includes('Shift')) || ev.shiftKey) - && (ev.key === standardKey || isPlusKey && ev.key === '+'); + return ( + isSpecialKeyExactlyRight(sk.includes('Ctrl'), ev.ctrlKey) && + isSpecialKeyExactlyRight(sk.includes('Alt'), ev.altKey) && + isSpecialKeyExactlyRight(sk.includes('Meta'), ev.metaKey) && + (!sk.includes('Shift') || ev.shiftKey) && + (ev.key === standardKey || (isPlusKey && ev.key === '+')) + ); } return false; }; diff --git a/src/app/util/de-dupe-by-key.ts b/src/app/util/de-dupe-by-key.ts index 863ada052..2be388c12 100644 --- a/src/app/util/de-dupe-by-key.ts +++ b/src/app/util/de-dupe-by-key.ts @@ -1,6 +1,4 @@ export const dedupeByKey = (arr: any[], key: string): any[] => { - const temp = arr.map(el => el[key]); - return arr.filter((el, i) => - temp.indexOf(el[key]) === i - ); + const temp = arr.map((el) => el[key]); + return arr.filter((el, i) => temp.indexOf(el[key]) === i); }; diff --git a/src/app/util/distinct-until-changed-object.ts b/src/app/util/distinct-until-changed-object.ts index 5bd7ffd66..7aaf7bb4e 100644 --- a/src/app/util/distinct-until-changed-object.ts +++ b/src/app/util/distinct-until-changed-object.ts @@ -2,7 +2,7 @@ import { isObject } from './is-object'; export const distinctUntilChangedObject = (a: any, b: any): boolean => { if ((isObject(a) && isObject(b)) || (Array.isArray(a) && Array.isArray(b))) { - return (JSON.stringify(a) === JSON.stringify(b)); + return JSON.stringify(a) === JSON.stringify(b); } else { return a === b; } diff --git a/src/app/util/download.ts b/src/app/util/download.ts index a69d36708..665d95ced 100644 --- a/src/app/util/download.ts +++ b/src/app/util/download.ts @@ -1,6 +1,6 @@ import { saveAs } from 'file-saver'; export function download(filename: string, stringData: string) { - const blob = new Blob([stringData], {type: 'text/plain;charset=utf-8'}); + const blob = new Blob([stringData], { type: 'text/plain;charset=utf-8' }); saveAs(blob, filename); } diff --git a/src/app/util/fake-entity-state-from-array.ts b/src/app/util/fake-entity-state-from-array.ts index 63523704c..a0056c7e6 100644 --- a/src/app/util/fake-entity-state-from-array.ts +++ b/src/app/util/fake-entity-state-from-array.ts @@ -1,9 +1,11 @@ import { Dictionary, EntityState } from '@ngrx/entity'; import { arrayToDictionary } from './array-to-dictionary'; -export const fakeEntityStateFromArray = (items: { [key: string]: any }[]): EntityState => { +export const fakeEntityStateFromArray = ( + items: { [key: string]: any }[], +): EntityState => { const dict = arrayToDictionary(items) as Dictionary; - const ids = items.map(item => item.id); + const ids = items.map((item) => item.id); return { entities: dict, ids, @@ -11,7 +13,7 @@ export const fakeEntityStateFromArray = (items: { [key: string]: any }[]): En }; export const fakeEntityStateFromNumbersArray = (...nrs: number[]): EntityState => { - const items: any = nrs.map(nr => ({id: '_' + nr})); + const items: any = nrs.map((nr) => ({ id: '_' + nr })); const dict = arrayToDictionary(items) as Dictionary; return { @@ -19,4 +21,3 @@ export const fakeEntityStateFromNumbersArray = (...nrs: number[]): EntityStat ids: Object.keys(dict), }; }; - diff --git a/src/app/util/filter-out-id.ts b/src/app/util/filter-out-id.ts index 969ea830a..d55e33239 100644 --- a/src/app/util/filter-out-id.ts +++ b/src/app/util/filter-out-id.ts @@ -1 +1,2 @@ -export const filterOutId = (idToFilterOut: string) => (id: string): boolean => id !== idToFilterOut; +export const filterOutId = (idToFilterOut: string) => (id: string): boolean => + id !== idToFilterOut; diff --git a/src/app/util/get-date-range-for-month.ts b/src/app/util/get-date-range-for-month.ts index c5e619a95..840a44008 100644 --- a/src/app/util/get-date-range-for-month.ts +++ b/src/app/util/get-date-range-for-month.ts @@ -1,4 +1,7 @@ -export const getDateRangeForMonth = (year: number, monthIN: number): { rangeStart: Date; rangeEnd: Date } => { +export const getDateRangeForMonth = ( + year: number, + monthIN: number, +): { rangeStart: Date; rangeEnd: Date } => { // denormalize to js month again const month = +monthIN; // firstDayOfMonth @@ -10,6 +13,6 @@ export const getDateRangeForMonth = (year: number, monthIN: number): { rangeStar return { rangeStart, - rangeEnd + rangeEnd, }; }; diff --git a/src/app/util/get-date-range-for-week.spec.ts b/src/app/util/get-date-range-for-week.spec.ts index 794ffb3eb..e8d1203eb 100644 --- a/src/app/util/get-date-range-for-week.spec.ts +++ b/src/app/util/get-date-range-for-week.spec.ts @@ -1,4 +1,8 @@ -import { getDateRangeForWeek, rangeEndWithTime, rangeStartWithTime } from './get-date-range-for-week'; +import { + getDateRangeForWeek, + rangeEndWithTime, + rangeStartWithTime, +} from './get-date-range-for-week'; describe('sortWorklogDates', () => { it('should return a valid range', () => { diff --git a/src/app/util/get-date-range-for-week.ts b/src/app/util/get-date-range-for-week.ts index c6384c801..dc066848b 100644 --- a/src/app/util/get-date-range-for-week.ts +++ b/src/app/util/get-date-range-for-week.ts @@ -1,6 +1,10 @@ // NOTE: cuts off at month start and end per default -export const getDateRangeForWeek = (year: number, weekNr: number, month?: number): { +export const getDateRangeForWeek = ( + year: number, + weekNr: number, + month?: number, +): { rangeStart: Date; rangeEnd: Date; } => { @@ -15,18 +19,14 @@ export const getDateRangeForWeek = (year: number, weekNr: number, month?: number // lastDayOfMonth const monthEnd = new Date(year, month, 0); - rangeStart = (rangeStart < monthStart) - ? monthStart - : rangeStart; + rangeStart = rangeStart < monthStart ? monthStart : rangeStart; - rangeEnd = (rangeEnd > monthEnd) - ? monthEnd - : rangeEnd; + rangeEnd = rangeEnd > monthEnd ? monthEnd : rangeEnd; } return { rangeStart: rangeStartWithTime(rangeStart), - rangeEnd: rangeEndWithTime(rangeEnd) + rangeEnd: rangeEndWithTime(rangeEnd), }; }; diff --git a/src/app/util/get-error-text.ts b/src/app/util/get-error-text.ts index c0b5b88f5..09fc7a566 100644 --- a/src/app/util/get-error-text.ts +++ b/src/app/util/get-error-text.ts @@ -2,13 +2,13 @@ import { isObject } from './is-object'; export function getErrorTxt(err: any): unknown { if (err && isObject(err.error)) { - return (err.error.message) - || (err.error.name) + return ( + err.error.message || + err.error.name || // for ngx translate... - || (isObject(err.error.error) - ? err.error.error.toString() - : err.error) - || err.error; + (isObject(err.error.error) ? err.error.error.toString() : err.error) || + err.error + ); } else if (err && err.toString) { return err.toString(); } else { diff --git a/src/app/util/get-text-from-array-buffer.ts b/src/app/util/get-text-from-array-buffer.ts index 3e1d14f90..b5c47fb87 100644 --- a/src/app/util/get-text-from-array-buffer.ts +++ b/src/app/util/get-text-from-array-buffer.ts @@ -1,6 +1,9 @@ // borrowed -export const getTextFromArrayBuffer = (arrayBuffer: ArrayBuffer, encoding: string = 'UTF-8'): Promise => { - return new Promise((resolve/*, reject*/) => { +export const getTextFromArrayBuffer = ( + arrayBuffer: ArrayBuffer, + encoding: string = 'UTF-8', +): Promise => { + return new Promise((resolve /*, reject*/) => { if (typeof Blob === 'undefined') { const buffer = new Buffer(new Uint8Array(arrayBuffer)); resolve(buffer.toString(encoding)); diff --git a/src/app/util/get-week-number.ts b/src/app/util/get-week-number.ts index 8cc5dca02..3c3bf65ca 100644 --- a/src/app/util/get-week-number.ts +++ b/src/app/util/get-week-number.ts @@ -7,7 +7,7 @@ export const getWeekNumber = (d: Date): number => { // Get first day of year const yearStart = new Date(Date.UTC(d.getUTCFullYear(), 0, 1)); // Calculate full weeks to nearest Thursday - const weekNo = Math.ceil((((+d - +yearStart) / 86400000) + 1) / 7); + const weekNo = Math.ceil(((+d - +yearStart) / 86400000 + 1) / 7); // Return array of year and week number // return [d.getUTCFullYear(), weekNo]; return weekNo; diff --git a/src/app/util/get-weeks-in-month.spec.ts b/src/app/util/get-weeks-in-month.spec.ts index 763b08162..cbb046372 100644 --- a/src/app/util/get-weeks-in-month.spec.ts +++ b/src/app/util/get-weeks-in-month.spec.ts @@ -4,29 +4,29 @@ describe('getWeeksInMonth', () => { it('should work for february 2019', () => { const result = getWeeksInMonth(1, 2019); - expect(result[0]).toEqual({start: 1, end: 3}); - expect(result[4]).toEqual({start: 25, end: 28}); + expect(result[0]).toEqual({ start: 1, end: 3 }); + expect(result[4]).toEqual({ start: 25, end: 28 }); expect(result.length).toBe(5); }); it('should work for march 2019', () => { const result = getWeeksInMonth(2, 2019); - expect(result[0]).toEqual({start: 1, end: 3}); - expect(result[4]).toEqual({start: 25, end: 31}); + expect(result[0]).toEqual({ start: 1, end: 3 }); + expect(result[4]).toEqual({ start: 25, end: 31 }); expect(result.length).toBe(5); }); it('should work for april 2019', () => { const result = getWeeksInMonth(3, 2019); - expect(result[0]).toEqual({start: 1, end: 7}); - expect(result[4]).toEqual({start: 29, end: 30}); + expect(result[0]).toEqual({ start: 1, end: 7 }); + expect(result[4]).toEqual({ start: 29, end: 30 }); expect(result.length).toBe(5); }); it('should work for february 2021', () => { const result = getWeeksInMonth(1, 2021); - expect(result[0]).toEqual({start: 1, end: 7}); - expect(result[3]).toEqual({start: 22, end: 28}); + expect(result[0]).toEqual({ start: 1, end: 7 }); + expect(result[3]).toEqual({ start: 22, end: 28 }); expect(result.length).toBe(4); }); }); diff --git a/src/app/util/get-weeks-in-month.ts b/src/app/util/get-weeks-in-month.ts index e340d95e4..18d687f5b 100644 --- a/src/app/util/get-weeks-in-month.ts +++ b/src/app/util/get-weeks-in-month.ts @@ -17,12 +17,10 @@ export const getWeeksInMonth = (month: number, year: number): WeeksInMonth[] => end = 7 - firstDate.getDay() + 1; } while (start <= numDays) { - weeks.push({start, end}); + weeks.push({ start, end }); start = end + 1; end = end + 7; - end = (start === 1 && end === 8) - ? 1 - : end; + end = start === 1 && end === 8 ? 1 : end; if (end > numDays) { end = numDays; } diff --git a/src/app/util/get-work-log-str.ts b/src/app/util/get-work-log-str.ts index f32501fd9..39faef679 100644 --- a/src/app/util/get-work-log-str.ts +++ b/src/app/util/get-work-log-str.ts @@ -3,6 +3,6 @@ import * as moment from 'moment'; import { WORKLOG_DATE_STR_FORMAT } from '../app.constants'; export const getWorklogStr = (date: Date | number | string = new Date()): string => { -// NOTE: locale is important as it might break a lot of stuff for non arabic numbers + // NOTE: locale is important as it might break a lot of stuff for non arabic numbers return moment(date).locale('en').format(WORKLOG_DATE_STR_FORMAT); }; diff --git a/src/app/util/is-email.ts b/src/app/util/is-email.ts index 5b1478c23..def47f95b 100644 --- a/src/app/util/is-email.ts +++ b/src/app/util/is-email.ts @@ -1,2 +1,3 @@ const emailRegex = /^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/; -export const isEmail = (str: string): boolean => typeof str === 'string' && !!str.match(emailRegex); +export const isEmail = (str: string): boolean => + typeof str === 'string' && !!str.match(emailRegex); diff --git a/src/app/util/is-image-url.ts b/src/app/util/is-image-url.ts index dfda27ac6..2e4938bc2 100644 --- a/src/app/util/is-image-url.ts +++ b/src/app/util/is-image-url.ts @@ -1,9 +1,9 @@ export function isImageUrlSimple(url: string): boolean { - return (url.match(/\.(jpeg|jpg|gif|png)$/i) !== null); + return url.match(/\.(jpeg|jpg|gif|png)$/i) !== null; } export function isImageUrl(url: string): Promise { - return new Promise(resolve => { + return new Promise((resolve) => { const timeout = 5000; const img = new Image(); let timedOut = false; diff --git a/src/app/util/is-mobile.ts b/src/app/util/is-mobile.ts index d1c9dfbc0..25371f67b 100644 --- a/src/app/util/is-mobile.ts +++ b/src/app/util/is-mobile.ts @@ -1 +1,3 @@ -export const IS_MOBILE = (/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent)); +export const IS_MOBILE = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test( + navigator.userAgent, +); diff --git a/src/app/util/is-online.ts b/src/app/util/is-online.ts index 41f6f49f7..52cc641a8 100644 --- a/src/app/util/is-online.ts +++ b/src/app/util/is-online.ts @@ -7,6 +7,4 @@ export const isOnline$ = merge( fromEvent(window, 'offline').pipe(mapTo(false)), fromEvent(window, 'online').pipe(mapTo(true)), of(navigator.onLine), -).pipe( - shareReplay(1), -); +).pipe(shareReplay(1)); diff --git a/src/app/util/is-today.util.ts b/src/app/util/is-today.util.ts index 6a96d9681..831a0e197 100644 --- a/src/app/util/is-today.util.ts +++ b/src/app/util/is-today.util.ts @@ -7,9 +7,11 @@ export const isToday = (date: number | Date): boolean => { const today = new Date(); // return (today.toDateString() === d.toDateString()); // return today.setHours(0, 0, 0, 0) === d.setHours(0, 0, 0, 0); - return d.getDate() === today.getDate() && + return ( + d.getDate() === today.getDate() && d.getMonth() === today.getMonth() && - d.getFullYear() === today.getFullYear(); + d.getFullYear() === today.getFullYear() + ); }; export const isYesterday = (date: number): boolean => { @@ -23,7 +25,9 @@ export const isYesterday = (date: number): boolean => { // return (yesterday.toDateString() === d.toDateString()); // return yesterday.setHours(0, 0, 0, 0) === d.setHours(0, 0, 0, 0); - return d.getDate() === yesterday.getDate() && + return ( + d.getDate() === yesterday.getDate() && d.getMonth() === yesterday.getMonth() && - d.getFullYear() === yesterday.getFullYear(); + d.getFullYear() === yesterday.getFullYear() + ); }; diff --git a/src/app/util/model-version.ts b/src/app/util/model-version.ts index 04d6b05f0..44861fa72 100644 --- a/src/app/util/model-version.ts +++ b/src/app/util/model-version.ts @@ -1,6 +1,10 @@ import { MODEL_VERSION_KEY } from '../app.constants'; -export const isMigrateModel = (modelData: any, localVersion: number, modelType: string = '?'): boolean => { +export const isMigrateModel = ( + modelData: any, + localVersion: number, + modelType: string = '?', +): boolean => { const importVersion = modelData && modelData[MODEL_VERSION_KEY]; if (!modelData) { @@ -8,16 +12,29 @@ export const isMigrateModel = (modelData: any, localVersion: number, modelType: } else if (importVersion === localVersion) { return false; } else if (importVersion > localVersion) { - const isNewMajor = ((Math.floor(importVersion) - Math.floor(localVersion)) >= 1); + const isNewMajor = Math.floor(importVersion) - Math.floor(localVersion) >= 1; if (isNewMajor) { - alert('Cannot load model. Version to load is newer than local. Please close the app and update your local productivity version first, before importing the data.'); + alert( + 'Cannot load model. Version to load is newer than local. Please close the app and update your local productivity version first, before importing the data.', + ); throw new Error('Cannot load model. Version to load is newer than local'); } else { - console.warn('Imported model newer than local version', 'importVersion', importVersion, 'localVersion', localVersion, 'modelData', modelData); + console.warn( + 'Imported model newer than local version', + 'importVersion', + importVersion, + 'localVersion', + localVersion, + 'modelData', + modelData, + ); return false; } } else { - console.log(`Migrating model "${modelType}" to version from ${importVersion} to ${localVersion}`, modelData); + console.log( + `Migrating model "${modelType}" to version from ${importVersion} to ${localVersion}`, + modelData, + ); return true; } }; diff --git a/src/app/util/mutation-observer-obs.ts b/src/app/util/mutation-observer-obs.ts index ca966a25d..bfbe958ba 100644 --- a/src/app/util/mutation-observer-obs.ts +++ b/src/app/util/mutation-observer-obs.ts @@ -1,6 +1,9 @@ import { Observable } from 'rxjs'; -export const observeMutation = (target: HTMLElement, config): Observable => { +export const observeMutation = ( + target: HTMLElement, + config, +): Observable => { return new Observable((observer) => { const mutation = new MutationObserver((mutations, instance) => { observer.next(mutations); diff --git a/src/app/util/numeric-converter.ts b/src/app/util/numeric-converter.ts index e1aa6186f..95a898ef5 100644 --- a/src/app/util/numeric-converter.ts +++ b/src/app/util/numeric-converter.ts @@ -8,7 +8,7 @@ const arabicNumberMap = { '٧': '7', '٨': '8', '٩': '9', - '٠': '0' + '٠': '0', }; export function convertToWesternArabic(data: string): string { diff --git a/src/app/util/promise-timeout.ts b/src/app/util/promise-timeout.ts index 6f52d59b7..b1c164e67 100644 --- a/src/app/util/promise-timeout.ts +++ b/src/app/util/promise-timeout.ts @@ -1,3 +1,3 @@ export function promiseTimeout(ms: number): Promise { - return new Promise(resolve => setTimeout(resolve, ms)); + return new Promise((resolve) => setTimeout(resolve, ms)); } diff --git a/src/app/util/real-timer.ts b/src/app/util/real-timer.ts index 0b04ed801..7b80fd62a 100644 --- a/src/app/util/real-timer.ts +++ b/src/app/util/real-timer.ts @@ -2,7 +2,7 @@ import { lazySetInterval } from '../../../electron/lazy-set-interval'; import { Observable } from 'rxjs'; export const realTimer$ = (intervalDuration: number): Observable => { - return new Observable(subscriber => { + return new Observable((subscriber) => { const idleStart = Date.now(); // subscriber.next(0); lazySetInterval(() => { diff --git a/src/app/util/round-duration.ts b/src/app/util/round-duration.ts index 2e52f01b6..a55fa5405 100644 --- a/src/app/util/round-duration.ts +++ b/src/app/util/round-duration.ts @@ -2,21 +2,31 @@ import * as moment from 'moment'; import { Duration } from 'moment'; import { RoundTimeOption } from '../features/project/project.model'; -export const roundDuration = (val: Duration | number, roundTo: RoundTimeOption, isRoundUp: boolean = false): Duration => { - const value = (typeof val === 'number') - ? val - : val.asMilliseconds(); +export const roundDuration = ( + val: Duration | number, + roundTo: RoundTimeOption, + isRoundUp: boolean = false, +): Duration => { + const value = typeof val === 'number' ? val : val.asMilliseconds(); const roundedMs = roundDurationVanilla(value, roundTo, isRoundUp); - return moment.duration({millisecond: roundedMs}); + return moment.duration({ millisecond: roundedMs }); }; -export const roundMinutes = (minutes: number, factor: number, isRoundUp: boolean): number => { - return (isRoundUp) +export const roundMinutes = ( + minutes: number, + factor: number, + isRoundUp: boolean, +): number => { + return isRoundUp ? Math.ceil(minutes / factor) * factor : Math.round(minutes / factor) * factor; }; -export const roundDurationVanilla = (val: number, roundTo: RoundTimeOption, isRoundUp: boolean = false): number => { +export const roundDurationVanilla = ( + val: number, + roundTo: RoundTimeOption, + isRoundUp: boolean = false, +): number => { const asMinutes = parseMsToMinutes(val); const A_MINUTE = 60000; diff --git a/src/app/util/round-time.ts b/src/app/util/round-time.ts index 38e1d0615..35e187f2b 100644 --- a/src/app/util/round-time.ts +++ b/src/app/util/round-time.ts @@ -1,10 +1,12 @@ import { RoundTimeOption } from '../features/project/project.model'; import { Moment } from 'moment'; -export const roundTime = (val: number | Date, roundTo: RoundTimeOption, isRoundUp = false): Date => { - const value = (typeof val === 'number') - ? new Date(val) - : val; +export const roundTime = ( + val: number | Date, + roundTo: RoundTimeOption, + isRoundUp = false, +): Date => { + const value = typeof val === 'number' ? new Date(val) : val; let rounded; switch (roundTo) { @@ -35,7 +37,11 @@ export const roundTime = (val: number | Date, roundTo: RoundTimeOption, isRoundU } }; -export const momentRoundTime = (value: Moment, roundTo: RoundTimeOption, isRoundUp = false): Moment => { +export const momentRoundTime = ( + value: Moment, + roundTo: RoundTimeOption, + isRoundUp = false, +): Moment => { let rounded; switch (roundTo) { diff --git a/src/app/util/sortWorklogDates.spec.ts b/src/app/util/sortWorklogDates.spec.ts index 6bd0c7149..30d39fcd8 100644 --- a/src/app/util/sortWorklogDates.spec.ts +++ b/src/app/util/sortWorklogDates.spec.ts @@ -24,13 +24,7 @@ describe('sortWorklogDates', () => { }); it('should sort a list of unsorted dates with zeros', () => { - const dates = [ - '2019-10-04', - '2019-09-29', - '2019-10-02', - '2019-09-30', - '2019-10-01' - ]; + const dates = ['2019-10-04', '2019-09-29', '2019-10-02', '2019-09-30', '2019-10-01']; const result = sortWorklogDates(dates); expect(result.length).toBe(dates.length); @@ -39,7 +33,7 @@ describe('sortWorklogDates', () => { '2019-09-30', '2019-10-01', '2019-10-02', - '2019-10-04' + '2019-10-04', ]); }); }); diff --git a/src/app/util/strip-trailing.ts b/src/app/util/strip-trailing.ts index 5c3346490..0dc395faa 100644 --- a/src/app/util/strip-trailing.ts +++ b/src/app/util/strip-trailing.ts @@ -1,5 +1,5 @@ export const stripTrailing = (str: string, toBeStripped: string) => { - return str.endsWith(toBeStripped) && toBeStripped.length > 0 ? - str.slice(0, -1 * toBeStripped.length) : - str; + return str.endsWith(toBeStripped) && toBeStripped.length > 0 + ? str.slice(0, -1 * toBeStripped.length) + : str; }; diff --git a/src/app/util/throw-handled-error.ts b/src/app/util/throw-handled-error.ts index 726ba70b3..d4b6a8525 100644 --- a/src/app/util/throw-handled-error.ts +++ b/src/app/util/throw-handled-error.ts @@ -1,7 +1,7 @@ import { HANDLED_ERROR_PROP_STR } from '../app.constants'; -export const throwHandledError = ((errorTxt: string) => { +export const throwHandledError = (errorTxt: string) => { const e = new Error(errorTxt); (e as any)[HANDLED_ERROR_PROP_STR] = errorTxt; throw e; -}); +}; diff --git a/src/app/util/timestamp-to-datetime-input-string.ts b/src/app/util/timestamp-to-datetime-input-string.ts index e3997d7dd..81f6e7b15 100644 --- a/src/app/util/timestamp-to-datetime-input-string.ts +++ b/src/app/util/timestamp-to-datetime-input-string.ts @@ -1,5 +1,5 @@ export function timestampToDatetimeInputString(timestamp: number): string { - const date = new Date((timestamp + _getTimeZoneOffsetInMs())); + const date = new Date(timestamp + _getTimeZoneOffsetInMs()); return date.toISOString().slice(0, 19); } diff --git a/src/app/util/unique.ts b/src/app/util/unique.ts index 2c10d8059..0a49683ce 100644 --- a/src/app/util/unique.ts +++ b/src/app/util/unique.ts @@ -1 +1,2 @@ -export const unique = (array: T[]): T[] => array.filter((v, i, a) => a.indexOf(v) === i); +export const unique = (array: T[]): T[] => + array.filter((v, i, a) => a.indexOf(v) === i); diff --git a/src/app/util/update-all-in-dictionary.ts b/src/app/util/update-all-in-dictionary.ts index db0f79132..afad4ad9e 100644 --- a/src/app/util/update-all-in-dictionary.ts +++ b/src/app/util/update-all-in-dictionary.ts @@ -1,6 +1,9 @@ import { Dictionary } from '@ngrx/entity'; -export const updateAllInDictionary = (oldD: Dictionary, changes: Partial): Dictionary => { +export const updateAllInDictionary = ( + oldD: Dictionary, + changes: Partial, +): Dictionary => { const newD: any = {}; const ids = Object.keys(oldD); diff --git a/src/assets/icons/caldav.svg b/src/assets/icons/caldav.svg index 6eb6dd828..91de2b9b4 100644 --- a/src/assets/icons/caldav.svg +++ b/src/assets/icons/caldav.svg @@ -1 +1,8 @@ - \ No newline at end of file + + + + diff --git a/src/hammer-config.class.ts b/src/hammer-config.class.ts index bc61e5aa3..1549e0b01 100644 --- a/src/hammer-config.class.ts +++ b/src/hammer-config.class.ts @@ -8,14 +8,14 @@ const DIRECTION_RIGHT = 4; // eslint-disable-next-line no-bitwise const DIRECTION_HORIZONTAL = DIRECTION_LEFT | DIRECTION_RIGHT; -@Injectable({providedIn: 'root'}) +@Injectable({ providedIn: 'root' }) export class MyHammerConfig extends HammerGestureConfig { overrides: { [key: string]: Record; } = { - swipe: {direction: DIRECTION_HORIZONTAL}, - pan: {direction: 6}, - pinch: {enable: false}, - rotate: {enable: false} + swipe: { direction: DIRECTION_HORIZONTAL }, + pan: { direction: 6 }, + pinch: { enable: false }, + rotate: { enable: false }, }; } diff --git a/src/karma.conf.js b/src/karma.conf.js index 58c7f97ba..bbf7da035 100644 --- a/src/karma.conf.js +++ b/src/karma.conf.js @@ -10,15 +10,15 @@ module.exports = function (config) { require('karma-chrome-launcher'), require('karma-jasmine-html-reporter'), require('karma-coverage-istanbul-reporter'), - require('@angular-devkit/build-angular/plugins/karma') + require('@angular-devkit/build-angular/plugins/karma'), ], client: { - clearContext: false // leave Jasmine Spec Runner output visible in browser + clearContext: false, // leave Jasmine Spec Runner output visible in browser }, coverageIstanbulReporter: { dir: require('path').join(__dirname, '../coverage'), reports: ['html', 'lcovonly'], - fixWebpackSourcePaths: true + fixWebpackSourcePaths: true, }, reporters: ['progress', 'kjhtml'], port: 9876, @@ -28,7 +28,7 @@ module.exports = function (config) { browsers: ['ChromeHeadless'], singleRun: false, customLaunchers: { - 'ChromeHeadless': { + ChromeHeadless: { base: 'Chrome', flags: [ // We must disable the Chrome sandbox when running Chrome inside Docker @@ -48,8 +48,8 @@ module.exports = function (config) { '--remote-debugging-port=9222', '--disable-web-security', ], - debug: true - } + debug: true, + }, }, browserNoActivityTimeout: 120000, }); diff --git a/src/main.ts b/src/main.ts index 07c0ed544..af651c41f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,32 +11,39 @@ if (environment.production || environment.stage) { enableProdMode(); } -platformBrowserDynamic().bootstrapModule(AppModule).then(() => { - // TODO make asset caching work for electron - if ('serviceWorker' in navigator && (environment.production || environment.stage) && !IS_ELECTRON) { - console.log('Registering Service worker'); - return navigator.serviceWorker.register('ngsw-worker.js'); - } else if ('serviceWorker' in navigator && IS_ELECTRON) { - navigator.serviceWorker.getRegistrations() - .then((registrations) => { - for (const registration of registrations) { - registration.unregister(); - } - }) - .catch((e) => { - console.error('ERROR when unregistering service worker'); - console.error(e); - }); - } - return; -}).catch((err: any) => { - console.log('Service Worker Registration Error'); - console.log(err); -}); +platformBrowserDynamic() + .bootstrapModule(AppModule) + .then(() => { + // TODO make asset caching work for electron + if ( + 'serviceWorker' in navigator && + (environment.production || environment.stage) && + !IS_ELECTRON + ) { + console.log('Registering Service worker'); + return navigator.serviceWorker.register('ngsw-worker.js'); + } else if ('serviceWorker' in navigator && IS_ELECTRON) { + navigator.serviceWorker + .getRegistrations() + .then((registrations) => { + for (const registration of registrations) { + registration.unregister(); + } + }) + .catch((e) => { + console.error('ERROR when unregistering service worker'); + console.error(e); + }); + } + return; + }) + .catch((err: any) => { + console.log('Service Worker Registration Error'); + console.log(err); + }); // fix mobile scrolling while dragging -window.addEventListener('touchmove', () => { -}); +window.addEventListener('touchmove', () => {}); if (!(environment.production || environment.stage) && IS_ANDROID_WEB_VIEW) { setTimeout(() => { diff --git a/src/polyfills.ts b/src/polyfills.ts index de95bcbd2..11120586e 100644 --- a/src/polyfills.ts +++ b/src/polyfills.ts @@ -65,9 +65,9 @@ import 'zone.js/dist/zone'; // Included with Angular CLI. // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames /* -* in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js -* with the following flag, it will bypass `zone.js` patch for IE/Edge -*/ + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + */ // (window as any).__Zone_enable_cross_context_check = true; // import 'zone.js/dist/zone-mix'; diff --git a/src/test.ts b/src/test.ts index d56a4e9e9..b6d614daa 100644 --- a/src/test.ts +++ b/src/test.ts @@ -2,14 +2,17 @@ import 'zone.js/dist/zone-testing'; import { getTestBed } from '@angular/core/testing'; -import { BrowserDynamicTestingModule, platformBrowserDynamicTesting } from '@angular/platform-browser-dynamic/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting, +} from '@angular/platform-browser-dynamic/testing'; declare const require: any; // First, initialize the Angular testing environment. getTestBed().initTestEnvironment( BrowserDynamicTestingModule, - platformBrowserDynamicTesting() + platformBrowserDynamicTesting(), ); // Then we find all the tests. const context = require.context('./', true, /\.spec\.ts$/); diff --git a/tools/extract-i18n.js b/tools/extract-i18n.js index e5cbeac3d..cd1788a69 100644 --- a/tools/extract-i18n.js +++ b/tools/extract-i18n.js @@ -26,14 +26,15 @@ module.exports = () => { }; const parsed = parse(tr); - const string = `const T = ${JSON.stringify(parsed, null, 2) - .replace(/"(\w+)":/g, '$1:')}; + const string = `const T = ${JSON.stringify(parsed, null, 2).replace( + /"(\w+)":/g, + '$1:', + )}; export { T }; -`.replace(/"/g, '\''); - +`.replace(/"/g, "'"); fs.writeFileSync(__dirname + '/../src/app/t.const.ts', string, { overwrite: true, - flag: 'w' + flag: 'w', }); }; diff --git a/tools/is-skip-postinstall.js b/tools/is-skip-postinstall.js index 8c4986c43..c341b39fb 100644 --- a/tools/is-skip-postinstall.js +++ b/tools/is-skip-postinstall.js @@ -1,5 +1,9 @@ console.log(process.env.SKIP_POST_INSTALL); -if (process.env.SKIP_POST_INSTALL && process.env.SKIP_POST_INSTALL !== 'false' && process.env.SKIP_POST_INSTALL !== '0') { +if ( + process.env.SKIP_POST_INSTALL && + process.env.SKIP_POST_INSTALL !== 'false' && + process.env.SKIP_POST_INSTALL !== '0' +) { console.log('\n\n!!! WARN: skipping postInstall !!!\n\n'); process.exit(0); } else {