mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-22 18:30:09 +00:00
refactor: format everything with prettier
This commit is contained in:
parent
8ee6191b14
commit
43022f8e83
533 changed files with 14924 additions and 11386 deletions
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import {NightwatchBrowser} from 'nightwatch';
|
||||
import { NightwatchBrowser } from 'nightwatch';
|
||||
|
||||
export interface AddTaskWithReminderParams {
|
||||
title: string;
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
);
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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}`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
|
|
@ -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: () => {},
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) => {
|
|||
}
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -53,4 +53,3 @@ export enum IPC {
|
|||
maybe_PROJECT_CHANGED = 'PROJECT_CHANGED',
|
||||
maybe_COMPLETE_DATA_RELOAD = 'COMPLETE_DATA_RELOAD',
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 = () => {
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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` },
|
||||
];
|
||||
|
|
|
|||
|
|
@ -23,5 +23,4 @@ import { GlobalProgressBarModule } from './global-progress-bar/global-progress-b
|
|||
GlobalProgressBarModule,
|
||||
],
|
||||
})
|
||||
export class CoreUiModule {
|
||||
}
|
||||
export class CoreUiModule {}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -11,5 +11,4 @@ import { LAYOUT_FEATURE_NAME } from './store/layout.reducer';
|
|||
],
|
||||
declarations: [],
|
||||
})
|
||||
export class LayoutModule {
|
||||
}
|
||||
export class LayoutModule {}
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,5 +25,4 @@ import { SimpleCounterModule } from '../../features/simple-counter/simple-counte
|
|||
declarations: [MainHeaderComponent],
|
||||
exports: [MainHeaderComponent],
|
||||
})
|
||||
export class MainHeaderModule {
|
||||
}
|
||||
export class MainHeaderModule {}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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)) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -3,9 +3,6 @@ import { CommonModule } from '@angular/common';
|
|||
|
||||
@NgModule({
|
||||
declarations: [],
|
||||
imports: [
|
||||
CommonModule
|
||||
]
|
||||
imports: [CommonModule],
|
||||
})
|
||||
export class CompressionModule {
|
||||
}
|
||||
export class CompressionModule {}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ function handleData(msgData: any) {
|
|||
}
|
||||
}
|
||||
|
||||
addEventListener('message', ({data}) => {
|
||||
addEventListener('message', ({ data }) => {
|
||||
try {
|
||||
const strToHandle = handleData(data);
|
||||
postMessage({
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
|
@ -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');
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 => {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,7 +50,9 @@ export interface LegacyAppDataForProjects {
|
|||
};
|
||||
}
|
||||
|
||||
export interface LegacyAppDataComplete extends LegacyAppBaseData, LegacyAppDataForProjects {
|
||||
export interface LegacyAppDataComplete
|
||||
extends LegacyAppBaseData,
|
||||
LegacyAppDataForProjects {
|
||||
lastActiveTime: number;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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, {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,5 +13,3 @@ export const loadFromDb = createAction(
|
|||
'[Persistence] Load from DB',
|
||||
props<{ dbKey: string }>(),
|
||||
);
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
})
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {}
|
||||
|
|
|
|||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue