refactor: format everything with prettier

This commit is contained in:
Johannes Millan 2021-05-06 22:37:37 +02:00
parent 8ee6191b14
commit 43022f8e83
533 changed files with 14924 additions and 11386 deletions

View file

@ -1,4 +1,4 @@
import {NightwatchBrowser} from 'nightwatch'; import { NightwatchBrowser } from 'nightwatch';
const ADD_NOTE_BTN = '#add-note-btn'; const ADD_NOTE_BTN = '#add-note-btn';
const TEXTAREA = 'dialog-add-note textarea'; const TEXTAREA = 'dialog-add-note textarea';
@ -7,8 +7,7 @@ const NOTES_WRAPPER = 'notes';
module.exports = { module.exports = {
async command(this: NightwatchBrowser, noteName: string) { async command(this: NightwatchBrowser, noteName: string) {
return this return this.moveToElement(NOTES_WRAPPER, 10, 50)
.moveToElement(NOTES_WRAPPER, 10, 50)
.waitForElementPresent(ADD_NOTE_BTN) .waitForElementPresent(ADD_NOTE_BTN)
.click(ADD_NOTE_BTN) .click(ADD_NOTE_BTN)
@ -16,7 +15,6 @@ module.exports = {
.setValue(TEXTAREA, noteName) .setValue(TEXTAREA, noteName)
.click(ADD_NOTE_SUBMIT_BTN) .click(ADD_NOTE_SUBMIT_BTN)
.moveToElement(NOTES_WRAPPER, 10, 50) .moveToElement(NOTES_WRAPPER, 10, 50);
; },
}
}; };

View file

@ -1,12 +1,11 @@
import {NightwatchBrowser} from 'nightwatch'; import { NightwatchBrowser } from 'nightwatch';
const ADD_TASK_GLOBAL_SEL = 'add-task-bar.global input'; const ADD_TASK_GLOBAL_SEL = 'add-task-bar.global input';
const ROUTER_WRAPPER = '.route-wrapper'; const ROUTER_WRAPPER = '.route-wrapper';
module.exports = { module.exports = {
async command(this: NightwatchBrowser, taskName: string) { async command(this: NightwatchBrowser, taskName: string) {
return this return this.waitForElementVisible(ROUTER_WRAPPER)
.waitForElementVisible(ROUTER_WRAPPER)
.setValue('body', 'A') .setValue('body', 'A')
.waitForElementVisible(ADD_TASK_GLOBAL_SEL) .waitForElementVisible(ADD_TASK_GLOBAL_SEL)
.setValue(ADD_TASK_GLOBAL_SEL, taskName) .setValue(ADD_TASK_GLOBAL_SEL, taskName)
@ -14,7 +13,6 @@ module.exports = {
.pause(30) .pause(30)
.setValue(ADD_TASK_GLOBAL_SEL, this.Keys.ESCAPE) .setValue(ADD_TASK_GLOBAL_SEL, this.Keys.ESCAPE)
.pause(30) .pause(30)
.waitForElementNotPresent(ADD_TASK_GLOBAL_SEL) .waitForElementNotPresent(ADD_TASK_GLOBAL_SEL);
; },
}
}; };

View file

@ -1,4 +1,4 @@
import { AddTaskWithReminderParams, NBrowser, } from '../n-browser-interface'; import { AddTaskWithReminderParams, NBrowser } from '../n-browser-interface';
const TASK = 'task'; const TASK = 'task';
const SCHEDULE_TASK_ITEM = 'task-additional-info-item:nth-child(2)'; 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 // being slightly longer than a minute prevents the edge case
// of the wrong minute if the rest before takes to long // 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 // NOTE: needs to
// be executed from work view // be executed from work view
module.exports = { module.exports = {
async command(this: NBrowser, { async command(
title, this: NBrowser,
taskSel = TASK, {
scheduleTime = Date.now() + DEFAULT_DELTA title,
}: AddTaskWithReminderParams) { taskSel = TASK,
scheduleTime = Date.now() + DEFAULT_DELTA,
}: AddTaskWithReminderParams,
) {
const d = new Date(scheduleTime); const d = new Date(scheduleTime);
const h = d.getHours(); const h = d.getHours();
const m = d.getMinutes(); const m = d.getMinutes();
return this return this.addTask(title)
.addTask(title)
.openPanelForTask(taskSel) .openPanelForTask(taskSel)
.waitForElementVisible(SCHEDULE_TASK_ITEM) .waitForElementVisible(SCHEDULE_TASK_ITEM)
.click(SCHEDULE_TASK_ITEM) .click(SCHEDULE_TASK_ITEM)
@ -43,6 +45,5 @@ module.exports = {
.waitForElementVisible(DIALOG_SUBMIT) .waitForElementVisible(DIALOG_SUBMIT)
.click(DIALOG_SUBMIT) .click(DIALOG_SUBMIT)
.waitForElementNotPresent(DIALOG); .waitForElementNotPresent(DIALOG);
} },
}; };

View file

@ -1,5 +1,5 @@
import {NightwatchBrowser} from 'nightwatch'; import { NightwatchBrowser } from 'nightwatch';
import {BASE} from '../e2e.const'; import { BASE } from '../e2e.const';
const BASE_URL = `${BASE}`; const BASE_URL = `${BASE}`;
@ -15,14 +15,12 @@ const SPLIT = `split`;
module.exports = { module.exports = {
async command(this: NightwatchBrowser) { async command(this: NightwatchBrowser) {
return this return this.url(BASE_URL)
.url(BASE_URL)
.waitForElementVisible(EXPAND_PROJECT_BTN) .waitForElementVisible(EXPAND_PROJECT_BTN)
.click(EXPAND_PROJECT_BTN) .click(EXPAND_PROJECT_BTN)
.waitForElementVisible(DEFAULT_PROJECT_BTN) .waitForElementVisible(DEFAULT_PROJECT_BTN)
.click(DEFAULT_PROJECT_BTN) .click(DEFAULT_PROJECT_BTN)
.waitForElementVisible(BACKLOG) .waitForElementVisible(BACKLOG)
.waitForElementVisible(SPLIT) .waitForElementVisible(SPLIT);
; },
}
}; };

View file

@ -1,18 +1,16 @@
import {NBrowser} from '../n-browser-interface'; import { NBrowser } from '../n-browser-interface';
const SIDE_INNER = '.additional-info-panel'; const SIDE_INNER = '.additional-info-panel';
// NOTE: needs to be executed from work view // NOTE: needs to be executed from work view
module.exports = { module.exports = {
async command(this: NBrowser, taskSel: string) { async command(this: NBrowser, taskSel: string) {
return this return this.waitForElementPresent(taskSel)
.waitForElementPresent(taskSel)
.pause(50) .pause(50)
.moveToElement(taskSel, 100, 15) .moveToElement(taskSel, 100, 15)
.click(taskSel) .click(taskSel)
.sendKeys(taskSel, this.Keys.ARROW_RIGHT) .sendKeys(taskSel, this.Keys.ARROW_RIGHT)
.waitForElementVisible(SIDE_INNER) .waitForElementVisible(SIDE_INNER)
.pause(50) .pause(50);
; },
}
}; };

View file

@ -1,4 +1,4 @@
import {NightwatchBrowser} from 'nightwatch'; import { NightwatchBrowser } from 'nightwatch';
export interface AddTaskWithReminderParams { export interface AddTaskWithReminderParams {
title: string; title: string;

View file

@ -1,20 +1,18 @@
module.exports = { module.exports = {
// An array of folders (excluding subfolders) where your tests are located; // 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. // if this is not specified, the test source must be passed as the second argument to the test runner.
src_folders: [ src_folders: ['../out-tsc/e2e/src'],
'../out-tsc/e2e/src' output_folder: './e2e-test-results',
], custom_commands_path: 'out-tsc/e2e/commands',
output_folder: "./e2e-test-results",
custom_commands_path: "out-tsc/e2e/commands",
test_workers: { test_workers: {
enabled: true, enabled: true,
workers: "auto" workers: 'auto',
}, },
webdriver: { webdriver: {
start_process: true, start_process: true,
port: 9515, port: 9515,
server_path: require('chromedriver').path, server_path: require('chromedriver').path,
cli_args: [] cli_args: [],
}, },
test_settings: { test_settings: {
@ -34,8 +32,8 @@ module.exports = {
], ],
w3c: false, w3c: false,
prefs: { prefs: {
"profile.default_content_setting_values.geolocation": 1, 'profile.default_content_setting_values.geolocation': 1,
"profile.default_content_setting_values.notifications": 2, 'profile.default_content_setting_values.notifications': 2,
}, },
}, },
}, },
@ -43,13 +41,13 @@ module.exports = {
enabled: true, // if you want to keep screenshots enabled: true, // if you want to keep screenshots
on_failure: true, on_failure: true,
on_error: true, on_error: true,
path: './e2e/screenshots' // save screenshots here path: './e2e/screenshots', // save screenshots here
}, },
globals: { globals: {
waitForConditionPollInterval: 500, waitForConditionPollInterval: 500,
waitForConditionTimeout: 10000, waitForConditionTimeout: 10000,
retryAssertionTimeout: 1000, retryAssertionTimeout: 1000,
} },
} },
} },
}; };

View file

@ -1,5 +1,5 @@
import {BASE} from '../e2e.const'; import { BASE } from '../e2e.const';
import {NBrowser} from '../n-browser-interface'; import { NBrowser } from '../n-browser-interface';
const URL = `${BASE}/#/tag/TODAY/daily-summary`; const URL = `${BASE}/#/tag/TODAY/daily-summary`;
const ADD_TASK_BTN_SEL = '.action-nav > button:first-child'; 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 = { module.exports = {
'@tags': ['daily-summary'], '@tags': ['daily-summary'],
'Daily summary message': (browser: NBrowser) => browser 'Daily summary message': (browser: NBrowser) =>
.url(URL) browser
.waitForElementVisible('.done-headline') .url(URL)
.assert.containsText('.done-headline', 'Take a moment to celebrate') .waitForElementVisible('.done-headline')
.end(), .assert.containsText('.done-headline', 'Take a moment to celebrate')
.end(),
'show any added task in table': (browser: NBrowser) => browser 'show any added task in table': (browser: NBrowser) =>
.url(URL) browser
.waitForElementVisible(ADD_TASK_BTN_SEL) .url(URL)
.click(ADD_TASK_BTN_SEL) .waitForElementVisible(ADD_TASK_BTN_SEL)
.waitForElementVisible(ADD_TASK_GLOBAL_SEL) .click(ADD_TASK_BTN_SEL)
.waitForElementVisible(ADD_TASK_GLOBAL_SEL)
.setValue(ADD_TASK_GLOBAL_SEL, 'test task hohoho') .setValue(ADD_TASK_GLOBAL_SEL, 'test task hohoho')
.setValue(ADD_TASK_GLOBAL_SEL, browser.Keys.ENTER) .setValue(ADD_TASK_GLOBAL_SEL, browser.Keys.ENTER)
.end(), .end(),
}; };

View file

@ -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 TOGGLE_BOOKMARK_BAR_BTN = '.action-nav button:nth-child(2)';
const BOOKMARK_BAR_OPTS_BTN = 'bookmark-bar .list-controls button:first-of-type'; 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 = { module.exports = {
'@tags': ['project', 'bookmark'], '@tags': ['project', 'bookmark'],
'create a bookmark': (browser: NBrowser) => browser 'create a bookmark': (browser: NBrowser) =>
.goToDefaultProject() browser
.goToDefaultProject()
.waitForElementVisible(TOGGLE_BOOKMARK_BAR_BTN) .waitForElementVisible(TOGGLE_BOOKMARK_BAR_BTN)
.click(TOGGLE_BOOKMARK_BAR_BTN) .click(TOGGLE_BOOKMARK_BAR_BTN)
.waitForElementVisible(BOOKMARK_BAR_OPTS_BTN) .waitForElementVisible(BOOKMARK_BAR_OPTS_BTN)
.click(BOOKMARK_BAR_OPTS_BTN) .click(BOOKMARK_BAR_OPTS_BTN)
.waitForElementVisible(ADD_BOOKMARK_BTN) .waitForElementVisible(ADD_BOOKMARK_BTN)
.click(ADD_BOOKMARK_BTN) .click(ADD_BOOKMARK_BTN)
.waitForElementVisible(BOOKMARK_TITLE_INP) .waitForElementVisible(BOOKMARK_TITLE_INP)
.setValue(BOOKMARK_TITLE_INP, 'Some bookmark title') .setValue(BOOKMARK_TITLE_INP, 'Some bookmark title')
.setValue(BOOKMARK_URL_INP, 'bookmark-url') .setValue(BOOKMARK_URL_INP, 'bookmark-url')
.click(BOOKMARK_SUBMIT_BTN) .click(BOOKMARK_SUBMIT_BTN)
.waitForElementVisible(FIRST_BOOKMARK) .waitForElementVisible(FIRST_BOOKMARK)
.assert.elementPresent(FIRST_BOOKMARK) .assert.elementPresent(FIRST_BOOKMARK)
.assert.containsText(FIRST_BOOKMARK, 'Some bookmark title') .assert.containsText(FIRST_BOOKMARK, 'Some bookmark title')
.end(), .end(),
}; };

View file

@ -1,31 +1,32 @@
import {NBrowser} from '../n-browser-interface'; import { NBrowser } from '../n-browser-interface';
const NOTES_WRAPPER = 'notes'; const NOTES_WRAPPER = 'notes';
const NOTE = 'notes note'; const NOTE = 'notes note';
const FIRST_NOTE = `${NOTE}:first-of-type`; const FIRST_NOTE = `${NOTE}:first-of-type`;
module.exports = { module.exports = {
'@tags': ['project', 'note'], '@tags': ['project', 'note'],
'create a note': (browser: NBrowser) => browser 'create a note': (browser: NBrowser) =>
.goToDefaultProject() browser
.addNote('Some new Note') .goToDefaultProject()
.addNote('Some new Note')
.moveToElement(NOTES_WRAPPER, 10, 50) .moveToElement(NOTES_WRAPPER, 10, 50)
.waitForElementVisible(FIRST_NOTE) .waitForElementVisible(FIRST_NOTE)
.assert.elementPresent(FIRST_NOTE) .assert.elementPresent(FIRST_NOTE)
.assert.containsText(FIRST_NOTE, 'Some new Note') .assert.containsText(FIRST_NOTE, 'Some new Note')
.end(), .end(),
'new note should be still available after reload': (browser: NBrowser) => browser 'new note should be still available after reload': (browser: NBrowser) =>
.goToDefaultProject() browser
.addNote('Some new Note') .goToDefaultProject()
.execute('window.location.reload()') .addNote('Some new Note')
.waitForElementPresent(NOTES_WRAPPER) .execute('window.location.reload()')
.moveToElement(NOTES_WRAPPER, 10, 50) .waitForElementPresent(NOTES_WRAPPER)
.waitForElementVisible(FIRST_NOTE) .moveToElement(NOTES_WRAPPER, 10, 50)
.assert.elementPresent(FIRST_NOTE) .waitForElementVisible(FIRST_NOTE)
.assert.containsText(FIRST_NOTE, 'Some new Note') .assert.elementPresent(FIRST_NOTE)
.end(), .assert.containsText(FIRST_NOTE, 'Some new Note')
.end(),
}; };

View file

@ -1,5 +1,5 @@
import {BASE} from '../e2e.const'; import { BASE } from '../e2e.const';
import {NBrowser} from '../n-browser-interface'; import { NBrowser } from '../n-browser-interface';
const BASE_URL = `${BASE}`; const BASE_URL = `${BASE}`;
@ -32,70 +32,70 @@ const GLOBAL_ERROR_ALERT = '.global-error-alert';
module.exports = { module.exports = {
'@tags': ['project'], '@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 // navigate to
.url(BASE_URL) .click(PROJECT_SETTINGS_BTN)
.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 .waitForElementVisible('.component-wrapper .mat-h1')
.click(PROJECT_SETTINGS_BTN) .assert.containsText('.component-wrapper .mat-h1', 'Project Specific Settings')
.end(),
.waitForElementVisible('.component-wrapper .mat-h1') 'create project': (browser: NBrowser) =>
.assert.containsText('.component-wrapper .mat-h1', 'Project Specific Settings') browser
.end(), .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 // navigate to
.url(BASE_URL) .waitForElementVisible(SECOND_PROJECT_BTN)
.waitForElementVisible(EXPAND_PROJECT_BTN) .click(SECOND_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) .waitForElementVisible(BACKLOG)
.assert.elementPresent(SECOND_PROJECT) .waitForElementVisible(SPLIT)
.assert.containsText(SECOND_PROJECT, 'Cool Test Project') .assert.containsText(WORK_CTX_TITLE, 'Cool Test Project')
.end(),
// navigate to 'navigate to default': (browser: NBrowser) =>
.waitForElementVisible(SECOND_PROJECT_BTN) browser
.click(SECOND_PROJECT_BTN) .goToDefaultProject()
.waitForElementVisible(BACKLOG) .assert.urlEquals(`${BASE}/#/project/DEFAULT/tasks`)
.waitForElementVisible(SPLIT) .assert.containsText(WORK_CTX_TITLE, 'Super Productivity')
.assert.containsText(WORK_CTX_TITLE, 'Cool Test Project') .end(),
.end(),
'navigate to daily summary from project without error': (browser: NBrowser) =>
browser
// Go to project page
.goToDefaultProject()
'navigate to default': (browser: NBrowser) => browser .click(READY_TO_WORK_BTN)
.goToDefaultProject()
.assert.urlEquals(`${BASE}/#/project/DEFAULT/tasks`) // navigate to
.assert.containsText(WORK_CTX_TITLE, 'Super Productivity') .waitForElementVisible(FINISH_DAY_BTN)
.end(), .click(FINISH_DAY_BTN)
.waitForElementPresent(DAILY_SUMMARY)
'navigate to daily summary from project without error': (browser: NBrowser) => browser .assert.urlEquals(`${BASE}/#/project/DEFAULT/daily-summary`)
// Go to project page .assert.elementNotPresent(GLOBAL_ERROR_ALERT)
.goToDefaultProject() .end(),
.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(),
}; };

View file

@ -1,5 +1,5 @@
import {BASE} from '../e2e.const'; import { BASE } from '../e2e.const';
import {NBrowser} from '../n-browser-interface'; import { NBrowser } from '../n-browser-interface';
const WORK_VIEW_URL = `${BASE}/`; const WORK_VIEW_URL = `${BASE}/`;
@ -19,37 +19,38 @@ const SCHEDULE_PAGE_TASK_2 = `${SCHEDULE_PAGE_TASKS}:nth-of-type(2)`;
module.exports = { module.exports = {
'@tags': ['task', 'reminder'], '@tags': ['task', 'reminder'],
'should add a scheduled tasks': (browser: NBrowser) => browser 'should add a scheduled tasks': (browser: NBrowser) =>
.url(WORK_VIEW_URL) browser
.waitForElementPresent(READY_TO_WORK_BTN) .url(WORK_VIEW_URL)
.addTaskWithReminder({title: '0 test task koko', scheduleTime: Date.now()}) .waitForElementPresent(READY_TO_WORK_BTN)
.waitForElementVisible(TASK) .addTaskWithReminder({ title: '0 test task koko', scheduleTime: Date.now() })
.waitForElementVisible(TASK_SCHEDULE_BTN) .waitForElementVisible(TASK)
.assert.elementPresent(TASK_SCHEDULE_BTN) .waitForElementVisible(TASK_SCHEDULE_BTN)
.assert.elementPresent(TASK_SCHEDULE_BTN)
// Navigate to scheduled page and check if entry is there // Navigate to scheduled page and check if entry is there
.click(SCHEDULE_ROUTE_BTN) .click(SCHEDULE_ROUTE_BTN)
.waitForElementVisible(SCHEDULE_PAGE_CMP) .waitForElementVisible(SCHEDULE_PAGE_CMP)
.waitForElementVisible(SCHEDULE_PAGE_TASK_1) .waitForElementVisible(SCHEDULE_PAGE_TASK_1)
.assert.containsText(SCHEDULE_PAGE_TASK_1, '0 test task koko') .assert.containsText(SCHEDULE_PAGE_TASK_1, '0 test task koko')
.end(), .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 // Navigate to scheduled page and check if entry is there
.url(WORK_VIEW_URL) .click(SCHEDULE_ROUTE_BTN)
.waitForElementPresent(READY_TO_WORK_BTN) .waitForElementVisible(SCHEDULE_PAGE_CMP)
.addTaskWithReminder({title: '0 test task koko', taskSel: TASK}) .waitForElementVisible(SCHEDULE_PAGE_TASK_1)
.addTaskWithReminder({title: '2 hihihi', taskSel: TASK_2}) .assert.containsText(SCHEDULE_PAGE_TASK_1, '0 test task koko')
.waitForElementVisible(TASK) .assert.containsText(SCHEDULE_PAGE_TASK_2, '2 hihihi')
.waitForElementVisible(TASK_SCHEDULE_BTN) .end(),
.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(),
}; };

View file

@ -23,62 +23,68 @@ const SCHEDULE_MAX_WAIT_TIME = 180000;
module.exports = { module.exports = {
'@tags': ['task', 'reminder', 'schedule'], '@tags': ['task', 'reminder', 'schedule'],
'should display a modal with a scheduled task if due': (browser: NBrowser) => browser 'should display a modal with a scheduled task if due': (browser: NBrowser) =>
.url(WORK_VIEW_URL) browser
.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
.url(WORK_VIEW_URL) .url(WORK_VIEW_URL)
// NOTE: tasks are sorted by due time .addTaskWithReminder({ title: '0 A task', scheduleTime: Date.now() })
.addTaskWithReminder({title: '0 B task'})
.addTaskWithReminder({title: '1 B task', scheduleTime: Date.now()})
.waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME) .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME)
.assert.elementPresent(DIALOG) .assert.elementPresent(DIALOG)
.waitForElementVisible(DIALOG_TASK1, SCHEDULE_MAX_WAIT_TIME) .waitForElementVisible(DIALOG_TASK1)
.waitForElementVisible(DIALOG_TASK2, SCHEDULE_MAX_WAIT_TIME) .assert.elementPresent(DIALOG_TASK1)
.assert.containsText(DIALOG_TASKS_WRAPPER, '0 B task') .assert.containsText(DIALOG_TASK1, '0 A task')
.assert.containsText(DIALOG_TASKS_WRAPPER, '1 B task') .end(),
.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 'should start single task': (browser: NBrowser) =>
.url(WORK_VIEW_URL) browser
.addTaskWithReminder({title: '0 C task', scheduleTime: Date.now()}) .url(WORK_VIEW_URL)
.waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME) .addTaskWithReminder({ title: '0 C task', scheduleTime: Date.now() })
.waitForElementVisible(DIALOG_TASK1) .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME)
.click(D_PLAY) .waitForElementVisible(DIALOG_TASK1)
.pause(100) .click(D_PLAY)
.assert.cssClassPresent(TODAY_TASK_1, 'isCurrent') .pause(100)
.end(), .assert.cssClassPresent(TODAY_TASK_1, 'isCurrent')
.end(),
'should manually empty list via add to today': (browser: NBrowser) => { 'should manually empty list via add to today': (browser: NBrowser) => {
const start = Date.now() + 100000; const start = Date.now() + 100000;
return browser return (
.url(WORK_VIEW_URL) browser
// NOTE: tasks are sorted by due time .url(WORK_VIEW_URL)
.addTaskWithReminder({title: '0 D task xyz', scheduleTime: start}) // NOTE: tasks are sorted by due time
.addTaskWithReminder({title: '1 D task xyz', scheduleTime: start}) .addTaskWithReminder({ title: '0 D task xyz', scheduleTime: start })
.addTaskWithReminder({title: '2 D task xyz', scheduleTime: Date.now()}) .addTaskWithReminder({ title: '1 D task xyz', scheduleTime: start })
.waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME + 120000) .addTaskWithReminder({ title: '2 D task xyz', scheduleTime: Date.now() })
// wait for all tasks to be present .waitForElementVisible(DIALOG, SCHEDULE_MAX_WAIT_TIME + 120000)
.waitForElementVisible(DIALOG_TASK1, SCHEDULE_MAX_WAIT_TIME + 120000) // wait for all tasks to be present
.waitForElementVisible(DIALOG_TASK2, SCHEDULE_MAX_WAIT_TIME + 120000) .waitForElementVisible(DIALOG_TASK1, SCHEDULE_MAX_WAIT_TIME + 120000)
.waitForElementVisible(DIALOG_TASK3, SCHEDULE_MAX_WAIT_TIME + 120000) .waitForElementVisible(DIALOG_TASK2, SCHEDULE_MAX_WAIT_TIME + 120000)
.pause(100) .waitForElementVisible(DIALOG_TASK3, SCHEDULE_MAX_WAIT_TIME + 120000)
.assert.containsText(DIALOG_TASKS_WRAPPER, '0 D task xyz') .pause(100)
.assert.containsText(DIALOG_TASKS_WRAPPER, '1 D task xyz') .assert.containsText(DIALOG_TASKS_WRAPPER, '0 D task xyz')
.assert.containsText(DIALOG_TASKS_WRAPPER, '2 D task xyz') .assert.containsText(DIALOG_TASKS_WRAPPER, '1 D task xyz')
.click(DIALOG_TASK1 + TO_TODAY_SUF) .assert.containsText(DIALOG_TASKS_WRAPPER, '2 D task xyz')
.click(DIALOG_TASK2 + TO_TODAY_SUF) .click(DIALOG_TASK1 + TO_TODAY_SUF)
.pause(50) .click(DIALOG_TASK2 + TO_TODAY_SUF)
.assert.containsText(DIALOG_TASK1, 'D task xyz') .pause(50)
.end(); .assert.containsText(DIALOG_TASK1, 'D task xyz')
} .end()
);
},
}; };

View file

@ -9,12 +9,13 @@ const READY_TO_WORK_BTN = '.ready-to-work-btn';
module.exports = { module.exports = {
'@tags': ['work-view', 'task', 'short-syntax'], '@tags': ['work-view', 'task', 'short-syntax'],
'should add task with project via short syntax': (browser: NBrowser) => browser 'should add task with project via short syntax': (browser: NBrowser) =>
.url(WORK_VIEW_URL) browser
.waitForElementVisible(READY_TO_WORK_BTN) .url(WORK_VIEW_URL)
.addTask('0 test task koko +s') .waitForElementVisible(READY_TO_WORK_BTN)
.waitForElementVisible(TASK) .addTask('0 test task koko +s')
.assert.visible(TASK) .waitForElementVisible(TASK)
.assert.containsText(TASK_TAGS, 'Super Productivity') .assert.visible(TASK)
.end(), .assert.containsText(TASK_TAGS, 'Super Productivity')
.end(),
}; };

View file

@ -1,6 +1,5 @@
import {BASE} from '../e2e.const'; import { BASE } from '../e2e.const';
import {NBrowser} from '../n-browser-interface'; import { NBrowser } from '../n-browser-interface';
const ADD_TASK_INITIAL = 'add-task-bar:not(.global) input'; const ADD_TASK_INITIAL = 'add-task-bar:not(.global) input';
const ADD_TASK_GLOBAL = 'add-task-bar.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 = { module.exports = {
'@tags': ['work-view', 'task'], '@tags': ['work-view', 'task'],
'should add task via key combo': (browser: NBrowser) => browser 'should add task via key combo': (browser: NBrowser) =>
.url(WORK_VIEW_URL) browser
.waitForElementVisible(READY_TO_WORK_BTN) .url(WORK_VIEW_URL)
.addTask('0 test task koko') .waitForElementVisible(READY_TO_WORK_BTN)
.waitForElementVisible(TASK) .addTask('0 test task koko')
.assert.visible(TASK) .waitForElementVisible(TASK)
.assert.containsText(TASK, '0 test task koko') .assert.visible(TASK)
.end(), .assert.containsText(TASK, '0 test task koko')
.end(),
'should add a task from initial bar': (browser: NBrowser) => browser 'should add a task from initial bar': (browser: NBrowser) =>
.url(WORK_VIEW_URL) browser
.waitForElementVisible(ADD_TASK_INITIAL) .url(WORK_VIEW_URL)
.waitForElementVisible(ADD_TASK_INITIAL)
.setValue(ADD_TASK_INITIAL, '1 test task hihi') .setValue(ADD_TASK_INITIAL, '1 test task hihi')
.setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER)
.waitForElementVisible(TASK) .waitForElementVisible(TASK)
.assert.visible(TASK) .assert.visible(TASK)
.assert.containsText(TASK, '1 test task hihi') .assert.containsText(TASK, '1 test task hihi')
.end(), .end(),
'should add 2 tasks from initial bar': (browser: NBrowser) => browser 'should add 2 tasks from initial bar': (browser: NBrowser) =>
.url(WORK_VIEW_URL) browser
.waitForElementVisible(ADD_TASK_INITIAL) .url(WORK_VIEW_URL)
.waitForElementVisible(ADD_TASK_INITIAL)
.setValue(ADD_TASK_INITIAL, '2 test task hihi') .setValue(ADD_TASK_INITIAL, '2 test task hihi')
.setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER)
.setValue(ADD_TASK_INITIAL, '3 some other task') .setValue(ADD_TASK_INITIAL, '3 some other task')
.setValue(ADD_TASK_INITIAL, browser.Keys.ENTER) .setValue(ADD_TASK_INITIAL, browser.Keys.ENTER)
.waitForElementVisible(TASK) .waitForElementVisible(TASK)
.assert.visible(TASK) .assert.visible(TASK)
.assert.containsText(TASK + ':nth-child(1)', '2 test task hihi') .assert.containsText(TASK + ':nth-child(1)', '2 test task hihi')
.assert.containsText(TASK + ':nth-child(2)', '3 some other task') .assert.containsText(TASK + ':nth-child(2)', '3 some other task')
.end(), .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 .setValue(ADD_TASK_GLOBAL, '4 test task hohoho')
.url(WORK_VIEW_URL) .setValue(ADD_TASK_GLOBAL, browser.Keys.ENTER)
.waitForElementVisible(ADD_TASK_BTN) .setValue(ADD_TASK_GLOBAL, '5 some other task xoxo')
.click(ADD_TASK_BTN) .setValue(ADD_TASK_GLOBAL, browser.Keys.ENTER)
.waitForElementVisible(ADD_TASK_GLOBAL)
.setValue(ADD_TASK_GLOBAL, '4 test task hohoho') .waitForElementVisible(TASK)
.setValue(ADD_TASK_GLOBAL, browser.Keys.ENTER) .assert.visible(TASK)
.setValue(ADD_TASK_GLOBAL, '5 some other task xoxo') // NOTE: global adds to top rather than bottom
.setValue(ADD_TASK_GLOBAL, browser.Keys.ENTER) .assert.containsText(TASK + ':nth-child(1)', '5 some other task xoxo')
.assert.containsText(TASK + ':nth-child(2)', '4 test task hohoho')
.end(),
.waitForElementVisible(TASK) 'should still show created task after reload': (browser: NBrowser) =>
.assert.visible(TASK) browser
// NOTE: global adds to top rather than bottom .url(WORK_VIEW_URL)
.assert.containsText(TASK + ':nth-child(1)', '5 some other task xoxo') .waitForElementVisible(READY_TO_WORK_BTN)
.assert.containsText(TASK + ':nth-child(2)', '4 test task hohoho') .addTask('0 test task lolo')
.end(), .waitForElementVisible(TASK)
.execute('window.location.reload()')
.waitForElementVisible(TASK)
'should still show created task after reload': (browser: NBrowser) => browser .assert.visible(TASK)
.url(WORK_VIEW_URL) .assert.containsText(TASK, '0 test task lolo')
.waitForElementVisible(READY_TO_WORK_BTN) .end(),
.addTask('0 test task lolo')
.waitForElementVisible(TASK)
.execute('window.location.reload()')
.waitForElementVisible(TASK)
.assert.visible(TASK)
.assert.containsText(TASK, '0 test task lolo')
.end(),
}; };

