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 TEXTAREA = 'dialog-add-note textarea';
@ -7,8 +7,7 @@ const NOTES_WRAPPER = 'notes';
module.exports = {
async command(this: NightwatchBrowser, noteName: string) {
return this
.moveToElement(NOTES_WRAPPER, 10, 50)
return this.moveToElement(NOTES_WRAPPER, 10, 50)
.waitForElementPresent(ADD_NOTE_BTN)
.click(ADD_NOTE_BTN)
@ -16,7 +15,6 @@ module.exports = {
.setValue(TEXTAREA, noteName)
.click(ADD_NOTE_SUBMIT_BTN)
.moveToElement(NOTES_WRAPPER, 10, 50)
;
}
.moveToElement(NOTES_WRAPPER, 10, 50);
},
};

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 ROUTER_WRAPPER = '.route-wrapper';
module.exports = {
async command(this: NightwatchBrowser, taskName: string) {
return this
.waitForElementVisible(ROUTER_WRAPPER)
return this.waitForElementVisible(ROUTER_WRAPPER)
.setValue('body', 'A')
.waitForElementVisible(ADD_TASK_GLOBAL_SEL)
.setValue(ADD_TASK_GLOBAL_SEL, taskName)
@ -14,7 +13,6 @@ module.exports = {
.pause(30)
.setValue(ADD_TASK_GLOBAL_SEL, this.Keys.ESCAPE)
.pause(30)
.waitForElementNotPresent(ADD_TASK_GLOBAL_SEL)
;
}
.waitForElementNotPresent(ADD_TASK_GLOBAL_SEL);
},
};

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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;
const interval = () => {

View file

@ -2,14 +2,16 @@ import { exec } from 'child_process';
export default (cb?, customCommands?) => {
const lockCommands = customCommands || {
darwin: '/System/Library/CoreServices/"Menu Extras"/User.menu/Contents/Resources/CGSession -suspend',
darwin:
'/System/Library/CoreServices/"Menu Extras"/User.menu/Contents/Resources/CGSession -suspend',
win32: 'rundll32.exe user32.dll, LockWorkStation',
linux: '(hash gnome-screensaver-command 2>/dev/null && gnome-screensaver-command -l) || (hash dm-tool 2>/dev/null && dm-tool lock) || (qdbus org.freedesktop.ScreenSaver /ScreenSaver Lock)'
linux:
'(hash gnome-screensaver-command 2>/dev/null && gnome-screensaver-command -l) || (hash dm-tool 2>/dev/null && dm-tool lock) || (qdbus org.freedesktop.ScreenSaver /ScreenSaver Lock)',
};
if (Object.keys(lockCommands).indexOf(process.platform) === -1) {
throw new Error(`lockscreen doesn't support your platform (${process.platform})`);
} else {
exec(lockCommands[process.platform], (err, stdout) => cb ? cb(err, stdout) : null);
exec(lockCommands[process.platform], (err, stdout) => (cb ? cb(err, stdout) : null));
}
};

View file

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

View file

@ -1,5 +1,13 @@
'use strict';
import { App, app, BrowserWindow, globalShortcut, ipcMain, powerMonitor, protocol } from 'electron';
import {
App,
app,
BrowserWindow,
globalShortcut,
ipcMain,
powerMonitor,
protocol,
} from 'electron';
import * as electronDl from 'electron-dl';
import { info } from 'electron-log';
@ -23,7 +31,7 @@ const ICONS_FOLDER = __dirname + '/assets/icons/';
const IS_MAC = process.platform === 'darwin';
const IS_LINUX = process.platform === 'linux';
const DESKTOP_ENV = process.env.DESKTOP_SESSION;
const IS_GNOME = (DESKTOP_ENV === 'gnome' || DESKTOP_ENV === 'gnome-xorg');
const IS_GNOME = DESKTOP_ENV === 'gnome' || DESKTOP_ENV === 'gnome-xorg';
const IS_DEV = process.env.NODE_ENV === 'DEV';
let isShowDevTools: boolean = IS_DEV;
@ -72,11 +80,11 @@ const appIN: MyApp = app;
// NOTE: to get rid of the warning => https://github.com/electron/electron/issues/18397
appIN.allowRendererProcessReuse = true;
initDebug({showDevTools: isShowDevTools}, IS_DEV);
initDebug({ showDevTools: isShowDevTools }, IS_DEV);
// NOTE: opening the folder crashes the mas build
if (!IS_MAC) {
electronDl({openFolderWhenDone: true});
electronDl({ openFolderWhenDone: true });
}
let mainWin: BrowserWindow;
// keep app active to keep time tracking running
@ -133,7 +141,9 @@ appIN.on('ready', () => {
const sendIdleMsgIfOverMin = (idleTime) => {
// sometimes when starting a second instance we get here although we don't want to
if (!mainWin) {
info('special case occurred when trackTimeFn is called even though, this is a second instance of the app');
info(
'special case occurred when trackTimeFn is called even though, this is a second instance of the app',
);
return;
}
@ -229,9 +239,9 @@ ipcMain.on(IPC.LOCK_SCREEN, () => {
}
});
ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, {progress, mode}) => {
ipcMain.on(IPC.SET_PROGRESS_BAR, (ev, { progress, mode }) => {
if (mainWin) {
mainWin.setProgressBar(Math.min(Math.max(progress, 0), 1), {mode});
mainWin.setProgressBar(Math.min(Math.max(progress, 0), 1), { mode });
}
});
@ -239,9 +249,12 @@ ipcMain.on(IPC.REGISTER_GLOBAL_SHORTCUTS_EVENT, (ev, cfg) => {
registerShowAppShortCuts(cfg);
});
ipcMain.on(IPC.JIRA_SETUP_IMG_HEADERS, (ev, {jiraCfg, wonkyCookie}: { jiraCfg: JiraCfg; wonkyCookie?: string }) => {
setupRequestHeadersForImages(jiraCfg, wonkyCookie);
});
ipcMain.on(
IPC.JIRA_SETUP_IMG_HEADERS,
(ev, { jiraCfg, wonkyCookie }: { jiraCfg: JiraCfg; wonkyCookie?: string }) => {
setupRequestHeadersForImages(jiraCfg, wonkyCookie);
},
);
ipcMain.on(IPC.JIRA_MAKE_REQUEST_EVENT, (ev, request) => {
sendJiraRequest(request);
@ -263,7 +276,7 @@ function createIndicator() {
showApp,
quitApp,
ICONS_FOLDER,
forceDarkTray
forceDarkTray,
});
}
@ -290,7 +303,7 @@ function registerShowAppShortCuts(cfg: KeyboardConfig) {
if (cfg) {
Object.keys(cfg)
.filter((key: (keyof KeyboardConfig)) => GLOBAL_KEY_CFG_KEYS.includes(key))
.filter((key: keyof KeyboardConfig) => GLOBAL_KEY_CFG_KEYS.includes(key))
.forEach((key) => {
let actionFn: () => void;
const shortcut = cfg[key];
@ -359,7 +372,9 @@ function showOrFocus(passedWin) {
// sometimes when starting a second instance we get here although we don't want to
if (!win) {
info('special case occurred when showOrFocus is called even though, this is a second instance of the app');
info(
'special case occurred when showOrFocus is called even though, this is a second instance of the app',
);
return;
}

View file

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

View file

@ -1,5 +1,5 @@
export const WORKLOG_DATE_STR_FORMAT = 'YYYY-MM-DD';
export const IS_ELECTRON = (navigator.userAgent.toLowerCase().indexOf(' electron/') > -1);
export const IS_ELECTRON = navigator.userAgent.toLowerCase().indexOf(' electron/') > -1;
export const TRACKING_INTERVAL = 1000;
export const MODEL_VERSION_KEY = '__modelVersion';
@ -118,10 +118,7 @@ export const AUTO_SWITCH_LNGS: LanguageCode[] = [
LanguageCode.tr,
];
export const RTL_LANGUAGES: LanguageCode[] = [
LanguageCode.ar,
LanguageCode.fa
];
export const RTL_LANGUAGES: LanguageCode[] = [LanguageCode.ar, LanguageCode.fa];
export enum THEME_COLOR_MAP {
'light-blue' = '#03a9f4',

View file

@ -1,5 +1,11 @@
import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
import {
ActivatedRouteSnapshot,
CanActivate,
Router,
RouterStateSnapshot,
UrlTree,
} from '@angular/router';
import { WorkContextService } from './features/work-context/work-context.service';
import { Observable, of } from 'rxjs';
import { concatMap, map, switchMap, take } from 'rxjs/operators';
@ -8,60 +14,61 @@ import { TagService } from './features/tag/tag.service';
import { ProjectService } from './features/project/project.service';
import { DataInitService } from './core/data-init/data-init.service';
@Injectable({providedIn: 'root'})
@Injectable({ providedIn: 'root' })
export class ActiveWorkContextGuard implements CanActivate {
constructor(
private _workContextService: WorkContextService,
private _router: Router,
) {
}
constructor(private _workContextService: WorkContextService, private _router: Router) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<UrlTree> {
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<UrlTree> {
return this._workContextService.activeWorkContextTypeAndId$.pipe(
take(1),
switchMap(({activeType, activeId}) => {
const {subPageType, param} = next.params;
const base = activeType === WorkContextType.TAG
? 'tag'
: 'project';
switchMap(({ activeType, activeId }) => {
const { subPageType, param } = next.params;
const base = activeType === WorkContextType.TAG ? 'tag' : 'project';
const url = `/${base}/${activeId}/${subPageType}${param ? '/' + param : ''}`;
return of(this._router.parseUrl(url));
})
}),
);
}
}
@Injectable({providedIn: 'root'})
@Injectable({ providedIn: 'root' })
export class ValidTagIdGuard implements CanActivate {
constructor(
private _tagService: TagService,
private _dataInitService: DataInitService,
) {
}
) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const {id} = next.params;
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> {
const { id } = next.params;
return this._dataInitService.isAllDataLoadedInitially$.pipe(
concatMap(() => this._tagService.getTagById$(id)),
take(1),
map(tag => !!tag),
map((tag) => !!tag),
);
}
}
@Injectable({providedIn: 'root'})
@Injectable({ providedIn: 'root' })
export class ValidProjectIdGuard implements CanActivate {
constructor(
private _projectService: ProjectService,
private _dataInitService: DataInitService,
) {
}
) {}
canActivate(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean> {
const {id} = next.params;
canActivate(
next: ActivatedRouteSnapshot,
state: RouterStateSnapshot,
): Observable<boolean> {
const { id } = next.params;
return this._dataInitService.isAllDataLoadedInitially$.pipe(
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 { AppComponent } from './app.component';
import { ServiceWorkerModule } from '@angular/service-worker';
@ -47,9 +47,7 @@ export function createTranslateLoader(http: HttpClient) {
}
@NgModule({
declarations: [
AppComponent,
],
declarations: [AppComponent],
imports: [
// Those features need to be included first for store not to mess up, probably because we use it initially at many places
ConfigModule,
@ -79,13 +77,12 @@ export function createTranslateLoader(http: HttpClient) {
BrowserAnimationsModule,
HttpClientModule,
HammerModule,
RouterModule.forRoot(APP_ROUTES, {useHash: true, relativeLinkResolution: 'legacy'}),
RouterModule.forRoot(APP_ROUTES, { useHash: true, relativeLinkResolution: 'legacy' }),
// NOTE: both need to be present to use forFeature stores
StoreModule.forRoot(reducers,
{
metaReducers: [undoTaskDeleteMetaReducer, actionLoggerReducer],
...(environment.production
? {
StoreModule.forRoot(reducers, {
metaReducers: [undoTaskDeleteMetaReducer, actionLoggerReducer],
...(environment.production
? {
runtimeChecks: {
strictStateImmutability: false,
strictActionImmutability: false,
@ -93,47 +90,44 @@ export function createTranslateLoader(http: HttpClient) {
strictActionSerializability: false,
},
}
: {
: {
runtimeChecks: {
strictStateImmutability: true,
strictActionImmutability: true,
strictStateSerializability: true,
strictActionSerializability: true,
},
})
}
),
}),
}),
EffectsModule.forRoot([]),
(!environment.production && !environment.stage) ? StoreDevtoolsModule.instrument() : [],
!environment.production && !environment.stage ? StoreDevtoolsModule.instrument() : [],
ReactiveFormsModule,
FormlyModule.forRoot({
extras: {
immutable: true
immutable: true,
},
validationMessages: [
{name: 'pattern', message: 'Invalid input'},
],
validationMessages: [{ name: 'pattern', message: 'Invalid input' }],
}),
ServiceWorkerModule.register('ngsw-worker.js', {
enabled: !IS_ELECTRON && (environment.production || environment.stage),
}),
ServiceWorkerModule.register('ngsw-worker.js', {enabled: !(IS_ELECTRON) && (environment.production || environment.stage)}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: createTranslateLoader,
deps: [HttpClient]
}
deps: [HttpClient],
},
}),
EntityDataModule
EntityDataModule,
],
bootstrap: [AppComponent],
providers: [
{provide: ErrorHandler, useClass: GlobalErrorHandler},
{provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig},
{ provide: ErrorHandler, useClass: GlobalErrorHandler },
{ provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig },
],
})
export class AppModule {
constructor(
private _languageService: LanguageService,
) {
constructor(private _languageService: LanguageService) {
this._languageService.setDefault(LanguageCode.en);
this._languageService.setFromBrowserLngIfAutoSwitchLng();
}

View file

@ -9,88 +9,112 @@ import { ProcrastinationComponent } from './features/procrastination/procrastina
import { SchedulePageComponent } from './pages/schedule-page/schedule-page.component';
import { ProjectSettingsPageComponent } from './pages/project-settings-page/project-settings-page.component';
import { TagTaskPageComponent } from './pages/tag-task-page/tag-task-page.component';
import { ActiveWorkContextGuard, ValidProjectIdGuard, ValidTagIdGuard } from './app.guard';
import {
ActiveWorkContextGuard,
ValidProjectIdGuard,
ValidTagIdGuard,
} from './app.guard';
import { TagSettingsPageComponent } from './pages/tag-settings-page/tag-settings-page.component';
import { TODAY_TAG } from './features/tag/tag.const';
export const APP_ROUTES: Routes = [
{path: 'config', component: ConfigPageComponent, data: {page: 'config'}},
{path: 'schedule', component: SchedulePageComponent, data: {page: 'schedule'}},
{path: 'procrastination', component: ProcrastinationComponent, data: {page: 'procrastination'}},
{ path: 'config', component: ConfigPageComponent, data: { page: 'config' } },
{ path: 'schedule', component: SchedulePageComponent, data: { page: 'schedule' } },
{
path: 'procrastination',
component: ProcrastinationComponent,
data: { page: 'procrastination' },
},
{
path: 'tag/:id/tasks',
component: TagTaskPageComponent,
data: {page: 'tag-tasks'},
canActivate: [ValidTagIdGuard]
data: { page: 'tag-tasks' },
canActivate: [ValidTagIdGuard],
},
{
path: 'tag/:id/settings',
component: TagSettingsPageComponent,
data: {page: 'tag-settings'},
canActivate: [ValidTagIdGuard]
data: { page: 'tag-settings' },
canActivate: [ValidTagIdGuard],
},
{
path: 'tag/:id/worklog',
component: WorklogComponent,
data: {page: 'worklog'},
canActivate: [ValidTagIdGuard]
data: { page: 'worklog' },
canActivate: [ValidTagIdGuard],
},
// {path: 'tag/:id/metrics', component: MetricPageComponent, data: {page: 'metrics'}, canActivate: [ValidContextIdGuard]},
{
path: 'tag/:id/daily-summary',
component: DailySummaryComponent,
data: {page: 'daily-summary'},
canActivate: [ValidTagIdGuard]
data: { page: 'daily-summary' },
canActivate: [ValidTagIdGuard],
},
{
path: 'tag/:id/daily-summary/:dayStr',
component: DailySummaryComponent,
data: {page: 'daily-summary'},
canActivate: [ValidTagIdGuard]
data: { page: 'daily-summary' },
canActivate: [ValidTagIdGuard],
},
{
path: 'project/:id/tasks',
component: ProjectTaskPageComponent,
data: {page: 'project-tasks'},
canActivate: [ValidProjectIdGuard]
data: { page: 'project-tasks' },
canActivate: [ValidProjectIdGuard],
},
{
path: 'project/:id/settings',
component: ProjectSettingsPageComponent,
data: {page: 'project-settings'},
canActivate: [ValidProjectIdGuard]
data: { page: 'project-settings' },
canActivate: [ValidProjectIdGuard],
},
{
path: 'project/:id/worklog',
component: WorklogComponent,
data: {page: 'worklog'},
canActivate: [ValidProjectIdGuard]
data: { page: 'worklog' },
canActivate: [ValidProjectIdGuard],
},
{
path: 'project/:id/metrics',
component: MetricPageComponent,
data: {page: 'metrics'},
canActivate: [ValidProjectIdGuard]
data: { page: 'metrics' },
canActivate: [ValidProjectIdGuard],
},
{
path: 'project/:id/daily-summary',
component: DailySummaryComponent,
data: {page: 'daily-summary'},
canActivate: [ValidProjectIdGuard]
data: { page: 'daily-summary' },
canActivate: [ValidProjectIdGuard],
},
{
path: 'project/:id/daily-summary/:dayStr',
component: DailySummaryComponent,
data: {page: 'daily-summary'},
canActivate: [ValidProjectIdGuard]
data: { page: 'daily-summary' },
canActivate: [ValidProjectIdGuard],
},
{
path: 'project-overview',
component: ProjectOverviewPageComponent,
data: { page: 'project-overview' },
},
{path: 'project-overview', component: ProjectOverviewPageComponent, data: {page: 'project-overview'}},
{path: 'active/:subPageType', canActivate: [ActiveWorkContextGuard], component: ConfigPageComponent},
{path: 'active/:subPageType/:param', canActivate: [ActiveWorkContextGuard], component: ConfigPageComponent},
{path: 'active', canActivate: [ActiveWorkContextGuard], component: ConfigPageComponent},
{
path: 'active/:subPageType',
canActivate: [ActiveWorkContextGuard],
component: ConfigPageComponent,
},
{
path: 'active/:subPageType/:param',
canActivate: [ActiveWorkContextGuard],
component: ConfigPageComponent,
},
{
path: 'active',
canActivate: [ActiveWorkContextGuard],
component: ConfigPageComponent,
},
{path: '**', redirectTo: `tag/${TODAY_TAG.id}/tasks`},
{ path: '**', redirectTo: `tag/${TODAY_TAG.id}/tasks` },
];

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,5 +25,4 @@ import { SimpleCounterModule } from '../../features/simple-counter/simple-counte
declarations: [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';
@NgModule({
imports: [
CommonModule,
RouterModule,
],
imports: [CommonModule, RouterModule],
declarations: [],
})
export class ShortcutModule {
}
export class ShortcutModule {}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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) => {
const event = ev as CustomEvent;
cb(event, event.detail);

View file

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

View file

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

View file

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

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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
if (!isHttpError && isErrorWithStack && !isHandledError(err)) {
return StackTrace.fromError(err)
.then((stackframes) => {
return stackframes
.splice(0, 20)
.map((sf) => {
return sf.toString();
}).join('\n');
});
return StackTrace.fromError(err).then((stackframes) => {
return stackframes
.splice(0, 20)
.map((sf) => {
return sf.toString();
})
.join('\n');
});
} else if (!isHandledError(err)) {
console.warn('Error without stack', err);
}
@ -31,26 +31,31 @@ async function _getStacktrace(err: Error | any): Promise<string> {
const _getStacktraceThrottled = pThrottle(_getStacktrace, 2, 5000);
export const logAdvancedStacktrace = (origErr: unknown, additionalLogFn?: (stack: string) => void) => _getStacktraceThrottled(origErr).then(stack => {
export const logAdvancedStacktrace = (
origErr: unknown,
additionalLogFn?: (stack: string) => void,
) =>
_getStacktraceThrottled(origErr)
.then((stack) => {
if (additionalLogFn) {
additionalLogFn(stack);
}
// append to dialog
const stacktraceEl = document.getElementById('stack-trace');
if (stacktraceEl) {
stacktraceEl.innerText = stack;
}
if (additionalLogFn) {
additionalLogFn(stack);
}
// append to dialog
const stacktraceEl = document.getElementById('stack-trace');
if (stacktraceEl) {
stacktraceEl.innerText = stack;
}
const githubIssueLink = document.getElementById('github-issue-url');
const githubIssueLink = document.getElementById('github-issue-url');
if (githubIssueLink) {
const errEscaped = _cleanHtml(origErr as string);
githubIssueLink.setAttribute('href', getGithubUrl(errEscaped, stack));
}
if (githubIssueLink) {
const errEscaped = _cleanHtml(origErr as string);
githubIssueLink.setAttribute('href', getGithubUrl(errEscaped, stack));
}
// NOTE: there is an issue with this sometimes -> https://github.com/stacktracejs/stacktrace.js/issues/202
}).catch(console.error);
// NOTE: there is an issue with this sometimes -> https://github.com/stacktracejs/stacktrace.js/issues/202
})
.catch(console.error);
const _cleanHtml = (str: string): string => {
const div = document.createElement('div');
@ -58,7 +63,12 @@ const _cleanHtml = (str: string): string => {
return div.textContent || div.innerText || '';
};
export const createErrorAlert = (eSvc: ElectronService, err: string = '', stackTrace: string, origErr: any) => {
export const createErrorAlert = (
eSvc: ElectronService,
err: string = '',
stackTrace: string,
origErr: any,
) => {
if (isWasErrorAlertCreated) {
return;
}
@ -92,7 +102,9 @@ export const createErrorAlert = (eSvc: ElectronService, err: string = '', stackT
}
});
document.body.append(errorAlert);
const innerWrapper = document.getElementById('error-alert-inner-wrapper') as HTMLElement;
const innerWrapper = document.getElementById(
'error-alert-inner-wrapper',
) as HTMLElement;
innerWrapper.append(btnReload);
isWasErrorAlertCreated = true;
@ -109,16 +121,25 @@ export const createErrorAlert = (eSvc: ElectronService, err: string = '', stackT
export const getSimpleMeta = (): string => {
const n = window.navigator;
return `META: SP${environment.version} ${IS_ELECTRON ? 'Electron' : 'Browser'} ${n.language} ${n.platform} ${n.userAgent}`;
return `META: SP${environment.version} ${IS_ELECTRON ? 'Electron' : 'Browser'} ${
n.language
} ${n.platform} ${n.userAgent}`;
};
export const isHandledError = (err: unknown): boolean => {
const errStr = (typeof err === 'string')
? err
: (typeof err === 'object' && err !== null && typeof (err as any).toString === 'function' && err.toString());
const errStr =
typeof err === 'string'
? err
: typeof err === 'object' &&
err !== null &&
typeof (err as any).toString === 'function' &&
err.toString();
// NOTE: for some unknown reason sometimes err is undefined while err.toString is not...
// this is why we also check the string value
return (err && (err as any).hasOwnProperty(HANDLED_ERROR_PROP_STR)) || !!((errStr as string).match(HANDLED_ERROR_PROP_STR));
return (
(err && (err as any).hasOwnProperty(HANDLED_ERROR_PROP_STR)) ||
!!(errStr as string).match(HANDLED_ERROR_PROP_STR)
);
};
const getGithubUrl = (errEscaped: string, stackTrace: string): string => {

View file

@ -3,13 +3,18 @@ import { TranslateService } from '@ngx-translate/core';
import { DateTimeAdapter } from 'ngx-date-time-picker-schedule';
import { DateAdapter } from '@angular/material/core';
import * as moment from 'moment';
import { AUTO_SWITCH_LNGS, LanguageCode, LanguageCodeMomentMap, RTL_LANGUAGES } from '../../app.constants';
import {
AUTO_SWITCH_LNGS,
LanguageCode,
LanguageCodeMomentMap,
RTL_LANGUAGES,
} from '../../app.constants';
import { BehaviorSubject, Observable } from 'rxjs';
import { GlobalConfigService } from 'src/app/features/config/global-config.service';
import { map, startWith } from 'rxjs/operators';
import { DEFAULT_GLOBAL_CONFIG } from 'src/app/features/config/default-global-config.const';
@Injectable({providedIn: 'root'})
@Injectable({ providedIn: 'root' })
export class LanguageService {
// I think a better approach is to add a field in every [lang].json file to specify the direction of the language
private isRTL: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
@ -48,15 +53,16 @@ export class LanguageService {
private _initMonkeyPatchFirstDayOfWeek() {
let firstDayOfWeek = DEFAULT_GLOBAL_CONFIG.misc.firstDayOfWeek;
this._globalConfigService.misc$.pipe(
map(cfg => cfg.firstDayOfWeek),
startWith(1),
).subscribe((_firstDayOfWeek: number) => {
// default should be monday, if we have an invalid value for some reason
firstDayOfWeek = (_firstDayOfWeek === 0 || _firstDayOfWeek > 0)
? _firstDayOfWeek
: 1;
});
this._globalConfigService.misc$
.pipe(
map((cfg) => cfg.firstDayOfWeek),
startWith(1),
)
.subscribe((_firstDayOfWeek: number) => {
// default should be monday, if we have an invalid value for some reason
firstDayOfWeek =
_firstDayOfWeek === 0 || _firstDayOfWeek > 0 ? _firstDayOfWeek : 1;
});
// overwrites default method to make this configurable
this._dateAdapter.getFirstDayOfWeek = () => firstDayOfWeek;
}

View file

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

View file

@ -13,15 +13,23 @@ import {
LS_TASK_ARCHIVE,
LS_TASK_ATTACHMENT_STATE,
LS_TASK_REPEAT_CFG_STATE,
LS_TASK_STATE
LS_TASK_STATE,
} from '../persistence/ls-keys.const';
import { migrateProjectState } from '../../features/project/migrate-projects-state.util';
import { GlobalConfigState } from '../../features/config/global-config.model';
import { migrateGlobalConfigState } from '../../features/config/migrate-global-config.util';
import { Task, TaskArchive, TaskState, TaskWithSubTasks } from 'src/app/features/tasks/task.model';
import {
Task,
TaskArchive,
TaskState,
TaskWithSubTasks,
} from 'src/app/features/tasks/task.model';
import { Reminder } from '../../features/reminder/reminder.model';
import { taskReducer } from '../../features/tasks/store/task.reducer';
import { TaskRepeatCfg, TaskRepeatCfgState } from '../../features/task-repeat-cfg/task-repeat-cfg.model';
import {
TaskRepeatCfg,
TaskRepeatCfgState,
} from '../../features/task-repeat-cfg/task-repeat-cfg.model';
import { taskRepeatCfgReducer } from '../../features/task-repeat-cfg/store/task-repeat-cfg.reducer';
import { EntityState } from '@ngrx/entity';
import { TaskAttachment } from '../../features/tasks/task-attachment/task-attachment.model';
@ -30,15 +38,21 @@ import {
LegacyAppDataComplete,
LegacyAppDataForProjects,
LegacyPersistenceBaseModel,
LegacyPersistenceForProjectModel
LegacyPersistenceForProjectModel,
} from './legacy-models';
import { BookmarkState } from '../../features/bookmark/store/bookmark.reducer';
import { Bookmark } from '../../features/bookmark/bookmark.model';
import { NoteState } from '../../features/note/store/note.reducer';
import { Note } from '../../features/note/note.model';
import { Metric, MetricState } from '../../features/metric/metric.model';
import { Improvement, ImprovementState } from '../../features/metric/improvement/improvement.model';
import { Obstruction, ObstructionState } from '../../features/metric/obstruction/obstruction.model';
import {
Improvement,
ImprovementState,
} from '../../features/metric/improvement/improvement.model';
import {
Obstruction,
ObstructionState,
} from '../../features/metric/obstruction/obstruction.model';
import { DatabaseService } from '../persistence/database.service';
import { DEFAULT_PROJECT_ID } from '../../features/project/project.const';
import { Action } from '@ngrx/store';
@ -48,20 +62,23 @@ import { Injectable } from '@angular/core';
providedIn: 'root',
})
export class LegacyPersistenceService {
// handled as private but needs to be assigned before the creations
_baseModels: any [] = [];
_projectModels: any [] = [];
_baseModels: any[] = [];
_projectModels: any[] = [];
// TODO auto generate ls keys from appDataKey where possible
project: any = this._cmBase<ProjectState>(LS_PROJECT_META_LIST, 'project', migrateProjectState);
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,
project: any = this._cmBase<ProjectState>(
LS_PROJECT_META_LIST,
'project',
migrateProjectState,
);
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>(
LS_TASK_REPEAT_CFG_STATE,
'taskRepeatCfg',
@ -77,18 +94,14 @@ export class LegacyPersistenceService {
taskAttachment: any = this._cmProject<EntityState<TaskAttachment>, TaskAttachment>(
LS_TASK_ATTACHMENT_STATE,
'taskAttachment',
(state) => state
(state) => state,
);
bookmark: any = this._cmProject<BookmarkState, Bookmark>(
LS_BOOKMARK_STATE,
'bookmark',
(state) => state,
);
note: any = this._cmProject<NoteState, Note>(
LS_NOTE_STATE,
'note',
(state) => state,
);
note: any = this._cmProject<NoteState, Note>(LS_NOTE_STATE, 'note', (state) => state);
metric: any = this._cmProject<MetricState, Metric>(
LS_METRIC_STATE,
'metric',
@ -106,9 +119,7 @@ export class LegacyPersistenceService {
);
private _isBlockSaving: boolean = false;
constructor(
private _databaseService: DatabaseService,
) {
constructor(private _databaseService: DatabaseService) {
// this.loadComplete().then(d => console.log('XXXXXXXXX', d, JSON.stringify(d).length));
// this.loadAllRelatedModelDataForProject('DEFAULT').then(d => console.log(d));
}
@ -198,9 +209,7 @@ export class LegacyPersistenceService {
getLastActive(): number {
const la = localStorage.getItem(LS_LAST_LOCAL_SYNC_MODEL_CHANGE);
// NOTE: we need to parse because new Date('1570549698000') is "Invalid Date"
const laParsed = Number.isNaN(Number(la))
? la
: +(la as any);
const laParsed = Number.isNaN(Number(la)) ? la : +(la as any);
// NOTE: to account for legacy string dates
return new Date(laParsed as any).getTime();
}
@ -208,7 +217,7 @@ export class LegacyPersistenceService {
// NOTE: not including backup
async loadCompleteLegacy(): Promise<LegacyAppDataComplete> {
const projectState = await this.project.load();
const pids = projectState ? projectState.ids as string[] : [DEFAULT_PROJECT_ID];
const pids = projectState ? (projectState.ids as string[]) : [DEFAULT_PROJECT_ID];
return {
lastActiveTime: this.getLastActive(),
@ -255,21 +264,29 @@ export class LegacyPersistenceService {
): LegacyPersistenceForProjectModel<S, M> {
const model = {
appDataKey,
load: (projectId: any): Promise<S> => this._loadFromDb(this._makeProjectKey(projectId, lsKey)).then(v => migrateFn(v, projectId)),
save: (projectId: any, data: any, isForce: any) => this._saveToDb(this._makeProjectKey(projectId, lsKey), data, isForce),
load: (projectId: any): Promise<S> =>
this._loadFromDb(this._makeProjectKey(projectId, lsKey)).then((v) =>
migrateFn(v, projectId),
),
save: (projectId: any, data: any, isForce: any) =>
this._saveToDb(this._makeProjectKey(projectId, lsKey), data, isForce),
};
this._projectModels.push(model);
return model;
}
private async _loadLegacyAppDataForProjects(projectIds: string[]): Promise<LegacyAppDataForProjects> {
const forProjectsData = await Promise.all(this._projectModels.map(async (modelCfg) => {
const modelState = await this._loadForProjectIds(projectIds, modelCfg.load);
return {
[modelCfg.appDataKey]: modelState,
};
}));
private async _loadLegacyAppDataForProjects(
projectIds: string[],
): Promise<LegacyAppDataForProjects> {
const forProjectsData = await Promise.all(
this._projectModels.map(async (modelCfg) => {
const modelState = await this._loadForProjectIds(projectIds, modelCfg.load);
return {
[modelCfg.appDataKey]: modelState,
};
}),
);
return Object.assign({}, ...forProjectsData);
}
@ -280,18 +297,24 @@ export class LegacyPersistenceService {
const dataForProject = await getDataFn(projectId);
return {
...prevAcc,
[projectId]: dataForProject
[projectId]: dataForProject,
};
}, Promise.resolve({}));
}
private _makeProjectKey(projectId: string, subKey: string, additional?: string) {
return LS_PROJECT_PREFIX + projectId + '_' + subKey + (additional ? '_' + additional : '');
return (
LS_PROJECT_PREFIX + projectId + '_' + subKey + (additional ? '_' + additional : '')
);
}
// DATA STORAGE INTERFACE
// ---------------------
private async _saveToDb(key: string, data: any, isForce: boolean = false): Promise<any> {
private async _saveToDb(
key: string,
data: any,
isForce: boolean = false,
): Promise<any> {
if (!this._isBlockSaving || isForce === true) {
return this._databaseService.save(key, data);
} 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 { initialObstructionState } from '../../features/metric/obstruction/store/obstruction.reducer';
const EMTPY_ENTITY = () => ({ids: [], entities: {}});
const EMTPY_ENTITY = () => ({ ids: [], entities: {} });
@Injectable({providedIn: 'root'})
@Injectable({ providedIn: 'root' })
export class MigrationService {
constructor(
private _persistenceService: PersistenceService,
private _legacyPersistenceService: LegacyPersistenceService,
private _translateService: TranslateService,
) {
}
) {}
migrateIfNecessaryToProjectState$(projectState: ProjectState): Observable<ProjectState | never> {
migrateIfNecessaryToProjectState$(
projectState: ProjectState,
): Observable<ProjectState | never> {
const isNeedsMigration = this._isNeedsMigration(projectState);
if (isNeedsMigration && this._isConfirmMigrateDialog()) {
return from(this._legacyPersistenceService.loadCompleteLegacy()).pipe(
map((legacyData) => this._migrate(legacyData)),
concatMap((migratedData) => this._persistenceService.importComplete(migratedData)),
concatMap((migratedData) =>
this._persistenceService.importComplete(migratedData),
),
concatMap((migratedData) => this._persistenceService.cleanDatabase()),
concatMap(() => this._persistenceService.project.loadState()),
);
}
return isNeedsMigration
? EMPTY
: of(projectState);
return isNeedsMigration ? EMPTY : of(projectState);
}
migrateIfNecessary(appDataComplete: LegacyAppDataComplete | AppDataComplete): AppDataComplete {
migrateIfNecessary(
appDataComplete: LegacyAppDataComplete | AppDataComplete,
): AppDataComplete {
const projectState = appDataComplete.project;
const isNeedsMigration = this._isNeedsMigration(projectState);
if (isNeedsMigration) {
@ -99,46 +102,68 @@ export class MigrationService {
return newAppData;
}
private _mTaskListsFromTaskToProjectState(legacyAppDataComplete: LegacyAppDataComplete): ProjectState {
private _mTaskListsFromTaskToProjectState(
legacyAppDataComplete: LegacyAppDataComplete,
): ProjectState {
const projectStateBefore = legacyAppDataComplete.project;
return {
...projectStateBefore,
entities: (projectStateBefore.ids as string[]).reduce((acc, id): Dictionary<Project> => {
const taskState = (legacyAppDataComplete.task as any)[id] || {};
return {
...acc,
[id]: {
...projectStateBefore.entities[id],
taskIds: (taskState as any).todaysTaskIds || [],
backlogTaskIds: (taskState as any).backlogTaskIds || [],
} as Project
};
}, {})
entities: (projectStateBefore.ids as string[]).reduce(
(acc, id): Dictionary<Project> => {
const taskState = (legacyAppDataComplete.task as any)[id] || {};
return {
...acc,
[id]: {
...projectStateBefore.entities[id],
taskIds: (taskState as any).todaysTaskIds || [],
backlogTaskIds: (taskState as any).backlogTaskIds || [],
} as Project,
};
},
{},
),
};
}
private _mTaskState(legacyAppDataComplete: LegacyAppDataComplete): TaskState {
const singleState = this._mTaskFromProjectToSingle(legacyAppDataComplete);
const standardMigration = migrateTaskState(singleState as TaskState);
return this._mTaskAttachmentsToTaskStates(legacyAppDataComplete, standardMigration) as TaskState;
return this._mTaskAttachmentsToTaskStates(
legacyAppDataComplete,
standardMigration,
) as TaskState;
}
private _mTaskArchiveState(legacyAppDataComplete: LegacyAppDataComplete): TaskArchive {
const singleState = this._mTaskArchiveFromProjectToSingle(legacyAppDataComplete) as TaskArchive;
const singleState = this._mTaskArchiveFromProjectToSingle(
legacyAppDataComplete,
) as TaskArchive;
const standardMigration = migrateTaskState(singleState as TaskState);
return this._mTaskAttachmentsToTaskStates(legacyAppDataComplete, standardMigration) as TaskArchive;
return this._mTaskAttachmentsToTaskStates(
legacyAppDataComplete,
standardMigration,
) as TaskArchive;
}
private _mTaskRepeatCfg(legacyAppDataComplete: LegacyAppDataComplete): TaskRepeatCfgState {
private _mTaskRepeatCfg(
legacyAppDataComplete: LegacyAppDataComplete,
): TaskRepeatCfgState {
const pids = legacyAppDataComplete.project.ids as string[];
const repeatStates = this._addProjectIdToEntity(pids, (legacyAppDataComplete.taskRepeatCfg as any), {tagIds: []});
return this._mergeEntities(repeatStates, initialTaskRepeatCfgState) as TaskRepeatCfgState;
const repeatStates = this._addProjectIdToEntity(
pids,
legacyAppDataComplete.taskRepeatCfg as any,
{ tagIds: [] },
);
return this._mergeEntities(
repeatStates,
initialTaskRepeatCfgState,
) as TaskRepeatCfgState;
}
private _addProjectIdToEntity(
pids: string[],
entityProjectStates: { [key: string]: EntityState<any> },
additionalChanges: Record<string, unknown> = {}
additionalChanges: Record<string, unknown> = {},
): EntityState<any>[] {
return pids.map((projectId) => {
const state = entityProjectStates[projectId];
@ -149,7 +174,12 @@ export class MigrationService {
...state,
entities: (state.ids as string[]).reduce((acc, entityId) => {
if (projectId !== state.entities[entityId].projectId) {
console.log('OVERWRITING PROJECT ID', projectId, state.entities[entityId].projectId, state.entities[entityId]);
console.log(
'OVERWRITING PROJECT ID',
projectId,
state.entities[entityId].projectId,
state.entities[entityId],
);
}
return {
...acc,
@ -157,74 +187,102 @@ export class MigrationService {
...state.entities[entityId],
projectId,
...additionalChanges,
}
},
};
}, {})
}, {}),
};
}) as any;
}
private _mTaskFromProjectToSingle(legacyAppDataComplete: LegacyAppDataComplete): TaskState {
private _mTaskFromProjectToSingle(
legacyAppDataComplete: LegacyAppDataComplete,
): TaskState {
const pids = legacyAppDataComplete.project.ids as string[];
const taskStates: TaskState[] = this._addProjectIdToEntity(pids, legacyAppDataComplete.task as any) as TaskState[];
const taskStates: TaskState[] = this._addProjectIdToEntity(
pids,
legacyAppDataComplete.task as any,
) as TaskState[];
return this._mergeEntities(taskStates, initialTaskState) as TaskState;
}
private _mTaskArchiveFromProjectToSingle(legacyAppDataComplete: LegacyAppDataComplete): TaskArchive {
private _mTaskArchiveFromProjectToSingle(
legacyAppDataComplete: LegacyAppDataComplete,
): TaskArchive {
const pids = legacyAppDataComplete.project.ids as string[];
const taskStates: TaskArchive[] = this._addProjectIdToEntity(pids, legacyAppDataComplete.taskArchive as any) as TaskArchive[];
const taskStates: TaskArchive[] = this._addProjectIdToEntity(
pids,
legacyAppDataComplete.taskArchive as any,
) as TaskArchive[];
return this._mergeEntities(taskStates, EMTPY_ENTITY()) as TaskArchive;
}
private _mTaskAttachmentsToTaskStates(legacyAppDataComplete: LegacyAppDataComplete, taskState: (TaskState | TaskArchive)):
TaskState | TaskArchive {
const attachmentStates = Object.keys(legacyAppDataComplete.taskAttachment as any).map(id => (legacyAppDataComplete.taskAttachment as any)[id]);
const allAttachmentState = this._mergeEntities(attachmentStates, initialTaskRepeatCfgState) as EntityState<TaskAttachment>;
private _mTaskAttachmentsToTaskStates(
legacyAppDataComplete: LegacyAppDataComplete,
taskState: TaskState | TaskArchive,
): TaskState | TaskArchive {
const attachmentStates = Object.keys(legacyAppDataComplete.taskAttachment as any).map(
(id) => (legacyAppDataComplete.taskAttachment as any)[id],
);
const allAttachmentState = this._mergeEntities(
attachmentStates,
initialTaskRepeatCfgState,
) as EntityState<TaskAttachment>;
return (taskState.ids as string[]).reduce((acc, id) => {
const {attachmentIds, ...tEnt} = acc.entities[id] as any;
const { attachmentIds, ...tEnt } = acc.entities[id] as any;
return {
...acc,
entities: {
...acc.entities,
[id]: {
...tEnt,
attachments: tEnt.attachments || (attachmentIds
? attachmentIds.map((attachmentId: string) => {
const result = allAttachmentState.entities[attachmentId];
if (!result) {
console.log('ATTACHMENT NOT FOUND: Will be removed', attachmentIds);
// throw new Error('Attachment not found');
} else {
console.log('ATTACHMENT FOUND', result.title);
}
return result;
}).filter((v: any) => !!v)
: [])
attachments:
tEnt.attachments ||
(attachmentIds
? attachmentIds
.map((attachmentId: string) => {
const result = allAttachmentState.entities[attachmentId];
if (!result) {
console.log(
'ATTACHMENT NOT FOUND: Will be removed',
attachmentIds,
);
// throw new Error('Attachment not found');
} else {
console.log('ATTACHMENT FOUND', result.title);
}
return result;
})
.filter((v: any) => !!v)
: []),
},
}
},
};
}, taskState);
}
private _mergeEntities(states: EntityState<any>[], initial: EntityState<any>): EntityState<any> {
return states.reduce(
(acc, s) => {
if (!s || !s.ids) {
return acc;
}
return {
...acc,
ids: [...acc.ids, ...s.ids] as string[],
// NOTE: that this can lead to overwrite when the ids are the same for some reason
entities: {...acc.entities, ...s.entities}
};
}, initial
);
private _mergeEntities(
states: EntityState<any>[],
initial: EntityState<any>,
): EntityState<any> {
return states.reduce((acc, s) => {
if (!s || !s.ids) {
return acc;
}
return {
...acc,
ids: [...acc.ids, ...s.ids] as string[],
// NOTE: that this can lead to overwrite when the ids are the same for some reason
entities: { ...acc.entities, ...s.entities },
};
}, initial);
}
private _isNeedsMigration(projectState: ProjectState): boolean {
return (projectState && (!(projectState as any).__modelVersion || (projectState as any).__modelVersion <= 3));
return (
projectState &&
(!(projectState as any).__modelVersion || (projectState as any).__modelVersion <= 3)
);
}
private _isConfirmMigrateDialog(): boolean {

View file

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

View file

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

View file

@ -10,7 +10,10 @@ export const removeFromRealLs = (key: string) => {
localStorage.removeItem(key);
};
export const saveToRealLs = (key: string, state: { [key: string]: unknown } | unknown[]) => {
export const saveToRealLs = (
key: string,
state: { [key: string]: unknown } | unknown[],
) => {
const serializedState = JSON.stringify(state);
localStorage.setItem(key, serializedState);
};

View file

@ -6,7 +6,7 @@ export type AllowedDBKeys = keyof AppDataComplete | 'SUP_COMPLETE_BACKUP';
export const LS_PREFIX = 'SUP_';
export const LS_PROJECT_PREFIX = LS_PREFIX + 'P_';
export const LS_GLOBAL_CFG = LS_PREFIX + 'GLOBAL_CFG';
export const LS_BACKUP: AllowedDBKeys = LS_PREFIX + 'COMPLETE_BACKUP' as AllowedDBKeys;
export const LS_BACKUP: AllowedDBKeys = (LS_PREFIX + 'COMPLETE_BACKUP') as AllowedDBKeys;
export const LS_REMINDER = LS_PREFIX + 'REMINDER';
export const LS_PROJECT_ARCHIVE = LS_PREFIX + 'ARCHIVE';
@ -45,7 +45,8 @@ export const LS_GOOGLE_SESSION = LS_PREFIX + 'GOOGLE_SESSION';
export const LS_ACTION_LOG = LS_PREFIX + 'ACTION_LOG';
export const LS_ACTION_BEFORE_LAST_ERROR_LOG = LS_PREFIX + 'LAST_ERROR_ACTION_LOG';
export const LS_CHECK_STRAY_PERSISTENCE_BACKUP = LS_PREFIX + 'CHECK_STRAY_PERSISTENCE_BACKUP';
export const LS_CHECK_STRAY_PERSISTENCE_BACKUP =
LS_PREFIX + 'CHECK_STRAY_PERSISTENCE_BACKUP';
export const LS_IS_PROJECT_LIST_EXPANDED = LS_PREFIX + 'IS_PROJECT_LIST_EXPANDED';
export const LS_IS_TAG_LIST_EXPANDED = LS_PREFIX + 'IS_TAG_LIST_EXPANDED';
@ -56,4 +57,3 @@ export const SS_PROJECT_TMP = SS_PREFIX + 'PROJECT_TMP_EDIT';
export const SS_WEB_APP_INSTALL = LS_PREFIX + 'WEB_APP_INSTALL';
export const SS_JIRA_WONKY_COOKIE = LS_PREFIX + 'JIRA_WONKY_COOKIE';
export const SS_TODO_TMP = SS_PREFIX + 'TODO_TMP_EDIT';

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import { AppBaseData, AppDataForProjects } from '../../imex/sync/sync.model';
import { Action } from '@ngrx/store';
export type ProjectDataLsKey
= 'CFG'
export type ProjectDataLsKey =
| 'CFG'
// | 'TASKS_STATE'
// | 'TASK_REPEAT_CFG_STATE'
// | 'TASK_ATTACHMENT_STATE'
@ -13,15 +13,17 @@ export type ProjectDataLsKey
| 'BOOKMARK_STATE'
| 'METRIC_STATE'
| 'IMPROVEMENT_STATE'
| 'OBSTRUCTION_STATE'
;
| 'OBSTRUCTION_STATE';
export interface PersistenceBaseModel<T> {
appDataKey: keyof AppBaseData;
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> {
@ -42,7 +44,11 @@ export interface PersistenceForProjectModel<S, M> {
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 */
remove(projectId: string): Promise<unknown>;

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

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

View file

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

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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

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