View file

@ -1,5 +1,12 @@
import { app, ipcMain } from 'electron'; 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 { IPC } from './ipc-events.const';
import { answerRenderer } from './better-ipc'; import { answerRenderer } from './better-ipc';
import { LocalBackupMeta } from '../src/app/imex/local-backup/local-backup.model'; import { LocalBackupMeta } from '../src/app/imex/local-backup/local-backup.model';
@ -24,22 +31,27 @@ export function initBackupAdapter(backupDir: string) {
if (!files.length) { if (!files.length) {
return false; return false;
} }
const filesWithMeta: LocalBackupMeta[] = files.map((fileName: string): LocalBackupMeta => ({ const filesWithMeta: LocalBackupMeta[] = files.map(
name: fileName, (fileName: string): LocalBackupMeta => ({
path: path.join(BACKUP_DIR, fileName), name: fileName,
folder: BACKUP_DIR, path: path.join(BACKUP_DIR, fileName),
created: statSync(path.join(BACKUP_DIR, fileName)).mtime.getTime() folder: BACKUP_DIR,
})); created: statSync(path.join(BACKUP_DIR, fileName)).mtime.getTime(),
}),
);
filesWithMeta.sort((a: LocalBackupMeta, b: LocalBackupMeta) => a.created - b.created); 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]; return filesWithMeta.reverse()[0];
}); });
// RESTORE_BACKUP // RESTORE_BACKUP
answerRenderer(IPC.BACKUP_LOAD_DATA, (backupPath): string => { answerRenderer(IPC.BACKUP_LOAD_DATA, (backupPath): string => {
console.log('Reading backup file: ', backupPath); 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 mm = today.getMonth() + 1; // January is 0!
const yyyy = today.getFullYear(); const yyyy = today.getFullYear();
const dds = (dd < 10) const dds = dd < 10 ? '0' + dd : dd.toString();
? '0' + dd const mms = mm < 10 ? '0' + mm : mm.toString();
: dd.toString();
const mms = (mm < 10)
? '0' + mm
: mm.toString();
return `${yyyy}-${mms}-${dds}`; return `${yyyy}-${mms}-${dds}`;
} }

View file

@ -1,10 +1,14 @@
import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron'; import { BrowserWindow, ipcMain, IpcMainEvent } from 'electron';
// TODO make available for both // 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 // TODO add all typing
export const answerRenderer = (browserWindowOrChannel, channelOrCallback, callbackOrNothing?) => { export const answerRenderer = (
browserWindowOrChannel,
channelOrCallback,
callbackOrNothing?,
) => {
let window; let window;
let channel; let channel;
let callback; let callback;
@ -25,7 +29,9 @@ export const answerRenderer = (browserWindowOrChannel, channelOrCallback, callba
const sendChannel = getSendChannel(channel); const sendChannel = getSendChannel(channel);
const listener = async (event: IpcMainEvent, data) => { 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) { if (window && window.id !== browserWindow?.id) {
return; return;
@ -37,7 +43,7 @@ export const answerRenderer = (browserWindowOrChannel, channelOrCallback, callba
} }
}; };
const {dataChannel, errorChannel, userData} = data; const { dataChannel, errorChannel, userData } = data;
try { try {
send(dataChannel, await callback(userData, browserWindow)); send(dataChannel, await callback(userData, browserWindow));

View file

@ -29,7 +29,7 @@ let iface;
function init(params) { function init(params) {
sessionBus = dbus.sessionBus(); sessionBus = dbus.sessionBus();
// Check the connection was successful // Check the connection was successful
if (!sessionBus) { if (!sessionBus) {
isDBusError = true; isDBusError = true;
errorHandler(`DBus: Could not connect to the DBus session bus.`); 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 there was an error, warn user and fail
if (e) { if (e) {
isDBusError = true; 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 // Return code 0x1 means we successfully had the name
@ -52,11 +54,13 @@ function init(params) {
*/ */
} else { } else {
isDBusError = true; 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() { function proceed() {
// First, we need to create our interface description (here we will only expose method calls) // First, we need to create our interface description (here we will only expose method calls)
ifaceDesc = { ifaceDesc = {
@ -108,7 +112,7 @@ function init(params) {
}, },
emit: () => { emit: () => {
// no nothing, as usual // no nothing, as usual
} },
}; };
// Now we need to actually export our interface on our object // Now we need to actually export our interface on our object
@ -144,20 +148,22 @@ if (!isDBusError) {
} }
if (iface) { if (iface) {
iface.emit('pomodoroUpdate', (isOnBreak ? 1 : 0), currentSessionTime, currentSessionInitialTime); iface.emit(
'pomodoroUpdate',
isOnBreak ? 1 : 0,
currentSessionTime,
currentSessionInitialTime,
);
} else { } else {
errorHandler('DBus: interface not ready yet'); errorHandler('DBus: interface not ready yet');
isErrorShownOnce = true; isErrorShownOnce = true;
} }
} },
}; };
} else { } else {
module.exports = { module.exports = {
init: () => { init: () => {},
}, setTask: () => {},
setTask: () => { updatePomodoro: () => {},
},
updatePomodoro: () => {
}
}; };
} }

View file

@ -4,16 +4,16 @@ import OpenDevToolsOptions = Electron.OpenDevToolsOptions;
const electron = require('electron'); const electron = require('electron');
const localShortcut = require('electron-localshortcut'); const localShortcut = require('electron-localshortcut');
const {app, BrowserWindow} = electron; const { app, BrowserWindow } = electron;
const isMacOS = process.platform === 'darwin'; const isMacOS = process.platform === 'darwin';
const devToolsOptions: OpenDevToolsOptions = { const devToolsOptions: OpenDevToolsOptions = {
mode: 'bottom' mode: 'bottom',
}; };
function toggleDevTools(win = BrowserWindow.getFocusedWindow()) { function toggleDevTools(win = BrowserWindow.getFocusedWindow()) {
if (win) { if (win) {
const {webContents} = win; const { webContents } = win;
if (webContents.isDevToolsOpened()) { if (webContents.isDevToolsOpened()) {
webContents.closeDevTools(); webContents.closeDevTools();
} else { } else {
@ -72,11 +72,14 @@ function inspectElements() {
// }; // };
export const initDebug = (opts, isAddReload) => { export const initDebug = (opts, isAddReload) => {
opts = Object.assign({ opts = Object.assign(
enabled: null, {
showDevTools: true, enabled: null,
...devToolsOptions, showDevTools: true,
}, opts); ...devToolsOptions,
},
opts,
);
console.log(opts); console.log(opts);
if (opts.enabled === false) { if (opts.enabled === false) {
@ -114,5 +117,3 @@ export const initDebug = (opts, isAddReload) => {
} }
}); });
}; };

View file

@ -42,7 +42,10 @@ function _handleError(e, additionalLogInfo, errObj) {
stack, stack,
}); });
} else { } 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.'); error('Electron Error: Frontend not loaded. Could not send error to renderer.');
throw errObj; throw errObj;
} }

View file

@ -3,16 +3,20 @@ import { IPC } from './ipc-events.const';
export const getGitLog = (data) => { export const getGitLog = (data) => {
const exec = require('child_process').exec; const exec = require('child_process').exec;
const cmd = 'git --no-pager log --graph --pretty=format:\'%s (%cr) <%an>\' --abbrev-commit --since=4am'; 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
});
});
exec(
cmd,
{
cwd: data.cwd,
},
(error, stdout) => {
const mainWin = getWin();
mainWin.webContents.send(IPC.GIT_LOG_RESPONSE, {
stdout,
requestId: data.requestId,
});
},
);
}; };

View file

@ -7,7 +7,7 @@ import { getWin } from './main-window';
let tray; let tray;
let isIndicatorRunning = false; let isIndicatorRunning = false;
let DIR: string; let DIR: string;
let shouldUseDarkColors : boolean; let shouldUseDarkColors: boolean;
const isGnomeShellExtensionRunning = false; const isGnomeShellExtensionRunning = false;
@ -16,7 +16,7 @@ export const initIndicator = ({
quitApp, quitApp,
app, app,
ICONS_FOLDER, ICONS_FOLDER,
forceDarkTray forceDarkTray,
}: { }: {
showApp: () => void; showApp: () => void;
quitApp: () => void; quitApp: () => void;
@ -30,9 +30,7 @@ export const initIndicator = ({
initAppListeners(app); initAppListeners(app);
initListeners(); initListeners();
const suf = shouldUseDarkColors const suf = shouldUseDarkColors ? '-d.png' : '-l.png';
? '-d.png'
: '-l.png';
tray = new Tray(DIR + `stopped${suf}`); tray = new Tray(DIR + `stopped${suf}`);
tray.setContextMenu(createContextMenu(showApp, quitApp)); tray.setContextMenu(createContextMenu(showApp, quitApp));
@ -55,10 +53,8 @@ function initAppListeners(app) {
} }
function initListeners() { function initListeners() {
ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, {progress}) => { ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress }) => {
const suf = shouldUseDarkColors const suf = shouldUseDarkColors ? '-d' : '-l';
? '-d'
: '-l';
if (typeof progress === 'number' && progress > 0 && isFinite(progress)) { if (typeof progress === 'number' && progress > 0 && isFinite(progress)) {
const f = Math.min(Math.round(progress * 15), 15); const f = Math.min(Math.round(progress * 15), 15);
const t = DIR + `running-anim${suf}/${f || 0}.png`; const t = DIR + `running-anim${suf}/${f || 0}.png`;
@ -86,9 +82,7 @@ function initListeners() {
tray.setTitle(msg); tray.setTitle(msg);
} else { } else {
tray.setTitle(''); tray.setTitle('');
const suf = shouldUseDarkColors const suf = shouldUseDarkColors ? '-d.png' : '-l.png';
? '-d.png'
: '-l.png';
setTrayIcon(tray, DIR + `stopped${suf}`); setTrayIcon(tray, DIR + `stopped${suf}`);
} }
} }
@ -133,11 +127,13 @@ function createIndicatorStr(task): string {
function createContextMenu(showApp, quitApp) { function createContextMenu(showApp, quitApp) {
return Menu.buildFromTemplate([ 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; let curIco: string;
function setTrayIcon(tr: Tray, icoPath: string) { function setTrayIcon(tr: Tray, icoPath: string) {
if (icoPath !== curIco) { if (icoPath !== curIco) {
curIco = icoPath; curIco = icoPath;

View file

@ -53,4 +53,3 @@ export enum IPC {
maybe_PROJECT_CHANGED = 'PROJECT_CHANGED', maybe_PROJECT_CHANGED = 'PROJECT_CHANGED',
maybe_COMPLETE_DATA_RELOAD = 'COMPLETE_DATA_RELOAD', maybe_COMPLETE_DATA_RELOAD = 'COMPLETE_DATA_RELOAD',
} }

View file

@ -7,8 +7,17 @@ import { JiraCfg } from '../src/app/features/issue/providers/jira/jira.model';
import fetch from 'node-fetch'; import fetch from 'node-fetch';
import { Agent } from 'https'; import { Agent } from 'https';
export const sendJiraRequest = ({requestId, requestInit, url, jiraCfg}: export const sendJiraRequest = ({
{ requestId: string; requestInit: RequestInit; url: string, jiraCfg: JiraCfg }) => { requestId,
requestInit,
url,
jiraCfg,
}: {
requestId: string;
requestInit: RequestInit;
url: string;
jiraCfg: JiraCfg;
}) => {
const mainWin = getWin(); const mainWin = getWin();
// console.log('--------------------------------------------------------------------'); // console.log('--------------------------------------------------------------------');
// console.log(url); // console.log(url);
@ -19,11 +28,11 @@ export const sendJiraRequest = ({requestId, requestInit, url, jiraCfg}:
// allow self signed certificates // allow self signed certificates
...(jiraCfg && jiraCfg.isAllowSelfSignedCertificate ...(jiraCfg && jiraCfg.isAllowSelfSignedCertificate
? { ? {
agent: new Agent({ agent: new Agent({
rejectUnauthorized: false, rejectUnauthorized: false,
}) }),
} }
: {}) : {}),
}) })
.then((response) => { .then((response) => {
// console.log('JIRA_RAW_RESPONSE', 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); console.log('Jira Error Error Response ELECTRON: ', response);
try { try {
console.log(JSON.stringify(response)); console.log(JSON.stringify(response));
} catch (e) { } catch (e) {}
}
throw Error(response.statusText); throw Error(response.statusText);
} }
return response; return response;
}) })
.then(res => res.text()) .then((res) => res.text())
.then(text => text ? JSON.parse(text) : {}) .then((text) => (text ? JSON.parse(text) : {}))
.then((response) => { .then((response) => {
mainWin.webContents.send(IPC.JIRA_CB_EVENT, { mainWin.webContents.send(IPC.JIRA_CB_EVENT, {
response, response,
@ -56,7 +64,7 @@ export const sendJiraRequest = ({requestId, requestInit, url, jiraCfg}:
// TODO simplify and do encoding in frontend service // TODO simplify and do encoding in frontend service
export const setupRequestHeadersForImages = (jiraCfg: JiraCfg, wonkyCookie?: string) => { export const setupRequestHeadersForImages = (jiraCfg: JiraCfg, wonkyCookie?: string) => {
const {host, protocol} = parseHostAndPort(jiraCfg); const { host, protocol } = parseHostAndPort(jiraCfg);
// TODO export to util fn // TODO export to util fn
const _b64EncodeUnicode = (str) => { const _b64EncodeUnicode = (str) => {
@ -64,12 +72,12 @@ export const setupRequestHeadersForImages = (jiraCfg: JiraCfg, wonkyCookie?: str
}; };
const encoded = _b64EncodeUnicode(`${jiraCfg.userName}:${jiraCfg.password}`); const encoded = _b64EncodeUnicode(`${jiraCfg.userName}:${jiraCfg.password}`);
const filter = { const filter = {
urls: [`${protocol}://${host}/*`] urls: [`${protocol}://${host}/*`],
}; };
if (jiraCfg.isWonkyCookieMode && !wonkyCookie) { if (jiraCfg.isWonkyCookieMode && !wonkyCookie) {
session.defaultSession.webRequest.onBeforeSendHeaders(filter, (details, callback) => { 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 { } else {
details.requestHeaders.authorization = `Basic ${encoded}`; details.requestHeaders.authorization = `Basic ${encoded}`;
} }
callback({requestHeaders: details.requestHeaders}); callback({ requestHeaders: details.requestHeaders });
}); });
}; };
const MATCH_PROTOCOL_REG_EX = /(^[^:]+):\/\//; const MATCH_PROTOCOL_REG_EX = /(^[^:]+):\/\//;
const MATCH_PORT_REG_EX = /:\d{2,4}/; 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 host: string = config.host as string;
let protocol; let protocol;
let port; let port;
@ -118,5 +128,5 @@ const parseHostAndPort = (config: JiraCfg): { host: string, protocol: string, po
} }
// console.log({host, protocol, port}); // console.log({host, protocol, port});
return {host, protocol, port}; return { host, protocol, port };
}; };

View file

@ -1,4 +1,7 @@
export const lazySetInterval = (func: () => void, intervalDuration: number): () => void => { export const lazySetInterval = (
func: () => void,
intervalDuration: number,
): (() => void) => {
let lastTimeoutId: any; let lastTimeoutId: any;
const interval = () => { const interval = () => {

View file

@ -2,14 +2,16 @@ import { exec } from 'child_process';
export default (cb?, customCommands?) => { export default (cb?, customCommands?) => {
const lockCommands = 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', 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) { if (Object.keys(lockCommands).indexOf(process.platform) === -1) {
throw new Error(`lockscreen doesn't support your platform (${process.platform})`); throw new Error(`lockscreen doesn't support your platform (${process.platform})`);
} else { } else {
exec(lockCommands[process.platform], (err, stdout) => cb ? cb(err, stdout) : null); exec(lockCommands[process.platform], (err, stdout) => (cb ? cb(err, stdout) : null));
} }
}; };

View file

@ -7,7 +7,7 @@ import {
Menu, Menu,
MenuItemConstructorOptions, MenuItemConstructorOptions,
MessageBoxReturnValue, MessageBoxReturnValue,
shell shell,
} from 'electron'; } from 'electron';
import { errorHandler } from './error-handler'; import { errorHandler } from './error-handler';
import { join, normalize } from 'path'; import { join, normalize } from 'path';
@ -23,7 +23,7 @@ const mainWinModule: {
isAppReady: boolean; isAppReady: boolean;
} = { } = {
win: undefined, win: undefined,
isAppReady: false isAppReady: false,
}; };
export const getWin = (): BrowserWindow => { export const getWin = (): BrowserWindow => {
@ -65,7 +65,7 @@ export const createWindow = ({
const mainWindowState = windowStateKeeper({ const mainWindowState = windowStateKeeper({
defaultWidth: 800, defaultWidth: 800,
defaultHeight: 800 defaultHeight: 800,
}); });
mainWin = new BrowserWindow({ mainWin = new BrowserWindow({
@ -81,18 +81,18 @@ export const createWindow = ({
nodeIntegration: true, nodeIntegration: true,
// make remote module work with those two settings // make remote module work with those two settings
enableRemoteModule: true, enableRemoteModule: true,
contextIsolation: false contextIsolation: false,
}, },
icon: ICONS_FOLDER + '/icon_256x256.png' icon: ICONS_FOLDER + '/icon_256x256.png',
}); });
mainWindowState.manage(mainWin); mainWindowState.manage(mainWin);
const url = customUrl const url = customUrl
? customUrl ? customUrl
: (IS_DEV) : IS_DEV
? 'http://localhost:4200' ? 'http://localhost:4200'
: format({ : format({
pathname: normalize(join(__dirname, '../dist/index.html')), pathname: normalize(join(__dirname, '../dist/index.html')),
protocol: 'file:', protocol: 'file:',
slashes: true, slashes: true,
@ -106,7 +106,7 @@ export const createWindow = ({
console.log('No custom styles detected at ' + CSS_FILE_PATH); console.log('No custom styles detected at ' + CSS_FILE_PATH);
} else { } else {
console.log('Loading custom styles from ' + CSS_FILE_PATH); 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); mainWin.webContents.insertCSS(styles).then(console.log).catch(console.error);
} }
}); });
@ -142,8 +142,7 @@ function initWinEventListeners(app: any) {
event.preventDefault(); event.preventDefault();
// needed for mac; especially for jira urls we might have a host like this www.host.de// // needed for mac; especially for jira urls we might have a host like this www.host.de//
const urlObj = new URL(url); const urlObj = new URL(url);
urlObj.pathname = urlObj.pathname urlObj.pathname = urlObj.pathname.replace('//', '/');
.replace('//', '/');
const wellFormedUrl = urlObj.toString(); const wellFormedUrl = urlObj.toString();
const wasOpened = shell.openExternal(wellFormedUrl); const wasOpened = shell.openExternal(wellFormedUrl);
if (!wasOpened) { if (!wasOpened) {
@ -162,27 +161,30 @@ function initWinEventListeners(app: any) {
function createMenu(quitApp) { function createMenu(quitApp) {
// Create application menu to enable copy & pasting on MacOS // Create application menu to enable copy & pasting on MacOS
const menuTpl = [{ const menuTpl = [
label: 'Application', {
submenu: [ label: 'Application',
{label: 'About Super Productivity', selector: 'orderFrontStandardAboutPanel:'}, submenu: [
{type: 'separator'}, { label: 'About Super Productivity', selector: 'orderFrontStandardAboutPanel:' },
{ { type: 'separator' },
label: 'Quit', click: quitApp {
} label: 'Quit',
] click: quitApp,
}, { },
label: 'Edit', ],
submenu: [ },
{label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:'}, {
{label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:'}, label: 'Edit',
{type: 'separator'}, submenu: [
{label: 'Cut', accelerator: 'CmdOrCtrl+X', selector: 'cut:'}, { label: 'Undo', accelerator: 'CmdOrCtrl+Z', selector: 'undo:' },
{label: 'Copy', accelerator: 'CmdOrCtrl+C', selector: 'copy:'}, { label: 'Redo', accelerator: 'Shift+CmdOrCtrl+Z', selector: 'redo:' },
{label: 'Paste', accelerator: 'CmdOrCtrl+V', selector: 'paste:'}, { type: 'separator' },
{label: 'Select All', accelerator: 'CmdOrCtrl+A', selector: 'selectAll:'} { 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[]; const menuTplOUT = menuTpl as MenuItemConstructorOptions[];
@ -191,9 +193,7 @@ function createMenu(quitApp) {
} }
// TODO this is ugly as f+ck // TODO this is ugly as f+ck
const appCloseHandler = ( const appCloseHandler = (app: App) => {
app: App,
) => {
let ids: string[] = []; let ids: string[] = [];
const _quitApp = () => { const _quitApp = () => {
@ -201,14 +201,14 @@ const appCloseHandler = (
mainWin.close(); mainWin.close();
}; };
ipcMain.on(IPC.REGISTER_BEFORE_CLOSE, (ev, {id}) => { ipcMain.on(IPC.REGISTER_BEFORE_CLOSE, (ev, { id }) => {
ids.push(id); ids.push(id);
}); });
ipcMain.on(IPC.UNREGISTER_BEFORE_CLOSE, (ev, {id}) => { ipcMain.on(IPC.UNREGISTER_BEFORE_CLOSE, (ev, { id }) => {
ids = ids.filter(idIn => idIn !== id); ids = ids.filter((idIn) => idIn !== id);
}); });
ipcMain.on(IPC.BEFORE_CLOSE_DONE, (ev, {id}) => { ipcMain.on(IPC.BEFORE_CLOSE_DONE, (ev, { id }) => {
ids = ids.filter(idIn => idIn !== id); ids = ids.filter((idIn) => idIn !== id);
console.log(IPC.BEFORE_CLOSE_DONE, id, ids); console.log(IPC.BEFORE_CLOSE_DONE, id, ids);
if (ids.length === 0) { if (ids.length === 0) {
mainWin.close(); mainWin.close();
@ -230,20 +230,21 @@ const appCloseHandler = (
return; return;
} }
if (appCfg && appCfg.misc.isConfirmBeforeExit && !(app as any).isQuiting) { if (appCfg && appCfg.misc.isConfirmBeforeExit && !(app as any).isQuiting) {
dialog.showMessageBox(mainWin, dialog
{ .showMessageBox(mainWin, {
type: 'question', type: 'question',
buttons: ['Yes', 'No'], buttons: ['Yes', 'No'],
title: 'Confirm', title: 'Confirm',
message: 'Are you sure you want to quit?' message: 'Are you sure you want to quit?',
}).then((choice: MessageBoxReturnValue) => { })
if (choice.response === 1) { .then((choice: MessageBoxReturnValue) => {
return; if (choice.response === 1) {
} else if (choice.response === 0) { return;
_quitApp(); } else if (choice.response === 0) {
return; _quitApp();
} return;
}); }
});
} else { } else {
_quitApp(); _quitApp();
} }
@ -253,9 +254,7 @@ const appCloseHandler = (
}); });
}; };
const appMinimizeHandler = ( const appMinimizeHandler = (app: App) => {
app: App,
) => {
if (!(app as any).isQuiting) { if (!(app as any).isQuiting) {
mainWin.on('minimize', (event) => { mainWin.on('minimize', (event) => {
getSettings(mainWin, (appCfg) => { getSettings(mainWin, (appCfg) => {

View file

@ -1,5 +1,13 @@
'use strict'; '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 * as electronDl from 'electron-dl';
import { info } from 'electron-log'; import { info } from 'electron-log';
@ -23,7 +31,7 @@ const ICONS_FOLDER = __dirname + '/assets/icons/';
const IS_MAC = process.platform === 'darwin'; const IS_MAC = process.platform === 'darwin';
const IS_LINUX = process.platform === 'linux'; const IS_LINUX = process.platform === 'linux';
const DESKTOP_ENV = process.env.DESKTOP_SESSION; 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'; const IS_DEV = process.env.NODE_ENV === 'DEV';
let isShowDevTools: boolean = IS_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 // NOTE: to get rid of the warning => https://github.com/electron/electron/issues/18397
appIN.allowRendererProcessReuse = true; appIN.allowRendererProcessReuse = true;
initDebug({showDevTools: isShowDevTools}, IS_DEV); initDebug({ showDevTools: isShowDevTools }, IS_DEV);
// NOTE: opening the folder crashes the mas build // NOTE: opening the folder crashes the mas build
if (!IS_MAC) { if (!IS_MAC) {
electronDl({openFolderWhenDone: true}); electronDl({ openFolderWhenDone: true });
} }
let mainWin: BrowserWindow; let mainWin: BrowserWindow;
// keep app active to keep time tracking running // keep app active to keep time tracking running
@ -133,7 +141,9 @@ appIN.on('ready', () => {
const sendIdleMsgIfOverMin = (idleTime) => { const sendIdleMsgIfOverMin = (idleTime) => {
// sometimes when starting a second instance we get here although we don't want to // sometimes when starting a second instance we get here although we don't want to
if (!mainWin) { 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; 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) { 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); registerShowAppShortCuts(cfg);
}); });
ipcMain.on(IPC.JIRA_SETUP_IMG_HEADERS, (ev, {jiraCfg, wonkyCookie}: { jiraCfg: JiraCfg; wonkyCookie?: string }) => { ipcMain.on(
setupRequestHeadersForImages(jiraCfg, wonkyCookie); IPC.JIRA_SETUP_IMG_HEADERS,
}); (ev, { jiraCfg, wonkyCookie }: { jiraCfg: JiraCfg; wonkyCookie?: string }) => {
setupRequestHeadersForImages(jiraCfg, wonkyCookie);
},
);
ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => { ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => {
sendJiraRequest(request); sendJiraRequest(request);
@ -263,7 +276,7 @@ function createIndicator() {
showApp, showApp,
quitApp, quitApp,
ICONS_FOLDER, ICONS_FOLDER,
forceDarkTray forceDarkTray,
}); });
} }
@ -290,7 +303,7 @@ function registerShowAppShortCuts(cfg: KeyboardConfig) {
if (cfg) { if (cfg) {
Object.keys(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) => { .forEach((key) => {
let actionFn: () => void; let actionFn: () => void;
const shortcut = cfg[key]; 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 // sometimes when starting a second instance we get here although we don't want to
if (!win) { 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; return;
} }

View file

@ -4,7 +4,7 @@ import {
HostListener, HostListener,
OnDestroy, OnDestroy,
ViewChild, ViewChild,
ViewContainerRef ViewContainerRef,
} from '@angular/core'; } from '@angular/core';
import { ChromeExtensionInterfaceService } from './core/chrome-extension-interface/chrome-extension-interface.service'; import { ChromeExtensionInterfaceService } from './core/chrome-extension-interface/chrome-extension-interface.service';
import { ShortcutService } from './core-ui/shortcut/shortcut.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', selector: 'app-root',
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'], styleUrls: ['./app.component.scss'],
animations: [ animations: [blendInOutAnimation, expandAnimation, warpRouteAnimation, fadeAnimation],
blendInOutAnimation,
expandAnimation,
warpRouteAnimation,
fadeAnimation
],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class AppComponent implements OnDestroy { export class AppComponent implements OnDestroy {
productivityTipTitle: string = productivityTip && productivityTip[0]; productivityTipTitle: string = productivityTip && productivityTip[0];
productivityTipText: string = productivityTip && productivityTip[1]; productivityTipText: string = productivityTip && productivityTip[1];
@ViewChild('notesElRef', {read: ViewContainerRef}) notesElRef?: ViewContainerRef; @ViewChild('notesElRef', { read: ViewContainerRef }) notesElRef?: ViewContainerRef;
@ViewChild('sideNavElRef', {read: ViewContainerRef}) sideNavElRef?: ViewContainerRef; @ViewChild('sideNavElRef', { read: ViewContainerRef }) sideNavElRef?: ViewContainerRef;
isRTL: boolean = false; isRTL: boolean = false;
@ -119,9 +114,15 @@ export class AppComponent implements OnDestroy {
this._initElectronErrorHandler(); this._initElectronErrorHandler();
this._uiHelperService.initElectron(); this._uiHelperService.initElectron();
(this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.TRANSFER_SETTINGS_REQUESTED, () => { (this._electronService.ipcRenderer as typeof ipcRenderer).on(
(this._electronService.ipcRenderer as typeof ipcRenderer).send(IPC.TRANSFER_SETTINGS_TO_ELECTRON, this._globalConfigService.cfg); IPC.TRANSFER_SETTINGS_REQUESTED,
}); () => {
(this._electronService.ipcRenderer as typeof ipcRenderer).send(
IPC.TRANSFER_SETTINGS_TO_ELECTRON,
this._globalConfigService.cfg,
);
},
);
} else { } else {
// WEB VERSION // WEB VERSION
if (this._swUpdate.isEnabled) { if (this._swUpdate.isEnabled) {
@ -164,7 +165,9 @@ export class AppComponent implements OnDestroy {
@HostListener('document:paste', ['$event']) @HostListener('document:paste', ['$event'])
async onPaste(ev: ClipboardEvent) { async onPaste(ev: ClipboardEvent) {
if (await this.workContextService.isActiveWorkContextProject$.pipe(first()).toPromise()) { if (
await this.workContextService.isActiveWorkContextProject$.pipe(first()).toPromise()
) {
this._bookmarkService.createFromPaste(ev); this._bookmarkService.createFromPaste(ev);
} }
} }
@ -184,14 +187,14 @@ export class AppComponent implements OnDestroy {
label: T.APP.B_INSTALL.INSTALL, label: T.APP.B_INSTALL.INSTALL,
fn: () => { fn: () => {
e.prompt(); e.prompt();
} },
}, },
action2: { action2: {
label: T.APP.B_INSTALL.IGNORE, label: T.APP.B_INSTALL.IGNORE,
fn: () => { fn: () => {
sessionStorage.setItem(SS_WEB_APP_INSTALL, 'true'); sessionStorage.setItem(SS_WEB_APP_INSTALL, 'true');
} },
} },
}); });
} }
@ -200,11 +203,15 @@ export class AppComponent implements OnDestroy {
} }
scrollToNotes() { scrollToNotes() {
(this.notesElRef as ViewContainerRef).element.nativeElement.scrollIntoView({behavior: 'smooth'}); (this.notesElRef as ViewContainerRef).element.nativeElement.scrollIntoView({
behavior: 'smooth',
});
} }
scrollToSidenav() { scrollToSidenav() {
(this.sideNavElRef as ViewContainerRef).element.nativeElement.scrollIntoView({behavior: 'smooth'}); (this.sideNavElRef as ViewContainerRef).element.nativeElement.scrollIntoView({
behavior: 'smooth',
});
} }
ngOnDestroy() { ngOnDestroy() {
@ -212,21 +219,26 @@ export class AppComponent implements OnDestroy {
} }
private _initElectronErrorHandler() { private _initElectronErrorHandler() {
(this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.ERROR, (ev, data: { (this._electronService.ipcRenderer as typeof ipcRenderer).on(
error: any; IPC.ERROR,
stack: any; (
errorStr: string | unknown; ev,
}) => { data: {
const errMsg = (typeof data.errorStr === 'string') error: any;
? data.errorStr stack: any;
: ' INVALID ERROR MSG :( '; errorStr: string | unknown;
},
) => {
const errMsg =
typeof data.errorStr === 'string' ? data.errorStr : ' INVALID ERROR MSG :( ';
this._snackService.open({ this._snackService.open({
msg: errMsg, msg: errMsg,
type: 'ERROR' type: 'ERROR',
}); });
console.error(data); console.error(data);
}); },
);
} }
private _initOfflineBanner() { private _initOfflineBanner() {
@ -246,46 +258,48 @@ export class AppComponent implements OnDestroy {
private _requestPersistence() { private _requestPersistence() {
if (navigator.storage) { if (navigator.storage) {
// try to avoid data-loss // try to avoid data-loss
Promise.all([ Promise.all([navigator.storage.persisted()])
navigator.storage.persisted(), .then(([persisted]): any => {
]).then(([persisted]): any => { if (!persisted) {
if (!persisted) { return navigator.storage.persist().then((granted) => {
return navigator.storage.persist()
.then(granted => {
if (granted) { if (granted) {
console.log('Persistent store granted'); console.log('Persistent store granted');
} else { } else {
const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED; const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED;
console.warn('Persistence not allowed'); console.warn('Persistence not allowed');
this._snackService.open({msg}); this._snackService.open({ msg });
} }
}); });
} else {
} else { console.log('Persistence already allowed');
console.log('Persistence already allowed'); }
} })
}).catch((e) => { .catch((e) => {
console.log(e); console.log(e);
const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED; const msg = T.GLOBAL_SNACK.PERSISTENCE_DISALLOWED;
this._snackService.open({msg}); this._snackService.open({ msg });
}); });
} }
} }
private _checkAvailableStorage() { private _checkAvailableStorage() {
if (environment.production) { if (environment.production) {
if ('storage' in navigator && 'estimate' in navigator.storage) { 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 u = usage || 0;
const q = quota || 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 usageInMib = Math.round(u / (1024 * 1024));
const quotaInMib = Math.round(q / (1024 * 1024)); const quotaInMib = Math.round(q / (1024 * 1024));
const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`; const details = `${usageInMib} out of ${quotaInMib} MiB used (${percentUsed}%)`;
console.log(details); console.log(details);
if ((quotaInMib - usageInMib) <= 333) { if (quotaInMib - usageInMib <= 333) {
alert(`There is only very little disk space available (${quotaInMib - usageInMib}mb). This might affect how the app is running.`); alert(
`There is only very little disk space available (${
quotaInMib - usageInMib
}mb). This might affect how the app is running.`,
);
} }
}); });
} }

View file

@ -1,5 +1,5 @@
export const WORKLOG_DATE_STR_FORMAT = 'YYYY-MM-DD'; 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 TRACKING_INTERVAL = 1000;
export const MODEL_VERSION_KEY = '__modelVersion'; export const MODEL_VERSION_KEY = '__modelVersion';
@ -118,10 +118,7 @@ export const AUTO_SWITCH_LNGS: LanguageCode[] = [
LanguageCode.tr, LanguageCode.tr,
]; ];
export const RTL_LANGUAGES: LanguageCode[] = [ export const RTL_LANGUAGES: LanguageCode[] = [LanguageCode.ar, LanguageCode.fa];
LanguageCode.ar,
LanguageCode.fa
];
export enum THEME_COLOR_MAP { export enum THEME_COLOR_MAP {
'light-blue' = '#03a9f4', 'light-blue' = '#03a9f4',

View file

@ -1,5 +1,11 @@
import { Injectable } from '@angular/core'; 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 { WorkContextService } from './features/work-context/work-context.service';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { concatMap, map, switchMap, take } from 'rxjs/operators'; 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 { ProjectService } from './features/project/project.service';
import { DataInitService } from './core/data-init/data-init.service'; import { DataInitService } from './core/data-init/data-init.service';
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class ActiveWorkContextGuard implements CanActivate { export class ActiveWorkContextGuard implements CanActivate {
constructor( constructor(private _workContextService: WorkContextService, private _router: Router) {}
private _workContextService: WorkContextService,
private _router: Router,
) {
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<UrlTree> { canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<UrlTree> {
return this._workContextService.activeWorkContextTypeAndId$.pipe( return this._workContextService.activeWorkContextTypeAndId$.pipe(
take(1), take(1),
switchMap(({activeType, activeId}) => { switchMap(({ activeType, activeId }) => {
const {subPageType, param} = next.params; const { subPageType, param } = next.params;
const base = activeType === WorkContextType.TAG const base = activeType === WorkContextType.TAG ? 'tag' : 'project';
? 'tag'
: 'project';
const url = `/${base}/${activeId}/${subPageType}${param ? '/' + param : ''}`; const url = `/${base}/${activeId}/${subPageType}${param ? '/' + param : ''}`;
return of(this._router.parseUrl(url)); return of(this._router.parseUrl(url));
}) }),
); );
} }
} }
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class ValidTagIdGuard implements CanActivate { export class ValidTagIdGuard implements CanActivate {
constructor( constructor(
private _tagService: TagService, private _tagService: TagService,
private _dataInitService: DataInitService, private _dataInitService: DataInitService,
) { ) {}
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { canActivate(
const {id} = next.params; next: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> {
const { id } = next.params;
return this._dataInitService.isAllDataLoadedInitially$.pipe( return this._dataInitService.isAllDataLoadedInitially$.pipe(
concatMap(() => this._tagService.getTagById$(id)), concatMap(() => this._tagService.getTagById$(id)),
take(1), take(1),
map(tag => !!tag), map((tag) => !!tag),
); );
} }
} }
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class ValidProjectIdGuard implements CanActivate { export class ValidProjectIdGuard implements CanActivate {
constructor( constructor(
private _projectService: ProjectService, private _projectService: ProjectService,
private _dataInitService: DataInitService, private _dataInitService: DataInitService,
) { ) {}
}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> { canActivate(
const {id} = next.params; next: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> {
const { id } = next.params;
return this._dataInitService.isAllDataLoadedInitially$.pipe( return this._dataInitService.isAllDataLoadedInitially$.pipe(
concatMap(() => this._projectService.getByIdOnce$(id)), concatMap(() => this._projectService.getByIdOnce$(id)),
map(project => !!project), map((project) => !!project),
); );
} }
} }

View file

@ -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 { ErrorHandler, NgModule } from '@angular/core';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';
@ -47,9 +47,7 @@ export function createTranslateLoader(http: HttpClient) {
} }
@NgModule({ @NgModule({
declarations: [ declarations: [AppComponent],
AppComponent,
],
imports: [ imports: [
// Those features need to be included first for store not to mess up, probably because we use it initially at many places // Those features need to be included first for store not to mess up, probably because we use it initially at many places
ConfigModule, ConfigModule,
@ -79,13 +77,12 @@ export function createTranslateLoader(http: HttpClient) {
BrowserAnimationsModule, BrowserAnimationsModule,
HttpClientModule, HttpClientModule,
HammerModule, 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 // NOTE: both need to be present to use forFeature stores
StoreModule.forRoot(reducers, StoreModule.forRoot(reducers, {
{ metaReducers: [undoTaskDeleteMetaReducer, actionLoggerReducer],
metaReducers: [undoTaskDeleteMetaReducer, actionLoggerReducer], ...(environment.production
...(environment.production ? {
? {
runtimeChecks: { runtimeChecks: {
strictStateImmutability: false, strictStateImmutability: false,
strictActionImmutability: false, strictActionImmutability: false,
@ -93,47 +90,44 @@ export function createTranslateLoader(http: HttpClient) {
strictActionSerializability: false, strictActionSerializability: false,
}, },
} }
: { : {
runtimeChecks: { runtimeChecks: {
strictStateImmutability: true, strictStateImmutability: true,
strictActionImmutability: true, strictActionImmutability: true,
strictStateSerializability: true, strictStateSerializability: true,
strictActionSerializability: true, strictActionSerializability: true,
}, },
}) }),
} }),
),
EffectsModule.forRoot([]), EffectsModule.forRoot([]),
(!environment.production && !environment.stage) ? StoreDevtoolsModule.instrument() : [], !environment.production && !environment.stage ? StoreDevtoolsModule.instrument() : [],
ReactiveFormsModule, ReactiveFormsModule,
FormlyModule.forRoot({ FormlyModule.forRoot({
extras: { extras: {
immutable: true immutable: true,
}, },
validationMessages: [ validationMessages: [{ name: 'pattern', message: 'Invalid input' }],
{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({ TranslateModule.forRoot({
loader: { loader: {
provide: TranslateLoader, provide: TranslateLoader,
useFactory: createTranslateLoader, useFactory: createTranslateLoader,
deps: [HttpClient] deps: [HttpClient],
} },
}), }),
EntityDataModule EntityDataModule,
], ],
bootstrap: [AppComponent], bootstrap: [AppComponent],
providers: [ providers: [
{provide: ErrorHandler, useClass: GlobalErrorHandler}, { provide: ErrorHandler, useClass: GlobalErrorHandler },
{provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig}, { provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig },
], ],
}) })
export class AppModule { export class AppModule {
constructor( constructor(private _languageService: LanguageService) {
private _languageService: LanguageService,
) {
this._languageService.setDefault(LanguageCode.en); this._languageService.setDefault(LanguageCode.en);
this._languageService.setFromBrowserLngIfAutoSwitchLng(); this._languageService.setFromBrowserLngIfAutoSwitchLng();
} }

View file

@ -9,88 +9,112 @@ import { ProcrastinationComponent } from './features/procrastination/procrastina
import { SchedulePageComponent } from './pages/schedule-page/schedule-page.component'; import { SchedulePageComponent } from './pages/schedule-page/schedule-page.component';
import { ProjectSettingsPageComponent } from './pages/project-settings-page/project-settings-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 { 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 { TagSettingsPageComponent } from './pages/tag-settings-page/tag-settings-page.component';
import { TODAY_TAG } from './features/tag/tag.const'; import { TODAY_TAG } from './features/tag/tag.const';
export const APP_ROUTES: Routes = [ export const APP_ROUTES: Routes = [
{path: 'config', component: ConfigPageComponent, data: {page: 'config'}}, { path: 'config', component: ConfigPageComponent, data: { page: 'config' } },
{path: 'schedule', component: SchedulePageComponent, data: {page: 'schedule'}}, { path: 'schedule', component: SchedulePageComponent, data: { page: 'schedule' } },
{path: 'procrastination', component: ProcrastinationComponent, data: {page: 'procrastination'}}, {
path: 'procrastination',
component: ProcrastinationComponent,
data: { page: 'procrastination' },
},
{ {
path: 'tag/:id/tasks', path: 'tag/:id/tasks',
component: TagTaskPageComponent, component: TagTaskPageComponent,
data: {page: 'tag-tasks'}, data: { page: 'tag-tasks' },
canActivate: [ValidTagIdGuard] canActivate: [ValidTagIdGuard],
}, },
{ {
path: 'tag/:id/settings', path: 'tag/:id/settings',
component: TagSettingsPageComponent, component: TagSettingsPageComponent,
data: {page: 'tag-settings'}, data: { page: 'tag-settings' },
canActivate: [ValidTagIdGuard] canActivate: [ValidTagIdGuard],
}, },
{ {
path: 'tag/:id/worklog', path: 'tag/:id/worklog',
component: WorklogComponent, component: WorklogComponent,
data: {page: 'worklog'}, data: { page: 'worklog' },
canActivate: [ValidTagIdGuard] canActivate: [ValidTagIdGuard],
}, },
// {path: 'tag/:id/metrics', component: MetricPageComponent, data: {page: 'metrics'}, canActivate: [ValidContextIdGuard]}, // {path: 'tag/:id/metrics', component: MetricPageComponent, data: {page: 'metrics'}, canActivate: [ValidContextIdGuard]},
{ {
path: 'tag/:id/daily-summary', path: 'tag/:id/daily-summary',
component: DailySummaryComponent, component: DailySummaryComponent,
data: {page: 'daily-summary'}, data: { page: 'daily-summary' },
canActivate: [ValidTagIdGuard] canActivate: [ValidTagIdGuard],
}, },
{ {
path: 'tag/:id/daily-summary/:dayStr', path: 'tag/:id/daily-summary/:dayStr',
component: DailySummaryComponent, component: DailySummaryComponent,
data: {page: 'daily-summary'}, data: { page: 'daily-summary' },
canActivate: [ValidTagIdGuard] canActivate: [ValidTagIdGuard],
}, },
{ {
path: 'project/:id/tasks', path: 'project/:id/tasks',
component: ProjectTaskPageComponent, component: ProjectTaskPageComponent,
data: {page: 'project-tasks'}, data: { page: 'project-tasks' },
canActivate: [ValidProjectIdGuard] canActivate: [ValidProjectIdGuard],
}, },
{ {
path: 'project/:id/settings', path: 'project/:id/settings',
component: ProjectSettingsPageComponent, component: ProjectSettingsPageComponent,
data: {page: 'project-settings'}, data: { page: 'project-settings' },
canActivate: [ValidProjectIdGuard] canActivate: [ValidProjectIdGuard],
}, },
{ {
path: 'project/:id/worklog', path: 'project/:id/worklog',
component: WorklogComponent, component: WorklogComponent,
data: {page: 'worklog'}, data: { page: 'worklog' },
canActivate: [ValidProjectIdGuard] canActivate: [ValidProjectIdGuard],
}, },
{ {
path: 'project/:id/metrics', path: 'project/:id/metrics',
component: MetricPageComponent, component: MetricPageComponent,
data: {page: 'metrics'}, data: { page: 'metrics' },
canActivate: [ValidProjectIdGuard] canActivate: [ValidProjectIdGuard],
}, },
{ {
path: 'project/:id/daily-summary', path: 'project/:id/daily-summary',
component: DailySummaryComponent, component: DailySummaryComponent,
data: {page: 'daily-summary'}, data: { page: 'daily-summary' },
canActivate: [ValidProjectIdGuard] canActivate: [ValidProjectIdGuard],
}, },
{ {
path: 'project/:id/daily-summary/:dayStr', path: 'project/:id/daily-summary/:dayStr',
component: DailySummaryComponent, component: DailySummaryComponent,
data: {page: 'daily-summary'}, data: { page: 'daily-summary' },
canActivate: [ValidProjectIdGuard] 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/:subPageType',
{path: 'active', canActivate: [ActiveWorkContextGuard], component: ConfigPageComponent}, 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` },
]; ];

View file

@ -23,5 +23,4 @@ import { GlobalProgressBarModule } from './global-progress-bar/global-progress-b
GlobalProgressBarModule, GlobalProgressBarModule,
], ],
}) })
export class CoreUiModule { export class CoreUiModule {}
}

View file

@ -1,42 +1,50 @@
import { Injectable } from '@angular/core'; 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 { Observable } from 'rxjs';
import { finalize } from 'rxjs/operators'; import { finalize } from 'rxjs/operators';
import { GlobalProgressBarService } from './global-progress-bar.service'; import { GlobalProgressBarService } from './global-progress-bar.service';
import axios from 'axios'; import axios from 'axios';
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class GlobalProgressBarInterceptorService implements HttpInterceptor { 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( axios.interceptors.response.use(
private globalProgressBarService: GlobalProgressBarService, (response) => {
) { this.globalProgressBarService.countDown();
return response;
axios.interceptors.request.use((config) => { },
this.globalProgressBarService.countUp(config.url as string); (error) => {
return config; this.globalProgressBarService.countDown();
}, (error) => { return Promise.reject(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<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> { intercept(
req: HttpRequest<unknown>,
next: HttpHandler,
): Observable<HttpEvent<unknown>> {
this.globalProgressBarService.countUp(req.url); this.globalProgressBarService.countUp(req.url);
return next.handle(req).pipe( return next.handle(req).pipe(
finalize(() => { finalize(() => {
this.globalProgressBarService.countDown(); this.globalProgressBarService.countDown();
}) }),
); );
} }
} }

View file

@ -1,17 +1,18 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { GlobalProgressBarService } from './global-progress-bar.service'; 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({ @Component({
selector: 'global-progress-bar', selector: 'global-progress-bar',
templateUrl: './global-progress-bar.component.html', templateUrl: './global-progress-bar.component.html',
styleUrls: ['./global-progress-bar.component.scss'], styleUrls: ['./global-progress-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeAnimation, fadeInOutBottomAnimation, fadeOutAnimation] animations: [fadeAnimation, fadeInOutBottomAnimation, fadeOutAnimation],
}) })
export class GlobalProgressBarComponent { export class GlobalProgressBarComponent {
constructor( constructor(public globalProgressBarService: GlobalProgressBarService) {}
public globalProgressBarService: GlobalProgressBarService,
) {
}
} }

View file

@ -7,18 +7,14 @@ import { GlobalProgressBarInterceptorService } from './global-progress-bar-inter
@NgModule({ @NgModule({
providers: [ providers: [
{provide: HTTP_INTERCEPTORS, useClass: GlobalProgressBarInterceptorService, multi: true} {
provide: HTTP_INTERCEPTORS,
useClass: GlobalProgressBarInterceptorService,
multi: true,
},
], ],
declarations: [ declarations: [GlobalProgressBarComponent],
GlobalProgressBarComponent, imports: [CommonModule, UiModule],
], exports: [GlobalProgressBarComponent],
imports: [
CommonModule,
UiModule,
],
exports: [
GlobalProgressBarComponent,
]
}) })
export class GlobalProgressBarModule { export class GlobalProgressBarModule {}
}

View file

@ -6,34 +6,31 @@ import { T } from '../../t.const';
const DELAY = 100; const DELAY = 100;
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class GlobalProgressBarService { export class GlobalProgressBarService {
nrOfRequests$: BehaviorSubject<number> = new BehaviorSubject(0); nrOfRequests$: BehaviorSubject<number> = new BehaviorSubject(0);
isShowGlobalProgressBar$: Observable<boolean> = this.nrOfRequests$.pipe( isShowGlobalProgressBar$: Observable<boolean> = this.nrOfRequests$.pipe(
map(nr => nr > 0), map((nr) => nr > 0),
distinctUntilChanged(), distinctUntilChanged(),
switchMap((isShow) => isShow switchMap((isShow) => (isShow ? of(true) : of(false).pipe(delay(DELAY)))),
? of(true)
: of(false).pipe(delay(DELAY))
),
startWith(false), startWith(false),
// @see https://blog.angular-university.io/angular-debugging/ // @see https://blog.angular-university.io/angular-debugging/
delay(0), delay(0),
); );
private _label$: BehaviorSubject<string | null> = new BehaviorSubject<string | null>(null); private _label$: BehaviorSubject<string | null> = new BehaviorSubject<string | null>(
null,
);
label$: Observable<string | null> = this._label$.pipe( label$: Observable<string | null> = this._label$.pipe(
distinctUntilChanged(), distinctUntilChanged(),
switchMap((label: string | null) => !!label switchMap((label: string | null) =>
? of(label) !!label ? of(label) : of(null).pipe(delay(DELAY)),
: of(null).pipe(delay(DELAY))
), ),
// @see https://blog.angular-university.io/angular-debugging/ // @see https://blog.angular-university.io/angular-debugging/
delay(0), delay(0),
); );
constructor() { constructor() {}
}
countUp(url: string) { countUp(url: string) {
this.nrOfRequests$.next(this.nrOfRequests$.getValue() + 1); this.nrOfRequests$.next(this.nrOfRequests$.getValue() + 1);
@ -53,11 +50,10 @@ export class GlobalProgressBarService {
if (PROGRESS_BAR_LABEL_MAP[url]) { if (PROGRESS_BAR_LABEL_MAP[url]) {
return PROGRESS_BAR_LABEL_MAP[url]; return PROGRESS_BAR_LABEL_MAP[url];
} else { } else {
const key = Object.keys(PROGRESS_BAR_LABEL_MAP).find((keyIn) => urlWithoutParams.includes(keyIn)); const key = Object.keys(PROGRESS_BAR_LABEL_MAP).find((keyIn) =>
return key urlWithoutParams.includes(keyIn),
? PROGRESS_BAR_LABEL_MAP[key] );
: T.GPB.UNKNOWN; return key ? PROGRESS_BAR_LABEL_MAP[key] : T.GPB.UNKNOWN;
} }
} }
} }

View file

@ -11,5 +11,4 @@ import { LAYOUT_FEATURE_NAME } from './store/layout.reducer';
], ],
declarations: [], declarations: [],
}) })
export class LayoutModule { export class LayoutModule {}
}

View file

@ -6,11 +6,15 @@ import {
showAddTaskBar, showAddTaskBar,
toggleAddTaskBar, toggleAddTaskBar,
toggleShowNotes, toggleShowNotes,
toggleSideNav toggleSideNav,
} from './store/layout.actions'; } from './store/layout.actions';
import { BehaviorSubject, EMPTY, merge, Observable, of } from 'rxjs'; import { BehaviorSubject, EMPTY, merge, Observable, of } from 'rxjs';
import { select, Store } from '@ngrx/store'; 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 { filter, map, switchMap, withLatestFrom } from 'rxjs/operators';
import { BreakpointObserver } from '@angular/cdk/layout'; import { BreakpointObserver } from '@angular/cdk/layout';
import { NavigationStart, Router } from '@angular/router'; import { NavigationStart, Router } from '@angular/router';
@ -25,28 +29,30 @@ const XS_MAX = 599;
providedIn: 'root', providedIn: 'root',
}) })
export class LayoutService { export class LayoutService {
isScreenXs$: Observable<boolean> = this._breakPointObserver.observe([ isScreenXs$: Observable<boolean> = this._breakPointObserver
`(max-width: ${XS_MAX}px)`, .observe([`(max-width: ${XS_MAX}px)`])
]).pipe(map(result => result.matches)); .pipe(map((result) => result.matches));
isShowAddTaskBar$: Observable<boolean> = this._store$.pipe(select(selectIsShowAddTaskBar)); isShowAddTaskBar$: Observable<boolean> = this._store$.pipe(
isNavAlwaysVisible$: Observable<boolean> = this._breakPointObserver.observe([ select(selectIsShowAddTaskBar),
`(min-width: ${NAV_ALWAYS_VISIBLE}px)`, );
]).pipe(map(result => result.matches)); isNavAlwaysVisible$: Observable<boolean> = this._breakPointObserver
isNotesNextNavOver$: Observable<boolean> = this._breakPointObserver.observe([ .observe([`(min-width: ${NAV_ALWAYS_VISIBLE}px)`])
`(min-width: ${NAV_OVER_NOTES_NEXT}px)`, .pipe(map((result) => result.matches));
]).pipe(map(result => result.matches)); isNotesNextNavOver$: Observable<boolean> = this._breakPointObserver
isNotesOver$: Observable<boolean> = this._breakPointObserver.observe([ .observe([`(min-width: ${NAV_OVER_NOTES_NEXT}px)`])
`(min-width: ${BOTH_OVER}px)`, .pipe(map((result) => result.matches));
]).pipe(map(result => !result.matches)); isNotesOver$: Observable<boolean> = this._breakPointObserver
isNavOver$: Observable<boolean> = this.isNotesNextNavOver$.pipe(map(v => !v)); .observe([`(min-width: ${BOTH_OVER}px)`])
.pipe(map((result) => !result.matches));
isNavOver$: Observable<boolean> = this.isNotesNextNavOver$.pipe(map((v) => !v));
isScrolled$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); isScrolled$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private _isShowSideNav$: Observable<boolean> = this._store$.pipe(select(selectIsShowSideNav)); private _isShowSideNav$: Observable<boolean> = this._store$.pipe(
select(selectIsShowSideNav),
);
isShowSideNav$: Observable<boolean> = this._isShowSideNav$.pipe( isShowSideNav$: Observable<boolean> = this._isShowSideNav$.pipe(
switchMap((isShow) => { switchMap((isShow) => {
return isShow return isShow ? of(isShow) : this.isNavAlwaysVisible$;
? of(isShow)
: this.isNavAlwaysVisible$;
}), }),
); );
@ -65,22 +71,23 @@ export class LayoutService {
private _workContextService: WorkContextService, private _workContextService: WorkContextService,
private _breakPointObserver: BreakpointObserver, private _breakPointObserver: BreakpointObserver,
) { ) {
this.isNavOver$.pipe( this.isNavOver$
switchMap((isNavOver) => isNavOver .pipe(
? merge( switchMap((isNavOver) =>
this._router.events.pipe( isNavOver
filter((ev) => ev instanceof NavigationStart) ? merge(
), this._router.events.pipe(filter((ev) => ev instanceof NavigationStart)),
this._workContextService.onWorkContextChange$ this._workContextService.onWorkContextChange$,
).pipe( ).pipe(
withLatestFrom(this._isShowSideNav$), withLatestFrom(this._isShowSideNav$),
filter(([, isShowSideNav]) => isShowSideNav), filter(([, isShowSideNav]) => isShowSideNav),
) )
: EMPTY : EMPTY,
),
) )
).subscribe(() => { .subscribe(() => {
this.hideSideNav(); this.hideSideNav();
}); });
} }
showAddTaskBar() { showAddTaskBar() {
@ -110,5 +117,4 @@ export class LayoutService {
public hideNotes() { public hideNotes() {
this._store$.dispatch(hideNotes()); this._store$.dispatch(hideNotes());
} }
} }

View file

@ -1,30 +1,15 @@
import { createAction } from '@ngrx/store'; import { createAction } from '@ngrx/store';
export const showAddTaskBar = createAction( export const showAddTaskBar = createAction('[Layout] Show AddTaskBar');
'[Layout] Show AddTaskBar',
);
export const hideAddTaskBar = createAction( export const hideAddTaskBar = createAction('[Layout] Hide AddTaskBar');
'[Layout] Hide AddTaskBar',
);
export const toggleAddTaskBar = createAction( export const toggleAddTaskBar = createAction('[Layout] Toggle AddTaskBar');
'[Layout] Toggle AddTaskBar',
);
export const hideSideNav = createAction( export const hideSideNav = createAction('[Layout] Hide SideBar');
'[Layout] Hide SideBar',
);
export const toggleSideNav = createAction( export const toggleSideNav = createAction('[Layout] Toggle SideBar');
'[Layout] Toggle SideBar',
);
export const toggleShowNotes = createAction( export const toggleShowNotes = createAction('[Layout] ToggleShow Notes');
'[Layout] ToggleShow Notes',
);
export const hideNotes = createAction(
'[Layout] Hide Notes',
);
export const hideNotes = createAction('[Layout] Hide Notes');

View file

@ -5,9 +5,15 @@ import {
showAddTaskBar, showAddTaskBar,
toggleAddTaskBar, toggleAddTaskBar,
toggleShowNotes, toggleShowNotes,
toggleSideNav toggleSideNav,
} from './layout.actions'; } 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'; export const LAYOUT_FEATURE_NAME = 'layout';
@ -25,35 +31,49 @@ const _initialLayoutState: LayoutState = {
isShowNotes: false, isShowNotes: false,
}; };
export const selectLayoutFeatureState = createFeatureSelector<LayoutState>(LAYOUT_FEATURE_NAME); export const selectLayoutFeatureState = createFeatureSelector<LayoutState>(
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<LayoutState>( const _reducer = createReducer<LayoutState>(
_initialLayoutState, _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( export function reducer(
state: LayoutState = _initialLayoutState, state: LayoutState = _initialLayoutState,
action: Action action: Action,
): LayoutState { ): LayoutState {
return _reducer(state, action); return _reducer(state, action);
} }

View file

@ -23,7 +23,7 @@
<mat-menu #activeWorkContextMenu="matMenu"> <mat-menu #activeWorkContextMenu="matMenu">
<work-context-menu [contextId]="activeWorkContext?.id" <work-context-menu [contextId]="activeWorkContext?.id"
[project]="projectService.currentProject$|async" [project]="projectService.currentProject$|async"
[contextType]="activeWorkContext?.type"></work-context-menu> [contextType]="activeWorkContext?.type"></work-context-menu>
</mat-menu> </mat-menu>
</ng-container> </ng-container>

View file

@ -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 { ProjectService } from '../../features/project/project.service';
import { LayoutService } from '../layout/layout.service'; import { LayoutService } from '../layout/layout.service';
import { BookmarkService } from '../../features/bookmark/bookmark.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', templateUrl: './main-header.component.html',
styleUrls: ['./main-header.component.scss'], styleUrls: ['./main-header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeAnimation, expandFadeHorizontalAnimation] animations: [fadeAnimation, expandFadeHorizontalAnimation],
}) })
export class MainHeaderComponent implements OnInit, OnDestroy { export class MainHeaderComponent implements OnInit, OnDestroy {
T: typeof T = T; T: typeof T = T;
progressCircleRadius: number = 10; progressCircleRadius: number = 10;
circumference: number = this.progressCircleRadius * Math.PI * 2; circumference: number = this.progressCircleRadius * Math.PI * 2;
@ViewChild('circleSvg', {static: true}) circleSvg?: ElementRef; @ViewChild('circleSvg', { static: true }) circleSvg?: ElementRef;
currentTaskContext$: Observable<Project | Tag | null> = this.taskService.currentTaskParentOrCurrent$.pipe( currentTaskContext$: Observable<
filter(ct => !!ct), Project | Tag | null
switchMap((currentTask) => this.workContextService.activeWorkContextId$.pipe( > = this.taskService.currentTaskParentOrCurrent$.pipe(
filter((activeWorkContextId) => !!activeWorkContextId), filter((ct) => !!ct),
switchMap((activeWorkContextId) => { switchMap((currentTask) =>
if (currentTask.projectId === activeWorkContextId || currentTask.tagIds.includes(activeWorkContextId as string)) { this.workContextService.activeWorkContextId$.pipe(
return of(null); filter((activeWorkContextId) => !!activeWorkContextId),
} switchMap((activeWorkContextId) => {
return currentTask.projectId if (
? this.projectService.getByIdOnce$(currentTask.projectId) currentTask.projectId === activeWorkContextId ||
: this._tagService.getTagById$(currentTask.tagIds[0]).pipe(first()); 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(); private _subs: Subscription = new Subscription();
@ -57,8 +72,7 @@ export class MainHeaderComponent implements OnInit, OnDestroy {
public readonly simpleCounterService: SimpleCounterService, public readonly simpleCounterService: SimpleCounterService,
private readonly _tagService: TagService, private readonly _tagService: TagService,
private readonly _renderer: Renderer2, private readonly _renderer: Renderer2,
) { ) {}
}
ngOnDestroy(): void { ngOnDestroy(): void {
this._subs.unsubscribe(); this._subs.unsubscribe();
@ -72,7 +86,11 @@ export class MainHeaderComponent implements OnInit, OnDestroy {
progress = 1; progress = 1;
} }
const dashOffset = this.circumference * -1 * progress; const dashOffset = this.circumference * -1 * progress;
this._renderer.setStyle(this.circleSvg.nativeElement, 'stroke-dashoffset', dashOffset); this._renderer.setStyle(
this.circleSvg.nativeElement,
'stroke-dashoffset',
dashOffset,
);
} }
}); });
} }

View file

@ -25,5 +25,4 @@ import { SimpleCounterModule } from '../../features/simple-counter/simple-counte
declarations: [MainHeaderComponent], declarations: [MainHeaderComponent],
exports: [MainHeaderComponent], exports: [MainHeaderComponent],
}) })
export class MainHeaderModule { export class MainHeaderModule {}
}

View file

@ -3,11 +3,7 @@ import { CommonModule } from '@angular/common';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@NgModule({ @NgModule({
imports: [ imports: [CommonModule, RouterModule],
CommonModule,
RouterModule,
],
declarations: [], declarations: [],
}) })
export class ShortcutModule { export class ShortcutModule {}
}

View file

@ -39,20 +39,22 @@ export class ShortcutService {
private _translateService: TranslateService, private _translateService: TranslateService,
private _ngZone: NgZone, private _ngZone: NgZone,
) { ) {
this._activatedRoute.queryParams this._activatedRoute.queryParams.subscribe((params) => {
.subscribe((params) => { if (params && params.backlogPos) {
if (params && params.backlogPos) { this.backlogPos = +params.backlogPos;
this.backlogPos = +params.backlogPos; }
} });
});
// GLOBAL SHORTCUTS // GLOBAL SHORTCUTS
if (IS_ELECTRON) { if (IS_ELECTRON) {
(this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.TASK_TOGGLE_START, () => { (this._electronService.ipcRenderer as typeof ipcRenderer).on(
this._ngZone.run(() => { IPC.TASK_TOGGLE_START,
this._taskService.toggleStartTask(); () => {
}); this._ngZone.run(() => {
}); this._taskService.toggleStartTask();
});
},
);
(this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.ADD_TASK, () => { (this._electronService.ipcRenderer as typeof ipcRenderer).on(IPC.ADD_TASK, () => {
this._ngZone.run(() => { this._ngZone.run(() => {
this._layoutService.showAddTaskBar(); this._layoutService.showAddTaskBar();
@ -78,8 +80,13 @@ export class ShortcutService {
const el = ev.target as HTMLElement; const el = ev.target as HTMLElement;
// don't run when inside input or text area and if no special keys are used // 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')) if (
&& !ev.ctrlKey && !ev.metaKey) { ((el && el.tagName === 'INPUT') ||
el.tagName === 'TEXTAREA' ||
el.getAttribute('contenteditable')) &&
!ev.ctrlKey &&
!ev.metaKey
) {
return; return;
} }
@ -96,15 +103,12 @@ export class ShortcutService {
backlogPos = 50; backlogPos = 50;
} }
this._router.navigate(['/active/tasks'], { this._router.navigate(['/active/tasks'], {
queryParams: {backlogPos} queryParams: { backlogPos },
}); });
} else if (checkKeyCombo(ev, keys.goToWorkView)) { } else if (checkKeyCombo(ev, keys.goToWorkView)) {
this._router.navigate(['/active/tasks']); this._router.navigate(['/active/tasks']);
} else if (checkKeyCombo(ev, keys.goToSettings)) { } else if (checkKeyCombo(ev, keys.goToSettings)) {
this._router.navigate(['/config']); this._router.navigate(['/config']);
} else if (checkKeyCombo(ev, keys.goToScheduledView)) { } else if (checkKeyCombo(ev, keys.goToScheduledView)) {
this._router.navigate(['/active/calendar']); this._router.navigate(['/active/calendar']);
@ -113,28 +117,29 @@ export class ShortcutService {
// //
// } else if (checkKeyCombo(ev, keys.goToFocusMode)) { // } else if (checkKeyCombo(ev, keys.goToFocusMode)) {
// this._router.navigate(['/focus-view']); // this._router.navigate(['/focus-view']);
} else if (checkKeyCombo(ev, keys.toggleSideNav)) { } else if (checkKeyCombo(ev, keys.toggleSideNav)) {
this._layoutService.toggleSideNav(); this._layoutService.toggleSideNav();
ev.preventDefault(); ev.preventDefault();
} else if (checkKeyCombo(ev, keys.addNewTask)) { } else if (checkKeyCombo(ev, keys.addNewTask)) {
this._layoutService.toggleAddTaskBar(); this._layoutService.toggleAddTaskBar();
ev.preventDefault(); ev.preventDefault();
} else if (checkKeyCombo(ev, keys.addNewNote)) { } else if (checkKeyCombo(ev, keys.addNewNote)) {
if (this._matDialog.openDialogs.length === 0) { if (this._matDialog.openDialogs.length === 0) {
this._matDialog.open(DialogAddNoteComponent); this._matDialog.open(DialogAddNoteComponent);
ev.preventDefault(); ev.preventDefault();
} }
} else if (checkKeyCombo(ev, keys.openProjectNotes)) { } else if (checkKeyCombo(ev, keys.openProjectNotes)) {
ev.preventDefault(); ev.preventDefault();
if (this._workContextService.activeWorkContextType === WorkContextType.PROJECT) { if (this._workContextService.activeWorkContextType === WorkContextType.PROJECT) {
this._layoutService.toggleNotes(); this._layoutService.toggleNotes();
} else { } else {
this._snackService.open({ 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)) { } else if (checkKeyCombo(ev, keys.toggleBookmarks)) {
@ -143,19 +148,26 @@ export class ShortcutService {
this._bookmarkService.toggleBookmarks(); this._bookmarkService.toggleBookmarks();
} else { } else {
this._snackService.open({ 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+*') } else if (
&& document.activeElement checkKeyCombo(ev, 'Ctrl+Shift+*') &&
&& document.activeElement.getAttribute('routerlink') === '/procrastination') { document.activeElement &&
document.activeElement.getAttribute('routerlink') === '/procrastination'
) {
throw new Error('Intentional Error Fun (dont worry)'); throw new Error('Intentional Error Fun (dont worry)');
} }
// special hidden dev tools combo to use them for production // special hidden dev tools combo to use them for production
if (IS_ELECTRON) { if (IS_ELECTRON) {
if (checkKeyCombo(ev, 'Ctrl+Shift+J')) { 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)) { } else if (checkKeyCombo(ev, keys.zoomIn)) {
this._uiHelperService.zoomBy(0.05); this._uiHelperService.zoomBy(0.05);
} else if (checkKeyCombo(ev, keys.zoomOut)) { } else if (checkKeyCombo(ev, keys.zoomOut)) {

View file

@ -8,7 +8,7 @@ import {
Output, Output,
QueryList, QueryList,
ViewChild, ViewChild,
ViewChildren ViewChildren,
} from '@angular/core'; } from '@angular/core';
import { ProjectService } from '../../features/project/project.service'; import { ProjectService } from '../../features/project/project.service';
import { T } from '../../t.const'; import { T } from '../../t.const';
@ -29,8 +29,10 @@ import { FocusKeyManager } from '@angular/cdk/a11y';
import { MatMenuItem } from '@angular/material/menu'; import { MatMenuItem } from '@angular/material/menu';
import { LayoutService } from '../layout/layout.service'; import { LayoutService } from '../layout/layout.service';
import { TaskService } from '../../features/tasks/task.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({ @Component({
selector: 'side-nav', selector: 'side-nav',
@ -44,33 +46,35 @@ export class SideNavComponent implements OnDestroy {
@ViewChildren('menuEntry') navEntries?: QueryList<MatMenuItem>; @ViewChildren('menuEntry') navEntries?: QueryList<MatMenuItem>;
keyboardFocusTimeout?: number; keyboardFocusTimeout?: number;
@ViewChild('projectExpandBtn', {read: ElementRef}) projectExpandBtn?: ElementRef; @ViewChild('projectExpandBtn', { read: ElementRef }) projectExpandBtn?: ElementRef;
isProjectsExpanded: boolean = this.fetchProjectListState(); isProjectsExpanded: boolean = this.fetchProjectListState();
isProjectsExpanded$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(this.isProjectsExpanded); isProjectsExpanded$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
projectList$: Observable<Project[]> = this.isProjectsExpanded$.pipe( this.isProjectsExpanded,
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; projectList$: Observable<Project[]> = 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: boolean = this.fetchTagListState();
isTagsExpanded$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(this.isTagsExpanded); isTagsExpanded$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(
this.isTagsExpanded,
);
tagList$: Observable<Tag[]> = this.isTagsExpanded$.pipe( tagList$: Observable<Tag[]> = this.isTagsExpanded$.pipe(
switchMap(isExpanded => isExpanded switchMap((isExpanded) =>
? this.tagService.tagsNoMyDay$ isExpanded
: combineLatest([ ? this.tagService.tagsNoMyDay$
this.tagService.tagsNoMyDay$, : combineLatest([
this.workContextService.activeWorkContextId$ this.tagService.tagsNoMyDay$,
]).pipe( this.workContextService.activeWorkContextId$,
map(([tags, id]) => tags.filter(t => t.id === id)) ]).pipe(map(([tags, id]) => tags.filter((t) => t.id === id))),
) ),
)
); );
T: typeof T = T; T: typeof T = T;
readonly PROJECTS_SIDE_NAV: string = 'PROJECTS_SIDE_NAV'; readonly PROJECTS_SIDE_NAV: string = 'PROJECTS_SIDE_NAV';
@ -92,32 +96,46 @@ export class SideNavComponent implements OnDestroy {
this._dragulaService.createGroup(this.PROJECTS_SIDE_NAV, { this._dragulaService.createGroup(this.PROJECTS_SIDE_NAV, {
direction: 'vertical', direction: 'vertical',
moves: (el, container, handle) => { 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.workContextService.activeWorkContextId$.subscribe(
this._subs.add(this._dragulaService.dropModel(this.PROJECTS_SIDE_NAV) (id) => (this.activeWorkContextId = id),
.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) => { this._subs.add(
if (this.navEntries && isShow) { this._dragulaService
this.keyManager = new FocusKeyManager<MatMenuItem>(this.navEntries) .dropModel(this.PROJECTS_SIDE_NAV)
.withVerticalOrientation(true); .subscribe(({ targetModel }) => {
window.clearTimeout(this.keyboardFocusTimeout); // const {target, source, targetModel, item} = params;
this.keyboardFocusTimeout = window.setTimeout(() => { const targetNewIds = targetModel.map((project: Project) => project.id);
this.keyManager?.setFirstItemActive(); this.projectService.updateOrder(targetNewIds);
}, 100); }),
// this.keyManager.change.subscribe((v) => console.log('this.keyManager.change', v)); );
} else if (!isShow) {
this._taskService.focusFirstTaskIfVisible(); this._subs.add(
} this._layoutService.isShowSideNav$.subscribe((isShow) => {
})); if (this.navEntries && isShow) {
this.keyManager = new FocusKeyManager<MatMenuItem>(
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']) @HostListener('keydown', ['$event'])
@ -144,10 +162,8 @@ export class SideNavComponent implements OnDestroy {
getThemeColor(color: THEME_COLOR_MAP | string): { [key: string]: string } { getThemeColor(color: THEME_COLOR_MAP | string): { [key: string]: string } {
const standardColor = (THEME_COLOR_MAP as any)[color]; const standardColor = (THEME_COLOR_MAP as any)[color];
const colorToUse = (standardColor) const colorToUse = standardColor ? standardColor : color;
? standardColor return { background: colorToUse };
: color;
return {background: colorToUse};
} }
onScrollToNotesClick() { onScrollToNotesClick() {
@ -155,7 +171,7 @@ export class SideNavComponent implements OnDestroy {
} }
fetchProjectListState() { fetchProjectListState() {
return (localStorage.getItem(LS_IS_PROJECT_LIST_EXPANDED) === 'true'); return localStorage.getItem(LS_IS_PROJECT_LIST_EXPANDED) === 'true';
} }
storeProjectListState(isExpanded: boolean) { storeProjectListState(isExpanded: boolean) {
@ -164,7 +180,7 @@ export class SideNavComponent implements OnDestroy {
} }
fetchTagListState() { fetchTagListState() {
return (localStorage.getItem(LS_IS_TAG_LIST_EXPANDED) === 'true'); return localStorage.getItem(LS_IS_TAG_LIST_EXPANDED) === 'true';
} }
storeTagListState(isExpanded: boolean) { storeTagListState(isExpanded: boolean) {
@ -179,21 +195,20 @@ export class SideNavComponent implements OnDestroy {
} }
toggleExpandProjectsLeftRight(ev: KeyboardEvent) { toggleExpandProjectsLeftRight(ev: KeyboardEvent) {
if ((ev.key === 'ArrowLeft' && this.isProjectsExpanded)) { if (ev.key === 'ArrowLeft' && this.isProjectsExpanded) {
this.storeProjectListState(false); this.storeProjectListState(false);
this.isProjectsExpanded$.next(this.isProjectsExpanded); this.isProjectsExpanded$.next(this.isProjectsExpanded);
} else if ((ev.key === 'ArrowRight') && !(this.isProjectsExpanded)) { } else if (ev.key === 'ArrowRight' && !this.isProjectsExpanded) {
this.storeProjectListState(true); this.storeProjectListState(true);
this.isProjectsExpanded$.next(this.isProjectsExpanded); this.isProjectsExpanded$.next(this.isProjectsExpanded);
} }
} }
checkFocusProject(ev: KeyboardEvent) { checkFocusProject(ev: KeyboardEvent) {
if ((ev.key === 'ArrowLeft') && this.projectExpandBtn?.nativeElement) { if (ev.key === 'ArrowLeft' && this.projectExpandBtn?.nativeElement) {
const targetIndex = this.navEntries?.toArray() const targetIndex = this.navEntries?.toArray().findIndex((value) => {
.findIndex((value) => { return value._getHostElement() === this.projectExpandBtn?.nativeElement;
return value._getHostElement() === this.projectExpandBtn?.nativeElement; });
});
if (targetIndex) { if (targetIndex) {
this.keyManager?.setActiveItem(targetIndex); this.keyManager?.setActiveItem(targetIndex);
} }
@ -207,21 +222,20 @@ export class SideNavComponent implements OnDestroy {
} }
toggleExpandTagsLeftRight(ev: KeyboardEvent) { toggleExpandTagsLeftRight(ev: KeyboardEvent) {
if ((ev.key === 'ArrowLeft' && this.isTagsExpanded)) { if (ev.key === 'ArrowLeft' && this.isTagsExpanded) {
this.storeTagListState(false); this.storeTagListState(false);
this.isTagsExpanded$.next(this.isTagsExpanded); this.isTagsExpanded$.next(this.isTagsExpanded);
} else if ((ev.key === 'ArrowRight') && !(this.isTagsExpanded)) { } else if (ev.key === 'ArrowRight' && !this.isTagsExpanded) {
this.storeTagListState(true); this.storeTagListState(true);
this.isTagsExpanded$.next(this.isTagsExpanded); this.isTagsExpanded$.next(this.isTagsExpanded);
} }
} }
checkFocusTag(ev: KeyboardEvent) { checkFocusTag(ev: KeyboardEvent) {
if ((ev.key === 'ArrowLeft') && this.tagExpandBtn?.nativeElement) { if (ev.key === 'ArrowLeft' && this.tagExpandBtn?.nativeElement) {
const targetIndex = this.navEntries?.toArray() const targetIndex = this.navEntries?.toArray().findIndex((value) => {
.findIndex((value) => { return value._getHostElement() === this.tagExpandBtn?.nativeElement;
return value._getHostElement() === this.tagExpandBtn?.nativeElement; });
});
if (targetIndex) { if (targetIndex) {
this.keyManager?.setActiveItem(targetIndex); this.keyManager?.setActiveItem(targetIndex);
} }

View file

@ -7,20 +7,8 @@ import { DragulaModule } from 'ng2-dragula';
import { WorkContextMenuModule } from '../work-context-menu/work-context-menu.module'; import { WorkContextMenuModule } from '../work-context-menu/work-context-menu.module';
@NgModule({ @NgModule({
imports: [UiModule, CommonModule, RouterModule, DragulaModule, WorkContextMenuModule],
imports: [ declarations: [SideNavComponent],
UiModule, exports: [SideNavComponent],
CommonModule,
RouterModule,
DragulaModule,
WorkContextMenuModule,
],
declarations: [
SideNavComponent,
],
exports: [
SideNavComponent,
]
}) })
export class SideNavModule { export class SideNavModule {}
}

View file

@ -17,7 +17,7 @@ import { Project } from '../../features/project/project.model';
selector: 'work-context-menu', selector: 'work-context-menu',
templateUrl: './work-context-menu.component.html', templateUrl: './work-context-menu.component.html',
styleUrls: ['./work-context-menu.component.scss'], styleUrls: ['./work-context-menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class WorkContextMenuComponent implements OnDestroy { export class WorkContextMenuComponent implements OnDestroy {
@Input() project!: Project; @Input() project!: Project;
@ -33,14 +33,11 @@ export class WorkContextMenuComponent implements OnDestroy {
private _tagService: TagService, private _tagService: TagService,
private _workContextService: WorkContextService, private _workContextService: WorkContextService,
private _router: Router, private _router: Router,
) { ) {}
}
@Input('contextType') set contextTypeSet(v: WorkContextType) { @Input('contextType') set contextTypeSet(v: WorkContextType) {
this.isForProject = (v === WorkContextType.PROJECT); this.isForProject = v === WorkContextType.PROJECT;
this.base = (this.isForProject) this.base = this.isForProject ? 'project' : 'tag';
? 'project'
: 'tag';
} }
ngOnDestroy(): void { ngOnDestroy(): void {
@ -48,19 +45,26 @@ export class WorkContextMenuComponent implements OnDestroy {
} }
deleteTag() { deleteTag() {
this._subs.add(this._confirmTagDelete().pipe( this._subs.add(
filter(isDelete => isDelete && !!this.contextId), this._confirmTagDelete()
switchMap(() => this._workContextService.activeWorkContextTypeAndId$.pipe(take(1))), .pipe(
tap(({activeId}) => console.log(activeId, this.contextId)), filter((isDelete) => isDelete && !!this.contextId),
switchMap(({activeId}) => (activeId === this.contextId) switchMap(() =>
? from(this._router.navigateByUrl('/')) this._workContextService.activeWorkContextTypeAndId$.pipe(take(1)),
: of(true) ),
), tap(({ activeId }) => console.log(activeId, this.contextId)),
).subscribe(() => { switchMap(({ activeId }) =>
if (this.contextId) { activeId === this.contextId
this._tagService.removeTag(this.contextId); ? from(this._router.navigateByUrl('/'))
} : of(true),
})); ),
)
.subscribe(() => {
if (this.contextId) {
this._tagService.removeTag(this.contextId);
}
}),
);
} }
edit(project: Project) { edit(project: Project) {
@ -77,14 +81,17 @@ export class WorkContextMenuComponent implements OnDestroy {
return this._tagService.getTagById$(this.contextId).pipe( return this._tagService.getTagById$(this.contextId).pipe(
first(), first(),
concatMap((tag: Tag) => this._matDialog.open(DialogConfirmComponent, { concatMap((tag: Tag) =>
restoreFocus: true, this._matDialog
data: { .open(DialogConfirmComponent, {
message: T.F.TAG.D_DELETE.CONFIRM_MSG, restoreFocus: true,
translateParams: {tagName: tag.title}, data: {
} message: T.F.TAG.D_DELETE.CONFIRM_MSG,
}).afterClosed()), translateParams: { tagName: tag.title },
},
})
.afterClosed(),
),
); );
} }
} }

View file

@ -5,17 +5,8 @@ import { UiModule } from '../../ui/ui.module';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@NgModule({ @NgModule({
declarations: [ declarations: [WorkContextMenuComponent],
WorkContextMenuComponent, imports: [UiModule, CommonModule, RouterModule],
], exports: [WorkContextMenuComponent],
imports: [
UiModule,
CommonModule,
RouterModule,
],
exports: [
WorkContextMenuComponent,
]
}) })
export class WorkContextMenuModule { export class WorkContextMenuModule {}
}

View file

@ -16,45 +16,55 @@ interface TaskWithCategoryText extends Task {
categoryHtml: string; categoryHtml: string;
} }
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class AndroidService { export class AndroidService {
private _todayTagTasksFlat$: Observable<TaskWithCategoryText[]> = this._dataInitService.isAllDataLoadedInitially$.pipe( private _todayTagTasksFlat$: Observable<
TaskWithCategoryText[]
> = this._dataInitService.isAllDataLoadedInitially$.pipe(
switchMap(() => this._tagService.getTagById$(TODAY_TAG.id)), switchMap(() => this._tagService.getTagById$(TODAY_TAG.id)),
switchMap(tag => this._taskService.getByIdsLive$(tag.taskIds)), switchMap((tag) => this._taskService.getByIdsLive$(tag.taskIds)),
map(tasks => tasks && tasks.sort((a, b) => (a.isDone === b.isDone) map(
? 0 (tasks) =>
: (a.isDone ? 1 : -1) tasks && tasks.sort((a, b) => (a.isDone === b.isDone ? 0 : a.isDone ? 1 : -1)),
)), ),
switchMap((tasks) => combineLatest([ switchMap((tasks) =>
this._tagService.tags$, combineLatest([this._tagService.tags$, this._projectService.list$]).pipe(
this._projectService.list$, map(([tags, projects]) => {
]).pipe(map(([tags, projects]) => { return tasks
return tasks .filter((task) => !!task)
.filter(task => !!task) .map((task) => ({
.map(task => ({ ...task,
...task, category: [
category: [ ...(task.projectId
...(task.projectId ? [(projects.find((p) => p.id === task.projectId) as Project).title]
? [(projects.find((p) => p.id === task.projectId) as Project).title] : []),
: []), ...(task.tagIds.length
...(task.tagIds.length ? tags
? tags .filter(
.filter(tag => tag.id !== TODAY_TAG.id && task.tagIds.includes(tag.id)) (tag) => tag.id !== TODAY_TAG.id && task.tagIds.includes(tag.id),
.map(tag => tag.title) )
: []) .map((tag) => tag.title)
].join(', '), : []),
categoryHtml: [ ].join(', '),
...(task.projectId categoryHtml: [
? [this._getCategoryHtml(projects.find(p => p.id === task.projectId) as Project)] ...(task.projectId
: []), ? [
...(task.tagIds.length this._getCategoryHtml(
? tags projects.find((p) => p.id === task.projectId) as Project,
.filter(tag => tag.id !== TODAY_TAG.id && task.tagIds.includes(tag.id)) ),
.map(tag => this._getCategoryHtml(tag)) ]
: []) : []),
].join(' ') ...(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() { init() {
this._todayTagTasksFlat$.subscribe(tasks => { this._todayTagTasksFlat$.subscribe((tasks) => {
androidInterface.updateTaskData(JSON.stringify(tasks)); androidInterface.updateTaskData(JSON.stringify(tasks));
}); });
} }

View file

@ -4,16 +4,8 @@ import { BannerComponent } from './banner/banner.component';
import { UiModule } from '../../ui/ui.module'; import { UiModule } from '../../ui/ui.module';
@NgModule({ @NgModule({
declarations: [ declarations: [BannerComponent],
BannerComponent imports: [CommonModule, UiModule],
], exports: [BannerComponent],
imports: [
CommonModule,
UiModule,
],
exports: [
BannerComponent
]
}) })
export class BannerModule { export class BannerModule {}
}

View file

@ -3,12 +3,12 @@ import { Banner, BannerId } from './banner.model';
import { Observable, ReplaySubject } from 'rxjs'; import { Observable, ReplaySubject } from 'rxjs';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class BannerService { export class BannerService {
private _banners: Banner[] = []; private _banners: Banner[] = [];
private _banners$: ReplaySubject<Banner[]> = new ReplaySubject(1); private _banners$: ReplaySubject<Banner[]> = new ReplaySubject(1);
activeBanner$: Observable<Banner | null> = this._banners$.pipe( activeBanner$: Observable<Banner | null> = this._banners$.pipe(
map((banners) => (banners && banners.length && banners[0]) || null) map((banners) => (banners && banners.length && banners[0]) || null),
); );
constructor() { constructor() {
@ -73,7 +73,7 @@ export class BannerService {
} }
open(banner: Banner) { 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) { if (bannerToUpdate) {
Object.assign(bannerToUpdate, banner); Object.assign(bannerToUpdate, banner);
} else { } else {
@ -83,7 +83,7 @@ export class BannerService {
} }
dismiss(bannerId: BannerId) { dismiss(bannerId: BannerId) {
const bannerIndex = this._banners.findIndex(bannerIN => bannerIN.id === bannerId); const bannerIndex = this._banners.findIndex((bannerIN) => bannerIN.id === bannerId);
if (bannerIndex > -1) { if (bannerIndex > -1) {
// NOTE splice mutates // NOTE splice mutates
this._banners.splice(bannerIndex, 1); this._banners.splice(bannerIndex, 1);
@ -93,8 +93,8 @@ export class BannerService {
// usually not required, but when we want to be sure // usually not required, but when we want to be sure
dismissAll(bannerId: BannerId) { dismissAll(bannerId: BannerId) {
if (this._banners.find(bannerIN => bannerIN.id === bannerId)) { if (this._banners.find((bannerIN) => bannerIN.id === bannerId)) {
this._banners = this._banners.filter(banner => banner.id !== bannerId); this._banners = this._banners.filter((banner) => banner.id !== bannerId);
this._banners$.next(this._banners); this._banners$.next(this._banners);
} }
} }

View file

@ -11,7 +11,7 @@ import { T } from '../../../t.const';
templateUrl: './banner.component.html', templateUrl: './banner.component.html',
styleUrls: ['./banner.component.scss'], styleUrls: ['./banner.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
animations: [slideAnimation] animations: [slideAnimation],
}) })
export class BannerComponent { export class BannerComponent {
T: typeof T = T; T: typeof T = T;
@ -27,22 +27,15 @@ export class BannerComponent {
} }
this._dirtyReference = activeBanner.id; this._dirtyReference = activeBanner.id;
return merge( return merge(of(null), timer(500).pipe(mapTo(activeBanner)));
of(null),
timer(500).pipe(mapTo(activeBanner))
);
} else { } else {
this._dirtyReference = null; this._dirtyReference = null;
return of(null); return of(null);
} }
}) }),
); );
constructor( constructor(public bannerService: BannerService, private _elementRef: ElementRef) {}
public bannerService: BannerService,
private _elementRef: ElementRef,
) {
}
@ViewChild('wrapperEl') set wrapperEl(content: ElementRef) { @ViewChild('wrapperEl') set wrapperEl(content: ElementRef) {
if (content && content.nativeElement) { if (content && content.nativeElement) {

View file

@ -1,6 +1,5 @@
export type ExtensionInterfaceEventName export type ExtensionInterfaceEventName =
= 'SP_EXTENSION_READY' | 'SP_EXTENSION_READY'
| 'SP_JIRA_RESPONSE' | 'SP_JIRA_RESPONSE'
| 'SP_JIRA_REQUEST' | 'SP_JIRA_REQUEST'
| 'IDLE_TIME' | 'IDLE_TIME';
;

View file

@ -2,10 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@NgModule({ @NgModule({
imports: [ imports: [CommonModule],
CommonModule
],
declarations: [], declarations: [],
}) })
export class ChromeExtensionInterfaceModule { export class ChromeExtensionInterfaceModule {}
}

View file

@ -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) => { interfaceEl.addEventListener(evName, (ev: Event) => {
const event = ev as CustomEvent; const event = ev as CustomEvent;
cb(event, event.detail); cb(event, event.detail);

View file

@ -3,9 +3,6 @@ import { CommonModule } from '@angular/common';
@NgModule({ @NgModule({
declarations: [], declarations: [],
imports: [ imports: [CommonModule],
CommonModule
]
}) })
export class CompressionModule { export class CompressionModule {}
}

View file

@ -3,21 +3,19 @@ import { SnackService } from '../snack/snack.service';
import * as shortid from 'shortid'; import * as shortid from 'shortid';
import { T } from '../../t.const'; import { T } from '../../t.const';
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class CompressionService { export class CompressionService {
private _w: Worker; private _w: Worker;
private _activeInstances: any = {}; private _activeInstances: any = {};
constructor( constructor(private readonly _snackService: SnackService) {
private readonly _snackService: SnackService,
) {
if (typeof (Worker as any) === 'undefined') { if (typeof (Worker as any) === 'undefined') {
throw new Error('No web worker support'); throw new Error('No web worker support');
} }
// Create a new // Create a new
this._w = new Worker('./lz.worker', { this._w = new Worker('./lz.worker', {
name: 'lz', name: 'lz',
type: 'module' type: 'module',
}); });
this._w.addEventListener('message', this._onData.bind(this)); this._w.addEventListener('message', this._onData.bind(this));
this._w.addEventListener('error', this._handleError.bind(this)); this._w.addEventListener('error', this._handleError.bind(this));
@ -26,40 +24,43 @@ export class CompressionService {
async compress(strToHandle: string): Promise<string> { async compress(strToHandle: string): Promise<string> {
return this._promisifyWorker({ return this._promisifyWorker({
type: 'COMPRESS', type: 'COMPRESS',
strToHandle strToHandle,
}); });
} }
async decompress(strToHandle: string): Promise<string> { async decompress(strToHandle: string): Promise<string> {
return this._promisifyWorker({ return this._promisifyWorker({
type: 'DECOMPRESS', type: 'DECOMPRESS',
strToHandle strToHandle,
}); });
} }
async compressUTF16(strToHandle: string): Promise<string> { async compressUTF16(strToHandle: string): Promise<string> {
return this._promisifyWorker({ return this._promisifyWorker({
type: 'COMPRESS_UTF16', type: 'COMPRESS_UTF16',
strToHandle strToHandle,
}); });
} }
async decompressUTF16(strToHandle: string): Promise<string> { async decompressUTF16(strToHandle: string): Promise<string> {
return this._promisifyWorker({ return this._promisifyWorker({
type: 'DECOMPRESS_UTF16', type: 'DECOMPRESS_UTF16',
strToHandle strToHandle,
}); });
} }
private _promisifyWorker(params: { type: string; strToHandle: string }): Promise<string> { private _promisifyWorker(params: {
type: string;
strToHandle: string;
}): Promise<string> {
const id = shortid(); const id = shortid();
const promise = new Promise(((resolve, reject) => { const promise = new Promise((resolve, reject) => {
this._activeInstances[id] = { this._activeInstances[id] = {
resolve, resolve,
reject, reject,
}; };
})) as Promise<string>; }) as Promise<string>;
this._w.postMessage({ this._w.postMessage({
...params, ...params,
@ -69,7 +70,7 @@ export class CompressionService {
} }
private async _onData(msg: MessageEvent) { private async _onData(msg: MessageEvent) {
const {id, strToHandle, err} = msg.data; const { id, strToHandle, err } = msg.data;
if (err) { if (err) {
this._activeInstances[id].reject(err); this._activeInstances[id].reject(err);
this._handleError(err); this._handleError(err);
@ -81,6 +82,6 @@ export class CompressionService {
private _handleError(err: any) { private _handleError(err: any) {
console.error(err); 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 });
} }
} }

View file

@ -18,7 +18,7 @@ function handleData(msgData: any) {
} }
} }
addEventListener('message', ({data}) => { addEventListener('message', ({ data }) => {
try { try {
const strToHandle = handleData(data); const strToHandle = handleData(data);
postMessage({ postMessage({

View file

@ -19,12 +19,6 @@ import { CompressionModule } from './compression/compression.module';
LocalBackupModule, LocalBackupModule,
CompressionModule, CompressionModule,
], ],
exports: [ exports: [PersistenceModule, ChromeExtensionInterfaceModule, SnackModule, BannerModule],
PersistenceModule,
ChromeExtensionInterfaceModule,
SnackModule,
BannerModule,
],
}) })
export class CoreModule { export class CoreModule {}
}

View file

@ -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 { isValidAppData } from '../../imex/sync/is-valid-app-data.util';
import { DataRepairService } from '../data-repair/data-repair.service'; import { DataRepairService } from '../data-repair/data-repair.service';
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class DataInitService { export class DataInitService {
isAllDataLoadedInitially$: Observable<boolean> = from(this._persistenceService.project.loadState(true)).pipe( isAllDataLoadedInitially$: Observable<boolean> = from(
concatMap((projectState: ProjectState) => this._migrationService.migrateIfNecessaryToProjectState$(projectState)), this._persistenceService.project.loadState(true),
).pipe(
concatMap((projectState: ProjectState) =>
this._migrationService.migrateIfNecessaryToProjectState$(projectState),
),
concatMap(() => from(this.reInit())), concatMap(() => from(this.reInit())),
switchMap(() => this._workContextService.isActiveWorkContextProject$), switchMap(() => this._workContextService.isActiveWorkContextProject$),
switchMap(isProject => isProject switchMap((isProject) =>
// NOTE: this probably won't work some of the time isProject
? this._projectService.isRelatedDataLoadedForCurrentProject$ ? // NOTE: this probably won't work some of the time
: of(true) this._projectService.isRelatedDataLoadedForCurrentProject$
: of(true),
), ),
filter(isLoaded => isLoaded), filter((isLoaded) => isLoaded),
take(1), take(1),
// only ever load once // only ever load once
shareReplay(1), shareReplay(1),
@ -38,9 +43,7 @@ export class DataInitService {
private _dataRepairService: DataRepairService, private _dataRepairService: DataRepairService,
) { ) {
// TODO better construction than this // TODO better construction than this
this.isAllDataLoadedInitially$.pipe( this.isAllDataLoadedInitially$.pipe(take(1)).subscribe(() => {
take(1)
).subscribe(() => {
// here because to avoid circular dependencies // here because to avoid circular dependencies
this._store$.dispatch(allDataWasLoaded()); this._store$.dispatch(allDataWasLoaded());
}); });
@ -52,17 +55,18 @@ export class DataInitService {
const appDataComplete = await this._persistenceService.loadComplete(); const appDataComplete = await this._persistenceService.loadComplete();
const isValid = isValidAppData(appDataComplete); const isValid = isValidAppData(appDataComplete);
if (isValid) { if (isValid) {
this._store$.dispatch(loadAllData({appDataComplete, isOmitTokens})); this._store$.dispatch(loadAllData({ appDataComplete, isOmitTokens }));
} else { } else {
if (this._dataRepairService.isRepairPossibleAndConfirmed(appDataComplete)) { if (this._dataRepairService.isRepairPossibleAndConfirmed(appDataComplete)) {
const fixedData = this._dataRepairService.repairData(appDataComplete); const fixedData = this._dataRepairService.repairData(appDataComplete);
this._store$.dispatch(loadAllData({ this._store$.dispatch(
appDataComplete: fixedData, loadAllData({
isOmitTokens, appDataComplete: fixedData,
})); isOmitTokens,
}),
);
await this._persistenceService.importComplete(fixedData); await this._persistenceService.importComplete(fixedData);
} }
} }
} }
} }

View file

@ -6,14 +6,10 @@ import { dataRepair } from './data-repair.util';
import { isDataRepairPossible } from './is-data-repair-possible.util'; import { isDataRepairPossible } from './is-data-repair-possible.util';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root',
}) })
export class DataRepairService { export class DataRepairService {
constructor(private _translateService: TranslateService) {}
constructor(
private _translateService: TranslateService,
) {
}
repairData(dataIn: AppDataComplete): AppDataComplete { repairData(dataIn: AppDataComplete): AppDataComplete {
return dataRepair(dataIn); return dataRepair(dataIn);

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,14 @@ import { TaskArchive, TaskCopy, TaskState } from '../../features/tasks/task.mode
import { unique } from '../../util/unique'; import { unique } from '../../util/unique';
import { TODAY_TAG } from '../../features/tag/tag.const'; 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 => { export const dataRepair = (data: AppDataComplete): AppDataComplete => {
if (!isDataRepairPossible(data)) { if (!isDataRepairPossible(data)) {
@ -36,7 +43,9 @@ export const dataRepair = (data: AppDataComplete): AppDataComplete => {
const _fixEntityStates = (data: AppDataComplete): AppDataComplete => { const _fixEntityStates = (data: AppDataComplete): AppDataComplete => {
ENTITY_STATE_KEYS.forEach((key) => { 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; return data;
@ -48,8 +57,8 @@ const _removeDuplicatesFromArchive = (data: AppDataComplete): AppDataComplete =>
const duplicateIds = taskIds.filter((id) => archiveTaskIds.includes(id)); const duplicateIds = taskIds.filter((id) => archiveTaskIds.includes(id));
if (duplicateIds.length) { if (duplicateIds.length) {
data.taskArchive.ids = archiveTaskIds.filter(id => !duplicateIds.includes(id)); data.taskArchive.ids = archiveTaskIds.filter((id) => !duplicateIds.includes(id));
duplicateIds.forEach(id => { duplicateIds.forEach((id) => {
if (data.taskArchive.entities[id]) { if (data.taskArchive.entities[id]) {
delete data.taskArchive.entities[id]; delete data.taskArchive.entities[id];
} }
@ -61,7 +70,9 @@ const _removeDuplicatesFromArchive = (data: AppDataComplete): AppDataComplete =>
return data; return data;
}; };
const _moveArchivedSubTasksToUnarchivedParents = (data: AppDataComplete): AppDataComplete => { const _moveArchivedSubTasksToUnarchivedParents = (
data: AppDataComplete,
): AppDataComplete => {
// to avoid ambiguity // to avoid ambiguity
const taskState: TaskState = data.task; const taskState: TaskState = data.task;
const taskArchiveState: TaskArchive = data.taskArchive; const taskArchiveState: TaskArchive = data.taskArchive;
@ -73,7 +84,7 @@ const _moveArchivedSubTasksToUnarchivedParents = (data: AppDataComplete): AppDat
orhphanedArchivedSubTasks.forEach((t: TaskCopy) => { orhphanedArchivedSubTasks.forEach((t: TaskCopy) => {
// delete archived if duplicate // delete archived if duplicate
if (taskState.ids.includes(t.id as string)) { 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]; delete taskArchiveState.entities[t.id];
// if entity is empty for some reason // if entity is empty for some reason
if (!taskState.entities[t.id]) { if (!taskState.entities[t.id]) {
@ -82,14 +93,14 @@ const _moveArchivedSubTasksToUnarchivedParents = (data: AppDataComplete): AppDat
} }
// copy to today if parent exists // copy to today if parent exists
else if (taskState.ids.includes(t.parentId as string)) { else if (taskState.ids.includes(t.parentId as string)) {
taskState.ids.push((t.id)); taskState.ids.push(t.id);
taskState.entities[t.id] = t; taskState.entities[t.id] = t;
const par: TaskCopy = taskState.entities[t.parentId as string] as TaskCopy; const par: TaskCopy = taskState.entities[t.parentId as string] as TaskCopy;
par.subTaskIds = unique([...par.subTaskIds, t.id]); par.subTaskIds = unique([...par.subTaskIds, t.id]);
// and delete from archive // 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]; delete taskArchiveState.entities[t.id];
} }
@ -103,7 +114,9 @@ const _moveArchivedSubTasksToUnarchivedParents = (data: AppDataComplete): AppDat
return data; return data;
}; };
const _moveUnArchivedSubTasksToArchivedParents = (data: AppDataComplete): AppDataComplete => { const _moveUnArchivedSubTasksToArchivedParents = (
data: AppDataComplete,
): AppDataComplete => {
// to avoid ambiguity // to avoid ambiguity
const taskState: TaskState = data.task; const taskState: TaskState = data.task;
const taskArchiveState: TaskArchive = data.taskArchive; const taskArchiveState: TaskArchive = data.taskArchive;
@ -115,7 +128,7 @@ const _moveUnArchivedSubTasksToArchivedParents = (data: AppDataComplete): AppDat
orhphanedUnArchivedSubTasks.forEach((t: TaskCopy) => { orhphanedUnArchivedSubTasks.forEach((t: TaskCopy) => {
// delete un-archived if duplicate // delete un-archived if duplicate
if (taskArchiveState.ids.includes(t.id as string)) { 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]; delete taskState.entities[t.id];
// if entity is empty for some reason // if entity is empty for some reason
if (!taskArchiveState.entities[t.id]) { if (!taskArchiveState.entities[t.id]) {
@ -124,14 +137,14 @@ const _moveUnArchivedSubTasksToArchivedParents = (data: AppDataComplete): AppDat
} }
// copy to archive if parent exists // copy to archive if parent exists
else if (taskArchiveState.ids.includes(t.parentId as string)) { else if (taskArchiveState.ids.includes(t.parentId as string)) {
taskArchiveState.ids.push((t.id)); taskArchiveState.ids.push(t.id);
taskArchiveState.entities[t.id] = t; taskArchiveState.entities[t.id] = t;
const par: TaskCopy = taskArchiveState.entities[t.parentId as string] as TaskCopy; const par: TaskCopy = taskArchiveState.entities[t.parentId as string] as TaskCopy;
par.subTaskIds = unique([...par.subTaskIds, t.id]); par.subTaskIds = unique([...par.subTaskIds, t.id]);
// and delete from today // 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]; delete taskState.entities[t.id];
} }
// make main if it doesn't // make main if it doesn't
@ -144,8 +157,10 @@ const _moveUnArchivedSubTasksToArchivedParents = (data: AppDataComplete): AppDat
return data; return data;
}; };
const _removeMissingTasksFromListsOrRestoreFromArchive = (data: AppDataComplete): AppDataComplete => { const _removeMissingTasksFromListsOrRestoreFromArchive = (
const {task, project, tag, taskArchive} = data; data: AppDataComplete,
): AppDataComplete => {
const { task, project, tag, taskArchive } = data;
const taskIds: string[] = task.ids; const taskIds: string[] = task.ids;
const taskArchiveIds: string[] = taskArchive.ids as string[]; const taskArchiveIds: string[] = taskArchive.ids as string[];
const taskIdsToRestoreFromArchive: string[] = []; const taskIdsToRestoreFromArchive: string[] = [];
@ -161,57 +176,70 @@ const _removeMissingTasksFromListsOrRestoreFromArchive = (data: AppDataComplete)
return taskIds.includes(id); return taskIds.includes(id);
}); });
projectItem.backlogTaskIds = projectItem.backlogTaskIds.filter((id: string): boolean => { projectItem.backlogTaskIds = projectItem.backlogTaskIds.filter(
if (taskArchiveIds.includes(id)) { (id: string): boolean => {
taskIdsToRestoreFromArchive.push(id); if (taskArchiveIds.includes(id)) {
return true; taskIdsToRestoreFromArchive.push(id);
} return true;
return taskIds.includes(id); }
}); return taskIds.includes(id);
},
);
}); });
tag.ids.forEach((tId: string | number) => { tag.ids.forEach((tId: string | number) => {
const tagItem = tag.entities[tId] as TagCopy; 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]; task.entities[id] = taskArchive.entities[id];
delete taskArchive.entities[id]; delete taskArchive.entities[id];
}); });
task.ids = [...taskIds, ...taskIdsToRestoreFromArchive]; task.ids = [...taskIds, ...taskIdsToRestoreFromArchive];
taskArchive.ids = taskArchiveIds.filter(id => !taskIdsToRestoreFromArchive.includes(id)); taskArchive.ids = taskArchiveIds.filter(
(id) => !taskIdsToRestoreFromArchive.includes(id),
);
if (taskIdsToRestoreFromArchive.length > 0) { if (taskIdsToRestoreFromArchive.length > 0) {
console.log(taskIdsToRestoreFromArchive.length + ' missing tasks restored from archive.'); console.log(
taskIdsToRestoreFromArchive.length + ' missing tasks restored from archive.',
);
} }
return data; return data;
}; };
const _resetEntityIdsFromObjects = <T>(data: AppBaseDataEntityLikeStates): AppBaseDataEntityLikeStates => { const _resetEntityIdsFromObjects = <T>(
data: AppBaseDataEntityLikeStates,
): AppBaseDataEntityLikeStates => {
return { return {
...data, ...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 _addOrphanedTasksToProjectLists = (data: AppDataComplete): AppDataComplete => {
const {task, project} = data; const { task, project } = data;
let allTaskIdsOnProjectLists: string[] = []; let allTaskIdsOnProjectLists: string[] = [];
project.ids.forEach((pId: string | number) => { project.ids.forEach((pId: string | number) => {
const projectItem = project.entities[pId] as ProjectCopy; 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]; const taskItem = task.entities[tid];
if (!taskItem) { if (!taskItem) {
throw new Error('Missing task'); 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]; const taskItem = task.entities[tid];
if (!taskItem) { if (!taskItem) {
throw new Error('Missing task'); throw new Error('Missing task');
@ -227,7 +255,7 @@ const _addOrphanedTasksToProjectLists = (data: AppDataComplete): AppDataComplete
}; };
const _removeNonExistentProjectIds = (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 projectIds: string[] = project.ids as string[];
const taskIds: string[] = task.ids; const taskIds: string[] = task.ids;
const taskArchiveIds: string[] = taskArchive.ids as string[]; const taskArchiveIds: string[] = taskArchive.ids as string[];
@ -252,70 +280,76 @@ const _removeNonExistentProjectIds = (data: AppDataComplete): AppDataComplete =>
const _cleanupNonExistingTasksFromLists = (data: AppDataComplete): AppDataComplete => { const _cleanupNonExistingTasksFromLists = (data: AppDataComplete): AppDataComplete => {
const projectIds: string[] = data.project.ids as string[]; const projectIds: string[] = data.project.ids as string[];
projectIds projectIds.forEach((pid) => {
.forEach(pid => { const projectItem = data.project.entities[pid];
const projectItem = data.project.entities[pid]; if (!projectItem) {
if (!projectItem) { console.log(data.project);
console.log(data.project); throw new Error('No project');
throw new Error('No project'); }
} (projectItem as ProjectCopy).taskIds = projectItem.taskIds.filter(
(projectItem as ProjectCopy).taskIds = projectItem.taskIds.filter(tid => !!data.task.entities[tid]); (tid) => !!data.task.entities[tid],
(projectItem as ProjectCopy).backlogTaskIds = projectItem.backlogTaskIds.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[]; const tagIds: string[] = data.tag.ids as string[];
tagIds tagIds
.map(id => data.tag.entities[id]) .map((id) => data.tag.entities[id])
.forEach(tagItem => { .forEach((tagItem) => {
if (!tagItem) { if (!tagItem) {
console.log(data.tag); console.log(data.tag);
throw new Error('No tag'); throw new Error('No tag');
}
(tagItem as TagCopy).taskIds = tagItem.taskIds.filter(tid => !!data.task.entities[tid]);
} }
); (tagItem as TagCopy).taskIds = tagItem.taskIds.filter(
(tid) => !!data.task.entities[tid],
);
});
return data; return data;
}; };
const _fixInconsistentProjectId = (data: AppDataComplete): AppDataComplete => { const _fixInconsistentProjectId = (data: AppDataComplete): AppDataComplete => {
const projectIds: string[] = data.project.ids as string[]; const projectIds: string[] = data.project.ids as string[];
projectIds projectIds
.map(id => data.project.entities[id]) .map((id) => data.project.entities[id])
.forEach(projectItem => { .forEach((projectItem) => {
if (!projectItem) { if (!projectItem) {
console.log(data.project); console.log(data.project);
throw new Error('No 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;
}
}
});
} }
); 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; return data;
}; };
@ -323,22 +357,21 @@ const _fixInconsistentProjectId = (data: AppDataComplete): AppDataComplete => {
const _fixInconsistentTagId = (data: AppDataComplete): AppDataComplete => { const _fixInconsistentTagId = (data: AppDataComplete): AppDataComplete => {
const tagIds: string[] = data.tag.ids as string[]; const tagIds: string[] = data.tag.ids as string[];
tagIds tagIds
.map(id => data.tag.entities[id]) .map((id) => data.tag.entities[id])
.forEach(tagItem => { .forEach((tagItem) => {
if (!tagItem) { if (!tagItem) {
console.log(data.tag); console.log(data.tag);
throw new Error('No 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];
}
});
} }
); 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; return data;
}; };
@ -346,8 +379,8 @@ const _fixInconsistentTagId = (data: AppDataComplete): AppDataComplete => {
const _addTodayTagIfNoProjectIdOrTagId = (data: AppDataComplete): AppDataComplete => { const _addTodayTagIfNoProjectIdOrTagId = (data: AppDataComplete): AppDataComplete => {
const taskIds: string[] = data.task.ids as string[]; const taskIds: string[] = data.task.ids as string[];
taskIds taskIds
.map(id => data.task.entities[id]) .map((id) => data.task.entities[id])
.forEach(task => { .forEach((task) => {
if (task && !task.parentId && !task.tagIds.length && !task.projectId) { if (task && !task.parentId && !task.tagIds.length && !task.projectId) {
const tag = data.tag.entities[TODAY_TAG.id] as Tag; const tag = data.tag.entities[TODAY_TAG.id] as Tag;
(task as any).tagIds = [TODAY_TAG.id]; (task as any).tagIds = [TODAY_TAG.id];
@ -357,8 +390,8 @@ const _addTodayTagIfNoProjectIdOrTagId = (data: AppDataComplete): AppDataComplet
const archivedTaskIds: string[] = data.taskArchive.ids as string[]; const archivedTaskIds: string[] = data.taskArchive.ids as string[];
archivedTaskIds archivedTaskIds
.map(id => data.taskArchive.entities[id]) .map((id) => data.taskArchive.entities[id])
.forEach(task => { .forEach((task) => {
if (task && !task.parentId && !task.tagIds.length && !task.projectId) { if (task && !task.parentId && !task.tagIds.length && !task.projectId) {
(task as any).tagIds = [TODAY_TAG.id]; (task as any).tagIds = [TODAY_TAG.id];
} }
@ -370,8 +403,8 @@ const _addTodayTagIfNoProjectIdOrTagId = (data: AppDataComplete): AppDataComplet
const _setTaskProjectIdAccordingToParent = (data: AppDataComplete): AppDataComplete => { const _setTaskProjectIdAccordingToParent = (data: AppDataComplete): AppDataComplete => {
const taskIds: string[] = data.task.ids as string[]; const taskIds: string[] = data.task.ids as string[];
taskIds taskIds
.map(id => data.task.entities[id]) .map((id) => data.task.entities[id])
.forEach(taskItem => { .forEach((taskItem) => {
if (!taskItem) { if (!taskItem) {
console.log(data.task); console.log(data.task);
throw new Error('No task'); throw new Error('No task');
@ -392,8 +425,8 @@ const _setTaskProjectIdAccordingToParent = (data: AppDataComplete): AppDataCompl
const archiveTaskIds: string[] = data.taskArchive.ids as string[]; const archiveTaskIds: string[] = data.taskArchive.ids as string[];
archiveTaskIds archiveTaskIds
.map(id => data.taskArchive.entities[id]) .map((id) => data.taskArchive.entities[id])
.forEach(taskItem => { .forEach((taskItem) => {
if (!taskItem) { if (!taskItem) {
console.log(data.taskArchive); console.log(data.taskArchive);
throw new Error('No archive task'); throw new Error('No archive task');
@ -418,8 +451,8 @@ const _setTaskProjectIdAccordingToParent = (data: AppDataComplete): AppDataCompl
const _cleanupOrphanedSubTasks = (data: AppDataComplete): AppDataComplete => { const _cleanupOrphanedSubTasks = (data: AppDataComplete): AppDataComplete => {
const taskIds: string[] = data.task.ids as string[]; const taskIds: string[] = data.task.ids as string[];
taskIds taskIds
.map(id => data.task.entities[id]) .map((id) => data.task.entities[id])
.forEach(taskItem => { .forEach((taskItem) => {
if (!taskItem) { if (!taskItem) {
console.log(data.task); console.log(data.task);
throw new Error('No task'); throw new Error('No task');
@ -440,8 +473,8 @@ const _cleanupOrphanedSubTasks = (data: AppDataComplete): AppDataComplete => {
const archiveTaskIds: string[] = data.taskArchive.ids as string[]; const archiveTaskIds: string[] = data.taskArchive.ids as string[];
archiveTaskIds archiveTaskIds
.map(id => data.taskArchive.entities[id]) .map((id) => data.taskArchive.entities[id])
.forEach(taskItem => { .forEach((taskItem) => {
if (!taskItem) { if (!taskItem) {
console.log(data.taskArchive); console.log(data.taskArchive);
throw new Error('No archive task'); throw new Error('No archive task');

View file

@ -2,7 +2,12 @@ import { AppDataComplete } from '../../imex/sync/sync.model';
export const isDataRepairPossible = (data: AppDataComplete): boolean => { export const isDataRepairPossible = (data: AppDataComplete): boolean => {
const d: any = data as any; const d: any = data as any;
return typeof d === 'object' && d !== null return (
&& typeof d.task === 'object' && d.task !== null typeof d === 'object' &&
&& typeof d.project === 'object' && d.project !== null; d !== null &&
typeof d.task === 'object' &&
d.task !== null &&
typeof d.project === 'object' &&
d.project !== null
);
}; };

View file

@ -6,9 +6,7 @@ export const createFromDrop = (ev: DragEvent): null | DropPasteInput => {
throw new Error('No drop data'); throw new Error('No drop data');
} }
const text = ev.dataTransfer.getData('text'); const text = ev.dataTransfer.getData('text');
return text return text ? _createTextBookmark(text) : _createFileBookmark(ev.dataTransfer);
? (_createTextBookmark(text))
: (_createFileBookmark(ev.dataTransfer));
}; };
export const createFromPaste = (ev: ClipboardEvent): null | DropPasteInput => { export const createFromPaste = (ev: ClipboardEvent): null | DropPasteInput => {

View file

@ -16,11 +16,11 @@ const getResponseChannels = (channel: string) => {
return { return {
sendChannel: getSendChannel(channel), sendChannel: getSendChannel(channel),
dataChannel: `%better-ipc-response-data-channel-${channel}-${id}`, 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 { export class ElectronService {
ipcRenderer?: typeof ipcRenderer; ipcRenderer?: typeof ipcRenderer;
webFrame?: typeof webFrame; webFrame?: typeof webFrame;
@ -74,7 +74,7 @@ export class ElectronService {
public callMain(channel: string, data: unknown) { public callMain(channel: string, data: unknown) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const {sendChannel, dataChannel, errorChannel} = getResponseChannels(channel); const { sendChannel, dataChannel, errorChannel } = getResponseChannels(channel);
const cleanup = () => { const cleanup = () => {
(this.ipcRenderer as typeof ipcRenderer).off(dataChannel, onData); (this.ipcRenderer as typeof ipcRenderer).off(dataChannel, onData);
@ -98,7 +98,7 @@ export class ElectronService {
const completeData = { const completeData = {
dataChannel, dataChannel,
errorChannel, errorChannel,
userData: data userData: data,
}; };
(this.ipcRenderer as typeof ipcRenderer).send(sendChannel, completeData); (this.ipcRenderer as typeof ipcRenderer).send(sendChannel, completeData);

View file

@ -6,29 +6,25 @@ import { map } from 'rxjs/operators';
import { IS_ELECTRON } from '../../app.constants'; import { IS_ELECTRON } from '../../app.constants';
import { ipcRenderer } from 'electron'; import { ipcRenderer } from 'electron';
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class ExecBeforeCloseService { export class ExecBeforeCloseService {
ipcRenderer: typeof ipcRenderer = this._electronService.ipcRenderer as typeof ipcRenderer; ipcRenderer: typeof ipcRenderer = this._electronService
.ipcRenderer as typeof ipcRenderer;
onBeforeClose$: Observable<string[]> = IS_ELECTRON onBeforeClose$: Observable<string[]> = IS_ELECTRON
? fromEvent(this.ipcRenderer, IPC.NOTIFY_ON_CLOSE).pipe( ? fromEvent(this.ipcRenderer, IPC.NOTIFY_ON_CLOSE).pipe(map(([, ids]: any) => ids))
map(([, ids]: any) => ids),
)
: of([]); : of([]);
constructor( constructor(private _electronService: ElectronService) {}
private _electronService: ElectronService,
) {
}
schedule(id: string) { schedule(id: string) {
this.ipcRenderer.send(IPC.REGISTER_BEFORE_CLOSE, {id}); this.ipcRenderer.send(IPC.REGISTER_BEFORE_CLOSE, { id });
} }
unschedule(id: string) { unschedule(id: string) {
this.ipcRenderer.send(IPC.UNREGISTER_BEFORE_CLOSE, {id}); this.ipcRenderer.send(IPC.UNREGISTER_BEFORE_CLOSE, { id });
} }
setDone(id: string) { setDone(id: string) {
this.ipcRenderer.send(IPC.BEFORE_CLOSE_DONE, {id}); this.ipcRenderer.send(IPC.BEFORE_CLOSE_DONE, { id });
} }
} }

View file

@ -3,7 +3,11 @@ import { isObject } from '../../util/is-object';
import { getErrorTxt } from '../../util/get-error-text'; import { getErrorTxt } from '../../util/get-error-text';
import { IS_ELECTRON } from '../../app.constants'; import { IS_ELECTRON } from '../../app.constants';
import { ElectronService } from '../electron/electron.service'; 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 { remote } from 'electron';
import { saveBeforeLastErrorActionLog } from '../../util/action-logger'; import { saveBeforeLastErrorActionLog } from '../../util/action-logger';
@ -11,17 +15,17 @@ import { saveBeforeLastErrorActionLog } from '../../util/action-logger';
export class GlobalErrorHandler implements ErrorHandler { export class GlobalErrorHandler implements ErrorHandler {
private _electronLogger: any; private _electronLogger: any;
constructor( constructor(private _electronService: ElectronService) {
private _electronService: ElectronService,
) {
if (IS_ELECTRON) { 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 // TODO Cleanup this mess
handleError(err: any) { handleError(err: any) {
const errStr = (typeof err === 'string') ? err : err.toString(); const errStr = typeof err === 'string' ? err : err.toString();
// eslint-disable-next-line // eslint-disable-next-line
const simpleStack = err && err.stack; const simpleStack = err && err.stack;
console.error('GLOBAL_ERROR_HANDLER', err); 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 // NOTE: dom exceptions will break all rendering that's why
if (err.constructor && err.constructor === DOMException) { if (err.constructor && err.constructor === DOMException) {
createErrorAlert(this._electronService, 'DOMException: ' + errorStr, simpleStack, err); createErrorAlert(
this._electronService,
'DOMException: ' + errorStr,
simpleStack,
err,
);
} else { } else {
createErrorAlert(this._electronService, errorStr, simpleStack, err); createErrorAlert(this._electronService, errorStr, simpleStack, err);
} }
@ -58,7 +67,7 @@ export class GlobalErrorHandler implements ErrorHandler {
private _getErrorStr(err: unknown): string { private _getErrorStr(err: unknown): string {
if (isObject(err)) { if (isObject(err)) {
const str = getErrorTxt(err); const str = getErrorTxt(err);
return (typeof str === 'string') return typeof str === 'string'
? str ? str
: 'Unable to parse error string. Please see console error'; : 'Unable to parse error string. Please see console error';
} else { } else {

View file

@ -15,14 +15,14 @@ async function _getStacktrace(err: Error | any): Promise<string> {
// Don't try to send stacktraces of HTTP errors as they are already logged on the server // Don't try to send stacktraces of HTTP errors as they are already logged on the server
if (!isHttpError && isErrorWithStack && !isHandledError(err)) { if (!isHttpError && isErrorWithStack && !isHandledError(err)) {
return StackTrace.fromError(err) return StackTrace.fromError(err).then((stackframes) => {
.then((stackframes) => { return stackframes
return stackframes .splice(0, 20)
.splice(0, 20) .map((sf) => {
.map((sf) => { return sf.toString();
return sf.toString(); })
}).join('\n'); .join('\n');
}); });
} else if (!isHandledError(err)) { } else if (!isHandledError(err)) {
console.warn('Error without stack', err); console.warn('Error without stack', err);
} }
@ -31,26 +31,31 @@ async function _getStacktrace(err: Error | any): Promise<string> {
const _getStacktraceThrottled = pThrottle(_getStacktrace, 2, 5000); 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) { const githubIssueLink = document.getElementById('github-issue-url');
additionalLogFn(stack);
}
// append to dialog
const stacktraceEl = document.getElementById('stack-trace');
if (stacktraceEl) {
stacktraceEl.innerText = stack;
}
const githubIssueLink = document.getElementById('github-issue-url'); if (githubIssueLink) {
const errEscaped = _cleanHtml(origErr as string);
githubIssueLink.setAttribute('href', getGithubUrl(errEscaped, stack));
}
if (githubIssueLink) { // NOTE: there is an issue with this sometimes -> https://github.com/stacktracejs/stacktrace.js/issues/202
const errEscaped = _cleanHtml(origErr as string); })
githubIssueLink.setAttribute('href', getGithubUrl(errEscaped, stack)); .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 _cleanHtml = (str: string): string => {
const div = document.createElement('div'); const div = document.createElement('div');
@ -58,7 +63,12 @@ const _cleanHtml = (str: string): string => {
return div.textContent || div.innerText || ''; 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) { if (isWasErrorAlertCreated) {
return; return;
} }
@ -92,7 +102,9 @@ export const createErrorAlert = (eSvc: ElectronService, err: string = '', stackT
} }
}); });
document.body.append(errorAlert); 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); innerWrapper.append(btnReload);
isWasErrorAlertCreated = true; isWasErrorAlertCreated = true;
@ -109,16 +121,25 @@ export const createErrorAlert = (eSvc: ElectronService, err: string = '', stackT
export const getSimpleMeta = (): string => { export const getSimpleMeta = (): string => {
const n = window.navigator; 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 => { export const isHandledError = (err: unknown): boolean => {
const errStr = (typeof err === 'string') const errStr =
? err typeof err === 'string'
: (typeof err === 'object' && err !== null && typeof (err as any).toString === 'function' && err.toString()); ? 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... // NOTE: for some unknown reason sometimes err is undefined while err.toString is not...
// this is why we also check the string value // 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 => { const getGithubUrl = (errEscaped: string, stackTrace: string): string => {

View file

@ -3,13 +3,18 @@ import { TranslateService } from '@ngx-translate/core';
import { DateTimeAdapter } from 'ngx-date-time-picker-schedule'; import { DateTimeAdapter } from 'ngx-date-time-picker-schedule';
import { DateAdapter } from '@angular/material/core'; import { DateAdapter } from '@angular/material/core';
import * as moment from 'moment'; 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 { BehaviorSubject, Observable } from 'rxjs';
import { GlobalConfigService } from 'src/app/features/config/global-config.service'; import { GlobalConfigService } from 'src/app/features/config/global-config.service';
import { map, startWith } from 'rxjs/operators'; import { map, startWith } from 'rxjs/operators';
import { DEFAULT_GLOBAL_CONFIG } from 'src/app/features/config/default-global-config.const'; import { DEFAULT_GLOBAL_CONFIG } from 'src/app/features/config/default-global-config.const';
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class LanguageService { 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 // 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<boolean> = new BehaviorSubject<boolean>(false); private isRTL: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
@ -48,15 +53,16 @@ export class LanguageService {
private _initMonkeyPatchFirstDayOfWeek() { private _initMonkeyPatchFirstDayOfWeek() {
let firstDayOfWeek = DEFAULT_GLOBAL_CONFIG.misc.firstDayOfWeek; let firstDayOfWeek = DEFAULT_GLOBAL_CONFIG.misc.firstDayOfWeek;
this._globalConfigService.misc$.pipe( this._globalConfigService.misc$
map(cfg => cfg.firstDayOfWeek), .pipe(
startWith(1), map((cfg) => cfg.firstDayOfWeek),
).subscribe((_firstDayOfWeek: number) => { startWith(1),
// default should be monday, if we have an invalid value for some reason )
firstDayOfWeek = (_firstDayOfWeek === 0 || _firstDayOfWeek > 0) .subscribe((_firstDayOfWeek: number) => {
? _firstDayOfWeek // default should be monday, if we have an invalid value for some reason
: 1; firstDayOfWeek =
}); _firstDayOfWeek === 0 || _firstDayOfWeek > 0 ? _firstDayOfWeek : 1;
});
// overwrites default method to make this configurable // overwrites default method to make this configurable
this._dateAdapter.getFirstDayOfWeek = () => firstDayOfWeek; this._dateAdapter.getFirstDayOfWeek = () => firstDayOfWeek;
} }

View file

@ -50,7 +50,9 @@ export interface LegacyAppDataForProjects {
}; };
} }
export interface LegacyAppDataComplete extends LegacyAppBaseData, LegacyAppDataForProjects { export interface LegacyAppDataComplete
extends LegacyAppBaseData,
LegacyAppDataForProjects {
lastActiveTime: number; lastActiveTime: number;
} }

View file

@ -13,15 +13,23 @@ import {
LS_TASK_ARCHIVE, LS_TASK_ARCHIVE,
LS_TASK_ATTACHMENT_STATE, LS_TASK_ATTACHMENT_STATE,
LS_TASK_REPEAT_CFG_STATE, LS_TASK_REPEAT_CFG_STATE,
LS_TASK_STATE LS_TASK_STATE,
} from '../persistence/ls-keys.const'; } from '../persistence/ls-keys.const';
import { migrateProjectState } from '../../features/project/migrate-projects-state.util'; import { migrateProjectState } from '../../features/project/migrate-projects-state.util';
import { GlobalConfigState } from '../../features/config/global-config.model'; import { GlobalConfigState } from '../../features/config/global-config.model';
import { migrateGlobalConfigState } from '../../features/config/migrate-global-config.util'; 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 { Reminder } from '../../features/reminder/reminder.model';
import { taskReducer } from '../../features/tasks/store/task.reducer'; 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 { taskRepeatCfgReducer } from '../../features/task-repeat-cfg/store/task-repeat-cfg.reducer';
import { EntityState } from '@ngrx/entity'; import { EntityState } from '@ngrx/entity';
import { TaskAttachment } from '../../features/tasks/task-attachment/task-attachment.model'; import { TaskAttachment } from '../../features/tasks/task-attachment/task-attachment.model';
@ -30,15 +38,21 @@ import {
LegacyAppDataComplete, LegacyAppDataComplete,
LegacyAppDataForProjects, LegacyAppDataForProjects,
LegacyPersistenceBaseModel, LegacyPersistenceBaseModel,
LegacyPersistenceForProjectModel LegacyPersistenceForProjectModel,
} from './legacy-models'; } from './legacy-models';
import { BookmarkState } from '../../features/bookmark/store/bookmark.reducer'; import { BookmarkState } from '../../features/bookmark/store/bookmark.reducer';
import { Bookmark } from '../../features/bookmark/bookmark.model'; import { Bookmark } from '../../features/bookmark/bookmark.model';
import { NoteState } from '../../features/note/store/note.reducer'; import { NoteState } from '../../features/note/store/note.reducer';
import { Note } from '../../features/note/note.model'; import { Note } from '../../features/note/note.model';
import { Metric, MetricState } from '../../features/metric/metric.model'; import { Metric, MetricState } from '../../features/metric/metric.model';
import { Improvement, ImprovementState } from '../../features/metric/improvement/improvement.model'; import {
import { Obstruction, ObstructionState } from '../../features/metric/obstruction/obstruction.model'; Improvement,
ImprovementState,
} from '../../features/metric/improvement/improvement.model';
import {
Obstruction,
ObstructionState,
} from '../../features/metric/obstruction/obstruction.model';
import { DatabaseService } from '../persistence/database.service'; import { DatabaseService } from '../persistence/database.service';
import { DEFAULT_PROJECT_ID } from '../../features/project/project.const'; import { DEFAULT_PROJECT_ID } from '../../features/project/project.const';
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
@ -48,20 +62,23 @@ import { Injectable } from '@angular/core';
providedIn: 'root', providedIn: 'root',
}) })
export class LegacyPersistenceService { export class LegacyPersistenceService {
// handled as private but needs to be assigned before the creations // handled as private but needs to be assigned before the creations
_baseModels: any [] = []; _baseModels: any[] = [];
_projectModels: any [] = []; _projectModels: any[] = [];
// TODO auto generate ls keys from appDataKey where possible // TODO auto generate ls keys from appDataKey where possible
project: any = this._cmBase<ProjectState>(LS_PROJECT_META_LIST, 'project', migrateProjectState); project: any = this._cmBase<ProjectState>(
globalConfig: any = this._cmBase<GlobalConfigState>(LS_GLOBAL_CFG, 'globalConfig', migrateGlobalConfigState); LS_PROJECT_META_LIST,
reminders: any = this._cmBase<Reminder[]>(LS_REMINDER, 'reminders'); 'project',
task: any = this._cmProject<TaskState, Task>( migrateProjectState,
LS_TASK_STATE,
'task',
taskReducer,
); );
globalConfig: any = this._cmBase<GlobalConfigState>(
LS_GLOBAL_CFG,
'globalConfig',
migrateGlobalConfigState,
);
reminders: any = this._cmBase<Reminder[]>(LS_REMINDER, 'reminders');
task: any = this._cmProject<TaskState, Task>(LS_TASK_STATE, 'task', taskReducer);
taskRepeatCfg: any = this._cmProject<TaskRepeatCfgState, TaskRepeatCfg>( taskRepeatCfg: any = this._cmProject<TaskRepeatCfgState, TaskRepeatCfg>(
LS_TASK_REPEAT_CFG_STATE, LS_TASK_REPEAT_CFG_STATE,
'taskRepeatCfg', 'taskRepeatCfg',
@ -77,18 +94,14 @@ export class LegacyPersistenceService {
taskAttachment: any = this._cmProject<EntityState<TaskAttachment>, TaskAttachment>( taskAttachment: any = this._cmProject<EntityState<TaskAttachment>, TaskAttachment>(
LS_TASK_ATTACHMENT_STATE, LS_TASK_ATTACHMENT_STATE,
'taskAttachment', 'taskAttachment',
(state) => state (state) => state,
); );
bookmark: any = this._cmProject<BookmarkState, Bookmark>( bookmark: any = this._cmProject<BookmarkState, Bookmark>(
LS_BOOKMARK_STATE, LS_BOOKMARK_STATE,
'bookmark', 'bookmark',
(state) => state, (state) => state,
); );
note: any = this._cmProject<NoteState, Note>( note: any = this._cmProject<NoteState, Note>(LS_NOTE_STATE, 'note', (state) => state);
LS_NOTE_STATE,
'note',
(state) => state,
);
metric: any = this._cmProject<MetricState, Metric>( metric: any = this._cmProject<MetricState, Metric>(
LS_METRIC_STATE, LS_METRIC_STATE,
'metric', 'metric',
@ -106,9 +119,7 @@ export class LegacyPersistenceService {
); );
private _isBlockSaving: boolean = false; private _isBlockSaving: boolean = false;
constructor( constructor(private _databaseService: DatabaseService) {
private _databaseService: DatabaseService,
) {
// this.loadComplete().then(d => console.log('XXXXXXXXX', d, JSON.stringify(d).length)); // this.loadComplete().then(d => console.log('XXXXXXXXX', d, JSON.stringify(d).length));
// this.loadAllRelatedModelDataForProject('DEFAULT').then(d => console.log(d)); // this.loadAllRelatedModelDataForProject('DEFAULT').then(d => console.log(d));
} }
@ -198,9 +209,7 @@ export class LegacyPersistenceService {
getLastActive(): number { getLastActive(): number {
const la = localStorage.getItem(LS_LAST_LOCAL_SYNC_MODEL_CHANGE); const la = localStorage.getItem(LS_LAST_LOCAL_SYNC_MODEL_CHANGE);
// NOTE: we need to parse because new Date('1570549698000') is "Invalid Date" // NOTE: we need to parse because new Date('1570549698000') is "Invalid Date"
const laParsed = Number.isNaN(Number(la)) const laParsed = Number.isNaN(Number(la)) ? la : +(la as any);
? la
: +(la as any);
// NOTE: to account for legacy string dates // NOTE: to account for legacy string dates
return new Date(laParsed as any).getTime(); return new Date(laParsed as any).getTime();
} }
@ -208,7 +217,7 @@ export class LegacyPersistenceService {
// NOTE: not including backup // NOTE: not including backup
async loadCompleteLegacy(): Promise<LegacyAppDataComplete> { async loadCompleteLegacy(): Promise<LegacyAppDataComplete> {
const projectState = await this.project.load(); 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 { return {
lastActiveTime: this.getLastActive(), lastActiveTime: this.getLastActive(),
@ -255,21 +264,29 @@ export class LegacyPersistenceService {
): LegacyPersistenceForProjectModel<S, M> { ): LegacyPersistenceForProjectModel<S, M> {
const model = { const model = {
appDataKey, appDataKey,
load: (projectId: any): Promise<S> => this._loadFromDb(this._makeProjectKey(projectId, lsKey)).then(v => migrateFn(v, projectId)), load: (projectId: any): Promise<S> =>
save: (projectId: any, data: any, isForce: any) => this._saveToDb(this._makeProjectKey(projectId, lsKey), data, isForce), 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); this._projectModels.push(model);
return model; return model;
} }
private async _loadLegacyAppDataForProjects(projectIds: string[]): Promise<LegacyAppDataForProjects> { private async _loadLegacyAppDataForProjects(
const forProjectsData = await Promise.all(this._projectModels.map(async (modelCfg) => { projectIds: string[],
const modelState = await this._loadForProjectIds(projectIds, modelCfg.load); ): Promise<LegacyAppDataForProjects> {
return { const forProjectsData = await Promise.all(
[modelCfg.appDataKey]: modelState, this._projectModels.map(async (modelCfg) => {
}; const modelState = await this._loadForProjectIds(projectIds, modelCfg.load);
})); return {
[modelCfg.appDataKey]: modelState,
};
}),
);
return Object.assign({}, ...forProjectsData); return Object.assign({}, ...forProjectsData);
} }
@ -280,18 +297,24 @@ export class LegacyPersistenceService {
const dataForProject = await getDataFn(projectId); const dataForProject = await getDataFn(projectId);
return { return {
...prevAcc, ...prevAcc,
[projectId]: dataForProject [projectId]: dataForProject,
}; };
}, Promise.resolve({})); }, Promise.resolve({}));
} }
private _makeProjectKey(projectId: string, subKey: string, additional?: string) { 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 // DATA STORAGE INTERFACE
// --------------------- // ---------------------
private async _saveToDb(key: string, data: any, isForce: boolean = false): Promise<any> { private async _saveToDb(
key: string,
data: any,
isForce: boolean = false,
): Promise<any> {
if (!this._isBlockSaving || isForce === true) { if (!this._isBlockSaving || isForce === true) {
return this._databaseService.save(key, data); return this._databaseService.save(key, data);
} else { } else {

View file

@ -22,35 +22,38 @@ import { initialMetricState } from '../../features/metric/store/metric.reducer';
import { initialImprovementState } from '../../features/metric/improvement/store/improvement.reducer'; import { initialImprovementState } from '../../features/metric/improvement/store/improvement.reducer';
import { initialObstructionState } from '../../features/metric/obstruction/store/obstruction.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 { export class MigrationService {
constructor( constructor(
private _persistenceService: PersistenceService, private _persistenceService: PersistenceService,
private _legacyPersistenceService: LegacyPersistenceService, private _legacyPersistenceService: LegacyPersistenceService,
private _translateService: TranslateService, private _translateService: TranslateService,
) { ) {}
}
migrateIfNecessaryToProjectState$(projectState: ProjectState): Observable<ProjectState | never> { migrateIfNecessaryToProjectState$(
projectState: ProjectState,
): Observable<ProjectState | never> {
const isNeedsMigration = this._isNeedsMigration(projectState); const isNeedsMigration = this._isNeedsMigration(projectState);
if (isNeedsMigration && this._isConfirmMigrateDialog()) { if (isNeedsMigration && this._isConfirmMigrateDialog()) {
return from(this._legacyPersistenceService.loadCompleteLegacy()).pipe( return from(this._legacyPersistenceService.loadCompleteLegacy()).pipe(
map((legacyData) => this._migrate(legacyData)), map((legacyData) => this._migrate(legacyData)),
concatMap((migratedData) => this._persistenceService.importComplete(migratedData)), concatMap((migratedData) =>
this._persistenceService.importComplete(migratedData),
),
concatMap((migratedData) => this._persistenceService.cleanDatabase()), concatMap((migratedData) => this._persistenceService.cleanDatabase()),
concatMap(() => this._persistenceService.project.loadState()), concatMap(() => this._persistenceService.project.loadState()),
); );
} }
return isNeedsMigration return isNeedsMigration ? EMPTY : of(projectState);
? EMPTY
: of(projectState);
} }
migrateIfNecessary(appDataComplete: LegacyAppDataComplete | AppDataComplete): AppDataComplete { migrateIfNecessary(
appDataComplete: LegacyAppDataComplete | AppDataComplete,
): AppDataComplete {
const projectState = appDataComplete.project; const projectState = appDataComplete.project;
const isNeedsMigration = this._isNeedsMigration(projectState); const isNeedsMigration = this._isNeedsMigration(projectState);
if (isNeedsMigration) { if (isNeedsMigration) {
@ -99,46 +102,68 @@ export class MigrationService {
return newAppData; return newAppData;
} }
private _mTaskListsFromTaskToProjectState(legacyAppDataComplete: LegacyAppDataComplete): ProjectState { private _mTaskListsFromTaskToProjectState(
legacyAppDataComplete: LegacyAppDataComplete,
): ProjectState {
const projectStateBefore = legacyAppDataComplete.project; const projectStateBefore = legacyAppDataComplete.project;
return { return {
...projectStateBefore, ...projectStateBefore,
entities: (projectStateBefore.ids as string[]).reduce((acc, id): Dictionary<Project> => { entities: (projectStateBefore.ids as string[]).reduce(
const taskState = (legacyAppDataComplete.task as any)[id] || {}; (acc, id): Dictionary<Project> => {
return { const taskState = (legacyAppDataComplete.task as any)[id] || {};
...acc, return {
[id]: { ...acc,
...projectStateBefore.entities[id], [id]: {
taskIds: (taskState as any).todaysTaskIds || [], ...projectStateBefore.entities[id],
backlogTaskIds: (taskState as any).backlogTaskIds || [], taskIds: (taskState as any).todaysTaskIds || [],
} as Project backlogTaskIds: (taskState as any).backlogTaskIds || [],
}; } as Project,
}, {}) };
},
{},
),
}; };
} }
private _mTaskState(legacyAppDataComplete: LegacyAppDataComplete): TaskState { private _mTaskState(legacyAppDataComplete: LegacyAppDataComplete): TaskState {
const singleState = this._mTaskFromProjectToSingle(legacyAppDataComplete); const singleState = this._mTaskFromProjectToSingle(legacyAppDataComplete);
const standardMigration = migrateTaskState(singleState as TaskState); 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 { 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); 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 pids = legacyAppDataComplete.project.ids as string[];
const repeatStates = this._addProjectIdToEntity(pids, (legacyAppDataComplete.taskRepeatCfg as any), {tagIds: []}); const repeatStates = this._addProjectIdToEntity(
return this._mergeEntities(repeatStates, initialTaskRepeatCfgState) as TaskRepeatCfgState; pids,
legacyAppDataComplete.taskRepeatCfg as any,
{ tagIds: [] },
);
return this._mergeEntities(
repeatStates,
initialTaskRepeatCfgState,
) as TaskRepeatCfgState;
} }
private _addProjectIdToEntity( private _addProjectIdToEntity(
pids: string[], pids: string[],
entityProjectStates: { [key: string]: EntityState<any> }, entityProjectStates: { [key: string]: EntityState<any> },
additionalChanges: Record<string, unknown> = {} additionalChanges: Record<string, unknown> = {},
): EntityState<any>[] { ): EntityState<any>[] {
return pids.map((projectId) => { return pids.map((projectId) => {
const state = entityProjectStates[projectId]; const state = entityProjectStates[projectId];
@ -149,7 +174,12 @@ export class MigrationService {
...state, ...state,
entities: (state.ids as string[]).reduce((acc, entityId) => { entities: (state.ids as string[]).reduce((acc, entityId) => {
if (projectId !== state.entities[entityId].projectId) { 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 { return {
...acc, ...acc,
@ -157,74 +187,102 @@ export class MigrationService {
...state.entities[entityId], ...state.entities[entityId],
projectId, projectId,
...additionalChanges, ...additionalChanges,
} },
}; };
}, {}) }, {}),
}; };
}) as any; }) as any;
} }
private _mTaskFromProjectToSingle(legacyAppDataComplete: LegacyAppDataComplete): TaskState { private _mTaskFromProjectToSingle(
legacyAppDataComplete: LegacyAppDataComplete,
): TaskState {
const pids = legacyAppDataComplete.project.ids as string[]; 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; 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 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; return this._mergeEntities(taskStates, EMTPY_ENTITY()) as TaskArchive;
} }
private _mTaskAttachmentsToTaskStates(legacyAppDataComplete: LegacyAppDataComplete, taskState: (TaskState | TaskArchive)): private _mTaskAttachmentsToTaskStates(
TaskState | TaskArchive { legacyAppDataComplete: LegacyAppDataComplete,
const attachmentStates = Object.keys(legacyAppDataComplete.taskAttachment as any).map(id => (legacyAppDataComplete.taskAttachment as any)[id]); taskState: TaskState | TaskArchive,
const allAttachmentState = this._mergeEntities(attachmentStates, initialTaskRepeatCfgState) as EntityState<TaskAttachment>; ): 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<TaskAttachment>;
return (taskState.ids as string[]).reduce((acc, id) => { 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 { return {
...acc, ...acc,
entities: { entities: {
...acc.entities, ...acc.entities,
[id]: { [id]: {
...tEnt, ...tEnt,
attachments: tEnt.attachments || (attachmentIds attachments:
? attachmentIds.map((attachmentId: string) => { tEnt.attachments ||
const result = allAttachmentState.entities[attachmentId]; (attachmentIds
if (!result) { ? attachmentIds
console.log('ATTACHMENT NOT FOUND: Will be removed', attachmentIds); .map((attachmentId: string) => {
// throw new Error('Attachment not found'); const result = allAttachmentState.entities[attachmentId];
} else { if (!result) {
console.log('ATTACHMENT FOUND', result.title); console.log(
} 'ATTACHMENT NOT FOUND: Will be removed',
return result; attachmentIds,
}).filter((v: any) => !!v) );
: []) // throw new Error('Attachment not found');
} else {
console.log('ATTACHMENT FOUND', result.title);
}
return result;
})
.filter((v: any) => !!v)
: []),
}, },
} },
}; };
}, taskState); }, taskState);
} }
private _mergeEntities(states: EntityState<any>[], initial: EntityState<any>): EntityState<any> { private _mergeEntities(
return states.reduce( states: EntityState<any>[],
(acc, s) => { initial: EntityState<any>,
if (!s || !s.ids) { ): EntityState<any> {
return acc; return states.reduce((acc, s) => {
} if (!s || !s.ids) {
return { return acc;
...acc, }
ids: [...acc.ids, ...s.ids] as string[], return {
// NOTE: that this can lead to overwrite when the ids are the same for some reason ...acc,
entities: {...acc.entities, ...s.entities} ids: [...acc.ids, ...s.ids] as string[],
}; // NOTE: that this can lead to overwrite when the ids are the same for some reason
}, initial entities: { ...acc.entities, ...s.entities },
); };
}, initial);
} }
private _isNeedsMigration(projectState: ProjectState): boolean { 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 { private _isConfirmMigrateDialog(): boolean {

View file

@ -15,8 +15,7 @@ export class NotifyService {
constructor( constructor(
private _translateService: TranslateService, private _translateService: TranslateService,
private _uiHelperService: UiHelperService, private _uiHelperService: UiHelperService,
) { ) {}
}
async notifyDesktop(options: NotifyModel): Promise<Notification | undefined> { async notifyDesktop(options: NotifyModel): Promise<Notification | undefined> {
if (!IS_MOBILE) { if (!IS_MOBILE) {
@ -26,10 +25,16 @@ export class NotifyService {
} }
async notify(options: NotifyModel): Promise<Notification | undefined> { async notify(options: NotifyModel): Promise<Notification | undefined> {
const title = options.title && this._translateService.instant(options.title, options.translateParams); const title =
const body = options.body && this._translateService.instant(options.body, options.translateParams); 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) { if (svcReg && svcReg.showNotification) {
// service worker also seems to need to request permission... // service worker also seems to need to request permission...
@ -43,7 +48,7 @@ export class NotifyService {
silent: false, silent: false,
data: { data: {
dateOfArrival: Date.now(), dateOfArrival: Date.now(),
primaryKey: 1 primaryKey: 1,
}, },
...options, ...options,
body, body,
@ -62,7 +67,7 @@ export class NotifyService {
silent: false, silent: false,
data: { data: {
dateOfArrival: Date.now(), dateOfArrival: Date.now(),
primaryKey: 1 primaryKey: 1,
}, },
...options, ...options,
body, body,
@ -88,6 +93,10 @@ export class NotifyService {
} }
private _isServiceWorkerAvailable(): boolean { private _isServiceWorkerAvailable(): boolean {
return 'serviceWorker' in navigator && (environment.production || environment.stage) && !IS_ELECTRON; return (
'serviceWorker' in navigator &&
(environment.production || environment.stage) &&
!IS_ELECTRON
);
} }
} }

View file

@ -25,7 +25,7 @@ export class DatabaseService {
isReady$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); isReady$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
private _afterReady$: Observable<boolean> = this.isReady$.pipe( private _afterReady$: Observable<boolean> = this.isReady$.pipe(
filter(isReady => isReady), filter((isReady) => isReady),
shareReplay(1), shareReplay(1),
); );
@ -35,9 +35,9 @@ export class DatabaseService {
this._init().then(); this._init().then();
} }
@retry({retries: MAX_RETRY_COUNT, delay: RETRY_DELAY}) @retry({ retries: MAX_RETRY_COUNT, delay: RETRY_DELAY })
async load(key: string): Promise<unknown> { async load(key: string): Promise<unknown> {
this._lastParams = {a: 'load', key}; this._lastParams = { a: 'load', key };
await this._afterReady(); await this._afterReady();
try { try {
return await (this.db as IDBPDatabase<MyDb>).get(DB_MAIN_NAME, key); return await (this.db as IDBPDatabase<MyDb>).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<unknown> { async save(key: string, data: unknown): Promise<unknown> {
this._lastParams = {a: 'save', key, data}; this._lastParams = { a: 'save', key, data };
await this._afterReady(); await this._afterReady();
try { try {
return await (this.db as IDBPDatabase<MyDb>).put(DB_MAIN_NAME, data, key); return await (this.db as IDBPDatabase<MyDb>).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<unknown> { async remove(key: string): Promise<unknown> {
this._lastParams = {a: 'remove', key}; this._lastParams = { a: 'remove', key };
await this._afterReady(); await this._afterReady();
try { try {
return await (this.db as IDBPDatabase<MyDb>).delete(DB_MAIN_NAME, key); return await (this.db as IDBPDatabase<MyDb>).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<unknown> { async clearDatabase(): Promise<unknown> {
this._lastParams = {a: 'clearDatabase'}; this._lastParams = { a: 'clearDatabase' };
await this._afterReady(); await this._afterReady();
try { try {
return await (this.db as IDBPDatabase<MyDb>).clear(DB_MAIN_NAME); return await (this.db as IDBPDatabase<MyDb>).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<IDBPDatabase<MyDb>> { private async _init(): Promise<IDBPDatabase<MyDb>> {
try { try {
this.db = await openDB<MyDb>(DB_NAME, VERSION, { this.db = await openDB<MyDb>(DB_NAME, VERSION, {

View file

@ -10,7 +10,10 @@ export const removeFromRealLs = (key: string) => {
localStorage.removeItem(key); 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); const serializedState = JSON.stringify(state);
localStorage.setItem(key, serializedState); localStorage.setItem(key, serializedState);
}; };

View file

@ -6,7 +6,7 @@ export type AllowedDBKeys = keyof AppDataComplete | 'SUP_COMPLETE_BACKUP';
export const LS_PREFIX = 'SUP_'; export const LS_PREFIX = 'SUP_';
export const LS_PROJECT_PREFIX = LS_PREFIX + 'P_'; export const LS_PROJECT_PREFIX = LS_PREFIX + 'P_';
export const LS_GLOBAL_CFG = LS_PREFIX + 'GLOBAL_CFG'; 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_REMINDER = LS_PREFIX + 'REMINDER';
export const LS_PROJECT_ARCHIVE = LS_PREFIX + 'ARCHIVE'; 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_LOG = LS_PREFIX + 'ACTION_LOG';
export const LS_ACTION_BEFORE_LAST_ERROR_LOG = LS_PREFIX + 'LAST_ERROR_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_PROJECT_LIST_EXPANDED = LS_PREFIX + 'IS_PROJECT_LIST_EXPANDED';
export const LS_IS_TAG_LIST_EXPANDED = LS_PREFIX + 'IS_TAG_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_WEB_APP_INSTALL = LS_PREFIX + 'WEB_APP_INSTALL';
export const SS_JIRA_WONKY_COOKIE = LS_PREFIX + 'JIRA_WONKY_COOKIE'; export const SS_JIRA_WONKY_COOKIE = LS_PREFIX + 'JIRA_WONKY_COOKIE';
export const SS_TODO_TMP = SS_PREFIX + 'TODO_TMP_EDIT'; export const SS_TODO_TMP = SS_PREFIX + 'TODO_TMP_EDIT';

View file

@ -1,5 +1,9 @@
import { Injectable } from '@angular/core'; 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 { DatabaseService } from './database.service';
import { LocalSyncMetaModel } from '../../imex/sync/sync.model'; import { LocalSyncMetaModel } from '../../imex/sync/sync.model';
import { SyncProvider } from '../../imex/sync/sync-provider.model'; import { SyncProvider } from '../../imex/sync/sync-provider.model';
@ -8,18 +12,20 @@ import { SyncProvider } from '../../imex/sync/sync-provider.model';
providedIn: 'root', providedIn: 'root',
}) })
export class PersistenceLocalService { export class PersistenceLocalService {
constructor( constructor(private _databaseService: DatabaseService) {}
private _databaseService: DatabaseService,
) {
}
async save(data: LocalSyncMetaModel): Promise<any> { async save(data: LocalSyncMetaModel): Promise<any> {
return await this._databaseService.save(LS_LOCAL_NON_SYNC, data); return await this._databaseService.save(LS_LOCAL_NON_SYNC, data);
} }
async load(): Promise<LocalSyncMetaModel> { async load(): Promise<LocalSyncMetaModel> {
const r = await this._databaseService.load(LS_LOCAL_NON_SYNC) as LocalSyncMetaModel; const r = (await this._databaseService.load(LS_LOCAL_NON_SYNC)) as LocalSyncMetaModel;
if (r && r[SyncProvider.Dropbox] && r[SyncProvider.GoogleDrive] && r[SyncProvider.WebDAV]) { if (
r &&
r[SyncProvider.Dropbox] &&
r[SyncProvider.GoogleDrive] &&
r[SyncProvider.WebDAV]
) {
return r; return r;
} }
return { return {
@ -44,8 +50,6 @@ export class PersistenceLocalService {
private _getLegacyLocalLastSync(p: SyncProvider): number { private _getLegacyLocalLastSync(p: SyncProvider): number {
const it = +(localStorage.getItem(LS_SYNC_LOCAL_LAST_SYNC + p) as any); const it = +(localStorage.getItem(LS_SYNC_LOCAL_LAST_SYNC + p) as any);
return isNaN(it) return isNaN(it) ? 0 : it || 0;
? 0
: it || 0;
} }
} }

View file

@ -13,5 +13,3 @@ export const loadFromDb = createAction(
'[Persistence] Load from DB', '[Persistence] Load from DB',
props<{ dbKey: string }>(), props<{ dbKey: string }>(),
); );

View file

@ -1,8 +1,8 @@
import { AppBaseData, AppDataForProjects } from '../../imex/sync/sync.model'; import { AppBaseData, AppDataForProjects } from '../../imex/sync/sync.model';
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
export type ProjectDataLsKey export type ProjectDataLsKey =
= 'CFG' | 'CFG'
// | 'TASKS_STATE' // | 'TASKS_STATE'
// | 'TASK_REPEAT_CFG_STATE' // | 'TASK_REPEAT_CFG_STATE'
// | 'TASK_ATTACHMENT_STATE' // | 'TASK_ATTACHMENT_STATE'
@ -13,15 +13,17 @@ export type ProjectDataLsKey
| 'BOOKMARK_STATE' | 'BOOKMARK_STATE'
| 'METRIC_STATE' | 'METRIC_STATE'
| 'IMPROVEMENT_STATE' | 'IMPROVEMENT_STATE'
| 'OBSTRUCTION_STATE' | 'OBSTRUCTION_STATE';
;
export interface PersistenceBaseModel<T> { export interface PersistenceBaseModel<T> {
appDataKey: keyof AppBaseData; appDataKey: keyof AppBaseData;
loadState(isSkipMigration?: boolean): Promise<T>; loadState(isSkipMigration?: boolean): Promise<T>;
saveState(state: T, flags: { isDataImport?: boolean; isSyncModelChange?: boolean }): Promise<unknown>; saveState(
state: T,
flags: { isDataImport?: boolean; isSyncModelChange?: boolean },
): Promise<unknown>;
} }
export interface PersistenceBaseEntityModel<S, M> extends PersistenceBaseModel<S> { export interface PersistenceBaseEntityModel<S, M> extends PersistenceBaseModel<S> {
@ -42,7 +44,11 @@ export interface PersistenceForProjectModel<S, M> {
load(projectId: string): Promise<S>; load(projectId: string): Promise<S>;
save(projectId: string, state: S, flags: { isDataImport?: boolean; isSyncModelChange?: boolean }): Promise<unknown>; save(
projectId: string,
state: S,
flags: { isDataImport?: boolean; isSyncModelChange?: boolean },
): Promise<unknown>;
/* @deprecated */ /* @deprecated */
remove(projectId: string): Promise<unknown>; remove(projectId: string): Promise<unknown>;

View file

@ -2,10 +2,7 @@ import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@NgModule({ @NgModule({
imports: [ imports: [CommonModule],
CommonModule,
],
declarations: [], declarations: [],
}) })
export class PersistenceModule { export class PersistenceModule {}
}

View file

@ -16,51 +16,53 @@ const testScheduler = new TestScheduler((actual, expected) => {
describe('PersistenceService', () => { describe('PersistenceService', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
provideMockStore({initialState: {}}), provideMockStore({ initialState: {} }),
{ {
provide: SnackService, useValue: { provide: SnackService,
open: () => false, useValue: {
}, open: () => false,
}, },
{ },
provide: DatabaseService, useValue: { {
clearDatabase: () => false, provide: DatabaseService,
save: () => false, useValue: {
remove: () => false, clearDatabase: () => false,
load: () => false, save: () => false,
}, remove: () => false,
load: () => false,
}, },
{ },
provide: CompressionService, useValue: { {
decompress: () => false, provide: CompressionService,
compress: () => false, useValue: {
}, decompress: () => false,
compress: () => false,
}, },
] },
} ],
); });
}); });
it('database update should trigger onAfterSave$', async (done) => { it('database update should trigger onAfterSave$', async (done) => {
const service: PersistenceService = TestBed.inject(PersistenceService); const service: PersistenceService = TestBed.inject(PersistenceService);
// once is required to fill up data // once is required to fill up data
await service.loadComplete(); await service.loadComplete();
service.onAfterSave$.subscribe(({data}) => { service.onAfterSave$.subscribe(({ data }) => {
expect(data).toEqual(createEmptyEntity()); expect(data).toEqual(createEmptyEntity());
done(); done();
}); });
service.tag.saveState(createEmptyEntity(), {isSyncModelChange: true}); service.tag.saveState(createEmptyEntity(), { isSyncModelChange: true });
}); });
describe('inMemoryComplete$', () => { describe('inMemoryComplete$', () => {
it('should start with loadComplete data', () => { it('should start with loadComplete data', () => {
testScheduler.run(({expectObservable}) => { testScheduler.run(({ expectObservable }) => {
const FAKE_VAL: any = 'VVV'; const FAKE_VAL: any = 'VVV';
const a$ = of(FAKE_VAL); const a$ = of(FAKE_VAL);
spyOn(PersistenceService.prototype, 'loadComplete').and.callFake(() => a$ as any); spyOn(PersistenceService.prototype, 'loadComplete').and.callFake(() => a$ as any);
const service: PersistenceService = TestBed.inject(PersistenceService); const service: PersistenceService = TestBed.inject(PersistenceService);
expectObservable(service.inMemoryComplete$).toBe('a', {a: FAKE_VAL}); expectObservable(service.inMemoryComplete$).toBe('a', { a: FAKE_VAL });
}); });
}); });

View file

@ -17,17 +17,25 @@ import {
LS_TAG_STATE, LS_TAG_STATE,
LS_TASK_ARCHIVE, LS_TASK_ARCHIVE,
LS_TASK_REPEAT_CFG_STATE, LS_TASK_REPEAT_CFG_STATE,
LS_TASK_STATE LS_TASK_STATE,
} from './ls-keys.const'; } from './ls-keys.const';
import { GlobalConfigState } from '../../features/config/global-config.model'; import { GlobalConfigState } from '../../features/config/global-config.model';
import { projectReducer, ProjectState } from '../../features/project/store/project.reducer'; import {
import { ArchiveTask, Task, TaskArchive, TaskState } from '../../features/tasks/task.model'; projectReducer,
ProjectState,
} from '../../features/project/store/project.reducer';
import {
ArchiveTask,
Task,
TaskArchive,
TaskState,
} from '../../features/tasks/task.model';
import { import {
AppBaseData, AppBaseData,
AppDataComplete, AppDataComplete,
AppDataCompleteOptionalSyncModelChange, AppDataCompleteOptionalSyncModelChange,
AppDataForProjects, AppDataForProjects,
DEFAULT_APP_BASE_DATA DEFAULT_APP_BASE_DATA,
} from '../../imex/sync/sync.model'; } from '../../imex/sync/sync.model';
import { BookmarkState } from '../../features/bookmark/store/bookmark.reducer'; import { BookmarkState } from '../../features/bookmark/store/bookmark.reducer';
import { NoteState } from '../../features/note/store/note.reducer'; import { NoteState } from '../../features/note/store/note.reducer';
@ -37,29 +45,48 @@ import { DEFAULT_PROJECT_ID } from '../../features/project/project.const';
import { import {
ExportedProject, ExportedProject,
ProjectArchive, ProjectArchive,
ProjectArchivedRelatedData ProjectArchivedRelatedData,
} from '../../features/project/project-archive.model'; } from '../../features/project/project-archive.model';
import { Project } from '../../features/project/project.model'; import { Project } from '../../features/project/project.model';
import { CompressionService } from '../compression/compression.service'; 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 { Metric, MetricState } from '../../features/metric/metric.model';
import { Improvement, ImprovementState } from '../../features/metric/improvement/improvement.model'; import {
import { Obstruction, ObstructionState } from '../../features/metric/obstruction/obstruction.model'; Improvement,
import { TaskRepeatCfg, TaskRepeatCfgState } from '../../features/task-repeat-cfg/task-repeat-cfg.model'; 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 { Bookmark } from '../../features/bookmark/bookmark.model';
import { Note } from '../../features/note/note.model'; import { Note } from '../../features/note/note.model';
import { Action, Store } from '@ngrx/store'; import { Action, Store } from '@ngrx/store';
import { taskRepeatCfgReducer } from '../../features/task-repeat-cfg/store/task-repeat-cfg.reducer'; import { taskRepeatCfgReducer } from '../../features/task-repeat-cfg/store/task-repeat-cfg.reducer';
import { Tag, TagState } from '../../features/tag/tag.model'; import { Tag, TagState } from '../../features/tag/tag.model';
import { migrateProjectState } from '../../features/project/migrate-projects-state.util'; 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 { migrateGlobalConfigState } from '../../features/config/migrate-global-config.util';
import { taskReducer } from '../../features/tasks/store/task.reducer'; import { taskReducer } from '../../features/tasks/store/task.reducer';
import { tagReducer } from '../../features/tag/store/tag.reducer'; import { tagReducer } from '../../features/tag/store/tag.reducer';
import { migrateTaskRepeatCfgState } from '../../features/task-repeat-cfg/migrate-task-repeat-cfg-state.util'; import { migrateTaskRepeatCfgState } from '../../features/task-repeat-cfg/migrate-task-repeat-cfg-state.util';
import { environment } from '../../../environments/environment'; import { environment } from '../../../environments/environment';
import { checkFixEntityStateConsistency } from '../../util/check-fix-entity-state-consistency'; 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 { simpleCounterReducer } from '../../features/simple-counter/store/simple-counter.reducer';
import { from, merge, Observable, Subject } from 'rxjs'; import { from, merge, Observable, Subject } from 'rxjs';
import { concatMap, debounceTime, shareReplay, skipWhile } from 'rxjs/operators'; import { concatMap, debounceTime, shareReplay, skipWhile } from 'rxjs/operators';
@ -73,54 +100,65 @@ import { obstructionReducer } from '../../features/metric/obstruction/store/obst
import { import {
migrateImprovementState, migrateImprovementState,
migrateMetricState, migrateMetricState,
migrateObstructionState migrateObstructionState,
} from '../../features/metric/migrate-metric-states.util'; } from '../../features/metric/migrate-metric-states.util';
@Injectable({ @Injectable({
providedIn: 'root', providedIn: 'root',
}) })
export class PersistenceService { export class PersistenceService {
// handled as private but needs to be assigned before the creations // handled as private but needs to be assigned before the creations
_baseModels: PersistenceBaseModel<unknown>[] = []; _baseModels: PersistenceBaseModel<unknown>[] = [];
_projectModels: PersistenceForProjectModel<unknown, unknown>[] = []; _projectModels: PersistenceForProjectModel<unknown, unknown>[] = [];
// TODO auto generate ls keys from appDataKey where possible // TODO auto generate ls keys from appDataKey where possible
globalConfig: PersistenceBaseModel<GlobalConfigState> = this._cmBase<GlobalConfigState>(LS_GLOBAL_CFG, 'globalConfig', migrateGlobalConfigState); globalConfig: PersistenceBaseModel<GlobalConfigState> = this._cmBase<GlobalConfigState>(
reminders: PersistenceBaseModel<Reminder[]> = this._cmBase<Reminder[]>(LS_REMINDER, 'reminders'); LS_GLOBAL_CFG,
'globalConfig',
project: PersistenceBaseEntityModel<ProjectState, Project> = this._cmBaseEntity<ProjectState, Project>( migrateGlobalConfigState,
LS_PROJECT_META_LIST,
'project',
projectReducer as any,
migrateProjectState,
); );
reminders: PersistenceBaseModel<Reminder[]> = this._cmBase<Reminder[]>(
LS_REMINDER,
'reminders',
);
project: PersistenceBaseEntityModel<ProjectState, Project> = this._cmBaseEntity<
ProjectState,
Project
>(LS_PROJECT_META_LIST, 'project', projectReducer as any, migrateProjectState);
tag: PersistenceBaseEntityModel<TagState, Tag> = this._cmBaseEntity<TagState, Tag>( tag: PersistenceBaseEntityModel<TagState, Tag> = this._cmBaseEntity<TagState, Tag>(
LS_TAG_STATE, LS_TAG_STATE,
'tag', 'tag',
tagReducer, tagReducer,
); );
simpleCounter: PersistenceBaseEntityModel<SimpleCounterState, SimpleCounter> = this._cmBaseEntity<SimpleCounterState, SimpleCounter>( simpleCounter: PersistenceBaseEntityModel<
SimpleCounterState,
SimpleCounter
> = this._cmBaseEntity<SimpleCounterState, SimpleCounter>(
LS_SIMPLE_COUNTER_STATE, LS_SIMPLE_COUNTER_STATE,
'simpleCounter', 'simpleCounter',
simpleCounterReducer, simpleCounterReducer,
); );
// METRIC MODELS // METRIC MODELS
metric: PersistenceBaseEntityModel<MetricState, Metric> = this._cmBaseEntity<MetricState, Metric>( metric: PersistenceBaseEntityModel<MetricState, Metric> = this._cmBaseEntity<
LS_METRIC_STATE, MetricState,
'metric', Metric
metricReducer as any, >(LS_METRIC_STATE, 'metric', metricReducer as any, migrateMetricState);
migrateMetricState, improvement: PersistenceBaseEntityModel<
); ImprovementState,
improvement: PersistenceBaseEntityModel<ImprovementState, Improvement> = this._cmBaseEntity<ImprovementState, Improvement>( Improvement
> = this._cmBaseEntity<ImprovementState, Improvement>(
LS_IMPROVEMENT_STATE, LS_IMPROVEMENT_STATE,
'improvement', 'improvement',
improvementReducer, improvementReducer,
migrateImprovementState, migrateImprovementState,
); );
obstruction: PersistenceBaseEntityModel<ObstructionState, Obstruction> = this._cmBaseEntity<ObstructionState, Obstruction>( obstruction: PersistenceBaseEntityModel<
ObstructionState,
Obstruction
> = this._cmBaseEntity<ObstructionState, Obstruction>(
LS_OBSTRUCTION_STATE, LS_OBSTRUCTION_STATE,
'obstruction', 'obstruction',
obstructionReducer as any, obstructionReducer as any,
@ -134,13 +172,14 @@ export class PersistenceService {
taskReducer, taskReducer,
migrateTaskState, migrateTaskState,
); );
taskArchive: PersistenceBaseEntityModel<TaskArchive, ArchiveTask> = this._cmBaseEntity<TaskArchive, ArchiveTask>( taskArchive: PersistenceBaseEntityModel<TaskArchive, ArchiveTask> = this._cmBaseEntity<
LS_TASK_ARCHIVE, TaskArchive,
'taskArchive', ArchiveTask
taskReducer as any, >(LS_TASK_ARCHIVE, 'taskArchive', taskReducer as any, migrateTaskArchiveState);
migrateTaskArchiveState, taskRepeatCfg: PersistenceBaseEntityModel<
); TaskRepeatCfgState,
taskRepeatCfg: PersistenceBaseEntityModel<TaskRepeatCfgState, TaskRepeatCfg> = this._cmBaseEntity<TaskRepeatCfgState, TaskRepeatCfg>( TaskRepeatCfg
> = this._cmBaseEntity<TaskRepeatCfgState, TaskRepeatCfg>(
LS_TASK_REPEAT_CFG_STATE, LS_TASK_REPEAT_CFG_STATE,
'taskRepeatCfg', 'taskRepeatCfg',
taskRepeatCfgReducer as any, taskRepeatCfgReducer as any,
@ -148,31 +187,42 @@ export class PersistenceService {
); );
// PROJECT MODELS // PROJECT MODELS
bookmark: PersistenceForProjectModel<BookmarkState, Bookmark> = this._cmProject<BookmarkState, Bookmark>( bookmark: PersistenceForProjectModel<BookmarkState, Bookmark> = this._cmProject<
LS_BOOKMARK_STATE, BookmarkState,
'bookmark', Bookmark
); >(LS_BOOKMARK_STATE, 'bookmark');
note: PersistenceForProjectModel<NoteState, Note> = this._cmProject<NoteState, Note>( note: PersistenceForProjectModel<NoteState, Note> = this._cmProject<NoteState, Note>(
LS_NOTE_STATE, LS_NOTE_STATE,
'note', 'note',
); );
// LEGACY PROJECT MODELS // LEGACY PROJECT MODELS
legacyMetric: PersistenceForProjectModel<MetricState, Metric> = this._cmProjectLegacy<MetricState, Metric>( legacyMetric: PersistenceForProjectModel<MetricState, Metric> = this._cmProjectLegacy<
LS_METRIC_STATE, MetricState,
'metric' as any, Metric
); >(LS_METRIC_STATE, 'metric' as any);
legacyImprovement: PersistenceForProjectModel<ImprovementState, Improvement> = this._cmProjectLegacy<ImprovementState, Improvement>( legacyImprovement: PersistenceForProjectModel<
ImprovementState,
Improvement
> = this._cmProjectLegacy<ImprovementState, Improvement>(
LS_IMPROVEMENT_STATE, LS_IMPROVEMENT_STATE,
'improvement' as any, 'improvement' as any,
); );
legacyObstruction: PersistenceForProjectModel<ObstructionState, Obstruction> = this._cmProjectLegacy<ObstructionState, Obstruction>( legacyObstruction: PersistenceForProjectModel<
ObstructionState,
Obstruction
> = this._cmProjectLegacy<ObstructionState, Obstruction>(
LS_OBSTRUCTION_STATE, LS_OBSTRUCTION_STATE,
'obstruction' as any, 'obstruction' as any,
); );
onAfterSave$: Subject<{ appDataKey: AllowedDBKeys; data: unknown; isDataImport: boolean; isSyncModelChange: boolean; projectId?: string }> onAfterSave$: Subject<{
= new Subject(); appDataKey: AllowedDBKeys;
data: unknown;
isDataImport: boolean;
isSyncModelChange: boolean;
projectId?: string;
}> = new Subject();
onAfterImport$: Subject<AppDataComplete> = new Subject(); onAfterImport$: Subject<AppDataComplete> = new Subject();
inMemoryComplete$: Observable<AppDataComplete> = merge( inMemoryComplete$: Observable<AppDataComplete> = merge(
@ -183,11 +233,9 @@ export class PersistenceService {
debounceTime(50), debounceTime(50),
concatMap(() => this.loadComplete()), concatMap(() => this.loadComplete()),
// TODO maybe not necessary // TODO maybe not necessary
skipWhile(complete => !isValidAppData(complete)), skipWhile((complete) => !isValidAppData(complete)),
), ),
).pipe( ).pipe(shareReplay(1));
shareReplay(1),
);
private _inMemoryComplete?: AppDataCompleteOptionalSyncModelChange; private _inMemoryComplete?: AppDataCompleteOptionalSyncModelChange;
private _isBlockSaving: boolean = false; private _isBlockSaving: boolean = false;
@ -208,37 +256,58 @@ export class PersistenceService {
async loadProjectArchive(): Promise<ProjectArchive> { async loadProjectArchive(): Promise<ProjectArchive> {
return await this._loadFromDb({ return await this._loadFromDb({
dbKey: 'archivedProjects', dbKey: 'archivedProjects',
legacyDBKey: LS_PROJECT_ARCHIVE legacyDBKey: LS_PROJECT_ARCHIVE,
}); });
} }
async saveProjectArchive(data: ProjectArchive, isDataImport: boolean = false): Promise<unknown> { async saveProjectArchive(
return await this._saveToDb({dbKey: 'archivedProjects', data, isDataImport, isSyncModelChange: false}); data: ProjectArchive,
isDataImport: boolean = false,
): Promise<unknown> {
return await this._saveToDb({
dbKey: 'archivedProjects',
data,
isDataImport,
isSyncModelChange: false,
});
} }
async loadArchivedProject(projectId: string): Promise<ProjectArchivedRelatedData> { async loadArchivedProject(projectId: string): Promise<ProjectArchivedRelatedData> {
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 projectDataCompressed = archive[projectId];
const decompressed = await this._compressionService.decompress(projectDataCompressed); const decompressed = await this._compressionService.decompress(projectDataCompressed);
const parsed = JSON.parse(decompressed); 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; return parsed;
} }
async removeArchivedProject(projectId: string): Promise<void> { async removeArchivedProject(projectId: string): Promise<void> {
const archive = await this._loadFromDb({ const archive = await this._loadFromDb({
dbKey: 'archivedProjects', dbKey: 'archivedProjects',
legacyDBKey: LS_PROJECT_ARCHIVE legacyDBKey: LS_PROJECT_ARCHIVE,
}); });
delete archive[projectId]; delete archive[projectId];
await this.saveProjectArchive(archive); await this.saveProjectArchive(archive);
} }
async saveArchivedProject(projectId: string, archivedProject: ProjectArchivedRelatedData) { async saveArchivedProject(
const current = await this.loadProjectArchive() || {}; projectId: string,
archivedProject: ProjectArchivedRelatedData,
) {
const current = (await this.loadProjectArchive()) || {};
const jsonStr = JSON.stringify(archivedProject); const jsonStr = JSON.stringify(archivedProject);
const compressedData = await this._compressionService.compress(jsonStr); 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({ return this.saveProjectArchive({
...current, ...current,
[projectId]: compressedData, [projectId]: compressedData,
@ -251,17 +320,21 @@ export class PersistenceService {
throw new Error('Project not found'); throw new Error('Project not found');
} }
return { return {
...allProjects.entities[projectId] as Project, ...(allProjects.entities[projectId] as Project),
relatedModels: await this.loadAllRelatedModelDataForProject(projectId), relatedModels: await this.loadAllRelatedModelDataForProject(projectId),
}; };
} }
async loadAllRelatedModelDataForProject(projectId: string): Promise<ProjectArchivedRelatedData> { async loadAllRelatedModelDataForProject(
const forProjectsData = await Promise.all(this._projectModels.map(async (modelCfg) => { projectId: string,
return { ): Promise<ProjectArchivedRelatedData> {
[modelCfg.appDataKey]: await modelCfg.load(projectId), const forProjectsData = await Promise.all(
}; this._projectModels.map(async (modelCfg) => {
})); return {
[modelCfg.appDataKey]: await modelCfg.load(projectId),
};
}),
);
const projectData = Object.assign({}, ...forProjectsData); const projectData = Object.assign({}, ...forProjectsData);
return { return {
...projectData, ...projectData,
@ -269,15 +342,22 @@ export class PersistenceService {
} }
async removeCompleteRelatedDataForProject(projectId: string): Promise<void> { async removeCompleteRelatedDataForProject(projectId: string): Promise<void> {
await Promise.all(this._projectModels.map((modelCfg) => { await Promise.all(
return modelCfg.remove(projectId); this._projectModels.map((modelCfg) => {
})); return modelCfg.remove(projectId);
}),
);
} }
async restoreCompleteRelatedDataForProject(projectId: string, data: ProjectArchivedRelatedData): Promise<void> { async restoreCompleteRelatedDataForProject(
await Promise.all(this._projectModels.map((modelCfg) => { projectId: string,
return modelCfg.save(projectId, data[modelCfg.appDataKey], {}); data: ProjectArchivedRelatedData,
})); ): Promise<void> {
await Promise.all(
this._projectModels.map((modelCfg) => {
return modelCfg.save(projectId, data[modelCfg.appDataKey], {});
}),
);
} }
async archiveProject(projectId: string): Promise<void> { async archiveProject(projectId: string): Promise<void> {
@ -304,9 +384,7 @@ export class PersistenceService {
getLastLocalSyncModelChange(): number | null { getLastLocalSyncModelChange(): number | null {
const la = localStorage.getItem(LS_LAST_LOCAL_SYNC_MODEL_CHANGE); const la = localStorage.getItem(LS_LAST_LOCAL_SYNC_MODEL_CHANGE);
// NOTE: we need to parse because new Date('1570549698000') is "Invalid Date" // NOTE: we need to parse because new Date('1570549698000') is "Invalid Date"
const laParsed = Number.isNaN(Number(la)) const laParsed = Number.isNaN(Number(la)) ? la : +(la as string);
? la
: +(la as string);
if (laParsed === null || laParsed === 0) { if (laParsed === null || laParsed === 0) {
return null; return null;
@ -317,16 +395,21 @@ export class PersistenceService {
} }
async loadBackup(): Promise<AppDataComplete> { async loadBackup(): Promise<AppDataComplete> {
return this._loadFromDb({dbKey: LS_BACKUP, legacyDBKey: LS_BACKUP}); return this._loadFromDb({ dbKey: LS_BACKUP, legacyDBKey: LS_BACKUP });
} }
async saveBackup(backup?: AppDataComplete): Promise<unknown> { async saveBackup(backup?: AppDataComplete): Promise<unknown> {
const data: AppDataComplete = backup || await this.loadComplete(); const data: AppDataComplete = backup || (await this.loadComplete());
return this._saveToDb({dbKey: LS_BACKUP, data, isDataImport: true, isSyncModelChange: true}); return this._saveToDb({
dbKey: LS_BACKUP,
data,
isDataImport: true,
isSyncModelChange: true,
});
} }
async clearBackup(): Promise<unknown> { async clearBackup(): Promise<unknown> {
return this._removeFromDb({dbKey: LS_BACKUP}); return this._removeFromDb({ dbKey: LS_BACKUP });
} }
// NOTE: not including backup // NOTE: not including backup
@ -334,9 +417,7 @@ export class PersistenceService {
let r; let r;
if (!this._inMemoryComplete) { if (!this._inMemoryComplete) {
const projectState = await this.project.loadState(); const projectState = await this.project.loadState();
const pids = projectState const pids = projectState ? (projectState.ids as string[]) : [DEFAULT_PROJECT_ID];
? projectState.ids as string[]
: [DEFAULT_PROJECT_ID];
if (!pids) { if (!pids) {
throw new Error('Project State is broken'); throw new Error('Project State is broken');
} }
@ -353,7 +434,7 @@ export class PersistenceService {
return { return {
...r, ...r,
// TODO remove legacy field // TODO remove legacy field
...({lastActiveTime: this.getLastLocalSyncModelChange()} as any), ...({ lastActiveTime: this.getLastLocalSyncModelChange() } as any),
lastLocalSyncModelChange: this.getLastLocalSyncModelChange(), lastLocalSyncModelChange: this.getLastLocalSyncModelChange(),
}; };
@ -363,21 +444,26 @@ export class PersistenceService {
console.log('IMPORT--->', data); console.log('IMPORT--->', data);
this._isBlockSaving = true; this._isBlockSaving = true;
const forBase = Promise.all(this._baseModels.map(async (modelCfg: PersistenceBaseModel<any>) => { const forBase = Promise.all(
return await modelCfg.saveState(data[modelCfg.appDataKey], {isDataImport: true}); this._baseModels.map(async (modelCfg: PersistenceBaseModel<any>) => {
})); return await modelCfg.saveState(data[modelCfg.appDataKey], {
const forProject = Promise.all(this._projectModels.map(async (modelCfg: PersistenceForProjectModel<any, any>) => { isDataImport: true,
if (!data[modelCfg.appDataKey]) { });
devError('No data for ' + modelCfg.appDataKey + ' - ' + data[modelCfg.appDataKey]); }),
return; );
} const forProject = Promise.all(
return await this._saveForProjectIds(data[modelCfg.appDataKey], modelCfg, true); this._projectModels.map(async (modelCfg: PersistenceForProjectModel<any, any>) => {
})); 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([ return await Promise.all([forBase, forProject])
forBase,
forProject,
])
.then(() => { .then(() => {
this.updateLastLocalSyncModelChange(data.lastLocalSyncModelChange); this.updateLastLocalSyncModelChange(data.lastLocalSyncModelChange);
this._inMemoryComplete = data; this._inMemoryComplete = data;
@ -424,9 +510,10 @@ export class PersistenceService {
): PersistenceBaseModel<T> { ): PersistenceBaseModel<T> {
const model = { const model = {
appDataKey, appDataKey,
loadState: (isSkipMigrate = false) => isSkipMigrate loadState: (isSkipMigrate = false) =>
? this._loadFromDb({dbKey: appDataKey, legacyDBKey: lsKey}) isSkipMigrate
: this._loadFromDb({dbKey: appDataKey, legacyDBKey: lsKey}).then(migrateFn), ? this._loadFromDb({ dbKey: appDataKey, legacyDBKey: lsKey })
: this._loadFromDb({ dbKey: appDataKey, legacyDBKey: lsKey }).then(migrateFn),
// In case we want to check on load // In case we want to check on load
// loadState: async (isSkipMigrate = false) => { // loadState: async (isSkipMigrate = false) => {
// const data = isSkipMigrate // const data = isSkipMigrate
@ -437,14 +524,22 @@ export class PersistenceService {
// } // }
// return data; // return data;
// }, // },
saveState: (data: any, { saveState: (
isDataImport = false, data: any,
isSyncModelChange {
}: { isDataImport?: boolean; isSyncModelChange: boolean }) => { isDataImport = false,
isSyncModelChange,
}: { isDataImport?: boolean; isSyncModelChange: boolean },
) => {
if (data && data.ids && data.entities) { if (data && data.ids && data.entities) {
data = checkFixEntityStateConsistency(data, appDataKey); data = checkFixEntityStateConsistency(data, appDataKey);
} }
return this._saveToDb({dbKey: appDataKey, data, isDataImport, isSyncModelChange}); return this._saveToDb({
dbKey: appDataKey,
data,
isDataImport,
isSyncModelChange,
});
}, },
}; };
if (!isSkipPush) { if (!isSkipPush) {
@ -463,15 +558,15 @@ export class PersistenceService {
...this._cmBase(lsKey, appDataKey, migrateFn, true), ...this._cmBase(lsKey, appDataKey, migrateFn, true),
getById: async (id: string): Promise<M> => { getById: async (id: string): Promise<M> => {
const state = await model.loadState() as any; const state = (await model.loadState()) as any;
return state && state.entities && state.entities[id] || null; return (state && state.entities && state.entities[id]) || null;
}, },
// NOTE: side effects are not executed!!! // NOTE: side effects are not executed!!!
execAction: async (action: Action): Promise<S> => { execAction: async (action: Action): Promise<S> => {
const state = await model.loadState(); const state = await model.loadState();
const newState = reducerFn(state, action); const newState = reducerFn(state, action);
await model.saveState(newState, {isDataImport: false}); await model.saveState(newState, { isDataImport: false });
return newState; return newState;
}, },
}; };
@ -486,11 +581,7 @@ export class PersistenceService {
appDataKey: keyof AppDataForProjects, appDataKey: keyof AppDataForProjects,
migrateFn: (state: S, projectId: string) => S = (v) => v, migrateFn: (state: S, projectId: string) => S = (v) => v,
): PersistenceForProjectModel<S, M> { ): PersistenceForProjectModel<S, M> {
const model = this._cmProjectLegacy<S, M>( const model = this._cmProjectLegacy<S, M>(lsKey, appDataKey, migrateFn);
lsKey,
appDataKey,
migrateFn,
);
this._projectModels.push(model); this._projectModels.push(model);
return model; return model;
} }
@ -503,39 +594,49 @@ export class PersistenceService {
): PersistenceForProjectModel<S, M> { ): PersistenceForProjectModel<S, M> {
const model = { const model = {
appDataKey, appDataKey,
load: (projectId: string): Promise<S> => this._loadFromDb({ load: (projectId: string): Promise<S> =>
dbKey: appDataKey, this._loadFromDb({
projectId, dbKey: appDataKey,
legacyDBKey: this._makeProjectKey(projectId, lsKey) projectId,
}).then(v => migrateFn(v, projectId)), legacyDBKey: this._makeProjectKey(projectId, lsKey),
save: (projectId: string, data: any, { }).then((v) => migrateFn(v, projectId)),
isDataImport = false, save: (
isSyncModelChange projectId: string,
}: { isDataImport?: boolean; isSyncModelChange?: boolean }) => this._saveToDb({ data: any,
dbKey: appDataKey, {
data, isDataImport = false,
isDataImport, isSyncModelChange,
projectId, }: { isDataImport?: boolean; isSyncModelChange?: boolean },
isSyncModelChange, ) =>
}), this._saveToDb({
remove: (projectId: string) => this._removeFromDb({dbKey: appDataKey, projectId}), dbKey: appDataKey,
data,
isDataImport,
projectId,
isSyncModelChange,
}),
remove: (projectId: string) => this._removeFromDb({ dbKey: appDataKey, projectId }),
ent: { ent: {
getById: async (projectId: string, id: string): Promise<M> => { getById: async (projectId: string, id: string): Promise<M> => {
const state = await model.load(projectId) as any; const state = (await model.load(projectId)) as any;
return state && state.entities && state.entities[id] || null; return (state && state.entities && state.entities[id]) || null;
}, },
}, },
}; };
return model; return model;
} }
private async _loadAppDataForProjects(projectIds: string[]): Promise<AppDataForProjects> { private async _loadAppDataForProjects(
const forProjectsData = await Promise.all(this._projectModels.map(async (modelCfg) => { projectIds: string[],
const modelState = await this._loadForProjectIds(projectIds, modelCfg.load); ): Promise<AppDataForProjects> {
return { const forProjectsData = await Promise.all(
[modelCfg.appDataKey]: modelState, this._projectModels.map(async (modelCfg) => {
}; const modelState = await this._loadForProjectIds(projectIds, modelCfg.load);
})); return {
[modelCfg.appDataKey]: modelState,
};
}),
);
return Object.assign({}, ...forProjectsData); return Object.assign({}, ...forProjectsData);
} }
@ -546,35 +647,45 @@ export class PersistenceService {
const dataForProject = await getDataFn(projectId); const dataForProject = await getDataFn(projectId);
return { return {
...prevAcc, ...prevAcc,
[projectId]: dataForProject [projectId]: dataForProject,
}; };
}, Promise.resolve({})); }, Promise.resolve({}));
} }
// eslint-disable-next-line // eslint-disable-next-line
private async _saveForProjectIds(data: any, projectModel: PersistenceForProjectModel<unknown, unknown>, isDataImport = false) { private async _saveForProjectIds(
data: any,
projectModel: PersistenceForProjectModel<unknown, unknown>,
isDataImport = false,
) {
const promises: Promise<any>[] = []; const promises: Promise<any>[] = [];
Object.keys(data).forEach(projectId => { Object.keys(data).forEach((projectId) => {
if (data[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); return await Promise.all(promises);
} }
private _makeProjectKey(projectId: string, subKey: string, additional?: string) { 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 // DATA STORAGE INTERFACE
// --------------------- // ---------------------
private _getIDBKey(dbKey: AllowedDBKeys, projectId?: string) { private _getIDBKey(dbKey: AllowedDBKeys, projectId?: string) {
return projectId return projectId ? 'p__' + projectId + '__' + dbKey : dbKey;
? '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; dbKey: AllowedDBKeys;
data: any; data: any;
projectId?: string; projectId?: string;
@ -583,19 +694,25 @@ export class PersistenceService {
}): Promise<any> { }): Promise<any> {
if (!this._isBlockSaving || isDataImport === true) { if (!this._isBlockSaving || isDataImport === true) {
const idbKey = this._getIDBKey(dbKey, projectId); 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); const r = await this._databaseService.save(idbKey, data);
this._updateInMemory({ this._updateInMemory({
projectId, projectId,
appDataKey: dbKey, appDataKey: dbKey,
data data,
}); });
if (isSyncModelChange) { if (isSyncModelChange) {
this.updateLastLocalSyncModelChange(); this.updateLastLocalSyncModelChange();
} }
this.onAfterSave$.next({appDataKey: dbKey, data, isDataImport, projectId, isSyncModelChange}); this.onAfterSave$.next({
appDataKey: dbKey,
data,
isDataImport,
projectId,
isSyncModelChange,
});
return r; return r;
} else { } else {
@ -604,14 +721,18 @@ export class PersistenceService {
} }
} }
private async _removeFromDb({dbKey, isDataImport = false, projectId}: { private async _removeFromDb({
dbKey,
isDataImport = false,
projectId,
}: {
dbKey: AllowedDBKeys; dbKey: AllowedDBKeys;
projectId?: string; projectId?: string;
isDataImport?: boolean; isDataImport?: boolean;
}): Promise<any> { }): Promise<any> {
const idbKey = this._getIDBKey(dbKey, projectId); const idbKey = this._getIDBKey(dbKey, projectId);
if (!this._isBlockSaving || isDataImport === true) { if (!this._isBlockSaving || isDataImport === true) {
this._store.dispatch(removeFromDb({dbKey})); this._store.dispatch(removeFromDb({ dbKey }));
return this._databaseService.remove(idbKey); return this._databaseService.remove(idbKey);
} else { } else {
console.warn('BLOCKED SAVING for ', dbKey); 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; legacyDBKey: string;
dbKey: AllowedDBKeys; dbKey: AllowedDBKeys;
projectId?: string; projectId?: string;
@ -628,10 +753,18 @@ export class PersistenceService {
// NOTE: too much clutter // NOTE: too much clutter
// this._store.dispatch(loadFromDb({dbKey})); // this._store.dispatch(loadFromDb({dbKey}));
// TODO remove legacy stuff // 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; appDataKey: AllowedDBKeys;
projectId?: string; projectId?: string;
data: any; data: any;
@ -644,11 +777,16 @@ export class PersistenceService {
complete: this._inMemoryComplete, complete: this._inMemoryComplete,
projectId, projectId,
appDataKey, appDataKey,
data data,
}); });
} }
private _extendAppDataComplete({complete, appDataKey, projectId, data}: { private _extendAppDataComplete({
complete,
appDataKey,
projectId,
data,
}: {
complete: AppDataComplete | AppDataCompleteOptionalSyncModelChange; complete: AppDataComplete | AppDataCompleteOptionalSyncModelChange;
appDataKey: AllowedDBKeys; appDataKey: AllowedDBKeys;
projectId?: string; projectId?: string;
@ -657,16 +795,14 @@ export class PersistenceService {
// console.log(appDataKey, data && data.ids && data.ids.length); // console.log(appDataKey, data && data.ids && data.ids.length);
return { return {
...complete, ...complete,
...( ...(projectId
projectId ? {
? {
[appDataKey]: { [appDataKey]: {
...((complete as any)[appDataKey]), ...(complete as any)[appDataKey],
[projectId]: data [projectId]: data,
} },
} }
: {[appDataKey]: data} : { [appDataKey]: data }),
)
}; };
} }
} }

View file

@ -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 { MAT_SNACK_BAR_DATA, MatSnackBarRef } from '@angular/material/snack-bar';
import { SnackParams } from '../snack.model'; import { SnackParams } from '../snack.model';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
@ -8,7 +14,7 @@ import { debounceTime } from 'rxjs/operators';
selector: 'snack-custom', selector: 'snack-custom',
templateUrl: './snack-custom.component.html', templateUrl: './snack-custom.component.html',
styleUrls: ['./snack-custom.component.scss'], styleUrls: ['./snack-custom.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class SnackCustomComponent implements OnInit, OnDestroy { export class SnackCustomComponent implements OnInit, OnDestroy {
private _subs: Subscription = new Subscription(); private _subs: Subscription = new Subscription();
@ -16,8 +22,7 @@ export class SnackCustomComponent implements OnInit, OnDestroy {
constructor( constructor(
@Inject(MAT_SNACK_BAR_DATA) public data: SnackParams, @Inject(MAT_SNACK_BAR_DATA) public data: SnackParams,
public snackBarRef: MatSnackBarRef<SnackCustomComponent>, public snackBarRef: MatSnackBarRef<SnackCustomComponent>,
) { ) {}
}
ngOnInit() { ngOnInit() {
if (this.data.promise) { if (this.data.promise) {
@ -31,7 +36,7 @@ export class SnackCustomComponent implements OnInit, OnDestroy {
if (!v) { if (!v) {
this.snackBarRef.dismiss(); this.snackBarRef.dismiss();
} }
}) }),
); );
} }
} }

View file

@ -5,15 +5,7 @@ import { SnackCustomComponent } from './snack-custom/snack-custom.component';
import { UiModule } from '../../ui/ui.module'; import { UiModule } from '../../ui/ui.module';
@NgModule({ @NgModule({
imports: [ imports: [UiModule, CommonModule, MatSnackBarModule],
UiModule, declarations: [SnackCustomComponent],
CommonModule,
MatSnackBarModule,
],
declarations: [
SnackCustomComponent,
],
}) })
export class SnackModule { export class SnackModule {}
}

View file

@ -16,7 +16,9 @@ import { debounce } from 'helpful-decorators';
}) })
export class SnackService { export class SnackService {
private _ref?: MatSnackBarRef<SnackCustomComponent | SimpleSnackBar>; private _ref?: MatSnackBarRef<SnackCustomComponent | SimpleSnackBar>;
private _onWorkContextChange$: Observable<unknown> = this._actions$.pipe(ofType(setActiveWorkContext)); private _onWorkContextChange$: Observable<unknown> = this._actions$.pipe(
ofType(setActiveWorkContext),
);
constructor( constructor(
private _store$: Store<any>, private _store$: Store<any>,
@ -32,7 +34,7 @@ export class SnackService {
open(params: SnackParams | string) { open(params: SnackParams | string) {
if (typeof params === 'string') { if (typeof params === 'string') {
params = {msg: params}; params = { msg: params };
} }
this._openSnack(params); this._openSnack(params);
} }
@ -61,7 +63,7 @@ export class SnackService {
translateParams = {}, translateParams = {},
showWhile$, showWhile$,
promise, promise,
isSpinner isSpinner,
} = params; } = params;
const cfg = { const cfg = {
@ -69,9 +71,10 @@ export class SnackService {
...config, ...config,
data: { data: {
...params, ...params,
msg: (isSkipTranslate) msg: isSkipTranslate
? msg ? 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) { if (actionStr && actionId && this._ref) {
this._ref.onAction() this._ref
.onAction()
.pipe(takeUntil(_destroy$)) .pipe(takeUntil(_destroy$))
.subscribe(() => { .subscribe(() => {
this._store$.dispatch({ this._store$.dispatch({
type: actionId, type: actionId,
payload: actionPayload payload: actionPayload,
}); });
destroySubs(); destroySubs();
}); });
this._ref.afterDismissed() this._ref
.afterDismissed()
.pipe(takeUntil(_destroy$)) .pipe(takeUntil(_destroy$))
.subscribe(() => { .subscribe(() => {
destroySubs(); destroySubs();

View file

@ -16,31 +16,38 @@ import { WorkContextService } from '../../features/work-context/work-context.ser
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { remote } from 'electron'; import { remote } from 'electron';
@Injectable({providedIn: 'root'}) @Injectable({ providedIn: 'root' })
export class GlobalThemeService { export class GlobalThemeService {
isDarkTheme$: Observable<boolean> = (IS_ELECTRON && this._electronService.isMacOS) isDarkTheme$: Observable<boolean> =
? new Observable(subscriber => { IS_ELECTRON && this._electronService.isMacOS
subscriber.next((this._electronService.remote as typeof remote).nativeTheme.shouldUseDarkColors); ? new Observable((subscriber) => {
(this._electronService.remote as typeof remote) subscriber.next(
.systemPreferences (this._electronService.remote as typeof remote).nativeTheme
.subscribeNotification('AppleInterfaceThemeChangedNotification', () => subscriber.next( .shouldUseDarkColors,
(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<string | null> = combineLatest([ backgroundImg$: Observable<string | null> = combineLatest([
this._workContextService.currentTheme$, this._workContextService.currentTheme$,
this.isDarkTheme$, this.isDarkTheme$,
]).pipe( ]).pipe(
map(([theme, isDarkMode]) => isDarkMode map(([theme, isDarkMode]) =>
? theme.backgroundImageDark isDarkMode ? theme.backgroundImageDark : theme.backgroundImageLight,
: theme.backgroundImageLight
), ),
distinctUntilChanged() distinctUntilChanged(),
); );
constructor( constructor(
@ -53,8 +60,7 @@ export class GlobalThemeService {
private _domSanitizer: DomSanitizer, private _domSanitizer: DomSanitizer,
private _chartThemeService: NgChartThemeService, private _chartThemeService: NgChartThemeService,
private _chromeExtensionInterfaceService: ChromeExtensionInterfaceService, private _chromeExtensionInterfaceService: ChromeExtensionInterfaceService,
) { ) {}
}
init() { init() {
// This is here to make web page reloads on non work context pages at least usable // 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]) => { icons.forEach(([name, path]) => {
this._matIconRegistry.addSvgIcon( this._matIconRegistry.addSvgIcon(
name, name,
this._domSanitizer.bypassSecurityTrustResourceUrl(path) this._domSanitizer.bypassSecurityTrustResourceUrl(path),
); );
}); });
} }
private _initThemeWatchers() { private _initThemeWatchers() {
// init theme watchers // 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)); this.isDarkTheme$.subscribe((isDarkTheme) => this._setDarkTheme(isDarkTheme));
} }
@ -155,22 +163,26 @@ export class GlobalThemeService {
} }
private _setChartTheme(isDarkTheme: boolean) { private _setChartTheme(isDarkTheme: boolean) {
const overrides = (isDarkTheme) const overrides = isDarkTheme
? { ? {
legend: { legend: {
labels: {fontColor: 'white'} labels: { fontColor: 'white' },
}, },
scales: { scales: {
xAxes: [{ xAxes: [
ticks: {fontColor: 'white'}, {
gridLines: {color: 'rgba(255,255,255,0.1)'} ticks: { fontColor: 'white' },
}], gridLines: { color: 'rgba(255,255,255,0.1)' },
yAxes: [{ },
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); this._chartThemeService.setColorschemesOptions(overrides);
} }

View file

@ -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 { BookmarkService } from '../bookmark.service';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { DialogEditBookmarkComponent } from '../dialog-edit-bookmark/dialog-edit-bookmark.component'; import { DialogEditBookmarkComponent } from '../dialog-edit-bookmark/dialog-edit-bookmark.component';
@ -14,10 +21,7 @@ import { T } from '../../../t.const';
templateUrl: './bookmark-bar.component.html', templateUrl: './bookmark-bar.component.html',
styleUrls: ['./bookmark-bar.component.scss'], styleUrls: ['./bookmark-bar.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
animations: [ animations: [fadeAnimation, slideAnimation],
fadeAnimation,
slideAnimation,
],
}) })
export class BookmarkBarComponent implements OnDestroy { export class BookmarkBarComponent implements OnDestroy {
isDragOver: boolean = false; isDragOver: boolean = false;
@ -41,16 +45,16 @@ export class BookmarkBarComponent implements OnDestroy {
// } // }
// }); // });
this._subs.add(this._dragulaService.dropModel(this.LIST_ID) this._subs.add(
.subscribe(({targetModel}: any) => { this._dragulaService.dropModel(this.LIST_ID).subscribe(({ targetModel }: any) => {
// const {target, source, targetModel, item} = params; // const {target, source, targetModel, item} = params;
const newIds = targetModel.map((m: Bookmark) => m.id); const newIds = targetModel.map((m: Bookmark) => m.id);
this.bookmarkService.reorderBookmarks(newIds); this.bookmarkService.reorderBookmarks(newIds);
}) }),
); );
} }
@ViewChild('bookmarkBar', {read: ElementRef}) set bookmarkBarEl(content: ElementRef) { @ViewChild('bookmarkBar', { read: ElementRef }) set bookmarkBarEl(content: ElementRef) {
if (content && content.nativeElement) { if (content && content.nativeElement) {
this.bookmarkBarHeight = content.nativeElement.offsetHeight; this.bookmarkBarHeight = content.nativeElement.offsetHeight;
} }
@ -79,12 +83,14 @@ export class BookmarkBarComponent implements OnDestroy {
} }
openEditDialog(bookmark?: Bookmark) { openEditDialog(bookmark?: Bookmark) {
this._matDialog.open(DialogEditBookmarkComponent, { this._matDialog
restoreFocus: true, .open(DialogEditBookmarkComponent, {
data: { restoreFocus: true,
bookmark data: {
}, bookmark,
}).afterClosed() },
})
.afterClosed()
.subscribe((bookmarkIN) => { .subscribe((bookmarkIN) => {
if (bookmarkIN) { if (bookmarkIN) {
if (bookmarkIN.id) { if (bookmarkIN.id) {

View file

@ -8,18 +8,16 @@ import { ElectronService } from '../../../core/electron/electron.service';
import { ipcRenderer, shell } from 'electron'; import { ipcRenderer, shell } from 'electron';
@Directive({ @Directive({
selector: '[bookmarkLink]' selector: '[bookmarkLink]',
}) })
export class BookmarkLinkDirective { export class BookmarkLinkDirective {
@Input() type?: BookmarkType; @Input() type?: BookmarkType;
@Input() href?: BookmarkType; @Input() href?: BookmarkType;
constructor( constructor(
private _electronService: ElectronService, private _electronService: ElectronService,
private _snackService: SnackService, private _snackService: SnackService,
) { ) {}
}
@HostListener('click', ['$event']) onClick(ev: Event) { @HostListener('click', ['$event']) onClick(ev: Event) {
if (!this.type || !this.href) { if (!this.type || !this.href) {
@ -39,7 +37,7 @@ export class BookmarkLinkDirective {
} else if (this.type === 'COMMAND') { } else if (this.type === 'COMMAND') {
this._snackService.open({ this._snackService.open({
msg: T.GLOBAL_SNACK.RUNNING_X, msg: T.GLOBAL_SNACK.RUNNING_X,
translateParams: {str: this.href}, translateParams: { str: this.href },
ico: 'laptop_windows', ico: 'laptop_windows',
}); });
this._exec(this.href); this._exec(this.href);

View file

@ -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; export type BookmarkType = DropPasteInputType;

View file

@ -16,17 +16,13 @@ import { BookmarkLinkDirective } from './bookmark-link/bookmark-link.directive';
UiModule, UiModule,
FormsModule, FormsModule,
StoreModule.forFeature(BOOKMARK_FEATURE_NAME, bookmarkReducer), StoreModule.forFeature(BOOKMARK_FEATURE_NAME, bookmarkReducer),
EffectsModule.forFeature([BookmarkEffects]) EffectsModule.forFeature([BookmarkEffects]),
], ],
declarations: [ declarations: [
BookmarkBarComponent, BookmarkBarComponent,
DialogEditBookmarkComponent, DialogEditBookmarkComponent,
BookmarkLinkDirective BookmarkLinkDirective,
],
exports: [
BookmarkBarComponent,
DialogEditBookmarkComponent
], ],
exports: [BookmarkBarComponent, DialogEditBookmarkComponent],
}) })
export class BookmarkModule { export class BookmarkModule {}
}

View file

@ -4,7 +4,7 @@ import {
BookmarkState, BookmarkState,
initialBookmarkState, initialBookmarkState,
selectAllBookmarks, selectAllBookmarks,
selectIsShowBookmarkBar selectIsShowBookmarkBar,
} from './store/bookmark.reducer'; } from './store/bookmark.reducer';
import { import {
AddBookmark, AddBookmark,
@ -14,7 +14,7 @@ import {
ReorderBookmarks, ReorderBookmarks,
ShowBookmarks, ShowBookmarks,
ToggleBookmarks, ToggleBookmarks,
UpdateBookmark UpdateBookmark,
} from './store/bookmark.actions'; } from './store/bookmark.actions';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Bookmark } from './bookmark.model'; 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 { DialogEditBookmarkComponent } from './dialog-edit-bookmark/dialog-edit-bookmark.component';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { PersistenceService } from '../../core/persistence/persistence.service'; 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'; import { DropPasteInput } from '../../core/drop-paste-input/drop-paste.model';
@Injectable({ @Injectable({
@ -30,14 +33,15 @@ import { DropPasteInput } from '../../core/drop-paste-input/drop-paste.model';
}) })
export class BookmarkService { export class BookmarkService {
bookmarks$: Observable<Bookmark[]> = this._store$.pipe(select(selectAllBookmarks)); bookmarks$: Observable<Bookmark[]> = this._store$.pipe(select(selectAllBookmarks));
isShowBookmarks$: Observable<boolean> = this._store$.pipe(select(selectIsShowBookmarkBar)); isShowBookmarks$: Observable<boolean> = this._store$.pipe(
select(selectIsShowBookmarkBar),
);
constructor( constructor(
private _store$: Store<BookmarkState>, private _store$: Store<BookmarkState>,
private _matDialog: MatDialog, private _matDialog: MatDialog,
private _persistenceService: PersistenceService, private _persistenceService: PersistenceService,
) { ) {}
}
async loadStateForProject(projectId: string) { async loadStateForProject(projectId: string) {
const lsBookmarkState = await this._persistenceService.bookmark.load(projectId); const lsBookmarkState = await this._persistenceService.bookmark.load(projectId);
@ -45,24 +49,26 @@ export class BookmarkService {
} }
loadState(state: BookmarkState) { loadState(state: BookmarkState) {
this._store$.dispatch(new LoadBookmarkState({state})); this._store$.dispatch(new LoadBookmarkState({ state }));
} }
addBookmark(bookmark: Bookmark) { addBookmark(bookmark: Bookmark) {
this._store$.dispatch(new AddBookmark({ this._store$.dispatch(
bookmark: { new AddBookmark({
...bookmark, bookmark: {
id: shortid() ...bookmark,
} id: shortid(),
})); },
}),
);
} }
deleteBookmark(id: string) { deleteBookmark(id: string) {
this._store$.dispatch(new DeleteBookmark({id})); this._store$.dispatch(new DeleteBookmark({ id }));
} }
updateBookmark(id: string, changes: Partial<Bookmark>) { updateBookmark(id: string, changes: Partial<Bookmark>) {
this._store$.dispatch(new UpdateBookmark({bookmark: {id, changes}})); this._store$.dispatch(new UpdateBookmark({ bookmark: { id, changes } }));
} }
showBookmarks() { showBookmarks() {
@ -78,7 +84,7 @@ export class BookmarkService {
} }
reorderBookmarks(ids: string[]) { reorderBookmarks(ids: string[]) {
this._store$.dispatch(new ReorderBookmarks({ids})); this._store$.dispatch(new ReorderBookmarks({ ids }));
} }
// HANDLE INPUT // HANDLE INPUT
@ -106,12 +112,14 @@ export class BookmarkService {
ev.preventDefault(); ev.preventDefault();
ev.stopPropagation(); ev.stopPropagation();
this._matDialog.open(DialogEditBookmarkComponent, { this._matDialog
restoreFocus: true, .open(DialogEditBookmarkComponent, {
data: { restoreFocus: true,
bookmark: {...bookmark}, data: {
}, bookmark: { ...bookmark },
}).afterClosed() },
})
.afterClosed()
.subscribe((bookmarkIN) => { .subscribe((bookmarkIN) => {
if (bookmarkIN) { if (bookmarkIN) {
if (bookmarkIN.id) { if (bookmarkIN.id) {
@ -121,6 +129,5 @@ export class BookmarkService {
} }
} }
}); });
} }
} }

View file

@ -17,7 +17,7 @@ interface BookmarkSelectType {
selector: 'dialog-edit-bookmark', selector: 'dialog-edit-bookmark',
templateUrl: './dialog-edit-bookmark.component.html', templateUrl: './dialog-edit-bookmark.component.html',
styleUrls: ['./dialog-edit-bookmark.component.scss'], styleUrls: ['./dialog-edit-bookmark.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class DialogEditBookmarkComponent implements OnInit { export class DialogEditBookmarkComponent implements OnInit {
T: typeof T = T; T: typeof T = T;
@ -30,29 +30,28 @@ export class DialogEditBookmarkComponent implements OnInit {
map((searchTerm) => { map((searchTerm) => {
// Note: the outer array signifies the observable stream the other is the value // Note: the outer array signifies the observable stream the other is the value
return this.customIcons.filter((icoStr) => return this.customIcons.filter((icoStr) =>
icoStr.toLowerCase().includes(searchTerm.toLowerCase()) icoStr.toLowerCase().includes(searchTerm.toLowerCase()),
); );
}), }),
); );
constructor( constructor(
private _matDialogRef: MatDialogRef<DialogEditBookmarkComponent>, private _matDialogRef: MatDialogRef<DialogEditBookmarkComponent>,
@Inject(MAT_DIALOG_DATA) public data: { bookmark: Bookmark } @Inject(MAT_DIALOG_DATA) public data: { bookmark: Bookmark },
) { ) {}
}
ngOnInit() { ngOnInit() {
this.bookmarkCopy = {...this.data.bookmark} as BookmarkCopy; this.bookmarkCopy = { ...this.data.bookmark } as BookmarkCopy;
if (!this.bookmarkCopy.type) { if (!this.bookmarkCopy.type) {
this.bookmarkCopy.type = 'LINK'; this.bookmarkCopy.type = 'LINK';
} }
this.types = [ this.types = [
{type: 'LINK', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.LINK}, { type: 'LINK', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.LINK },
{type: 'IMG', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.IMG}, { type: 'IMG', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.IMG },
]; ];
if (IS_ELECTRON) { if (IS_ELECTRON) {
this.types.push({type: 'FILE', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.FILE}); 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: 'COMMAND', title: T.F.BOOKMARK.DIALOG_EDIT.TYPES.COMMAND });
} }
} }
@ -68,7 +67,10 @@ export class DialogEditBookmarkComponent implements OnInit {
return; 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.bookmarkCopy.path = 'http://' + this.bookmarkCopy.path;
} }
this.close(this.bookmarkCopy); this.close(this.bookmarkCopy);

View file

@ -18,29 +18,25 @@ export enum BookmarkActionTypes {
export class LoadBookmarkState implements Action { export class LoadBookmarkState implements Action {
readonly type: string = BookmarkActionTypes.LoadBookmarkState; readonly type: string = BookmarkActionTypes.LoadBookmarkState;
constructor(public payload: { state: BookmarkState }) { constructor(public payload: { state: BookmarkState }) {}
}
} }
export class AddBookmark implements Action { export class AddBookmark implements Action {
readonly type: string = BookmarkActionTypes.AddBookmark; readonly type: string = BookmarkActionTypes.AddBookmark;
constructor(public payload: { bookmark: Bookmark }) { constructor(public payload: { bookmark: Bookmark }) {}
}
} }
export class UpdateBookmark implements Action { export class UpdateBookmark implements Action {
readonly type: string = BookmarkActionTypes.UpdateBookmark; readonly type: string = BookmarkActionTypes.UpdateBookmark;
constructor(public payload: { bookmark: Update<Bookmark> }) { constructor(public payload: { bookmark: Update<Bookmark> }) {}
}
} }
export class DeleteBookmark implements Action { export class DeleteBookmark implements Action {
readonly type: string = BookmarkActionTypes.DeleteBookmark; readonly type: string = BookmarkActionTypes.DeleteBookmark;
constructor(public payload: { id: string }) { constructor(public payload: { id: string }) {}
}
} }
export class ShowBookmarks implements Action { export class ShowBookmarks implements Action {
@ -58,17 +54,15 @@ export class ToggleBookmarks implements Action {
export class ReorderBookmarks implements Action { export class ReorderBookmarks implements Action {
readonly type: string = BookmarkActionTypes.ReorderBookmarks; readonly type: string = BookmarkActionTypes.ReorderBookmarks;
constructor(public payload: { ids: string[] }) { constructor(public payload: { ids: string[] }) {}
}
} }
export type BookmarkActions = export type BookmarkActions =
LoadBookmarkState | LoadBookmarkState
| AddBookmark | AddBookmark
| UpdateBookmark | UpdateBookmark
| DeleteBookmark | DeleteBookmark
| ShowBookmarks | ShowBookmarks
| HideBookmarks | HideBookmarks
| ToggleBookmarks | ToggleBookmarks
| ReorderBookmarks | ReorderBookmarks;
;

View file

@ -10,38 +10,39 @@ import { WorkContextService } from '../../work-context/work-context.service';
@Injectable() @Injectable()
export class BookmarkEffects { export class BookmarkEffects {
@Effect({ dispatch: false })
@Effect({dispatch: false}) updateBookmarks$: Observable<unknown> = this._actions$ updateBookmarks$: Observable<unknown> = this._actions$.pipe(
.pipe( ofType(
ofType( BookmarkActionTypes.AddBookmark,
BookmarkActionTypes.AddBookmark, BookmarkActionTypes.UpdateBookmark,
BookmarkActionTypes.UpdateBookmark, BookmarkActionTypes.DeleteBookmark,
BookmarkActionTypes.DeleteBookmark, BookmarkActionTypes.ShowBookmarks,
BookmarkActionTypes.ShowBookmarks, BookmarkActionTypes.HideBookmarks,
BookmarkActionTypes.HideBookmarks, BookmarkActionTypes.ToggleBookmarks,
BookmarkActionTypes.ToggleBookmarks, ),
), switchMap(() =>
switchMap(() => combineLatest([ combineLatest([
this._workContextService.activeWorkContextIdIfProject$, this._workContextService.activeWorkContextIdIfProject$,
this._store$.pipe(select(selectBookmarkFeatureState)), this._store$.pipe(select(selectBookmarkFeatureState)),
]).pipe(first())), ]).pipe(first()),
tap(([projectId, state]) => this._saveToLs(projectId, state)), ),
); tap(([projectId, state]) => this._saveToLs(projectId, state)),
);
constructor( constructor(
private _actions$: Actions, private _actions$: Actions,
private _store$: Store<any>, private _store$: Store<any>,
private _persistenceService: PersistenceService, private _persistenceService: PersistenceService,
private _workContextService: WorkContextService, private _workContextService: WorkContextService,
) { ) {}
}
private _saveToLs(currentProjectId: string, bookmarkState: BookmarkState) { private _saveToLs(currentProjectId: string, bookmarkState: BookmarkState) {
if (currentProjectId) { if (currentProjectId) {
this._persistenceService.bookmark.save(currentProjectId, bookmarkState, {isSyncModelChange: true}); this._persistenceService.bookmark.save(currentProjectId, bookmarkState, {
isSyncModelChange: true,
});
} else { } else {
throw new Error('No current project id'); throw new Error('No current project id');
} }
} }
} }

View file

@ -6,7 +6,7 @@ import {
DeleteBookmark, DeleteBookmark,
LoadBookmarkState, LoadBookmarkState,
ReorderBookmarks, ReorderBookmarks,
UpdateBookmark UpdateBookmark,
} from './bookmark.actions'; } from './bookmark.actions';
import { Bookmark } from '../bookmark.model'; import { Bookmark } from '../bookmark.model';
import { createFeatureSelector, createSelector } from '@ngrx/store'; import { createFeatureSelector, createSelector } from '@ngrx/store';
@ -19,19 +19,29 @@ export interface BookmarkState extends EntityState<Bookmark> {
} }
export const adapter: EntityAdapter<Bookmark> = createEntityAdapter<Bookmark>(); export const adapter: EntityAdapter<Bookmark> = createEntityAdapter<Bookmark>();
export const selectBookmarkFeatureState = createFeatureSelector<BookmarkState>(BOOKMARK_FEATURE_NAME); export const selectBookmarkFeatureState = createFeatureSelector<BookmarkState>(
export const {selectIds, selectEntities, selectAll, selectTotal} = adapter.getSelectors(); BOOKMARK_FEATURE_NAME,
);
export const {
selectIds,
selectEntities,
selectAll,
selectTotal,
} = adapter.getSelectors();
export const selectAllBookmarks = createSelector(selectBookmarkFeatureState, selectAll); 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({ export const initialBookmarkState: BookmarkState = adapter.getInitialState({
// additional entity state properties // additional entity state properties
isShowBookmarks: false isShowBookmarks: false,
}); });
export function bookmarkReducer( export function bookmarkReducer(
state: BookmarkState = initialBookmarkState, state: BookmarkState = initialBookmarkState,
action: BookmarkActions action: BookmarkActions,
): BookmarkState { ): BookmarkState {
switch (action.type) { switch (action.type) {
case BookmarkActionTypes.AddBookmark: { case BookmarkActionTypes.AddBookmark: {
@ -47,16 +57,16 @@ export function bookmarkReducer(
} }
case BookmarkActionTypes.LoadBookmarkState: case BookmarkActionTypes.LoadBookmarkState:
return {...(action as LoadBookmarkState).payload.state}; return { ...(action as LoadBookmarkState).payload.state };
case BookmarkActionTypes.ShowBookmarks: case BookmarkActionTypes.ShowBookmarks:
return {...state, isShowBookmarks: true}; return { ...state, isShowBookmarks: true };
case BookmarkActionTypes.HideBookmarks: case BookmarkActionTypes.HideBookmarks:
return {...state, isShowBookmarks: false}; return { ...state, isShowBookmarks: false };
case BookmarkActionTypes.ToggleBookmarks: case BookmarkActionTypes.ToggleBookmarks:
return {...state, isShowBookmarks: !state.isShowBookmarks}; return { ...state, isShowBookmarks: !state.isShowBookmarks };
case BookmarkActionTypes.ReorderBookmarks: { case BookmarkActionTypes.ReorderBookmarks: {
const oldIds = state.ids as string[]; const oldIds = state.ids as string[];
@ -67,7 +77,7 @@ export function bookmarkReducer(
// check if we have the same values inside the arrays // check if we have the same values inside the arrays
if (oldIds.slice(0).sort().join(',') === newIds.slice(0).sort().join(',')) { if (oldIds.slice(0).sort().join(',') === newIds.slice(0).sort().join(',')) {
return {...state, ids: newIds}; return { ...state, ids: newIds };
} else { } else {
console.error('Bookmark lost while reordering. Not executing reorder'); console.error('Bookmark lost while reordering. Not executing reorder');
return state; return state;
@ -79,5 +89,3 @@ export function bookmarkReducer(
} }
} }
} }

View file

@ -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 { FormlyFieldConfig, FormlyFormOptions } from '@ngx-formly/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { GlobalConfigSectionKey } from '../global-config.model'; import { GlobalConfigSectionKey } from '../global-config.model';
@ -13,20 +19,21 @@ import { exists } from '../../../util/exists';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class ConfigFormComponent { export class ConfigFormComponent {
T: typeof T = T; T: typeof T = T;
config?: Record<string, unknown>; config?: Record<string, unknown>;
@Input() sectionKey?: GlobalConfigSectionKey | ProjectCfgFormKey; @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[]; fields?: FormlyFieldConfig[];
form: FormGroup = new FormGroup({}); form: FormGroup = new FormGroup({});
options: FormlyFormOptions = {}; options: FormlyFormOptions = {};
constructor() { constructor() {}
}
@Input() set cfg(cfg: Record<string, unknown>) { @Input() set cfg(cfg: Record<string, unknown>) {
this.config = {...cfg}; this.config = { ...cfg };
} }
// somehow needed for the form to work // somehow needed for the form to work

Some files were not shown because too many files have changed in this diff Show more