diff --git a/.travis.yml b/.travis.yml index 56a3451b5..873d8d0b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -56,7 +56,7 @@ script: bash ${TRAVIS_BUILD_DIR}/build/docker/run-linux-win.sh ${PUB} else yarn install && yarn add 7zip-bin-mac - yarn run buildAllElectronNoTests + yarn run buildAllElectron:noTests travis_wait 30 yarn dist:mac:dl -p $PUB fi diff --git a/CHANGELOG.md b/CHANGELOG.md index eba661523..e68cf8a87 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,159 @@ +# [5.9.0](https://github.com/johannesjo/super-productivity/compare/v5.8.2...v5.9.0) (2020-09-24) + + +### Bug Fixes + +* another read-only error [#538](https://github.com/johannesjo/super-productivity/issues/538) ([8f2afcc](https://github.com/johannesjo/super-productivity/commit/8f2afcc171901d09b4af2e5281ddb044a3329743)) +* disabling done sound not working [#534](https://github.com/johannesjo/super-productivity/issues/534) ([2179118](https://github.com/johannesjo/super-productivity/commit/2179118f9468962876f647199dc0d1e93bcfbd13)) +* lint ([763e991](https://github.com/johannesjo/super-productivity/commit/763e99158211c977e7e1b621eb175698998a66bd)) +* use encodeURIComponent instead of encodeURI [#523](https://github.com/johannesjo/super-productivity/issues/523) ([ba06b0b](https://github.com/johannesjo/super-productivity/commit/ba06b0b60057fd510de68d3622762bf9035f9a0b)) + + +### Features + +* **autoRepair:** add a little bit of logging ([70e4e20](https://github.com/johannesjo/super-productivity/commit/70e4e20943d97beb02227b1b627f2875e5be41c6)) +* **autoRepair:** make _removeDuplicatesFromArchive work ([6a111d9](https://github.com/johannesjo/super-productivity/commit/6a111d92ce31e94ac86a0a69aa899d918df6b692)) +* **autoRepair:** make _removeMissingIdsFromLists work ([192ef5a](https://github.com/johannesjo/super-productivity/commit/192ef5ad8679b012aee75bbd593cf61014d8e95d)) +* **autoRepair:** make fix duplicate tasks work ([002ec09](https://github.com/johannesjo/super-productivity/commit/002ec096f2466e9c0a4651aff8a046379437bf49)) +* **autoRepair:** make restore from archive work ([c706f26](https://github.com/johannesjo/super-productivity/commit/c706f26586badb6a4c1a5e6fe03ffc822a5402d1)) +* **autoRepair:** make restoring orphaned tasks work ([5fe3fbe](https://github.com/johannesjo/super-productivity/commit/5fe3fbe139eb67850a51521598f252784b83573d)) +* **autoRepair:** make stray backup stuff translateable ([35cd773](https://github.com/johannesjo/super-productivity/commit/35cd7736995cfc7ffcabad35887f5b2829cc0f54)) +* **autoRepair:** re-enable stray backup check ([ec6c844](https://github.com/johannesjo/super-productivity/commit/ec6c8449861981bfb8ed7c8c13426b4820f2e4a5)) +* **autoRepair:** trigger for data import and data init if data is broken ([a80054a](https://github.com/johannesjo/super-productivity/commit/a80054a2fc47866e7c05fe14964022715ba70778)) +* add missing null checks for is valid app data ([577383b](https://github.com/johannesjo/super-productivity/commit/577383bb44c469eb43654ca31eb83fa7bd150c70)) +* also check for miss-matched ids in entity states ([81ca47c](https://github.com/johannesjo/super-productivity/commit/81ca47c0d9047c399a45de7eb058890f5c638490)) + + + +## [5.8.2](https://github.com/johannesjo/super-productivity/compare/v5.8.1...v5.8.2) (2020-09-22) + + +### Bug Fixes + +* private policy link [#531](https://github.com/johannesjo/super-productivity/issues/531) ([8ccf7b1](https://github.com/johannesjo/super-productivity/commit/8ccf7b1ea4619f08ab71e6544d950c14b5ad3c98)) + + + +## [5.8.1](https://github.com/johannesjo/super-productivity/compare/v5.8.0...v5.8.1) (2020-09-21) + + + +# [5.8.0](https://github.com/johannesjo/super-productivity/compare/v5.7.7...v5.8.0) (2020-09-20) + + +### Bug Fixes + +* **task:** undone done when marking a lot of tasks as done in fast succession ([59574c5](https://github.com/johannesjo/super-productivity/commit/59574c55a4ce271842d3177ace25761dff297a73)) + + +### Features + +* **startTrackingReminder:** make configurable and add translations for it [#507](https://github.com/johannesjo/super-productivity/issues/507) ([4ad922c](https://github.com/johannesjo/super-productivity/commit/4ad922c5c2b50aca18c0831bb182895b792aab87)) +* make dark mode default ([b6c72f5](https://github.com/johannesjo/super-productivity/commit/b6c72f57c077bfe2f5214831d06502a02fd37539)) +* **startTrackingReminder:** implement timer as real timer [#507](https://github.com/johannesjo/super-productivity/issues/507) ([71d18a6](https://github.com/johannesjo/super-productivity/commit/71d18a67c320f628e506296723c062656de9bcaf)) +* **startTrackingReminder:** make dialog work and translations [#507](https://github.com/johannesjo/super-productivity/issues/507) ([2cac5c7](https://github.com/johannesjo/super-productivity/commit/2cac5c72023bfa020555839a0038957aadb2819c)) +* **startTrackingReminder:** make reset timer work [#507](https://github.com/johannesjo/super-productivity/issues/507) ([7f6cb61](https://github.com/johannesjo/super-productivity/commit/7f6cb61d859b375af36d3c869d5c68f9ee818edb)) +* **startTrackingReminder:** make timer work [#507](https://github.com/johannesjo/super-productivity/issues/507) ([fc37243](https://github.com/johannesjo/super-productivity/commit/fc37243e4355a861481971eafad4c856db631f29)) +* **startTrackingReminder:** outline service [#507](https://github.com/johannesjo/super-productivity/issues/507) ([455c2b3](https://github.com/johannesjo/super-productivity/commit/455c2b322bab3793b7c6abbdb76f30e4394f8e99)) +* **startTrackingReminder:** prepare banner [#507](https://github.com/johannesjo/super-productivity/issues/507) ([68e802e](https://github.com/johannesjo/super-productivity/commit/68e802eeb32781b1b884ee51154b652c28830f5b)) +* **task:** improve current/focus border styling ([8308862](https://github.com/johannesjo/super-productivity/commit/83088629750dd8b6f884bfe2054689439b27a5a7)) +* **task:** use solid border for task additional info panel as well ([934843a](https://github.com/johannesjo/super-productivity/commit/934843a58215028aece5e8ad6d17c6ee473ce52f)) + + + +## [5.7.7](https://github.com/johannesjo/super-productivity/compare/v5.7.6...v5.7.7) (2020-09-18) + + +### Bug Fixes + +* allow deletion of multiple tasks in fast succession ([8b41fb9](https://github.com/johannesjo/super-productivity/commit/8b41fb9fe9532f54653e04596a75ed0f050952ae)) +* persistence request for mobile ([113dafd](https://github.com/johannesjo/super-productivity/commit/113dafdc5b600259ba62e7fa664ca9b6c5e9d647)) +* weird error for reminders ([c4c2a33](https://github.com/johannesjo/super-productivity/commit/c4c2a33289d8b68854f29e5aa3e4f86e4613ceb6)) +* weird reminder error ([260a80c](https://github.com/johannesjo/super-productivity/commit/260a80c2c6239cffec10312c908b35ca843f1c48)) +* **task:** case when adding task via short syntax with project id to today ([2e96c0f](https://github.com/johannesjo/super-productivity/commit/2e96c0fa52270232c3238667ab6401c15bcb613b)) + + +### Features + +* add additional debug info for undo task delete meta reducer ([01ea6d9](https://github.com/johannesjo/super-productivity/commit/01ea6d9343a897681c721039d7e9cf9f6df467be)) +* add debugging actions for persistence ([dfe459e](https://github.com/johannesjo/super-productivity/commit/dfe459e65f7846a4bdc6f2125a419ad26e3f7f68)) +* add error alert ([389063a](https://github.com/johannesjo/super-productivity/commit/389063aaeb4ab7562b034cb3d533d904d754457e)) +* limit inMemoryComplete$ to valid only ([5021969](https://github.com/johannesjo/super-productivity/commit/502196951d7ad98a34b35c62322c5ac9fe42ec05)) +* make adjustments for stage behaviour ([75042ee](https://github.com/johannesjo/super-productivity/commit/75042ee4c4a1cd76968047927e695e227e83ff7a)) +* remove load from db action because it's too much clutter ([ea05fda](https://github.com/johannesjo/super-productivity/commit/ea05fda38069ab09f975a4be4146b24f49a052d7)) + + + +## [5.7.6](https://github.com/johannesjo/super-productivity/compare/v5.7.5...v5.7.6) (2020-09-17) + + +### Features + +* improve error handling ([6892605](https://github.com/johannesjo/super-productivity/commit/68926057ec43716e467381350ba569bf7ec9294e)) + + + +## [5.7.5](https://github.com/johannesjo/super-productivity/compare/v5.7.4...v5.7.5) (2020-09-17) + + +### Bug Fixes + +* prevent dropbox from syncing invalid ([6ddc711](https://github.com/johannesjo/super-productivity/commit/6ddc7115c60f8ad2516c6e38d0bbe4fc802a34d3)) + + + +## [5.7.4](https://github.com/johannesjo/super-productivity/compare/v5.7.3...v5.7.4) (2020-09-17) + + +### Bug Fixes + +* stray import stuff being wrong ([b2fae30](https://github.com/johannesjo/super-productivity/commit/b2fae30bc7eafcca5b5449f11dd19d6b0440a007)) + + + +## [5.7.3](https://github.com/johannesjo/super-productivity/compare/v5.7.2...v5.7.3) (2020-09-17) + + +### Features + +* **i18n:** add missing translations ([a229186](https://github.com/johannesjo/super-productivity/commit/a22918681274ffdd576184b3f281ae07f2e5b45b)) +* improve pre-check ([ed9fc7a](https://github.com/johannesjo/super-productivity/commit/ed9fc7a8b681cca2d7acfc2ea0718427cbe06187)) +* improve warning for empty data sync ([5f27d43](https://github.com/johannesjo/super-productivity/commit/5f27d4397750c3272a164f36b6d15aff904104ee)) +* make dropbox sync confirms translatable ([e8c97ee](https://github.com/johannesjo/super-productivity/commit/e8c97ee32f8271db89bd0ab312d96a92514fbce1)) + + + +## [5.7.2](https://github.com/johannesjo/super-productivity/compare/v5.7.1...v5.7.2) (2020-09-17) + + +### Features + +* add pre-check for invalid local data before saving ([955aed7](https://github.com/johannesjo/super-productivity/commit/955aed7e0c3edf1c656d0ddc2307a939f86f73e2)) + + + +## [5.7.1](https://github.com/johannesjo/super-productivity/compare/v5.7.0...v5.7.1) (2020-09-17) + + +### Bug Fixes + +* editing sub task tags should not be possible [#522](https://github.com/johannesjo/super-productivity/issues/522) ([eb11530](https://github.com/johannesjo/super-productivity/commit/eb11530193f91232fe2e109a8ad792a2180e258d)) +* model check for empty data ([4a8a784](https://github.com/johannesjo/super-productivity/commit/4a8a7841f8db6cd23541caf74bf7a955ddc1eecb)) +* note reminder from tag view [#524](https://github.com/johannesjo/super-productivity/issues/524) ([f9e508f](https://github.com/johannesjo/super-productivity/commit/f9e508fb13f5d3a44d284da166af8c84893cad7e)) + + +### Features + +* add auto import backup when something went wrong with data import previously ([f281470](https://github.com/johannesjo/super-productivity/commit/f281470f193f93692a3bd37b0f59c34a2c894bb8)) +* alert on indexeddb error ([09f8a22](https://github.com/johannesjo/super-productivity/commit/09f8a22ad018a9052bea3ded77a4a48d2528d07b)) +* allow drag & drop attachments on task additional info ([9377b80](https://github.com/johannesjo/super-productivity/commit/9377b80742e1fe3e9fb4b02f3b4059e06174775c)) +* improve persistence permission request ([0e8cb34](https://github.com/johannesjo/super-productivity/commit/0e8cb34219dbf464c30ee5d1c2c8c2cdb2f90bb7)) +* **doneSound:** make pitching work for a bigger amount of tasks ([51489d2](https://github.com/johannesjo/super-productivity/commit/51489d263df6338e2f12c62a62697784a28bdf9c)) +* **style:** add separators to worklog ([11127f1](https://github.com/johannesjo/super-productivity/commit/11127f10c4f4e6924a3257a9dca2a59c4d2fc656)) +* **style:** use checkmark for bookmark-bar edit mode ([46d4ccb](https://github.com/johannesjo/super-productivity/commit/46d4ccb4bed5aad65135d0206b03665638cc3ff0)) + + + # [5.7.0](https://github.com/johannesjo/super-productivity/compare/v5.6.5...v5.7.0) (2020-09-07) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..089403ce2 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +:hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: + +Welcome you awesome human being! You want to contribute to this repository? This is gonna be great! + +The best way to start is to open up a [new issue](https://github.com/johannesjo/super-productivity/issues/new) or [to search for already existing ones](https://github.com/johannesjo/super-productivity/issues) covering your topic. + +If you're contributing code, please try to use the [conventional commits](https://www.conventionalcommits.org/en/v1.0.0-beta.4/#summary) commit message conventions. + +If you want to discuss something private, please write an email to contact@super-productivity.com. + +### Code of conduct +Don't be a d***! + +### Options for contributing +In case you want to contribute, but you wouldn't know how, here are some suggestions: + +1. **Spread the word:** More users means more people testing and contributing to the app which in turn means better stability and possibly more and better features. You can vote for Super Productivity on [Slant](https://www.slant.co/topics/14021/viewpoints/7/~productivity-tools-for-linux~super-productivity), [Product Hunt](https://www.producthunt.com/posts/super-productivity), [Softpedia](https://www.softpedia.com/get/Office-tools/Diary-Organizers-Calendar/Super-Productivity.shtml) or on [AlternativeTo](https://alternativeto.net/software/super-productivity/), you can [tweet about it](https://twitter.com/intent/tweet?text=I%20like%20Super%20Productivity%20%20https%3A%2F%2Fsuper-productivity.com), share it on [LinkedIn](http://www.linkedin.com/shareArticle?mini=true&url=https://super-productivity.com&title=I%20like%20Super%20Productivity&), [reddit](http://www.reddit.com/submit?url=https%3A%2F%2Fsuper-productivity.com&title=I%20like%20Super%20Productivity) or any of your favorite social media platforms. Every little bit helps! + +2. **[Make a feature or improvement request](https://github.com/johannesjo/super-productivity/issues/new)**: Something can be be done better? Something essential missing? Let us know! + +3. **[Report bugs](https://github.com/johannesjo/super-productivity/issues/new)** + +4. **Contribute**: You don't have to be programmer to help. Some of the icons really need improvement and many of the translations could use some love. + +:hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: :hearts::hearts: diff --git a/README.md b/README.md index fd58f29aa..e43367aa1 100644 --- a/README.md +++ b/README.md @@ -110,9 +110,11 @@ brew cask install superproductivity There is a [very early(!) Android version available](https://play.google.com/store/apps/details?id=com.superproductivity.superproductivity&hl=gsw). The sources can be [found here](https://github.com/johannesjo/super-productivity-android). ## :hearts: Contributing +Please check out the [CONTRIBUTING.md](CONTRIBUTING.md) + There are several ways to help. -1. **Spread the word:** More users means more possible people testing and contributing to the app which in turn means better stability and possibly more and better features. You can vote for Super Productivity on [Product Hunt](https://www.producthunt.com/posts/super-productivity), [Softpedia](https://www.softpedia.com/get/Office-tools/Diary-Organizers-Calendar/Super-Productivity.shtml) or on [AlternativeTo](https://alternativeto.net/software/super-productivity/), you can [tweet about it](https://twitter.com/intent/tweet?text=I%20like%20Super%20Productivity%20%20https%3A%2F%2Fsuper-productivity.com), share it on [LinkedIn](http://www.linkedin.com/shareArticle?mini=true&url=https://super-productivity.com&title=I%20like%20Super%20Productivity&) or [reddit](http://www.reddit.com/submit?url=https%3A%2F%2Fsuper-productivity.com&title=I%20like%20Super%20Productivity). Every little bit helps ! +1. **Spread the word:** More users means more people testing and contributing to the app which in turn means better stability and possibly more and better features. You can vote for Super Productivity on [Slant](https://www.slant.co/topics/14021/viewpoints/7/~productivity-tools-for-linux~super-productivity), [Product Hunt](https://www.producthunt.com/posts/super-productivity), [Softpedia](https://www.softpedia.com/get/Office-tools/Diary-Organizers-Calendar/Super-Productivity.shtml) or on [AlternativeTo](https://alternativeto.net/software/super-productivity/), you can [tweet about it](https://twitter.com/intent/tweet?text=I%20like%20Super%20Productivity%20%20https%3A%2F%2Fsuper-productivity.com), share it on [LinkedIn](http://www.linkedin.com/shareArticle?mini=true&url=https://super-productivity.com&title=I%20like%20Super%20Productivity&), [reddit](http://www.reddit.com/submit?url=https%3A%2F%2Fsuper-productivity.com&title=I%20like%20Super%20Productivity) or any of your favorite social media platforms. Every little bit helps! 2. **[Make a feature or improvement request](https://github.com/johannesjo/super-productivity/issues/new)**: Something can be be done better? Something essential missing? Let us know! diff --git a/angular.json b/angular.json index 27a49b5f9..0b19cdd0f 100644 --- a/angular.json +++ b/angular.json @@ -67,6 +67,55 @@ "vendorChunk": false, "buildOptimizer": true, "serviceWorker": true + }, + "productionWeb": { + "baseHref": "", + "budgets": [ + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": true, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "serviceWorker": true + }, + "stage": { + "budgets": [ + { + "type": "anyComponentStyle", + "maximumWarning": "6kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "optimization": true, + "outputHashing": "all", + "sourceMap": true, + "extractCss": true, + "namedChunks": false, + "aot": true, + "extractLicenses": true, + "vendorChunk": false, + "buildOptimizer": true, + "serviceWorker": true } } }, diff --git a/n2e/src/short-syntax.e2e.ts b/n2e/src/short-syntax.e2e.ts new file mode 100644 index 000000000..9d87e6be6 --- /dev/null +++ b/n2e/src/short-syntax.e2e.ts @@ -0,0 +1,20 @@ +import { BASE } from '../e2e.const'; +import { NBrowser } from '../n-browser-interface'; + +const TASK = 'task'; +const TASK_TAGS = 'task tag'; +const WORK_VIEW_URL = `${BASE}/`; +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(), +}; diff --git a/package.json b/package.json index 6c6d3864e..42c6ba080 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "superProductivity", - "version": "5.7.0", + "version": "5.9.0", "description": "Personal Task Management App to help you with your daily struggle with JIRA etc.", "main": "./electron/main.js", "author": "johannesjo (http://super-productivity.com)", @@ -22,13 +22,13 @@ "start": "yarn electron:build && cross-env NODE_ENV=DEV electron .", "startFrontend": "ng serve", "serveProd": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng serve --prod", - "buildFrontend": "yarn preCheck && node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod", - "buildFrontendElectron": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod --base-href=''", - "buildFrontendElectron:preCheck": "yarn preCheck && yarn buildFrontendElectron", - "buildFrontendElectron:watch": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod --base-href='' --watch", - "buildAllElectron": "yarn buildFrontendElectron:preCheck && yarn electron:build", - "buildAllElectronNoTests": "yarn lint && yarn buildFrontendElectron && yarn electron:build", - "buildApp": "yarn preCheck && yarn buildAllElectron && yarn electron-builder", + "buildFrontend:prod": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod", + "buildFrontend:stage": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod --configuration stage", + "buildFrontend:prod:watch": "node --max_old_space_size=4096 ./node_modules/@angular/cli/bin/ng build --aot --prod --watch", + "buildAllElectron:prod": "yarn preCheck && yarn buildFrontend:prod && yarn electron:build", + "buildAllElectron:stage": "yarn preCheck && yarn buildFrontend:stage && yarn electron:build", + "buildAllElectron:noTests": "yarn lint && yarn buildFrontend:prod && yarn electron:build", + "buildApp": "yarn preCheck && yarn buildAllElectron:prod && yarn electron-builder", "test": "ng test --watch=false", "test:watch": "ng test --browsers ChromeHeadless", "lint": "ng lint", @@ -41,15 +41,16 @@ "electron:watch": "tsc -p electron/tsconfig.electron.json --watch", "electronBuilderOnly": "electron-builder", "pack": "electron-builder --dir", - "localInstall": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildAllElectron && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb", - "localInstall:quick": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildFrontendElectron && yarn electron:build && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb", - "localInstall:mac": "sudo echo 'Starting local install. Don`t forget APPLEID & APPLEIDPASS !!' && yarn buildAllElectron && sudo echo '' && electron-builder && sudo cp -rf app-builds/mac/superProductivity.app/ /Applications/superProductivity.app", - "dist": "yarn buildAllElectron && electron-builder", + "localInstall": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildAllElectron:stage && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb", + "localInstall:prod": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildAllElectron:prod && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb", + "localInstall:quick": "sudo echo 'Starting local install' && rm -Rf ./dist/ && rm -Rf ./app-builds/ && yarn buildFrontend:stage && yarn electron:build && electron-builder --linux deb && sudo dpkg -i app-builds/superProductivity*.deb", + "localInstall:mac": "sudo echo 'Starting local install. Don`t forget APPLEID & APPLEIDPASS !!' && yarn buildAllElectron:prod && sudo echo '' && electron-builder && sudo cp -rf app-builds/mac/superProductivity.app/ /Applications/superProductivity.app", + "dist": "yarn buildAllElectron:prod && electron-builder", "dist:only": "electron-builder", - "dist:linuxAndWin": "yarn buildAllElectron && electron-builder --linux --win", - "dist:win": "yarn buildAllElectronNoTests && electron-builder --win", + "dist:linuxAndWin": "yarn buildAllElectron:prod && electron-builder --linux --win", + "dist:win": "yarn buildAllElectron:noTests && electron-builder --win", "dist:win:only": "electron-builder --win", - "dist:win:appx": "yarn buildAllElectron && electron-builder --win --config=build/electron-builder.appx.yaml", + "dist:win:appx": "yarn buildAllElectron:prod && electron-builder --win --config=build/electron-builder.appx.yaml", "dist:win:store": "git stash && git pull && yarn && git stash pop && yarn dist:win", "dist:mac:dl": "cp dl.provisionprofile embedded.provisionprofile && electron-builder --mac", "dist:mac:mas": "cp mas.provisionprofile embedded.provisionprofile; electron-builder --mac mas --config=build/electron-builder.mas.yaml", @@ -130,7 +131,7 @@ "codelyzer": "^6.0.0", "conventional-changelog-cli": "^2.0.21", "cross-env": "^7.0.2", - "electron": "^10.1.0", + "electron": "^10.1.2", "electron-builder": "^22.7.0", "electron-notarize": "^1.0.0", "electron-reload": "^1.2.5", @@ -138,8 +139,8 @@ "hammerjs": "^2.0.8", "helpful-decorators": "^2.1.0", "husky": "^4.2.5", - "idb": "^5.0.3", - "jasmine-core": "~3.6.0", + "idb": "^5.0.6", + "jasmine-core": "^3.4.0", "jasmine-marbles": "^0.6.0", "jasmine-spec-reporter": "~5.0.0", "jira2md": "git+https://github.com/johannesjo/J2M.git", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 5e4d2e12f..ab8719d09 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -39,6 +39,7 @@ import { SyncService } from './imex/sync/sync.service'; import { environment } from '../environments/environment'; import { RouterOutlet } from '@angular/router'; import { ipcRenderer } from 'electron'; +import { TrackingReminderService } from './features/time-tracking/tracking-reminder/tracking-reminder.service'; const w = window as any; const productivityTip: string[] = w.productivityTips && w.productivityTips[w.randomIndex]; @@ -81,6 +82,7 @@ export class AppComponent implements OnDestroy { private _androidService: AndroidService, private _initialDialogService: InitialDialogService, private _bookmarkService: BookmarkService, + private _startTrackingReminderService: TrackingReminderService, public readonly syncService: SyncService, public readonly imexMetaService: ImexMetaService, public readonly workContextService: WorkContextService, @@ -91,19 +93,6 @@ export class AppComponent implements OnDestroy { document.dir = this.isRTL ? 'rtl' : 'ltr'; }); - // try to avoid data-loss - if (navigator.storage && navigator.storage.persist) { - 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}); - } - }); - } - // check for dialog this._initialDialogService.showDialogIfNecessary$().subscribe(); @@ -113,6 +102,10 @@ export class AppComponent implements OnDestroy { // init offline banner in lack of a better place for it this._initOfflineBanner(); + // basically init + this._startTrackingReminderService.init(); + + this._requestPersistence(); this._checkAvailableStorage(); if (IS_ANDROID_WEB_VIEW) { @@ -245,6 +238,29 @@ export class AppComponent implements OnDestroy { }); } + private _requestPersistence() { + if (navigator.storage) { + // try to avoid data-loss + Promise.all([ + navigator.storage.persisted(), + ]).then(([persisted]) => { + if (!persisted) { + 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}); + } + }); + } else { + console.log('Persistence already allowed'); + } + }); + } + } + private _checkAvailableStorage() { if (environment.production) { if ('storage' in navigator && 'estimate' in navigator.storage) { diff --git a/src/app/app.module.ts b/src/app/app.module.ts index f7306508d..e0f158ee6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -102,7 +102,7 @@ export function createTranslateLoader(http: HttpClient) { } ), EffectsModule.forRoot([]), - !environment.production ? StoreDevtoolsModule.instrument() : [], + (!environment.production && !environment.stage) ? StoreDevtoolsModule.instrument() : [], ReactiveFormsModule, FormlyModule.forRoot({ extras: { @@ -112,7 +112,7 @@ export function createTranslateLoader(http: HttpClient) { {name: 'pattern', message: 'Invalid input'}, ], }), - ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production}), + ServiceWorkerModule.register('ngsw-worker.js', {enabled: environment.production || environment.stage}), TranslateModule.forRoot({ loader: { provide: TranslateLoader, diff --git a/src/app/core/banner/banner.model.ts b/src/app/core/banner/banner.model.ts index b48ff12ef..9ecdb762b 100644 --- a/src/app/core/banner/banner.model.ts +++ b/src/app/core/banner/banner.model.ts @@ -1,5 +1,6 @@ export enum BannerId { TakeABreak = 'TakeABreak', + StartTrackingReminder = 'StartTrackingReminder', GoogleLogin = 'GoogleLogin', JiraUnblock = 'JiraUnblock', InstallWebApp = 'InstallWebApp', diff --git a/src/app/core/data-init/data-init.service.ts b/src/app/core/data-init/data-init.service.ts index 84fa52cc2..c88c2720c 100644 --- a/src/app/core/data-init/data-init.service.ts +++ b/src/app/core/data-init/data-init.service.ts @@ -9,6 +9,8 @@ import { PersistenceService } from '../persistence/persistence.service'; import { ProjectState } from '../../features/project/store/project.reducer'; import { MigrationService } from '../migration/migration.service'; 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'}) export class DataInitService { @@ -33,6 +35,7 @@ export class DataInitService { private _projectService: ProjectService, private _workContextService: WorkContextService, private _store$: Store, + private _dataRepairService: DataRepairService, ) { // TODO better construction than this this.isAllDataLoadedInitially$.pipe( @@ -47,9 +50,17 @@ export class DataInitService { // because the data load is triggered, but not necessarily already reflected inside the store async reInit(isOmitTokens: boolean = false): Promise { const appDataComplete = await this._persistenceService.loadComplete(); - // if (!environment.production) { - // const isValid = isValidAppData(appDataComplete); - // } - this._store$.dispatch(loadAllData({appDataComplete, isOmitTokens})); + const isValid = isValidAppData(appDataComplete); + if (isValid) { + this._store$.dispatch(loadAllData({appDataComplete, isOmitTokens})); + } else { + if (this._dataRepairService.isRepairConfirmed()) { + const fixedData = this._dataRepairService.repairData(appDataComplete); + this._store$.dispatch(loadAllData({ + appDataComplete: fixedData, + isOmitTokens, + })); + } + } } } diff --git a/src/app/core/data-repair/data-repair.service.spec.ts b/src/app/core/data-repair/data-repair.service.spec.ts new file mode 100644 index 000000000..fd2440fc3 --- /dev/null +++ b/src/app/core/data-repair/data-repair.service.spec.ts @@ -0,0 +1,16 @@ +// import { TestBed } from '@angular/core/testing'; +// +// import { DataRepairService } from './data-repair.service'; +// +// describe('DataRepairService', () => { +// let service: DataRepairService; +// +// beforeEach(() => { +// TestBed.configureTestingModule({}); +// service = TestBed.inject(DataRepairService); +// }); +// +// it('should be created', () => { +// expect(service).toBeTruthy(); +// }); +// }); diff --git a/src/app/core/data-repair/data-repair.service.ts b/src/app/core/data-repair/data-repair.service.ts new file mode 100644 index 000000000..c236e3b1c --- /dev/null +++ b/src/app/core/data-repair/data-repair.service.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@angular/core'; +import { AppDataComplete } from '../../imex/sync/sync.model'; +import { T } from '../../t.const'; +import { TranslateService } from '@ngx-translate/core'; +import { dataRepair } from './data-repair.util'; + +@Injectable({ + providedIn: 'root' +}) +export class DataRepairService { + + constructor( + private _translateService: TranslateService, + ) { + } + + repairData(dataIn: AppDataComplete): AppDataComplete { + return dataRepair(dataIn); + } + + isRepairConfirmed(): boolean { + return confirm(this._translateService.instant(T.CONFIRM.AUTO_FIX)); + } +} diff --git a/src/app/core/data-repair/data-repair.util.spec.ts b/src/app/core/data-repair/data-repair.util.spec.ts new file mode 100644 index 000000000..5693d5362 --- /dev/null +++ b/src/app/core/data-repair/data-repair.util.spec.ts @@ -0,0 +1,407 @@ +import { AppDataComplete } from '../../imex/sync/sync.model'; +import { createAppDataCompleteMock } from '../../util/app-data-mock'; +import { dataRepair } from './data-repair.util'; +import { fakeEntityStateFromArray } from '../../util/fake-entity-state-from-array'; +import { DEFAULT_TASK, Task } from '../../features/tasks/task.model'; +import { createEmptyEntity } from '../../util/create-empty-entity'; +import { Tag, TagState } from '../../features/tag/tag.model'; +import { ProjectState } from '../../features/project/store/project.reducer'; +import { Project } from '../../features/project/project.model'; + +describe('dataRepair()', () => { + let mock: AppDataComplete; + beforeEach(() => { + mock = createAppDataCompleteMock(); + }); + + it('should delete tasks with same id in "task" and "taskArchive" from taskArchive', () => { + const taskState = { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + }]) + } as any; + expect(dataRepair({ + ...mock, + task: taskState, + taskArchive: fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + }]), + })).toEqual({ + ...mock, + task: taskState, + taskArchive: { + ...createEmptyEntity() + }, + }); + }); + + it('should delete missing tasks for tags today list', () => { + const taskState = { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + }]) + } as any; + + const tagState: TagState = { + ...fakeEntityStateFromArray([{ + title: 'TEST_TAG', + id: 'TEST_ID_TAG', + taskIds: ['goneTag', 'TEST', 'noneExisting'], + }] as Partial []), + }; + + expect(dataRepair({ + ...mock, + tag: tagState, + task: taskState, + })).toEqual({ + ...mock, + task: taskState as any, + tag: { + ...tagState, + entities: { + TEST_ID_TAG: { + title: 'TEST_TAG', + id: 'TEST_ID_TAG', + taskIds: ['TEST'], + }, + } as any + } + }); + }); + + it('should delete missing tasks for projects today list', () => { + const taskState = { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + }]) + } as any; + + const projectState: ProjectState = { + ...fakeEntityStateFromArray([{ + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['goneProject', 'TEST', 'noneExisting'], + backlogTaskIds: [], + }] as Partial []), + }; + + expect(dataRepair({ + ...mock, + project: projectState, + task: taskState, + })).toEqual({ + ...mock, + task: taskState as any, + project: { + ...projectState, + entities: { + TEST_ID_PROJECT: { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['TEST'], + backlogTaskIds: [], + }, + } as any + } + }); + }); + + it('should delete missing tasks for projects backlog list', () => { + const taskState = { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'TEST', + title: 'TEST', + }]) + } as any; + + const projectState: ProjectState = { + ...fakeEntityStateFromArray([{ + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: [], + backlogTaskIds: ['goneProject', 'TEST', 'noneExisting'], + }] as Partial []), + }; + + expect(dataRepair({ + ...mock, + project: projectState, + task: taskState, + })).toEqual({ + ...mock, + task: taskState as any, + project: { + ...projectState, + entities: { + TEST_ID_PROJECT: { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: [], + backlogTaskIds: ['TEST'], + }, + } as any + } + }); + }); + + describe('should fix duplicate entities for', () => { + it('task', () => { + expect(dataRepair({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + }, { + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + }, { + ...DEFAULT_TASK, + id: 'NO_DUPE', + title: 'NO_DUPE', + }]) + } as any, + })).toEqual({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + }, { + ...DEFAULT_TASK, + id: 'NO_DUPE', + title: 'NO_DUPE', + }]) + } as any, + }); + }); + + it('taskArchive', () => { + expect(dataRepair({ + ...mock, + taskArchive: { + ...mock.taskArchive, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + }, { + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + }, { + ...DEFAULT_TASK, + id: 'NO_DUPE', + title: 'NO_DUPE', + }]) + } as any, + })).toEqual({ + ...mock, + taskArchive: { + ...mock.taskArchive, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'DUPE', + title: 'DUPE', + }, { + ...DEFAULT_TASK, + id: 'NO_DUPE', + title: 'NO_DUPE', + }]) + } as any, + }); + }); + }); + + describe('should fix inconsistent entity states for', () => { + it('task', () => { + expect(dataRepair({ + ...mock, + task: { + ids: ['AAA, XXX', 'YYY'], + entities: { + AAA: {}, + CCC: {}, + } + } as any, + })).toEqual({ + ...mock, + task: { + ids: ['AAA', 'CCC'], + entities: { + AAA: {}, + CCC: {}, + } + } as any, + }); + }); + it('taskArchive', () => { + expect(dataRepair({ + ...mock, + taskArchive: { + ids: ['AAA, XXX', 'YYY'], + entities: { + AAA: {}, + CCC: {}, + } + } as any, + })).toEqual({ + ...mock, + taskArchive: { + ids: ['AAA', 'CCC'], + entities: { + AAA: {}, + CCC: {}, + } + } as any, + }); + }); + }); + + it('should restore missing tasks from taskArchive if available', () => { + const taskArchiveState = { + ...mock.taskArchive, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'goneToArchiveToday', + title: 'goneToArchiveToday', + projectId: 'TEST_ID_PROJECT', + }, { + ...DEFAULT_TASK, + id: 'goneToArchiveBacklog', + title: 'goneToArchiveBacklog', + projectId: 'TEST_ID_PROJECT', + }]) + } as any; + + const projectState: ProjectState = { + ...fakeEntityStateFromArray([{ + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['goneToArchiveToday', 'GONE'], + backlogTaskIds: ['goneToArchiveBacklog', 'GONE'], + }] as Partial []), + }; + + expect(dataRepair({ + ...mock, + project: projectState, + taskArchive: taskArchiveState, + task: { + ...mock.task, + ...createEmptyEntity() + } as any, + })).toEqual({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'goneToArchiveToday', + title: 'goneToArchiveToday', + projectId: 'TEST_ID_PROJECT', + }, { + ...DEFAULT_TASK, + id: 'goneToArchiveBacklog', + title: 'goneToArchiveBacklog', + projectId: 'TEST_ID_PROJECT', + }]) + } as any, + project: { + ...projectState, + entities: { + TEST_ID_PROJECT: { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['goneToArchiveToday'], + backlogTaskIds: ['goneToArchiveBacklog'], + }, + } as any + } + }); + }); + + it('should add orphan tasks to their project list', () => { + const taskState = { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + id: 'orphanedTask', + title: 'orphanedTask', + projectId: 'TEST_ID_PROJECT', + parentId: null, + }, { + ...DEFAULT_TASK, + id: 'orphanedTaskOtherProject', + title: 'orphanedTaskOtherProject', + projectId: 'TEST_ID_PROJECT_OTHER', + parentId: null, + }, { + ...DEFAULT_TASK, + id: 'regularTaskOtherProject', + title: 'regularTaskOtherProject', + projectId: 'TEST_ID_PROJECT_OTHER', + parentId: null, + }]) + } as any; + + const projectState: ProjectState = { + ...fakeEntityStateFromArray([{ + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['GONE'], + backlogTaskIds: [], + }, { + title: 'TEST_PROJECT_OTHER', + id: 'TEST_ID_PROJECT_OTHER', + taskIds: ['regularTaskOtherProject'], + backlogTaskIds: [], + }] as Partial []), + }; + + expect(dataRepair({ + ...mock, + project: projectState, + task: taskState, + })).toEqual({ + ...mock, + task: taskState, + project: { + ...projectState, + entities: { + TEST_ID_PROJECT: { + title: 'TEST_PROJECT', + id: 'TEST_ID_PROJECT', + taskIds: ['orphanedTask'], + backlogTaskIds: [], + }, + TEST_ID_PROJECT_OTHER: { + title: 'TEST_PROJECT_OTHER', + id: 'TEST_ID_PROJECT_OTHER', + taskIds: ['regularTaskOtherProject', 'orphanedTaskOtherProject'], + backlogTaskIds: [], + }, + } as any + } + }); + }); +}); diff --git a/src/app/core/data-repair/data-repair.util.ts b/src/app/core/data-repair/data-repair.util.ts new file mode 100644 index 000000000..c003bd8b5 --- /dev/null +++ b/src/app/core/data-repair/data-repair.util.ts @@ -0,0 +1,127 @@ +import { AppBaseDataEntityLikeStates, AppDataComplete } from '../../imex/sync/sync.model'; +import { TagCopy } from '../../features/tag/tag.model'; +import { ProjectCopy } from '../../features/project/project.model'; + +const ENTITY_STATE_KEYS: (keyof AppDataComplete)[] = ['task', 'taskArchive', 'taskRepeatCfg', 'tag', 'project', 'simpleCounter']; + +export const dataRepair = (data: AppDataComplete): AppDataComplete => { + // console.time('dataRepair'); + let dataOut: AppDataComplete = data; + // let dataOut: AppDataComplete = dirtyDeepCopy(data); + dataOut = _fixEntityStates(dataOut); + dataOut = _removeMissingTasksFromListsOrRestoreFromArchive(dataOut); + dataOut = _removeDuplicatesFromArchive(dataOut); + dataOut = _addOrphanedTasksToProjectLists(dataOut); + // console.timeEnd('dataRepair'); + return dataOut; +}; + +const _fixEntityStates = (data: AppDataComplete): AppDataComplete => { + ENTITY_STATE_KEYS.forEach((key) => { + data[key] = _resetEntityIdsFromObjects(data[key] as AppBaseDataEntityLikeStates) as any; + }); + + return data; +}; + +const _removeDuplicatesFromArchive = (data: AppDataComplete): AppDataComplete => { + const taskIds = data.task.ids as string[]; + const archiveTaskIds = data.taskArchive.ids as string[]; + const duplicateIds = taskIds.filter((id) => archiveTaskIds.includes(id)); + + if (duplicateIds.length) { + data.taskArchive.ids = archiveTaskIds.filter(id => !duplicateIds.includes(id)); + duplicateIds.forEach(id => { + if (data.taskArchive.entities[id]) { + delete data.taskArchive.entities[id]; + } + }); + if (duplicateIds.length > 0) { + console.log(duplicateIds.length + ' duplicates removed from archive.'); + } + } + return 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[] = []; + + project.ids.forEach((pId: string | number) => { + const projectItem = project.entities[pId] as ProjectCopy; + + projectItem.taskIds = projectItem.taskIds.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)); + }); + + 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)); + + if (taskIdsToRestoreFromArchive.length > 0) { + console.log(taskIdsToRestoreFromArchive.length + ' missing tasks restored from archive.'); + } + return data; +}; + +const _resetEntityIdsFromObjects = (data: AppBaseDataEntityLikeStates): AppBaseDataEntityLikeStates => { + return { + ...data, + ids: Object.keys(data.entities) + }; +}; + +const _addOrphanedTasksToProjectLists = (data: AppDataComplete): AppDataComplete => { + 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); + }); + 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; + }); + + orphanedTaskIds.forEach(tid => { + const taskItem = task.entities[tid]; + if (!taskItem) { + throw new Error('Missing task'); + } + project.entities[taskItem.projectId as string]?.taskIds.push(tid); + }); + + if (orphanedTaskIds.length > 0) { + console.log(orphanedTaskIds.length + ' orphaned tasks found & restored.'); + } + + return data; +}; + diff --git a/src/app/core/migration/migration.service.ts b/src/app/core/migration/migration.service.ts index 5dd986c7d..7cd42bc2f 100644 --- a/src/app/core/migration/migration.service.ts +++ b/src/app/core/migration/migration.service.ts @@ -68,7 +68,7 @@ export class MigrationService { lastLocalSyncModelChange: legacyAppDataComplete.lastActiveTime, archivedProjects: legacyAppDataComplete.archivedProjects, globalConfig: legacyAppDataComplete.globalConfig, - reminders: legacyAppDataComplete.reminders, + reminders: legacyAppDataComplete.reminders || [], // new tag: initialTagState, simpleCounter: initialSimpleCounterState, @@ -82,6 +82,13 @@ export class MigrationService { task: this._mTaskState(legacyAppDataComplete), taskArchive: this._mTaskArchiveState(legacyAppDataComplete), + + // NEW PROJECT MODELS + note: {}, + metric: {}, + improvement: {}, + obstruction: {}, + bookmark: {}, }; console.log('DATA AFTER MIGRATIONS', newAppData); diff --git a/src/app/core/notify/notify.service.ts b/src/app/core/notify/notify.service.ts index 6e167faf5..4e3d8f5d9 100644 --- a/src/app/core/notify/notify.service.ts +++ b/src/app/core/notify/notify.service.ts @@ -88,6 +88,6 @@ export class NotifyService { } private _isServiceWorkerAvailable(): boolean { - return 'serviceWorker' in navigator && environment.production && !IS_ELECTRON; + return 'serviceWorker' in navigator && (environment.production || environment.stage) && !IS_ELECTRON; } } diff --git a/src/app/core/persistence/database.service.ts b/src/app/core/persistence/database.service.ts index 68127fee5..7840a9e14 100644 --- a/src/app/core/persistence/database.service.ts +++ b/src/app/core/persistence/database.service.ts @@ -77,6 +77,7 @@ export class DatabaseService { } catch (e) { console.error('Database initialization failed'); console.error('_lastParams', this._lastParams); + alert('IndexedDB Error'); throw new Error(e); } diff --git a/src/app/core/persistence/ls-keys.const.ts b/src/app/core/persistence/ls-keys.const.ts index a786a2a6e..90646f07d 100644 --- a/src/app/core/persistence/ls-keys.const.ts +++ b/src/app/core/persistence/ls-keys.const.ts @@ -43,6 +43,8 @@ export const LS_DROPBOX_LOCAL_LAST_SYNC_CHECK = LS_PREFIX + 'DROPBOX_LOCAL_LAST_ 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_NO_PERSISTENCE_NOTE = LS_PREFIX + 'NO_PERSISTENCE_NOTE'; // SESSION STORAGE const SS_PREFIX = 'SUP_SS_'; diff --git a/src/app/core/persistence/persistence.actions.ts b/src/app/core/persistence/persistence.actions.ts new file mode 100644 index 000000000..5a7f38a74 --- /dev/null +++ b/src/app/core/persistence/persistence.actions.ts @@ -0,0 +1,17 @@ +import { createAction, props } from '@ngrx/store'; + +// log only +export const saveToDb = createAction( + '[Persistence] Save to DB', + props<{ dbKey: string; data: any }>(), +); +export const removeFromDb = createAction( + '[Persistence] Remove from DB', + props<{ dbKey: string; }>(), +); +export const loadFromDb = createAction( + '[Persistence] Load from DB', + props<{ dbKey: string; }>(), +); + + diff --git a/src/app/core/persistence/persistence.service.spec.ts b/src/app/core/persistence/persistence.service.spec.ts index 874fd639c..2a248508e 100644 --- a/src/app/core/persistence/persistence.service.spec.ts +++ b/src/app/core/persistence/persistence.service.spec.ts @@ -6,6 +6,7 @@ import { PersistenceService } from './persistence.service'; import { TestScheduler } from 'rxjs/testing'; import { of } from 'rxjs'; import { createEmptyEntity } from '../../util/create-empty-entity'; +import { provideMockStore } from '@ngrx/store/testing'; const testScheduler = new TestScheduler((actual, expected) => { // asserting the two objects are equal @@ -16,6 +17,7 @@ describe('PersistenceService', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [ + provideMockStore({initialState: {}}), { provide: SnackService, useValue: { open: () => false, diff --git a/src/app/core/persistence/persistence.service.ts b/src/app/core/persistence/persistence.service.ts index 5bae3a3cb..a9ce9a9de 100644 --- a/src/app/core/persistence/persistence.service.ts +++ b/src/app/core/persistence/persistence.service.ts @@ -48,7 +48,7 @@ import { Obstruction, ObstructionState } from '../../features/metric/obstruction 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 } from '@ngrx/store'; +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'; @@ -62,8 +62,10 @@ import { checkFixEntityStateConsistency } from '../../util/check-fix-entity-stat 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, shareReplay } from 'rxjs/operators'; +import { concatMap, shareReplay, skipWhile } from 'rxjs/operators'; import { devError } from '../../util/dev-error'; +import { isValidAppData } from '../../imex/sync/is-valid-app-data.util'; +import { removeFromDb, saveToDb } from './persistence.actions'; @Injectable({ providedIn: 'root', @@ -147,7 +149,7 @@ export class PersistenceService { this.onAfterSave$.pipe( concatMap(() => this.loadComplete()), // TODO maybe not necessary - // skipWhile(complete => !isValidAppData(complete)), + skipWhile(complete => !isValidAppData(complete)), ), ).pipe( shareReplay(1), @@ -159,6 +161,7 @@ export class PersistenceService { constructor( private _databaseService: DatabaseService, private _compressionService: CompressionService, + private _store: Store, ) { // this.inMemoryComplete$.subscribe((v) => console.log('inMemoryComplete$', v)); } @@ -285,6 +288,10 @@ export class PersistenceService { return this._saveToDb({dbKey: LS_BACKUP, data, isDataImport: true, isSyncModelChange: true}); } + async clearBackup(): Promise { + return this._removeFromDb({dbKey: LS_BACKUP}); + } + // NOTE: not including backup async loadComplete(): Promise { let r; @@ -520,6 +527,7 @@ export class PersistenceService { }): Promise { if (!this._isBlockSaving || isDataImport === true) { const idbKey = this._getIDBKey(dbKey, projectId); + this._store.dispatch(saveToDb({dbKey, data})); const r = await this._databaseService.save(idbKey, data); this._updateInMemory({ @@ -547,6 +555,7 @@ export class PersistenceService { }): Promise { const idbKey = this._getIDBKey(dbKey, projectId); if (!this._isBlockSaving || isDataImport === true) { + this._store.dispatch(removeFromDb({dbKey})); return this._databaseService.remove(idbKey); } else { console.warn('BLOCKED SAVING for ', dbKey); @@ -560,6 +569,8 @@ export class PersistenceService { projectId?: string, }): Promise { const idbKey = this._getIDBKey(dbKey, projectId); + // 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; } diff --git a/src/app/features/config/default-global-config.const.ts b/src/app/features/config/default-global-config.const.ts index 6f276643a..2b5e14a72 100644 --- a/src/app/features/config/default-global-config.const.ts +++ b/src/app/features/config/default-global-config.const.ts @@ -6,7 +6,7 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = { lng: null }, misc: { - isDarkMode: false, + isDarkMode: true, isConfirmBeforeExit: false, isNotifyWhenTimeEstimateExceeded: false, isAutMarkParentAsDone: false, @@ -112,5 +112,9 @@ export const DEFAULT_GLOBAL_CONFIG: GlobalConfigState = { isPlayDoneSound: true, isIncreaseDoneSoundPitch: true, doneSound: 'done2.mp3', + }, + trackingReminder: { + isEnabled: true, + minTime: minute * 2, } }; diff --git a/src/app/features/config/form-cfgs/tracking-reminder-form.const.ts b/src/app/features/config/form-cfgs/tracking-reminder-form.const.ts new file mode 100644 index 000000000..bf082aedd --- /dev/null +++ b/src/app/features/config/form-cfgs/tracking-reminder-form.const.ts @@ -0,0 +1,25 @@ +// tslint:disable:max-line-length +import { ConfigFormSection, TrackingReminderConfig } from '../global-config.model'; +import { T } from '../../../t.const'; + +export const TRACKING_REMINDER_FORM_CFG: ConfigFormSection = { + title: T.GCF.TRACKING_REMINDER.TITLE, + help: T.GCF.TRACKING_REMINDER.HELP, + key: 'trackingReminder', + items: [ + { + key: 'isEnabled', + type: 'checkbox', + templateOptions: { + label: T.GCF.TRACKING_REMINDER.L_IS_ENABLED, + }, + }, + { + key: 'minTime', + type: 'duration', + templateOptions: { + label: T.GCF.TRACKING_REMINDER.L_MIN_TIME + }, + }, + ] +}; diff --git a/src/app/features/config/global-config-form-config.const.ts b/src/app/features/config/global-config-form-config.const.ts index a4b0319a5..055ceedff 100644 --- a/src/app/features/config/global-config-form-config.const.ts +++ b/src/app/features/config/global-config-form-config.const.ts @@ -12,12 +12,14 @@ import { EVALUATION_SETTINGS_FORM_CFG } from './form-cfgs/evaluation-settings-fo import { SIMPLE_COUNTER_FORM } from './form-cfgs/simple-counter-form.const'; import { DROPBOX_SYNC_FORM } from './form-cfgs/dropbox-sync-form.const'; import { SOUND_FORM_CFG } from './form-cfgs/sound-form.const'; +import { TRACKING_REMINDER_FORM_CFG } from './form-cfgs/tracking-reminder-form.const'; export const GLOBAL_CONFIG_FORM_CONFIG: ConfigFormConfig = [ (LANGUAGE_SELECTION_FORM_FORM as GenericConfigFormSection), (MISC_SETTINGS_FORM_CFG as GenericConfigFormSection), (IDLE_FORM_CFG as GenericConfigFormSection), (KEYBOARD_SETTINGS_FORM_CFG as GenericConfigFormSection), + (TRACKING_REMINDER_FORM_CFG as GenericConfigFormSection), (SOUND_FORM_CFG as GenericConfigFormSection), ]; diff --git a/src/app/features/config/global-config.model.ts b/src/app/features/config/global-config.model.ts index 422281693..4340ce5e7 100644 --- a/src/app/features/config/global-config.model.ts +++ b/src/app/features/config/global-config.model.ts @@ -125,6 +125,11 @@ export type SoundConfig = Readonly<{ volume: number; }>; +export type TrackingReminderConfig = Readonly<{ + isEnabled: boolean; + minTime: number; +}>; + export type GlobalConfigState = Readonly<{ lang: LanguageConfig; misc: MiscConfig; @@ -137,6 +142,7 @@ export type GlobalConfigState = Readonly<{ keyboard: KeyboardConfig; localBackup: LocalBackupConfig; sound: SoundConfig; + trackingReminder: TrackingReminderConfig; [MODEL_VERSION_KEY]?: number; }>; diff --git a/src/app/features/config/migrate-global-config.util.ts b/src/app/features/config/migrate-global-config.util.ts index fc7f91e14..6d315a42a 100644 --- a/src/app/features/config/migrate-global-config.util.ts +++ b/src/app/features/config/migrate-global-config.util.ts @@ -3,7 +3,7 @@ import { DEFAULT_GLOBAL_CONFIG } from './default-global-config.const'; import { MODEL_VERSION_KEY } from '../../app.constants'; import { isMigrateModel } from '../../util/model-version'; -const MODEL_VERSION = 1.2; +const MODEL_VERSION = 1.3; export const migrateGlobalConfigState = (globalConfigState: GlobalConfigState): GlobalConfigState => { if (!isMigrateModel(globalConfigState, MODEL_VERSION, 'GlobalConfig')) { diff --git a/src/app/features/dropbox/dropbox-sync.service.ts b/src/app/features/dropbox/dropbox-sync.service.ts index 26c7cbf4c..abe7ca9cf 100644 --- a/src/app/features/dropbox/dropbox-sync.service.ts +++ b/src/app/features/dropbox/dropbox-sync.service.ts @@ -22,6 +22,8 @@ import { DialogDbxSyncConflictComponent } from './dialog-dbx-sync-conflict/dialo import { SnackService } from '../../core/snack/snack.service'; import { environment } from '../../../environments/environment'; import { T } from '../../t.const'; +import { isValidAppData } from '../../imex/sync/is-valid-app-data.util'; +import { TranslateService } from '@ngx-translate/core'; @Injectable({providedIn: 'root'}) export class DropboxSyncService { @@ -58,6 +60,7 @@ export class DropboxSyncService { private _dataInitService: DataInitService, private _snackService: SnackService, private _matDialog: MatDialog, + private _translateService: TranslateService, ) { // TODO initial syncing (do with immediate triggers) } @@ -79,7 +82,7 @@ export class DropboxSyncService { if (isAxiosError && e.response.data && e.response.data.error_summary === 'path/not_found/..') { dbxLog('DBX: File not found => ↑↑↑ Initial Upload ↑↑↑'); - local = await this._syncService.inMemory$.pipe(take(1)).toPromise(); + local = await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise(); return await this._uploadAppData(local); } else if (isAxiosError && e.response.status === 401) { this._snackService.open({msg: T.F.DROPBOX.S.AUTH_ERROR, type: 'ERROR'}); @@ -109,7 +112,7 @@ export class DropboxSyncService { if (rev && rev === localRev) { dbxLog('DBX PRE1: ↔ Same Rev', rev); // NOTE: same rev, doesn't mean. that we can't have local changes - local = await this._syncService.inMemory$.pipe(take(1)).toPromise(); + local = await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise(); if (lastSync === local.lastLocalSyncModelChange) { dbxLog('DBX PRE1: No local changes to sync'); return; @@ -120,9 +123,10 @@ export class DropboxSyncService { // simple check based on file meta data // ------------------------------------ // if not defined yet - local = local || await this._syncService.inMemory$.pipe(take(1)).toPromise(); + local = local || await this._syncService.inMemoryComplete$.pipe(take(1)).toPromise(); if (local.lastLocalSyncModelChange === 0) { - if (!confirm('lastLocalSyncModelChange is 0. Which means data has been deleted or something is wrong. Proceed with Dropbox sync?')) { + console.log(local); + if (!(this._c(T.F.DROPBOX.C.EMPTY_SYNC))) { return; } } @@ -169,7 +173,7 @@ export class DropboxSyncService { case UpdateCheckResult.RemoteNotUpToDateDespiteSync: { dbxLog('DBX: X Remote not up to date despite sync'); - if (confirm('Try to re-load data from remote once again?')) { + if (this._c(T.F.DROPBOX.C.TRY_LOAD_REMOTE_AGAIN)) { return this.sync(); } else { return this._handleConflict({remote, local, lastSync, downloadMeta: r.meta}); @@ -192,11 +196,11 @@ export class DropboxSyncService { case UpdateCheckResult.ErrorLastSyncNewerThanLocal: { dbxLog('DBX: XXX Wrong Data'); if (local.lastLocalSyncModelChange > remote.lastLocalSyncModelChange) { - if (confirm('Upload local data anyway?')) { + if (this._c(T.F.DROPBOX.C.FORCE_UPLOAD)) { return await this._uploadAppData(local, true); } } else { - if (confirm('Import remote data anyway?')) { + if (this._c(T.F.DROPBOX.C.FORCE_IMPORT)) { return await this._importData(remote, r.meta.rev); } } @@ -239,6 +243,12 @@ export class DropboxSyncService { } private async _uploadAppData(data: AppDataComplete, isForceOverwrite: boolean = false): Promise { + if (!isValidAppData(data)) { + console.log(data); + alert('The data you are trying to upload is invalid'); + throw new Error('The data you are trying to upload is invalid'); + } + try { const r = await this._dropboxApiService.upload({ path: DROPBOX_SYNC_FILE_PATH, @@ -254,7 +264,7 @@ export class DropboxSyncService { } catch (e) { console.error(e); dbxLog('DBX: X Upload Request Error'); - if (confirm('An Error occurred while uploading your local data. Try to force the update?')) { + if (this._c(T.F.DROPBOX.C.FORCE_UPLOAD_AFTER_ERROR)) { return this._uploadAppData(data, true); } } @@ -329,4 +339,9 @@ export class DropboxSyncService { } }).afterClosed(); } + + private _c(str: string): boolean { + return confirm(this._translateService.instant(str)); + }; + } diff --git a/src/app/features/issue/providers/github/github-api.service.ts b/src/app/features/issue/providers/github/github-api.service.ts index b713353ad..1419501f9 100644 --- a/src/app/features/issue/providers/github/github-api.service.ts +++ b/src/app/features/issue/providers/github/github-api.service.ts @@ -53,7 +53,7 @@ export class GithubApiService { `+repo:${cfg.repo}`; return this._sendRequest$({ - url: `${BASE}search/issues?q=${encodeURI(searchText + repoQuery)}` + url: `${BASE}search/issues?q=${encodeURIComponent(searchText + repoQuery)}` }, cfg) .pipe( map((res: GithubIssueSearchResult) => { diff --git a/src/app/features/issue/providers/jira/jira-api.service.ts b/src/app/features/issue/providers/jira/jira-api.service.ts index caa05509d..9d4ffa4df 100644 --- a/src/app/features/issue/providers/jira/jira-api.service.ts +++ b/src/app/features/issue/providers/jira/jira-api.service.ts @@ -109,7 +109,7 @@ export class JiraApiService { issuePicker$(searchTerm: string, cfg: JiraCfg): Observable { const searchStr = `${searchTerm}`; - const jql = (cfg.searchJqlQuery ? `${encodeURI(cfg.searchJqlQuery)}` : ''); + const jql = (cfg.searchJqlQuery ? `${encodeURIComponent(cfg.searchJqlQuery)}` : ''); return this._sendRequest$({ jiraReqCfg: { diff --git a/src/app/features/reminder/reminder.service.ts b/src/app/features/reminder/reminder.service.ts index c2a661045..df2067279 100644 --- a/src/app/features/reminder/reminder.service.ts +++ b/src/app/features/reminder/reminder.service.ts @@ -126,6 +126,8 @@ export class ReminderService { throw new Error('A reminder for this ' + type + ' already exists'); } + // TODO find out why we need to do this + this._reminders = dirtyDeepCopy(this._reminders); this._reminders.push({ id, workContextId: this._workContextService.activeWorkContextId as string, @@ -148,6 +150,8 @@ export class ReminderService { updateReminder(reminderId: string, reminderChanges: Partial) { const i = this._reminders.findIndex(reminder => reminder.id === reminderId); if (i > -1) { + // TODO find out why we need to do this + this._reminders = dirtyDeepCopy(this._reminders); this._reminders[i] = Object.assign({}, this._reminders[i], reminderChanges); } this._saveModel(this._reminders); @@ -157,6 +161,8 @@ export class ReminderService { const i = this._reminders.findIndex(reminder => reminder.id === reminderIdToRemove); if (i > -1) { + // TODO find out why we need to do this + this._reminders = dirtyDeepCopy(this._reminders); this._reminders.splice(i, 1); this._saveModel(this._reminders); } else { diff --git a/src/app/features/tasks/add-task-bar/add-task-bar.component.ts b/src/app/features/tasks/add-task-bar/add-task-bar.component.ts index 81e09cc4a..5e69491a9 100644 --- a/src/app/features/tasks/add-task-bar/add-task-bar.component.ts +++ b/src/app/features/tasks/add-task-bar/add-task-bar.component.ts @@ -128,10 +128,6 @@ export class AddTaskBarComponent implements AfterViewInit, OnDestroy { this._subs.add(this.activatedIssueTask$.subscribe((v) => this.activatedIssueTask = v)); this._subs.add(this.shortSyntaxTags$.subscribe((v) => this.shortSyntaxTags = v)); this._subs.add(this.inputVal$.subscribe((v) => this.inputVal = v)); - - // TODO remove - this._subs.add(this.shortSyntaxTags$.subscribe((v) => console.log('shortSyntaxTags$', v))); - this._subs.add(this.filteredIssueSuggestions$.subscribe((val) => console.log('filteredIssueSuggestions$', val))); } ngAfterViewInit(): void { diff --git a/src/app/features/tasks/store/task-db.effects.ts b/src/app/features/tasks/store/task-db.effects.ts index 3b932469c..bfaad8a5d 100644 --- a/src/app/features/tasks/store/task-db.effects.ts +++ b/src/app/features/tasks/store/task-db.effects.ts @@ -67,6 +67,7 @@ export class TaskDbEffects { private _persistenceService: PersistenceService) { } + // @debounce(50) private _saveToLs(taskState: TaskState, isSyncModelChange: boolean = false) { this._persistenceService.task.saveState({ ...taskState, diff --git a/src/app/features/tasks/store/task-related-model.effects.ts b/src/app/features/tasks/store/task-related-model.effects.ts index 73b5f0a1e..df64be65d 100644 --- a/src/app/features/tasks/store/task-related-model.effects.ts +++ b/src/app/features/tasks/store/task-related-model.effects.ts @@ -10,7 +10,7 @@ import { UpdateTask, UpdateTaskTags } from './task.actions'; -import { concatMap, filter, first, map, mapTo, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators'; +import { concatMap, delay, filter, first, map, mapTo, mergeMap, switchMap, tap, withLatestFrom } from 'rxjs/operators'; import { PersistenceService } from '../../../core/persistence/persistence.service'; import { Task, TaskArchive, TaskWithSubTasks } from '../task.model'; import { ReminderService } from '../../reminder/reminder.service'; @@ -154,6 +154,8 @@ export class TaskRelatedModelEffects { // we only want to execute this for task title updates return (changeProps.length === 1 && changeProps[0] === 'title'); }), + // dirty fix to execute this after setDefaultProjectId$ effect + delay(20), concatMap((action: AddTask | UpdateTask): Observable => { return this._taskService.getByIdOnce$(action.payload.task.id as string); }), @@ -162,8 +164,6 @@ export class TaskRelatedModelEffects { this._projectService.list$, ), mergeMap(([task, tags, projects]) => { - console.log('ADDED TASK', task); - const r = shortSyntax(task, tags, projects); if (!r) { return EMPTY; @@ -197,14 +197,17 @@ export class TaskRelatedModelEffects { } if (tagIds && tagIds.length) { + const isEqualTags = (JSON.stringify(tagIds) === JSON.stringify(task.tagIds)); if (!task.tagIds) { throw new Error('Task Old TagIds need to be passed'); } - actions.push(new UpdateTaskTags({ - task, - newTagIds: unique(tagIds), - oldTagIds: task.tagIds, - })); + if (!isEqualTags) { + actions.push(new UpdateTaskTags({ + task, + newTagIds: unique(tagIds), + oldTagIds: task.tagIds, + })); + } } return actions; diff --git a/src/app/features/tasks/store/task-ui.effects.ts b/src/app/features/tasks/store/task-ui.effects.ts index 373838fef..e270c89d7 100644 --- a/src/app/features/tasks/store/task-ui.effects.ts +++ b/src/app/features/tasks/store/task-ui.effects.ts @@ -74,6 +74,7 @@ export class TaskUiEffects { ), filter(({payload: {task: {changes}}}: UpdateTask) => !!changes.isDone), withLatestFrom(this._workContextService.flatDoneTodayNr$, this._globalConfigService.sound$), + filter(([, , soundCfg]) => soundCfg.isPlayDoneSound), tap(([, doneToday, soundCfg]) => playDoneSound(soundCfg, doneToday)), ); diff --git a/src/app/features/tasks/task-additional-info/task-additional-info-item/task-additional-info-item.component.scss b/src/app/features/tasks/task-additional-info/task-additional-info-item/task-additional-info-item.component.scss index dc513895b..6a8fb7e54 100644 --- a/src/app/features/tasks/task-additional-info/task-additional-info-item/task-additional-info-item.component.scss +++ b/src/app/features/tasks/task-additional-info/task-additional-info-item/task-additional-info-item.component.scss @@ -22,7 +22,7 @@ //overflow: visible; :host:focus & { - border: 1px dashed $c-focus-border; + border: 1px solid $c-focus-border; border-color: $c-focus-border !important; } diff --git a/src/app/features/tasks/task/task.component.scss b/src/app/features/tasks/task/task.component.scss index 350d9283e..0d7841645 100644 --- a/src/app/features/tasks/task/task.component.scss +++ b/src/app/features/tasks/task/task.component.scss @@ -103,13 +103,14 @@ // NOTE: somehow ng build messes up, if we don't include it like that border-color: $task-c-focus; border-width: 1px; - border-style: dashed !important; + border-style: solid !important; } } :host.isCurrent.isCurrent.isCurrent.isCurrent.isCurrent & { border-color: $task-c-current !important; border-width: 1px !important; + border-style: dashed; border-radius: 12px !important; bottom: -2px; diff --git a/src/app/features/tasks/task/task.component.ts b/src/app/features/tasks/task/task.component.ts index 29f2e89c1..1950cc841 100644 --- a/src/app/features/tasks/task/task.component.ts +++ b/src/app/features/tasks/task/task.component.ts @@ -38,6 +38,7 @@ import { TODAY_TAG } from '../../tag/tag.const'; import { DialogEditTagsForTaskComponent } from '../../tag/dialog-edit-tags/dialog-edit-tags-for-task.component'; import { WorkContextService } from '../../work-context/work-context.service'; import { environment } from '../../../../environments/environment'; +import { throttle } from 'helpful-decorators'; @Component({ selector: 'task', @@ -59,7 +60,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { ShowSubTasksMode: typeof ShowSubTasksMode = ShowSubTasksMode; contextMenuPosition: { x: string; y: string } = {x: '0px', y: '0px'}; progress: number = 0; - isDev: boolean = !environment.production; + isDev: boolean = !(environment.production || environment.stage); @ViewChild('contentEditableOnClickEl', {static: true}) contentEditableOnClickEl?: ElementRef; @ViewChild('blockLeftEl') blockLeftElRef?: ElementRef; @ViewChild('blockRightEl') blockRightElRef?: ElementRef; @@ -91,6 +92,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { private _dragEnterTarget?: HTMLElement; private _destroy$: Subject = new Subject(); private _currentPanTimeout?: number; + private _isTaskDeleteTriggered: boolean = false; constructor( private readonly _taskService: TaskService, @@ -236,6 +238,11 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { } deleteTask() { + if (this._isTaskDeleteTriggered) { + return; + } + + this._isTaskDeleteTriggered = true; this._taskService.remove(this.task); this.focusNext(true); } @@ -284,6 +291,14 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { this._taskService.addSubTaskTo(this.task.parentId || this.task.id); } + @throttle(200, {leading: true, trailing: false}) + toggleDoneKeyboard() { + this.toggleTaskDone(); + if (!this.task.parentId) { + this.focusNext(true); + } + } + toggleTaskDone() { if (this.task.parentId) { this.focusNext(true); @@ -601,14 +616,7 @@ export class TaskComponent implements OnInit, OnDestroy, AfterViewInit { this.editReminder(); } if (checkKeyCombo(ev, keys.taskToggleDone)) { - this.toggleTaskDone(); - if (!this.task.parentId) { - if (this.task.isDone) { - this.focusPrevious(true); - } else { - this.focusNext(true); - } - } + this.toggleDoneKeyboard(); } if (checkKeyCombo(ev, keys.taskAddSubTask)) { this.addSubTask(); diff --git a/src/app/features/time-tracking/idle.service.ts b/src/app/features/time-tracking/idle.service.ts index 1dd7d12cc..d4e42ed4d 100644 --- a/src/app/features/time-tracking/idle.service.ts +++ b/src/app/features/time-tracking/idle.service.ts @@ -125,12 +125,13 @@ export class IdleService { if (task) { if (typeof task === 'string') { - this._taskService.add(task, false, { + const currId = this._taskService.add(task, false, { timeSpent, timeSpentOnDay: { [getWorklogStr()]: timeSpent } }); + this._taskService.setCurrentId(currId); } else { this._taskService.addTimeSpent(task, timeSpent); this._taskService.setCurrentId(task.id); diff --git a/src/app/features/time-tracking/time-tracking.module.ts b/src/app/features/time-tracking/time-tracking.module.ts index cf1158453..3372191f3 100644 --- a/src/app/features/time-tracking/time-tracking.module.ts +++ b/src/app/features/time-tracking/time-tracking.module.ts @@ -6,6 +6,7 @@ import { UiModule } from '../../ui/ui.module'; import { FormsModule } from '@angular/forms'; import { TakeABreakModule } from './take-a-break/take-a-break.module'; import { TasksModule } from '../tasks/tasks.module'; +import { DialogTrackingReminderComponent } from './tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component'; @NgModule({ imports: [ @@ -15,7 +16,8 @@ import { TasksModule } from '../tasks/tasks.module'; TasksModule, ], declarations: [ - DialogIdleComponent + DialogIdleComponent, + DialogTrackingReminderComponent ], exports: [ TakeABreakModule, diff --git a/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.html b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.html new file mode 100644 index 000000000..b0a14ad39 --- /dev/null +++ b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.html @@ -0,0 +1,37 @@ +
+
+

{{T.F.TIME_TRACKING.D_TRACKING_REMINDER.UNTRACKED_TIME|translate}}

+
{{data.remindCounter$|async|msToString:true}}
+ + + +
+ {{T.F.TIME_TRACKING.D_TRACKING_REMINDER.TRACK_TO|translate}} + +
+
+ + +
+ + + +
+
diff --git a/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.scss b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.scss new file mode 100644 index 000000000..dde3ff3d3 --- /dev/null +++ b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.scss @@ -0,0 +1,21 @@ +@import "../../../../../variables"; + +.dialog-content { + p, + .track-to-label, + .time { + text-align: center; + } +} + +.time { + font-size: 20px; + font-weight: bold; + margin-bottom: $s; +} + +.track-to-label { + margin-bottom: $s; + font-size: $s*2; + font-weight: bold; +} diff --git a/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.spec.ts b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.spec.ts new file mode 100644 index 000000000..be2a247b0 --- /dev/null +++ b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.spec.ts @@ -0,0 +1,25 @@ +// import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +// +// import { DialogIdleComponent } from './dialog-idle.component'; +// +// describe('DialogIdleComponent', () => { +// let component: DialogIdleComponent; +// let fixture: ComponentFixture; +// +// beforeEach(async(() => { +// TestBed.configureTestingModule({ +// declarations: [DialogIdleComponent] +// }) +// .compileComponents(); +// })); +// +// beforeEach(() => { +// fixture = TestBed.createComponent(DialogIdleComponent); +// component = fixture.componentInstance; +// fixture.detectChanges(); +// }); +// +// it('should create', () => { +// expect(component).toBeTruthy(); +// }); +// }); diff --git a/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.ts b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.ts new file mode 100644 index 000000000..3da40738c --- /dev/null +++ b/src/app/features/time-tracking/tracking-reminder/dialog-tracking-reminder/dialog-tracking-reminder.component.ts @@ -0,0 +1,56 @@ +import { ChangeDetectionStrategy, Component, Inject, OnInit } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; +import { TaskService } from '../../../tasks/task.service'; +import { Observable } from 'rxjs'; +import { Task } from '../../../tasks/task.model'; +import { T } from '../../../../t.const'; + +@Component({ + selector: 'dialog-tracking-reminder', + templateUrl: './dialog-tracking-reminder.component.html', + styleUrls: ['./dialog-tracking-reminder.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DialogTrackingReminderComponent implements OnInit { + T: typeof T = T; + lastCurrentTask$: Observable = this._taskService.getByIdOnce$(this.data.lastCurrentTaskId); + selectedTask: Task | null = null; + newTaskTitle?: string; + isCreate?: boolean; + + constructor( + private _taskService: TaskService, + private _matDialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: any, + ) { + _matDialogRef.disableClose = true; + } + + ngOnInit() { + this.lastCurrentTask$.subscribe((task) => { + this.selectedTask = task; + this.isCreate = false; + }); + } + + onTaskChange(taskOrTaskTitle: Task | string) { + this.isCreate = (typeof taskOrTaskTitle === 'string'); + if (this.isCreate) { + this.newTaskTitle = taskOrTaskTitle as string; + this.selectedTask = null; + } else { + this.selectedTask = taskOrTaskTitle as Task; + this.newTaskTitle = undefined; + } + } + + cancel() { + this._matDialogRef.close(); + } + + track() { + this._matDialogRef.close({ + task: this.selectedTask || this.newTaskTitle, + }); + } +} diff --git a/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.spec.ts b/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.spec.ts new file mode 100644 index 000000000..43176ef3b --- /dev/null +++ b/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.spec.ts @@ -0,0 +1,16 @@ +// import { TestBed } from '@angular/core/testing'; +// +// import { StartTimerReminderService } from './start-timer-reminder.service'; +// +// describe('StartTimerReminderService', () => { +// let service: StartTimerReminderService; +// +// beforeEach(() => { +// TestBed.configureTestingModule({}); +// service = TestBed.inject(StartTimerReminderService); +// }); +// +// it('should be created', () => { +// expect(service).toBeTruthy(); +// }); +// }); diff --git a/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.ts b/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.ts new file mode 100644 index 000000000..f871b66b8 --- /dev/null +++ b/src/app/features/time-tracking/tracking-reminder/tracking-reminder.service.ts @@ -0,0 +1,128 @@ +import { Injectable } from '@angular/core'; +import { IdleService } from '../idle.service'; +import { TaskService } from '../../tasks/task.service'; +import { GlobalConfigService } from '../../config/global-config.service'; +import { combineLatest, EMPTY, merge, Observable, of, Subject } from 'rxjs'; +import { distinctUntilChanged, filter, map, shareReplay, switchMap, withLatestFrom } from 'rxjs/operators'; +import { realTimer$ } from '../../../util/real-timer'; +import { BannerService } from '../../../core/banner/banner.service'; +import { BannerId } from '../../../core/banner/banner.model'; +import { msToString } from '../../../ui/duration/ms-to-string.pipe'; +import { MatDialog } from '@angular/material/dialog'; +import { DialogTrackingReminderComponent } from './dialog-tracking-reminder/dialog-tracking-reminder.component'; +import { Task } from '../../tasks/task.model'; +import { getWorklogStr } from '../../../util/get-work-log-str'; +import { T } from '../../../t.const'; +import { TranslateService } from '@ngx-translate/core'; +import { TrackingReminderConfig } from '../../config/global-config.model'; + +@Injectable({ + providedIn: 'root' +}) +export class TrackingReminderService { + _cfg$: Observable = this._globalConfigService.cfg$.pipe(map(cfg => cfg?.trackingReminder)); + + _counter$: Observable = realTimer$(1000); + + _manualReset$: Subject = new Subject(); + + _resetableCounter$: Observable = merge( + of(true), + this._manualReset$, + ).pipe( + switchMap(() => this._counter$), + ); + + remindCounter$: Observable = this._cfg$.pipe( + switchMap((cfg) => !cfg.isEnabled + ? EMPTY + : combineLatest([ + this._taskService.currentTaskId$, + this._idleService.isIdle$, + ]).pipe( + map(([currentTaskId, isIdle]) => !currentTaskId && !isIdle), + distinctUntilChanged(), + switchMap((isEnabled) => isEnabled + ? this._resetableCounter$ + : of(0) + ), + filter(time => time > cfg.minTime), + ) + ), + shareReplay(), + ); + + constructor( + private _idleService: IdleService, + private _taskService: TaskService, + private _globalConfigService: GlobalConfigService, + private _bannerService: BannerService, + private _matDialog: MatDialog, + private _translateService: TranslateService, + ) { + } + + init() { + this.remindCounter$.subscribe((count) => { + this._triggerBanner(count, {}); + }); + } + + private _triggerBanner(duration: number, cfg: any) { + // don't update if this or other dialogs are open + if (this._matDialog.openDialogs.length !== 0) { + return; + } + + const durationStr = msToString(duration); + this._bannerService.open({ + id: BannerId.StartTrackingReminder, + ico: 'timer', + msg: this._translateService.instant(T.F.TIME_TRACKING.B_TTR.MSG, {time: durationStr}), + action: { + label: T.F.TIME_TRACKING.B_TTR.ADD_TO_TASK, + fn: () => this._openDialog(), + }, + action2: { + label: T.G.DISMISS, + fn: () => this._dismissBanner(), + }, + }); + } + + private _openDialog() { + this._matDialog.open(DialogTrackingReminderComponent, { + data: { + remindCounter$: this.remindCounter$, + } + }).afterClosed() + .pipe( + withLatestFrom(this.remindCounter$), + ) + .subscribe(async ([{task} = {task: undefined}, remindCounter]: [{ task: Task | string | undefined }, number]): Promise => { + this._manualReset$.next(); + const timeSpent = remindCounter; + + if (task) { + if (typeof task === 'string') { + const currId = this._taskService.add(task, false, { + timeSpent, + timeSpentOnDay: { + [getWorklogStr()]: timeSpent + } + }); + this._taskService.setCurrentId(currId); + } else { + this._taskService.addTimeSpent(task, timeSpent); + this._taskService.setCurrentId(task.id); + } + } + this._dismissBanner(); + }); + } + + private _dismissBanner() { + this._bannerService.dismiss(BannerId.StartTrackingReminder); + this._manualReset$.next(); + } +} diff --git a/src/app/imex/sync/data-import.service.ts b/src/app/imex/sync/data-import.service.ts index c8cefba08..749e1434b 100644 --- a/src/app/imex/sync/data-import.service.ts +++ b/src/app/imex/sync/data-import.service.ts @@ -8,6 +8,9 @@ import { T } from '../../t.const'; import { MigrationService } from '../../core/migration/migration.service'; import { DataInitService } from '../../core/data-init/data-init.service'; import { isValidAppData } from './is-valid-app-data.util'; +import { DataRepairService } from '../../core/data-repair/data-repair.service'; +import { LS_CHECK_STRAY_PERSISTENCE_BACKUP } from '../../core/persistence/ls-keys.const'; +import { TranslateService } from '@ngx-translate/core'; // TODO some of this can be done in a background script @@ -23,19 +26,26 @@ export class DataImportService { private _imexMetaService: ImexMetaService, private _migrationService: MigrationService, private _dataInitService: DataInitService, + private _dataRepairService: DataRepairService, + private _translateService: TranslateService, ) { + this._isCheckForStrayBackupAndImport(); } async getCompleteSyncData(): Promise { return await this._persistenceService.loadComplete(); } - async importCompleteSyncData(data: AppDataComplete, isBackupReload: boolean = false) { + async importCompleteSyncData(data: AppDataComplete, isBackupReload: boolean = false, isSkipStrayBackupCheck: boolean = false) { this._snackService.open({msg: T.S.SYNC.IMPORTING, ico: 'cloud_download'}); this._imexMetaService.setDataImportInProgress(true); // get rid of outdated project data if (!isBackupReload) { + if (!isSkipStrayBackupCheck && await this._isCheckForStrayBackupAndImport()) { + return; + } + await this._persistenceService.saveBackup(); await this._persistenceService.clearDatabaseExceptBackup(); } @@ -46,6 +56,7 @@ export class DataImportService { // save data to database first then load to store from there await this._persistenceService.importComplete(migratedData); await this._loadAllFromDatabaseToStore(); + await this._persistenceService.clearBackup(); this._imexMetaService.setDataImportInProgress(false); this._snackService.open({type: 'SUCCESS', msg: T.S.SYNC.SUCCESS}); @@ -55,9 +66,12 @@ export class DataImportService { msg: T.S.SYNC.ERROR_FALLBACK_TO_BACKUP, }); console.error(e); - await this._loadBackup(); + await this._importBackup(); this._imexMetaService.setDataImportInProgress(false); } + } else if (this._dataRepairService.isRepairConfirmed()) { + const fixedData = this._dataRepairService.repairData(data); + await this.importCompleteSyncData(fixedData, isBackupReload, true); } else { this._snackService.open({type: 'ERROR', msg: T.S.SYNC.ERROR_INVALID_DATA}); console.error(data); @@ -73,8 +87,30 @@ export class DataImportService { ]); } - private async _loadBackup(): Promise { + private async _importBackup(): Promise { const data = await this._persistenceService.loadBackup(); return this.importCompleteSyncData(data, true); } + + private async _isCheckForStrayBackupAndImport(): Promise { + const backup = await this._persistenceService.loadBackup(); + if (!localStorage.getItem(LS_CHECK_STRAY_PERSISTENCE_BACKUP)) { + if (backup) { + await this._persistenceService.clearBackup(); + } + localStorage.setItem(LS_CHECK_STRAY_PERSISTENCE_BACKUP, 'true'); + } + + if (backup) { + if (confirm(this._translateService.instant(T.CONFIRM.RESTORE_STRAY_BACKUP))) { + await this._importBackup(); + return true; + } else { + if (confirm(this._translateService.instant(T.CONFIRM.DELETE_STRAY_BACKUP))) { + await this._persistenceService.clearBackup(); + } + } + } + return false; + } } diff --git a/src/app/imex/sync/is-valid-app-data.util.spec.ts b/src/app/imex/sync/is-valid-app-data.util.spec.ts new file mode 100644 index 000000000..d8ce0677c --- /dev/null +++ b/src/app/imex/sync/is-valid-app-data.util.spec.ts @@ -0,0 +1,135 @@ +import { AppDataComplete } from './sync.model'; +import { isValidAppData } from './is-valid-app-data.util'; +import { MODEL_VERSION_KEY } from '../../app.constants'; +import { DEFAULT_TASK, Task } from '../../features/tasks/task.model'; +import { fakeEntityStateFromArray } from '../../util/fake-entity-state-from-array'; +import { Project } from '../../features/project/project.model'; +import { Tag } from '../../features/tag/tag.model'; +import { createAppDataCompleteMock } from '../../util/app-data-mock'; + +// const BASE_STATE_KEYS: (keyof AppBaseData)[] = [ +// 'task', +// 'taskArchive', +// 'tag', +// 'project', +// ]; +// const PROJECT_STATE_KEYS: (keyof AppDataForProjects)[] = [ +// 'note', +// 'bookmark', +// 'metric', +// 'improvement', +// 'obstruction', +// ]; + +describe('isValidAppData()', () => { + let mock: AppDataComplete; + beforeEach(() => { + mock = createAppDataCompleteMock(); + spyOn(window, 'alert').and.stub(); + }); + + it('should work for valid data', () => { + expect(isValidAppData(mock)).toBe(true); + }); + + describe('should return false for', () => { + ['note', 'bookmark', 'improvement', 'obstruction', 'metric', 'task', 'tag', 'globalConfig', 'taskArchive'].forEach((prop) => { + it('missing prop ' + prop, () => { + expect(isValidAppData({ + ...mock, + [prop]: null, + })).toBe(false); + }); + }); + }); + + describe('should error for', () => { + describe('inconsistent entity state', () => { + ['task', 'taskArchive', 'taskRepeatCfg', 'tag', 'project', 'simpleCounter'].forEach(prop => { + it(prop, () => { + expect(() => isValidAppData({ + ...mock, + [prop]: { + ...mock[prop], + entities: {}, + ids: ['asasdasd'] + }, + })).toThrowError(`Inconsistent entity state "${prop}"`); + }); + }); + }); + + it('inconsistent task state', () => { + expect(() => isValidAppData({ + ...mock, + task: { + ...mock.task, + entities: {'A asdds': DEFAULT_TASK}, + ids: ['asasdasd'] + }, + })).toThrowError(`Inconsistent entity state "task"`); + }); + + it('missing today task data for projects', () => { + expect(() => isValidAppData({ + ...mock, + // NOTE: it's empty + task: mock.task, + project: { + ...fakeEntityStateFromArray([{ + title: 'TEST_T', + id: 'TEST_ID', + taskIds: ['gone'], + }] as Partial []), + [MODEL_VERSION_KEY]: 5 + }, + })).toThrowError(`Inconsistent Task State: Missing task id gone for Project/Tag TEST_T`); + }); + + it('missing backlog task data for projects', () => { + expect(() => isValidAppData({ + ...mock, + // NOTE: it's empty + task: mock.task, + project: { + ...fakeEntityStateFromArray([{ + title: 'TEST_T', + id: 'TEST_ID', + taskIds: [], + backlogTaskIds: ['goneBL'], + }] as Partial []), + [MODEL_VERSION_KEY]: 5 + }, + })).toThrowError(`Inconsistent Task State: Missing task id goneBL for Project/Tag TEST_T`); + }); + + it('missing today task data for tags', () => { + expect(() => isValidAppData({ + ...mock, + // NOTE: it's empty + task: mock.task, + tag: { + ...fakeEntityStateFromArray([{ + title: 'TEST_TAG', + id: 'TEST_ID_TAG', + taskIds: ['goneTag'], + }] as Partial []), + [MODEL_VERSION_KEY]: 5 + }, + })).toThrowError(`Inconsistent Task State: Missing task id goneTag for Project/Tag TEST_TAG`); + }); + + xit('missing tag for task', () => { + expect(() => isValidAppData({ + ...mock, + task: { + ...mock.task, + ...fakeEntityStateFromArray([{ + ...DEFAULT_TASK, + tagIds: ['Non existent'] + }]) + } as any, + })).toThrowError(`No tagX`); + }); + }); +}); diff --git a/src/app/imex/sync/is-valid-app-data.util.ts b/src/app/imex/sync/is-valid-app-data.util.ts index 78c334f1b..ab084bb41 100644 --- a/src/app/imex/sync/is-valid-app-data.util.ts +++ b/src/app/imex/sync/is-valid-app-data.util.ts @@ -5,40 +5,42 @@ import { devError } from '../../util/dev-error'; import { Tag } from '../../features/tag/tag.model'; import { Project } from '../../features/project/project.model'; -// TODO unit test this -export const isValidAppData = (data: AppDataComplete, isSkipInconsistentTaskStateError = false): boolean => { +export const isValidAppData = (d: AppDataComplete, isSkipInconsistentTaskStateError = false): boolean => { + const dAny: any = d; // TODO remove this later on - const isCapableModelVersion = data.project - && data.project[MODEL_VERSION_KEY] - && typeof data.project[MODEL_VERSION_KEY] === 'number' - && (data.project[MODEL_VERSION_KEY] as number) >= 5; + const isCapableModelVersion = + (typeof dAny === 'object') + && d.project + && d.project[MODEL_VERSION_KEY] + && typeof d.project[MODEL_VERSION_KEY] === 'number' + && (d.project[MODEL_VERSION_KEY] as number) >= 5; // console.time('time isValidAppData'); const isValid = (isCapableModelVersion) - ? (typeof (data as any) === 'object') - && typeof (data as any).note === 'object' - && typeof (data as any).bookmark === 'object' - && typeof (data as any).improvement === 'object' - && typeof (data as any).obstruction === 'object' - && typeof (data as any).metric === 'object' - && typeof (data as any).task === 'object' - && typeof (data as any).tag === 'object' - && typeof (data as any).globalConfig === 'object' - && typeof (data as any).taskArchive === 'object' - && typeof (data as any).project === 'object' - && Array.isArray(data.reminders) - && _isEntityStatesConsistent(data) - && _isTaskIdsConsistent(data, isSkipInconsistentTaskStateError) + ? (typeof dAny === 'object') && dAny !== null + && typeof dAny.note === 'object' && dAny.note !== null + && typeof dAny.bookmark === 'object' && dAny.bookmark !== null + && typeof dAny.improvement === 'object' && dAny.improvement !== null + && typeof dAny.obstruction === 'object' && dAny.obstruction !== null + && typeof dAny.metric === 'object' && dAny.metric !== null + && typeof dAny.task === 'object' && dAny.task !== null + && typeof dAny.tag === 'object' && dAny.tag !== null + && typeof dAny.globalConfig === 'object' && dAny.globalConfig !== null + && typeof dAny.taskArchive === 'object' && dAny.taskArchive !== null + && typeof dAny.project === 'object' && dAny.project !== null + && Array.isArray(d.reminders) + && _isEntityStatesConsistent(d) + && (isSkipInconsistentTaskStateError || _isAllTasksAvailable(d)) - : typeof (data as any) === 'object' + : typeof dAny === 'object' ; // console.timeEnd('time isValidAppData'); return isValid; }; -const _isTaskIdsConsistent = (data: AppDataComplete, isSkipInconsistentTaskStateError = false): boolean => { +const _isAllTasksAvailable = (data: AppDataComplete): boolean => { let allIds: string [] = []; (data.tag.ids as string[]) @@ -65,8 +67,7 @@ const _isTaskIdsConsistent = (data: AppDataComplete, isSkipInconsistentTaskState ); const notFound = allIds.find(id => !(data.task.ids.includes(id))); - - if (notFound && !isSkipInconsistentTaskStateError) { + if (notFound) { const tag = (data.tag.ids as string[]) .map(id => data.tag.entities[id]) .find(tagI => (tagI as Tag).taskIds.includes(notFound)); @@ -84,8 +85,10 @@ const _isEntityStatesConsistent = (data: AppDataComplete): boolean => { const baseStateKeys: (keyof AppBaseData)[] = [ 'task', 'taskArchive', + 'taskRepeatCfg', 'tag', 'project', + 'simpleCounter', ]; const projectStateKeys: (keyof AppDataForProjects)[] = [ 'note', diff --git a/src/app/imex/sync/sync.model.ts b/src/app/imex/sync/sync.model.ts index a9eef544a..39ae4da50 100644 --- a/src/app/imex/sync/sync.model.ts +++ b/src/app/imex/sync/sync.model.ts @@ -28,7 +28,7 @@ export interface AppBaseData { project: ProjectState; archivedProjects: ProjectArchive; globalConfig: GlobalConfigState; - reminders?: Reminder[]; + reminders: Reminder[]; task: TaskState; tag: TagState; @@ -40,21 +40,28 @@ export interface AppBaseData { taskAttachment?: TaskAttachmentState; } +export type AppBaseDataEntityLikeStates = + ProjectState + | TaskState + | TaskRepeatCfgState + | TaskArchive + | SimpleCounterState; + // NOTE: [key:string] always refers to projectId export interface AppDataForProjects { - note?: { + note: { [key: string]: NoteState; }; - bookmark?: { + bookmark: { [key: string]: BookmarkState; }; - metric?: { + metric: { [key: string]: MetricState; }; - improvement?: { + improvement: { [key: string]: ImprovementState; }; - obstruction?: { + obstruction: { [key: string]: ObstructionState; }; } diff --git a/src/app/imex/sync/sync.service.ts b/src/app/imex/sync/sync.service.ts index 41b8412ff..ed03db8ec 100644 --- a/src/app/imex/sync/sync.service.ts +++ b/src/app/imex/sync/sync.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@angular/core'; -import { combineLatest, EMPTY, fromEvent, merge, Observable, of, ReplaySubject } from 'rxjs'; +import { combineLatest, EMPTY, fromEvent, merge, Observable, of, ReplaySubject, throwError } from 'rxjs'; import { - auditTime, + auditTime, catchError, concatMap, debounceTime, distinctUntilChanged, @@ -14,7 +14,7 @@ import { switchMap, take, tap, - throttleTime + throttleTime, timeout } from 'rxjs/operators'; import { GlobalConfigService } from '../../features/config/global-config.service'; import { SyncProvider } from './sync-provider'; @@ -40,7 +40,10 @@ import { IPC } from '../../../../electron/ipc-events.const'; providedIn: 'root', }) export class SyncService { - inMemory$: Observable = this._persistenceService.inMemoryComplete$; + inMemoryComplete$: Observable = this._persistenceService.inMemoryComplete$.pipe( + timeout(5000), + catchError(() => throwError('Error while trying to get inMemoryComplete$')), + ); private _onUpdateLocalDataTrigger$: Observable<{ appDataKey: AllowedDBKeys, data: any, isDataImport: boolean, projectId?: string }> = this._persistenceService.onAfterSave$.pipe( diff --git a/src/app/pages/config-page/config-page.component.html b/src/app/pages/config-page/config-page.component.html index 72ccfd7de..91c8baab6 100644 --- a/src/app/pages/config-page/config-page.component.html +++ b/src/app/pages/config-page/config-page.component.html @@ -46,6 +46,6 @@ diff --git a/src/app/root-store/meta/undo-task-delete.meta-reducer.ts b/src/app/root-store/meta/undo-task-delete.meta-reducer.ts index c966c45b0..2a75c9193 100644 --- a/src/app/root-store/meta/undo-task-delete.meta-reducer.ts +++ b/src/app/root-store/meta/undo-task-delete.meta-reducer.ts @@ -141,6 +141,10 @@ const _createTaskDeleteState = (state: RootState, task: TaskWithSubTasks): UndoT taskIdsForProjectBacklog = (project as Project).backlogTaskIds; taskIdsForProject = (project as Project).taskIds; if (!taskIdsForProject || !taskIdsForProjectBacklog || (!taskIdsForProjectBacklog.length && !taskIdsForProject.length)) { + console.log('------ERR_ADDITIONAL_INFO------'); + console.log('project', project); + console.log('taskIdsForProject', taskIdsForProject); + console.log('taskIdsForProjectBacklog', taskIdsForProjectBacklog); throw new Error('Invalid project data'); } } @@ -149,6 +153,10 @@ const _createTaskDeleteState = (state: RootState, task: TaskWithSubTasks): UndoT const tagTaskIdMap = (task.tagIds).reduce((acc, id) => { const tag = tagState.entities[id]; if (!tag) { + console.log('------ERR_ADDITIONAL_INFO------'); + console.log('id', id); + console.log('tagState', tagState); + console.log('tagTaskIdMap', tagTaskIdMap); throw new Error('Task Restore Error: Missing tag'); } diff --git a/src/app/t.const.ts b/src/app/t.const.ts index b8e8923f4..00398bf9b 100644 --- a/src/app/t.const.ts +++ b/src/app/t.const.ts @@ -16,6 +16,11 @@ export const T = { 'BL': { 'NO_TASKS': 'BL.NO_TASKS' }, + 'CONFIRM': { + 'AUTO_FIX': 'CONFIRM.AUTO_FIX', + 'DELETE_STRAY_BACKUP': 'CONFIRM.DELETE_STRAY_BACKUP', + 'RESTORE_STRAY_BACKUP': 'CONFIRM.RESTORE_STRAY_BACKUP' + }, 'DATETIME_INPUT': { 'IN': 'DATETIME_INPUT.IN', 'TOMORROW': 'DATETIME_INPUT.TOMORROW' @@ -77,6 +82,13 @@ export const T = { } }, 'DROPBOX': { + 'C': { + 'EMPTY_SYNC': 'F.DROPBOX.C.EMPTY_SYNC', + 'FORCE_IMPORT': 'F.DROPBOX.C.FORCE_IMPORT', + 'FORCE_UPLOAD': 'F.DROPBOX.C.FORCE_UPLOAD', + 'FORCE_UPLOAD_AFTER_ERROR': 'F.DROPBOX.C.FORCE_UPLOAD_AFTER_ERROR', + 'TRY_LOAD_REMOTE_AGAIN': 'F.DROPBOX.C.TRY_LOAD_REMOTE_AGAIN' + }, 'D_CONFLICT': { 'LAST_CHANGE': 'F.DROPBOX.D_CONFLICT.LAST_CHANGE', 'LAST_SYNC': 'F.DROPBOX.D_CONFLICT.LAST_SYNC', @@ -742,6 +754,10 @@ export const T = { 'ALREADY_DID': 'F.TIME_TRACKING.B.ALREADY_DID', 'SNOOZE': 'F.TIME_TRACKING.B.SNOOZE' }, + 'B_TTR': { + 'ADD_TO_TASK': 'F.TIME_TRACKING.B_TTR.ADD_TO_TASK', + 'MSG': 'F.TIME_TRACKING.B_TTR.MSG' + }, 'D_IDLE': { 'BREAK': 'F.TIME_TRACKING.D_IDLE.BREAK', 'CREATE_AND_TRACK': 'F.TIME_TRACKING.D_IDLE.CREATE_AND_TRACK', @@ -750,6 +766,13 @@ export const T = { 'TASK': 'F.TIME_TRACKING.D_IDLE.TASK', 'TASK_BREAK': 'F.TIME_TRACKING.D_IDLE.TASK_BREAK', 'TRACK_TO': 'F.TIME_TRACKING.D_IDLE.TRACK_TO' + }, + 'D_TRACKING_REMINDER': { + 'UNTRACKED_TIME': 'F.TIME_TRACKING.D_TRACKING_REMINDER.UNTRACKED_TIME', + 'TRACK_TO': 'F.TIME_TRACKING.D_TRACKING_REMINDER.TRACK_TO', + 'CREATE_AND_TRACK': 'F.TIME_TRACKING.D_TRACKING_REMINDER.CREATE_AND_TRACK', + 'IDLE_FOR': 'F.TIME_TRACKING.D_TRACKING_REMINDER.IDLE_FOR', + 'TASK': 'F.TIME_TRACKING.D_TRACKING_REMINDER.TASK' } }, 'WORKLOG': { @@ -951,10 +974,10 @@ export const T = { 'TITLE': 'GCF.POMODORO.TITLE' }, 'SOUND': { - 'TITLE': 'GCF.SOUND.TITLE', 'DONE_SOUND': 'GCF.SOUND.DONE_SOUND', 'IS_INCREASE_DONE_PITCH': 'GCF.SOUND.IS_INCREASE_DONE_PITCH', 'IS_PLAY_DONE_SOUND': 'GCF.SOUND.IS_PLAY_DONE_SOUND', + 'TITLE': 'GCF.SOUND.TITLE', 'VOLUME': 'GCF.SOUND.VOLUME' }, 'TAKE_A_BREAK': { @@ -966,6 +989,12 @@ export const T = { 'MIN_WORKING_TIME': 'GCF.TAKE_A_BREAK.MIN_WORKING_TIME', 'MOTIVATIONAL_IMG': 'GCF.TAKE_A_BREAK.MOTIVATIONAL_IMG', 'TITLE': 'GCF.TAKE_A_BREAK.TITLE' + }, + 'TRACKING_REMINDER': { + 'HELP': 'GCF.TRACKING_REMINDER.HELP', + 'TITLE': 'GCF.TRACKING_REMINDER.TITLE', + 'L_IS_ENABLED': 'GCF.TRACKING_REMINDER.L_IS_ENABLED', + 'L_MIN_TIME': 'GCF.TRACKING_REMINDER.L_MIN_TIME' } }, 'GLOBAL_SNACK': { diff --git a/src/app/util/action-logger.ts b/src/app/util/action-logger.ts index c07ec25ef..a164909dd 100644 --- a/src/app/util/action-logger.ts +++ b/src/app/util/action-logger.ts @@ -1,7 +1,7 @@ import { loadFromRealLs, saveToRealLs } from '../core/persistence/local-storage'; import { LS_ACTION_BEFORE_LAST_ERROR_LOG, LS_ACTION_LOG } from '../core/persistence/ls-keys.const'; -const NUMBER_OF_ACTIONS_TO_SAVE = 25; +const NUMBER_OF_ACTIONS_TO_SAVE = 30; const getActionLog = (): string[] => { const current = loadFromRealLs(LS_ACTION_LOG); diff --git a/src/app/util/app-data-mock.ts b/src/app/util/app-data-mock.ts new file mode 100644 index 000000000..8c243a4b4 --- /dev/null +++ b/src/app/util/app-data-mock.ts @@ -0,0 +1,39 @@ +import { AppDataComplete } from '../imex/sync/sync.model'; +import { MODEL_VERSION_KEY } from '../app.constants'; +import { DEFAULT_GLOBAL_CONFIG } from '../features/config/default-global-config.const'; +import { createEmptyEntity } from './create-empty-entity'; + +export const createAppDataCompleteMock = (): AppDataComplete => ({ + project: { + ...createEmptyEntity(), + [MODEL_VERSION_KEY]: 5 + }, + archivedProjects: {}, + globalConfig: DEFAULT_GLOBAL_CONFIG, + + task: { + ...createEmptyEntity(), + ids: [], + currentTaskId: null, + selectedTaskId: null, + taskAdditionalInfoTargetPanel: null, + lastCurrentTaskId: null, + isDataLoaded: false, + }, + tag: createEmptyEntity(), + simpleCounter: { + ...createEmptyEntity(), + ids: [] + }, + taskArchive: createEmptyEntity(), + taskRepeatCfg: createEmptyEntity(), + lastLocalSyncModelChange: 0, + + // OPTIONAL though they are really not + reminders: [], + note: {}, + bookmark: {}, + metric: {}, + improvement: {}, + obstruction: {}, +}); diff --git a/src/app/util/array-equals.ts b/src/app/util/array-equals.ts new file mode 100644 index 000000000..f8895094c --- /dev/null +++ b/src/app/util/array-equals.ts @@ -0,0 +1,6 @@ +export const arrayEquals = (arr1: string[], arr2: string[]): boolean => { + for (let i = 0; i < arr1.length; ++i) { + if (arr1[i] !== arr2[i]) return false; + } + return true; +}; diff --git a/src/app/util/check-fix-entity-state-consistency.ts b/src/app/util/check-fix-entity-state-consistency.ts index 03fc838cc..a6a9caa6a 100644 --- a/src/app/util/check-fix-entity-state-consistency.ts +++ b/src/app/util/check-fix-entity-state-consistency.ts @@ -1,4 +1,5 @@ import { devError } from './dev-error'; +import { arrayEquals } from './array-equals'; export const checkFixEntityStateConsistency = (data: any, additionalStr = ''): any => { if (!isEntityStateConsistent(data, additionalStr)) { @@ -23,7 +24,8 @@ export const isEntityStateConsistent = (data: any, additionalStr = ''): boolean if (!data || !data.entities || !data.ids - || Object.keys(data.entities).length !== data.ids.length) { + || Object.keys(data.entities).length !== data.ids.length + || !arrayEquals(Object.keys(data.entities).sort(), [...data.ids].sort())) { console.log(data); devError(`Inconsistent entity state "${additionalStr}"`); return false; diff --git a/src/app/util/real-timer.ts b/src/app/util/real-timer.ts new file mode 100644 index 000000000..0b04ed801 --- /dev/null +++ b/src/app/util/real-timer.ts @@ -0,0 +1,13 @@ +import { lazySetInterval } from '../../../electron/lazy-set-interval'; +import { Observable } from 'rxjs'; + +export const realTimer$ = (intervalDuration: number): Observable => { + return new Observable(subscriber => { + const idleStart = Date.now(); + // subscriber.next(0); + lazySetInterval(() => { + const delta = Date.now() - idleStart; + subscriber.next(delta); + }, intervalDuration); + }); +}; diff --git a/src/assets/i18n/ar.json b/src/assets/i18n/ar.json index 9c4524021..19d393e34 100644 --- a/src/assets/i18n/ar.json +++ b/src/assets/i18n/ar.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "أنت تحاول مزامنة كائن بيانات فارغ. هل هذه أول مزامنة لك مع تطبيق جديد (تقريبًا)؟", + "FORCE_IMPORT": "هل تريد استيراد البيانات عن بُعد على أي حال؟", + "FORCE_UPLOAD": "تحميل البيانات المحلية على أي حال؟", + "FORCE_UPLOAD_AFTER_ERROR": "حدث خطأ أثناء تحميل البيانات المحلية الخاصة بك. حاول فرض التحديث؟", + "TRY_LOAD_REMOTE_AGAIN": "حاول إعادة تحميل البيانات من جهاز التحكم عن بعد مرة أخرى؟" + }, "D_CONFLICT": { "LAST_CHANGE": "اخر تغير:", "LAST_SYNC": "المزامنة الأخيرة:", @@ -607,8 +614,12 @@ "TIME": "زمن" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "إضافة مهمة موجودة \"{{taskTitle}}\"", + "ADD_ISSUE_TASK": "إضافة العدد رقم{{issueNr}} من {{issueType}}", "ADD_TASK": "إضافة مهمة", "ADD_TASK_TO_BACKLOG": "إضافة مهمة إلى تراكم", + "CREATE_TASK": "إنشاء مهمة جديدة", + "EXAMPLE": "مثال: \"بعض عناوين المهمة + اسم المشروع # علامة بعض # علامة أخرى بعض 10 م / 3 س\"", "START": "اضغط على إدخال مرة أخرى للبدء" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "مدة الاستراحات الطويلة", "TITLE": "إعدادات بومودورو" }, + "SOUND": { + "DONE_SOUND": "المهمة تمت الصوت", + "IS_INCREASE_DONE_PITCH": "زيادة الملعب لكل مهمة يتم القيام بها", + "IS_PLAY_DONE_SOUND": "تشغيل الصوت عندما يتم وضع علامة على المهمة \"تم\"", + "TITLE": "صوت", + "VOLUME": "الصوت" + }, "TAKE_A_BREAK": { "HELP": "

يتيح لك تكوين تذكير متكرر عندما تعمل لفترة زمنية محددة دون أخذ استراحة.

يمكنك تعديل الرسالة المعروضة. سيتم استبدال ${duration} بالوقت الذي يستغرقه دون انقطاع.

", "IS_ENABLED": "تمكين التذكير باخذ استراحة", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "نسخ إلى الحافظة", "ERR_COMPRESSION": "خطأ لواجهة الضغط", "PERSISTENCE_DISALLOWED": "لن يتم استمرار البيانات بشكل دائم. أن تدرك أن هذا يمكن أن يؤدي إلى فقدان البيانات!", - "RUNNING_X": "تشغيل \"{{str}}\"." + "RUNNING_X": "تشغيل \"{{str}}\".", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": " تم الضغط على{{keyCombo}} ، ولكن لا يتوفر اختصار الإشارات المرجعية المفتوح إلا في سياق المشروع.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": " تم الضغط على{{keyCombo}} ، ولكن فتح اختصار الملاحظات متاح فقط في سياق المشروع." }, "GPB": { "ASSETS": "جارٍ تحميل الأصول ...", diff --git a/src/assets/i18n/de.json b/src/assets/i18n/de.json index 436b458d6..a6c37c187 100644 --- a/src/assets/i18n/de.json +++ b/src/assets/i18n/de.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "Sie versuchen, ein leeres Datenobjekt zu synchronisieren. Ist dies Ihre allererste Synchronisierung einer (fast) jungfräulichen App?", + "FORCE_IMPORT": "Remote-Daten trotzdem importieren?", + "FORCE_UPLOAD": "Lokale Daten trotzdem hochladen?", + "FORCE_UPLOAD_AFTER_ERROR": "Beim Hochladen Ihrer lokalen Daten ist ein Fehler aufgetreten. Versuchen Sie das Update zu erzwingen?", + "TRY_LOAD_REMOTE_AGAIN": "Versuchen Sie erneut, Daten von der Fernbedienung neu zu laden?" + }, "D_CONFLICT": { "LAST_CHANGE": "Letzte Änderung:", "LAST_SYNC": "letzte Synchronisierung:", @@ -607,8 +614,12 @@ "TIME": "Zeit" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "Vorhandene Aufgabe hinzufügen \"{{taskTitle}}\"", + "ADD_ISSUE_TASK": "Fügen Sie das Problem #{{issueNr}} von {{issueType}}hinzu", "ADD_TASK": "Aufgabe hinzufügen", "ADD_TASK_TO_BACKLOG": "Aufgabe zum Rückstand hinzufügen", + "CREATE_TASK": "Neue Aufgabe erstellen", + "EXAMPLE": "Beispiel: \"Einige Aufgabentitel + Projektname #Einiges Tag #Einiges anderes Tag 10m / 3h\"", "START": "Drücken Sie die Eingabetaste noch einmal, um zu beginnen" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Dauer längerer Pausen", "TITLE": "Pomodoro TImer" }, + "SOUND": { + "DONE_SOUND": "Aufgabe erledigt Ton", + "IS_INCREASE_DONE_PITCH": "Erhöhen Sie die Tonhöhe für jede erledigte Aufgabe", + "IS_PLAY_DONE_SOUND": "Ton abspielen, wenn die Aufgabe als erledigt markiert ist", + "TITLE": "Klingen", + "VOLUME": "Volumen" + }, "TAKE_A_BREAK": { "HELP": "

Ermöglicht die Konfiguration einer wiederkehrenden Erinnerung, wenn Sie eine bestimmte Zeit ohne Pause gearbeitet haben.

Sie können die angezeigte Nachricht ändern. ${duration} wird durch die ohne Unterbrechung verbrachte Zeit ersetzt.

", "IS_ENABLED": "Aktivieren Sie die Erinnerungsfunktion für eine Pause", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "In die Zwischenablage kopiert", "ERR_COMPRESSION": "Fehler bei der Komprimierung", "PERSISTENCE_DISALLOWED": "Daten werden nicht dauerhaft gespeichert. Beachten Sie, dass dies zu Datenverlust führen kann !!", - "RUNNING_X": "Wird gestartet: \"{{str}}\"." + "RUNNING_X": "Wird gestartet: \"{{str}}\".", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} gedrückt, aber die Verknüpfung zum Öffnen von Lesezeichen ist nur im Projektkontext verfügbar.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} gedrückt, aber die Verknüpfung für offene Notizen ist nur im Projektkontext verfügbar." }, "GPB": { "ASSETS": "Laden von Assets ...", diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index e51bc73ba..bff8a6e35 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -16,6 +16,11 @@ "BL": { "NO_TASKS": "There are currently no tasks in your backlog" }, + "CONFIRM": { + "AUTO_FIX": "Your data seems to be damaged. Do you want to try to automatically fix it? This might result in partial data loss.", + "DELETE_STRAY_BACKUP": "Do you want to delete the back to avoid seeing this dialog?", + "RESTORE_STRAY_BACKUP": "During last sync there might have been some error. Do you want to restore the last backup?" + }, "DATETIME_INPUT": { "IN": "in {{time}}", "TOMORROW": "tomorrow {{time}}" @@ -77,6 +82,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "You're trying to sync an empty data object. Is this your very first sync of a (almost) virgin app?", + "FORCE_IMPORT": "Import remote data anyway?", + "FORCE_UPLOAD": "Upload local data anyway?", + "FORCE_UPLOAD_AFTER_ERROR": "An Error occurred while uploading your local data. Try to force the update?", + "TRY_LOAD_REMOTE_AGAIN": "Try to re-load data from remote once again?" + }, "D_CONFLICT": { "LAST_CHANGE": "last change:", "LAST_SYNC": "last sync:", @@ -742,14 +754,25 @@ "ALREADY_DID": "I already did", "SNOOZE": "Snooze {{time}}" }, + "B_TTR": { + "ADD_TO_TASK": "Add to Task", + "MSG": "You have not been tracking time for {{time}}" + }, "D_IDLE": { "BREAK": "Break", - "CREATE_AND_TRACK": "Create and track to:", + "CREATE_AND_TRACK": "Create and track to", "IDLE_FOR": "You have been idle for:", "SKIP": "Skip", "TASK": "Task", "TASK_BREAK": "Task+Break", - "TRACK_TO": "Track to:" + "TRACK_TO": "Track to" + }, + "D_TRACKING_REMINDER": { + "UNTRACKED_TIME": "Untracked time:", + "TRACK_TO": "Track to:", + "CREATE_AND_TRACK": "Create and track to", + "IDLE_FOR": "You have been idle for:", + "TASK": "Task" } }, "WORKLOG": { @@ -951,10 +974,10 @@ "TITLE": "Pomodoro Timer" }, "SOUND": { - "TITLE": "Sound", "DONE_SOUND": "Task done sound", "IS_INCREASE_DONE_PITCH": "Increase pitch for every task done", "IS_PLAY_DONE_SOUND": "Play sound when task is marked done", + "TITLE": "Sound", "VOLUME": "Volume" }, "TAKE_A_BREAK": { @@ -966,6 +989,12 @@ "MIN_WORKING_TIME": "Trigger take a break notification after X working without one", "MOTIVATIONAL_IMG": "Motivational image (web url)", "TITLE": "Break Reminder" + }, + "TRACKING_REMINDER": { + "HELP": "Configure a banner to show up in case you forgot to start time tracking.", + "TITLE": "Time Tracking Reminder", + "L_IS_ENABLED": "Enabled", + "L_MIN_TIME": "Time to wait before showing Banner" } }, "GLOBAL_SNACK": { diff --git a/src/assets/i18n/es.json b/src/assets/i18n/es.json index 1cb203d65..12c3082cb 100644 --- a/src/assets/i18n/es.json +++ b/src/assets/i18n/es.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "Estás intentando sincronizar un objeto de datos vacío. ¿Es esta tu primera sincronización de una aplicación (casi) virgen?", + "FORCE_IMPORT": "¿Importar datos remotos de todos modos?", + "FORCE_UPLOAD": "¿Subir datos locales de todos modos?", + "FORCE_UPLOAD_AFTER_ERROR": "Se produjo un error al cargar sus datos locales. ¿Intentas forzar la actualización?", + "TRY_LOAD_REMOTE_AGAIN": "¿Intentar volver a cargar datos desde el control remoto?" + }, "D_CONFLICT": { "LAST_CHANGE": "ultimo cambio:", "LAST_SYNC": "Última sincronización:", @@ -607,8 +614,12 @@ "TIME": "hora" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "Agregar tarea existente \"{{taskTitle}}\"", + "ADD_ISSUE_TASK": "Agregar problema n. °{{issueNr}} de {{issueType}}", "ADD_TASK": "Agregar tarea", "ADD_TASK_TO_BACKLOG": "Agregar tarea al registro de trabajo", + "CREATE_TASK": "Crear nueva tarea", + "EXAMPLE": "Ejemplo: \"Un título de tarea + nombre de proyecto # alguna etiqueta # alguna otra etiqueta 10m / 3h\"", "START": "Presiona entrar una vez más para comenzar" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Duración de los descansos más largos", "TITLE": "Pomodoro Timer" }, + "SOUND": { + "DONE_SOUND": "Sonido de tarea realizada", + "IS_INCREASE_DONE_PITCH": "Aumente el tono para cada tarea realizada", + "IS_PLAY_DONE_SOUND": "Reproducir sonido cuando la tarea esté marcada como terminada", + "TITLE": "Sonar", + "VOLUME": "Volumen" + }, "TAKE_A_BREAK": { "HELP": "

Le permite configurar un recordatorio recurrente cuando ha trabajado durante un período de tiempo específico sin tomar un descanso.

Puede modificar el mensaje que se muestra. ${duration} será reemplazado con el tiempo dedicado sin descanso.

", "IS_ENABLED": "Habilitar recordatorio para tomar un descanso", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "Copiado al portapapeles", "ERR_COMPRESSION": "Error para la interfaz de compresión", "PERSISTENCE_DISALLOWED": "Los datos no se conservarán de forma permanente. ¡Tenga en cuenta que esto puede conducir a la pérdida de datos!", - "RUNNING_X": "Ejecutando \"{{str}}\"." + "RUNNING_X": "Ejecutando \"{{str}}\".", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} presionado, pero el acceso directo a marcadores abiertos solo está disponible en el contexto del proyecto.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} presionado, pero el acceso directo de notas abiertas solo está disponible en el contexto del proyecto." }, "GPB": { "ASSETS": "Cargando activos ...", diff --git a/src/assets/i18n/fa.json b/src/assets/i18n/fa.json index 543702e63..f338e6fed 100644 --- a/src/assets/i18n/fa.json +++ b/src/assets/i18n/fa.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "شما در حال تلاش برای همگام سازی یک شی empty داده خالی هستید. آیا این اولین همگام سازی شما با یک برنامه (تقریبا) بکر است؟", + "FORCE_IMPORT": "به هر حال داده از راه دور وارد می شود؟", + "FORCE_UPLOAD": "به هر حال داده های محلی بارگذاری می شوند؟", + "FORCE_UPLOAD_AFTER_ERROR": "هنگام بارگذاری داده های محلی شما خطایی روی داد. سعی کنید به زودی بروزرسانی کنید؟", + "TRY_LOAD_REMOTE_AGAIN": "سعی می کنید بار دیگر داده ها را از راه دور بارگیری کنید؟" + }, "D_CONFLICT": { "LAST_CHANGE": "آخرین تغییر:", "LAST_SYNC": "آخرین همگام سازی:", @@ -607,8 +614,12 @@ "TIME": "زمان" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "افزودن کار موجود \"{{taskTitle}}\"", + "ADD_ISSUE_TASK": "شماره شماره{{issueNr}} را از {{issueType}}اضافه کنید", "ADD_TASK": "اضافه کردن کار", "ADD_TASK_TO_BACKLOG": "اضافه کردن کار به بک لاگ", + "CREATE_TASK": "ایجاد کار جدید", + "EXAMPLE": "مثال: \"برخی از عنوان های کار + پروژه نام # برخی از برچسب ها # برخی دیگر از برچسب ها 10m / 3h\"", "START": "جهت شروع یکبار دیگر دکمه اینتر را بزنید" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Duration of longer breaks", "TITLE": "Pomodoro Timer" }, + "SOUND": { + "DONE_SOUND": "وظیفه تمام شد صدا", + "IS_INCREASE_DONE_PITCH": "برای انجام هر کاری سطح صدا را افزایش دهید", + "IS_PLAY_DONE_SOUND": "وقتی کار به پایان رسید ، صدا را پخش کنید", + "TITLE": "صدا", + "VOLUME": "جلد" + }, "TAKE_A_BREAK": { "HELP": "

Allows you to configure a reoccurring reminder when you have worked for a specified amount of time without taking a break.

You can modify the message displayed. ${duration} will be replaced with the time spent without a break.

", "IS_ENABLED": "Enable take a break reminder", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "در کلیپ برد کپی شد", "ERR_COMPRESSION": "خطایی برای رابط فشرده سازی", "PERSISTENCE_DISALLOWED": "Data will be not persisted permanently. Be aware that this can lead to data loss!!", - "RUNNING_X": "Running \"{{str}}\"." + "RUNNING_X": "Running \"{{str}}\".", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} فشرده شده است ، اما میانبر نشانک های باز فقط در متن پروژه در دسترس است.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} میانبر فشرده شده است ، اما باز کردن یادداشت های باز فقط وقتی در متن پروژه باشد ، در دسترس است." }, "GPB": { "ASSETS": "در حال بارگیری دارایی ...", diff --git a/src/assets/i18n/fr.json b/src/assets/i18n/fr.json index b3683e5de..178611a2d 100644 --- a/src/assets/i18n/fr.json +++ b/src/assets/i18n/fr.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "Vous essayez de synchroniser un objet de données vide. Est-ce votre toute première synchronisation d'une application (presque) vierge?", + "FORCE_IMPORT": "Importer quand même des données distantes?", + "FORCE_UPLOAD": "Télécharger quand même des données locales?", + "FORCE_UPLOAD_AFTER_ERROR": "Une erreur s'est produite lors du téléchargement de vos données locales. Essayez de forcer la mise à jour?", + "TRY_LOAD_REMOTE_AGAIN": "Essayez à nouveau de recharger les données depuis la télécommande?" + }, "D_CONFLICT": { "LAST_CHANGE": "dernier changement:", "LAST_SYNC": "Dernière synchronisation:", @@ -607,8 +614,12 @@ "TIME": "Temps" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "Ajouter la tâche existante \"{{taskTitle}}\"", + "ADD_ISSUE_TASK": "Ajouter le problème n °{{issueNr}} de {{issueType}}", "ADD_TASK": "Ajouter une tâche", "ADD_TASK_TO_BACKLOG": "Ajouter une tâche au backlog", + "CREATE_TASK": "Créer une nouvelle tâche", + "EXAMPLE": "Exemple: \"Un titre de tâche + projectName # une balise # une autre balise 10m / 3h\"", "START": "Appuyez sur Entrée une fois de plus pour démarrer" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Durée des pauses plus longues", "TITLE": "Pomodoro Timer" }, + "SOUND": { + "DONE_SOUND": "Tâche terminée son", + "IS_INCREASE_DONE_PITCH": "Augmentez la hauteur pour chaque tâche effectuée", + "IS_PLAY_DONE_SOUND": "Jouer le son lorsque la tâche est marquée comme terminée", + "TITLE": "Du son", + "VOLUME": "Le volume" + }, "TAKE_A_BREAK": { "HELP": "

vous permet de configurer un rappel récurrent lorsque vous avez travaillé pendant une durée déterminée sans prendre de pause.

Vous pouvez modifier le message affiché. ${duration} sera remplacé par le temps passé sans pause.

", "IS_ENABLED": "Activer le rappel de pause", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "Copié dans le presse-papier", "ERR_COMPRESSION": "Erreur pour l'interface de compression", "PERSISTENCE_DISALLOWED": "Les données ne seront pas conservées de manière permanente. Sachez que cela peut entraîner une perte de données!", - "RUNNING_X": "\"{{str}}\" en cours." + "RUNNING_X": "\"{{str}}\" en cours.", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} enfoncé, mais le raccourci vers les favoris ouverts n'est disponible que dans le contexte du projet.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} enfoncé, mais le raccourci Notes ouvertes n'est disponible que dans le contexte du projet." }, "GPB": { "ASSETS": "Chargement des éléments ...", diff --git a/src/assets/i18n/it.json b/src/assets/i18n/it.json index 272c003fa..6484a93c3 100644 --- a/src/assets/i18n/it.json +++ b/src/assets/i18n/it.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "Stai tentando di sincronizzare un oggetto dati vuoto. Questa è la tua prima sincronizzazione di un'app (quasi) vergine?", + "FORCE_IMPORT": "Importare comunque i dati remoti?", + "FORCE_UPLOAD": "Caricare comunque i dati locali?", + "FORCE_UPLOAD_AFTER_ERROR": "Si è verificato un errore durante il caricamento dei dati locali. Prova a forzare l'aggiornamento?", + "TRY_LOAD_REMOTE_AGAIN": "Vuoi provare a ricaricare i dati da remoto ancora una volta?" + }, "D_CONFLICT": { "LAST_CHANGE": "Ultima modifica:", "LAST_SYNC": "ultima sincronizzazione:", @@ -607,8 +614,12 @@ "TIME": "Tempo" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "Aggiungi attività esistente \"{{taskTitle}}\"", + "ADD_ISSUE_TASK": "Aggiungi problema n.{{issueNr}} da {{issueType}}", "ADD_TASK": "Aggiungi task", "ADD_TASK_TO_BACKLOG": "Aggiungi task al backlog", + "CREATE_TASK": "Crea nuova attività", + "EXAMPLE": "Esempio: \"Qualche titolo di attività + nomeProgetto #some tag #some altro tag 10m / 3h\"", "START": "Premi ancora invio per iniziare" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Durata delle pause più lunghe", "TITLE": "Impostazioni del pomodoro" }, + "SOUND": { + "DONE_SOUND": "Compito fatto suono", + "IS_INCREASE_DONE_PITCH": "Aumenta il tono per ogni attività svolta", + "IS_PLAY_DONE_SOUND": "Riproduci un suono quando l'attività è contrassegnata come completata", + "TITLE": "Suono", + "VOLUME": "Volume" + }, "TAKE_A_BREAK": { "HELP": "

Ti permette di configurare un promemoria ricorrente quando hai lavorato per uno specifica quantità di tempo senza prenderti una pausa.

Puoi modificare il messaggio mostrato. ${duration} sarà rimpiazzata con il tempo speso senza una pausa.

", "IS_ENABLED": "Abilita il promemoria di fare una pausa", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "Copiato negli appunti", "ERR_COMPRESSION": "Errore per l'interfaccia di compressione", "PERSISTENCE_DISALLOWED": "I dati non saranno salvati permanentemente. Fai attenzione che questo può portare a perdita dei dati!!", - "RUNNING_X": "Eseguendo \"{{str}}\"." + "RUNNING_X": "Eseguendo \"{{str}}\".", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} premuto, ma la scorciatoia Apri segnalibri è disponibile solo nel contesto del progetto.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} premuto, ma la scorciatoia per le note aperte è disponibile solo nel contesto del progetto." }, "GPB": { "ASSETS": "Caricamento risorse in corso ...", diff --git a/src/assets/i18n/ja.json b/src/assets/i18n/ja.json index d01b654e9..89b6e8141 100644 --- a/src/assets/i18n/ja.json +++ b/src/assets/i18n/ja.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "空のデータオブジェクトを同期しようとしています。これは(ほぼ)未使用のアプリの最初の同期ですか?", + "FORCE_IMPORT": "とにかくリモートデータをインポートしますか?", + "FORCE_UPLOAD": "とにかくローカルデータをアップロードしますか?", + "FORCE_UPLOAD_AFTER_ERROR": "ローカルデータのアップロード中にエラーが発生しました。強制的に更新しますか?", + "TRY_LOAD_REMOTE_AGAIN": "リモートからもう一度データを再ロードしてみますか?" + }, "D_CONFLICT": { "LAST_CHANGE": "最終変更:", "LAST_SYNC": "最終同期:", @@ -607,8 +614,12 @@ "TIME": "時間" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "既存のタスク「{{taskTitle}}」を追加", + "ADD_ISSUE_TASK": " {{issueType}}から問題番号{{issueNr}} を追加", "ADD_TASK": "タスクを追加", "ADD_TASK_TO_BACKLOG": "バックログにタスクを追加", + "CREATE_TASK": "新しいタスクを作成する", + "EXAMPLE": "例:「Some task title + projectName #some tag #some other tag 10m / 3h」", "START": "もう一度Enterキーを押して開始します" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "長い休憩時間", "TITLE": "ポモドーロタイマー" }, + "SOUND": { + "DONE_SOUND": "タスク完了音", + "IS_INCREASE_DONE_PITCH": "実行するすべてのタスクのピッチを上げる", + "IS_PLAY_DONE_SOUND": "タスクが完了とマークされたときに音を鳴らす", + "TITLE": "音", + "VOLUME": "ボリューム" + }, "TAKE_A_BREAK": { "HELP": "

休憩せずに指定した時間作業したときに繰り返し発生するアラームを設定できます。

表示されたメッセージを変更できます。 ${duration} は中断せずに費やした時間に置き換えられます。

", "IS_ENABLED": "休憩のお知らせを有効にする", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "クリップボードにコピー", "ERR_COMPRESSION": "圧縮インターフェースのエラー", "PERSISTENCE_DISALLOWED": "データは永続的に保持されません。これはデータの損失につながる可能性があることに注意してください!!", - "RUNNING_X": "\"{{str}}\"を実行しています。" + "RUNNING_X": "\"{{str}}\"を実行しています。", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} が押されましたが、ブックマークを開くショートカットはプロジェクトコンテキストでのみ使用できます。", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} が押されましたが、メモを開くショートカットはプロジェクトコンテキストでのみ使用できます。" }, "GPB": { "ASSETS": "アセットを読み込んでいます...", diff --git a/src/assets/i18n/ko.json b/src/assets/i18n/ko.json index b26a9703b..5101d147f 100644 --- a/src/assets/i18n/ko.json +++ b/src/assets/i18n/ko.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "빈 데이터 개체를 동기화하려고합니다. 이것이 (거의) 처녀 앱의 첫 번째 동기화입니까?", + "FORCE_IMPORT": "그래도 원격 데이터를 가져 오시겠습니까?", + "FORCE_UPLOAD": "그래도 로컬 데이터를 업로드 하시겠습니까?", + "FORCE_UPLOAD_AFTER_ERROR": "로컬 데이터를 업로드하는 동안 오류가 발생했습니다. 강제로 업데이트 하시겠습니까?", + "TRY_LOAD_REMOTE_AGAIN": "원격에서 데이터를 다시로드 하시겠습니까?" + }, "D_CONFLICT": { "LAST_CHANGE": "마지막 변경:", "LAST_SYNC": "마지막 동기화 :", @@ -607,8 +614,12 @@ "TIME": "시각" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "기존 작업 \"{{taskTitle}}\"추가", + "ADD_ISSUE_TASK": " {{issueType}}의{{issueNr}} 호 추가", "ADD_TASK": "작업 추가", "ADD_TASK_TO_BACKLOG": "백 로그에 작업 추가", + "CREATE_TASK": "새 할 일 만들기", + "EXAMPLE": "예 : \"일부 작업 제목 + projectName #some tag #some other tag 10m / 3h\"", "START": "시작하려면 한 번 더 입력하십시오." }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "긴 휴식 시간", "TITLE": "포모 도로 타이머" }, + "SOUND": { + "DONE_SOUND": "작업 완료 소리", + "IS_INCREASE_DONE_PITCH": "모든 작업에 대한 피치 높이기", + "IS_PLAY_DONE_SOUND": "작업이 완료로 표시되면 소리 재생", + "TITLE": "소리", + "VOLUME": "음량" + }, "TAKE_A_BREAK": { "HELP": "

중단하지 않고 지정된 시간 동안 작업 한 경우 다시 알림을 구성 할 수 있습니다.

표시된 메시지를 수정할 수 있습니다. ${duration} 은 휴식 시간없이 바뀝니다.

", "IS_ENABLED": "휴식 알림을 사용하도록 설정", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "클립 보드에 복사 됨", "ERR_COMPRESSION": "압축 인터페이스 오류", "PERSISTENCE_DISALLOWED": "데이터는 영구적으로 유지되지 않습니다. 이로 인해 데이터가 손실 될 수 있습니다 !!", - "RUNNING_X": "\"{{str}}\"실행 중." + "RUNNING_X": "\"{{str}}\"실행 중.", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} 을 눌렀지만 북마크 열기 바로 가기는 프로젝트 컨텍스트에서만 사용할 수 있습니다.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} 을 눌렀지만 노트 열기 단축키는 프로젝트 컨텍스트에서만 사용할 수 있습니다." }, "GPB": { "ASSETS": "자산로드 중 ...", diff --git a/src/assets/i18n/nl.json b/src/assets/i18n/nl.json index 4f984c928..c6f563dd1 100644 --- a/src/assets/i18n/nl.json +++ b/src/assets/i18n/nl.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "U probeert een leeg gegevensobject te synchroniseren. Is dit uw allereerste synchronisatie van een (bijna) nieuwe app?", + "FORCE_IMPORT": "Toch externe gegevens importeren?", + "FORCE_UPLOAD": "Lokale gegevens toch uploaden?", + "FORCE_UPLOAD_AFTER_ERROR": "Er is een fout opgetreden tijdens het uploaden van uw lokale gegevens. Probeer de update te forceren?", + "TRY_LOAD_REMOTE_AGAIN": "Wilt u nogmaals gegevens van de afstandsbediening opnieuw laden?" + }, "D_CONFLICT": { "LAST_CHANGE": "laatste wijziging:", "LAST_SYNC": "laatste gesynchroniseerd:", @@ -607,8 +614,12 @@ "TIME": "Tijd" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "Voeg bestaande taak \"{{taskTitle}}\" toe", + "ADD_ISSUE_TASK": "Voeg probleem #{{issueNr}} van {{issueType}}toe", "ADD_TASK": "Voeg taak toe", "ADD_TASK_TO_BACKLOG": "Taak toevoegen aan achterstand", + "CREATE_TASK": "Maak een nieuwe taak", + "EXAMPLE": "Voorbeeld: \"Een taaktitel + projectnaam # een tag # een andere tag 10 m / 3 uur\"", "START": "Druk nogmaals op Enter om te beginnen" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Duur van langere pauzes", "TITLE": "Pomodoro Timer" }, + "SOUND": { + "DONE_SOUND": "Taak gedaan geluid", + "IS_INCREASE_DONE_PITCH": "Verhoog de toon voor elke uitgevoerde taak", + "IS_PLAY_DONE_SOUND": "Speel geluid af wanneer de taak is gemarkeerd als voltooid", + "TITLE": "Geluid", + "VOLUME": "Volume" + }, "TAKE_A_BREAK": { "HELP": "

Hiermee kunt u een terugkerende herinnering configureren wanneer u een bepaalde tijd hebt gewerkt zonder een pauze te nemen.

U kunt het weergegeven bericht wijzigen. $ {duration} wordt vervangen door de tijd die u zonder pauze doorbrengt.

", "IS_ENABLED": "Schakel een pauzeherinnering in", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "Gekopieerd naar het klembord", "ERR_COMPRESSION": "Fout voor compressie-interface", "PERSISTENCE_DISALLOWED": "Gegevens worden niet permanent bewaard. Houd er rekening mee dat dit kan leiden tot gegevensverlies !!", - "RUNNING_X": "\"{{Str}}\" wordt uitgevoerd." + "RUNNING_X": "\"{{Str}}\" wordt uitgevoerd.", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} ingedrukt, maar de snelkoppeling naar bladwijzers openen is alleen beschikbaar in projectcontext.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} ingedrukt, maar de snelkoppeling voor open notities is alleen beschikbaar in projectcontext." }, "GPB": { "ASSETS": "Activa laden ...", diff --git a/src/assets/i18n/pt.json b/src/assets/i18n/pt.json index 6c34fcb11..c1ef0dea7 100644 --- a/src/assets/i18n/pt.json +++ b/src/assets/i18n/pt.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "Você está tentando sincronizar um objeto de dados vazio. Esta é a sua primeira sincronização de um aplicativo (quase) virgem?", + "FORCE_IMPORT": "Importar dados remotos mesmo assim?", + "FORCE_UPLOAD": "Enviar dados locais mesmo assim?", + "FORCE_UPLOAD_AFTER_ERROR": "Ocorreu um erro ao enviar seus dados locais. Tentar forçar a atualização?", + "TRY_LOAD_REMOTE_AGAIN": "Tentar recarregar os dados remotamente mais uma vez?" + }, "D_CONFLICT": { "LAST_CHANGE": "última mudança:", "LAST_SYNC": "última sincronização:", @@ -607,8 +614,12 @@ "TIME": "tempo" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "Adicionar tarefa existente \"{{taskTitle}}\"", + "ADD_ISSUE_TASK": "Adicione a edição nº{{issueNr}} de {{issueType}}", "ADD_TASK": "Adicionar tarefa", "ADD_TASK_TO_BACKLOG": "Adicionar tarefa ao backlog", + "CREATE_TASK": "Criar nova tarefa", + "EXAMPLE": "Exemplo: \"Algum título da tarefa + nome do projeto # alguma tag # alguma outra tag 10m / 3h\"", "START": "Pressione enter mais uma vez para iniciar" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Duração de pausas mais longas", "TITLE": "Configurações do Pomodoro" }, + "SOUND": { + "DONE_SOUND": "Som de tarefa concluída", + "IS_INCREASE_DONE_PITCH": "Aumente o tom para cada tarefa realizada", + "IS_PLAY_DONE_SOUND": "Tocar som quando a tarefa for marcada como concluída", + "TITLE": "Som", + "VOLUME": "Volume" + }, "TAKE_A_BREAK": { "HELP": "

Permite configurar um lembrete recorrente quando você trabalha por um período de tempo especificado sem fazer uma pausa.

Você pode modificar a mensagem exibida. ${duration} será substituído pelo tempo gasto sem interrupção.

", "IS_ENABLED": "Ativar lembrete de pausa", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "Copiado para a área de transferência", "ERR_COMPRESSION": "Erro para interface de compactação", "PERSISTENCE_DISALLOWED": "Os dados não serão mantidos permanentemente. Esteja ciente de que isso pode levar à perda de dados!", - "RUNNING_X": "Rodando \"{{str}}\"." + "RUNNING_X": "Rodando \"{{str}}\".", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} pressionado, mas o atalho para abrir favoritos só está disponível no contexto do projeto.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} pressionado, mas o atalho para abrir notas só está disponível no contexto do projeto." }, "GPB": { "ASSETS": "Carregando recursos...", diff --git a/src/assets/i18n/ru.json b/src/assets/i18n/ru.json index abcd2c8b8..173e2678e 100644 --- a/src/assets/i18n/ru.json +++ b/src/assets/i18n/ru.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "Вы пытаетесь синхронизировать пустой объект данных. Это ваша самая первая синхронизация (почти) девственного приложения?", + "FORCE_IMPORT": "Все равно импортировать удаленные данные?", + "FORCE_UPLOAD": "Все равно загрузить локальные данные?", + "FORCE_UPLOAD_AFTER_ERROR": "Произошла ошибка при загрузке ваших локальных данных. Пробовать принудительно обновить?", + "TRY_LOAD_REMOTE_AGAIN": "Попробовать еще раз перезагрузить данные с пульта?" + }, "D_CONFLICT": { "LAST_CHANGE": "Последнее изменение:", "LAST_SYNC": "последняя синхронизация:", @@ -607,8 +614,12 @@ "TIME": "время" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "Добавить существующую задачу \"{{taskTitle}}\"", + "ADD_ISSUE_TASK": "Добавить выпуск №{{issueNr}} из {{issueType}}", "ADD_TASK": "Добавить задачу", "ADD_TASK_TO_BACKLOG": "Добавить задачу в отставание", + "CREATE_TASK": "Создать новую задачу", + "EXAMPLE": "Пример: \"Название задачи + название проекта # какой-то тег # какой-то другой тег 10 м / 3 ч\"", "START": "Нажмите ввод еще раз, чтобы начать" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Продолжительность более длительных перерывов", "TITLE": "Помодоро Таймер" }, + "SOUND": { + "DONE_SOUND": "Звук выполненной задачи", + "IS_INCREASE_DONE_PITCH": "Увеличивайте высоту звука для каждой выполненной задачи", + "IS_PLAY_DONE_SOUND": "Воспроизвести звук, когда задача отмечена как выполненная", + "TITLE": "звук", + "VOLUME": "объем" + }, "TAKE_A_BREAK": { "HELP": "

Позволяет настроить повторяющееся напоминание, если вы работали в течение определенного времени без перерыва.

Вы можете изменить отображаемое сообщение. ${duration} будет заменено временем, проведенным без перерыва.

", "IS_ENABLED": "Включить напоминание о перерыве", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "Скопировано в буфер обмена", "ERR_COMPRESSION": "Ошибка для интерфейса сжатия", "PERSISTENCE_DISALLOWED": "Данные не будут сохранены постоянно. Имейте в виду, что это может привести к потере данных!", - "RUNNING_X": "Запуск \"{{str}}\"." + "RUNNING_X": "Запуск \"{{str}}\".", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} нажата, но ярлык для открытия закладок доступен только в контексте проекта.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} нажата, но ярлык для открытия заметок доступен только в контексте проекта." }, "GPB": { "ASSETS": "Загрузка активов ...", diff --git a/src/assets/i18n/tr.json b/src/assets/i18n/tr.json index 4941fd398..6308a2272 100644 --- a/src/assets/i18n/tr.json +++ b/src/assets/i18n/tr.json @@ -77,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "Boş bir veri nesnesini senkronize etmeye çalışıyorsunuz. Bu, (neredeyse) bakir bir uygulamanın ilk senkronizasyonunuz mu?", + "FORCE_IMPORT": "Yine de uzak veriler içe aktarılsın mı?", + "FORCE_UPLOAD": "Yine de yerel veriler yüklensin mi?", + "FORCE_UPLOAD_AFTER_ERROR": "Yerel verilerinizi yüklerken bir hata oluştu. Güncellemeyi zorlamaya mı çalışıyorsunuz?", + "TRY_LOAD_REMOTE_AGAIN": "Verileri bir kez daha uzaktan kumandadan yeniden yüklemeyi mi deneyin?" + }, "D_CONFLICT": { "LAST_CHANGE": "son değişiklik:", "LAST_SYNC": "son senkronizasyon:", @@ -607,8 +614,12 @@ "TIME": "zaman" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "Mevcut \"{{taskTitle}}\" görevini ekle", + "ADD_ISSUE_TASK": " {{issueType}}kaynaklı{{issueNr}} numaralı sorunu ekleyin", "ADD_TASK": "Görev ekle", "ADD_TASK_TO_BACKLOG": "İş gününe görev ekle", + "CREATE_TASK": "Yeni görev oluştur", + "EXAMPLE": "Örnek: \"Bazı görev başlığı + projeAdı # bazı etiket # bazı diğer etiket 10a / 3sa\"", "START": "Başlamak için bir kez daha enter tuşuna basın" }, "B": { @@ -946,6 +957,13 @@ "LONGER_BREAK_DURATION": "Uzun molaların süresi", "TITLE": "Pomodoro Zamanlayıcı" }, + "SOUND": { + "DONE_SOUND": "Görev tamamlandı sesi", + "IS_INCREASE_DONE_PITCH": "Yapılan her görev için perdeyi artırın", + "IS_PLAY_DONE_SOUND": "Görev tamamlandı olarak işaretlendiğinde ses çal", + "TITLE": "Ses", + "VOLUME": "hacim" + }, "TAKE_A_BREAK": { "HELP": "

Mola vermeden belirli bir süre çalıştığınızda tekrar eden bir hatırlatıcı yapılandırmanıza izin verir.

Görüntülenen mesajı değiştirebilirsiniz. ${duration} , ara vermeden harcadığınız zamanla değiştirilecektir.

", "IS_ENABLED": "Ara ver hatırlatma yapmayı etkinleştir", @@ -961,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "Panoya kopyalandı", "ERR_COMPRESSION": "Sıkıştırma arayüzü için hata", "PERSISTENCE_DISALLOWED": "Veri kalıcı olarak sürdürülmeyecek. Bunun veri kaybına neden olabileceğini unutmayın!", - "RUNNING_X": "\"{{str}}\" çalıştırılıyor." + "RUNNING_X": "\"{{str}}\" çalıştırılıyor.", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": "{{keyCombo}} basıldı, ancak yer imlerini aç kısayolu yalnızca proje bağlamındayken kullanılabilir.", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": "{{keyCombo}} basılı, ancak açık notlar kısayolu yalnızca proje bağlamındayken kullanılabilir." }, "GPB": { "ASSETS": "Varlıklar yükleniyor ...", diff --git a/src/assets/i18n/zh.json b/src/assets/i18n/zh.json index 1b8e50c7d..0c92ed549 100644 --- a/src/assets/i18n/zh.json +++ b/src/assets/i18n/zh.json @@ -14,10 +14,7 @@ "UPDATE_WEB_APP": "新版本可用,是否加载新版本?" }, "BL": { - "FROM_OTHER_PROJECTS": "来自其他项目", - "NO_TASKS": "目前您的待办事项中没有任务", - "SCHEDULED": "已计划", - "TITLE": "待办事项" + "NO_TASKS": "目前您的待办事项中没有任务" }, "DATETIME_INPUT": { "IN": "在 {{time}}", @@ -80,6 +77,13 @@ } }, "DROPBOX": { + "C": { + "EMPTY_SYNC": "您正在尝试同步一个空的数据对象。这是您(几乎)原始应用程序的第一次同步吗?", + "FORCE_IMPORT": "反正导入远程数据?", + "FORCE_UPLOAD": "仍要上传本地数据吗?", + "FORCE_UPLOAD_AFTER_ERROR": "上载本地数据时发生错误。尝试强制更新?", + "TRY_LOAD_REMOTE_AGAIN": "尝试再次从远程重新加载数据?" + }, "D_CONFLICT": { "LAST_CHANGE": "最后一次变更:", "LAST_SYNC": "上次同步:", @@ -610,8 +614,12 @@ "TIME": "时间" }, "ADD_TASK_BAR": { + "ADD_EXISTING_TASK": "添加现有任务“{{taskTitle}}”", + "ADD_ISSUE_TASK": "从 {{issueType}}添加问题#{{issueNr}} ", "ADD_TASK": "添加任务", "ADD_TASK_TO_BACKLOG": "将任务添加到待办事项中", + "CREATE_TASK": "创建新任务", + "EXAMPLE": "示例:“某些任务标题+ projectName#一些标签#一些其他标签10m / 3h”", "START": "再次按回车键开始" }, "B": { @@ -949,6 +957,13 @@ "LONGER_BREAK_DURATION": "长休息时长", "TITLE": "番茄钟" }, + "SOUND": { + "DONE_SOUND": "任务完成声音", + "IS_INCREASE_DONE_PITCH": "为完成的每项任务增加音调", + "IS_PLAY_DONE_SOUND": "任务标记为完成时播放声音", + "TITLE": "声音", + "VOLUME": "体积" + }, "TAKE_A_BREAK": { "HELP": "

允许您在工作了指定的时间而不休息时配置重复提醒。

您可以修改显示的消息。 ${duration}将被替换为连续工作时长。

", "IS_ENABLED": "启用休息提醒", @@ -964,7 +979,9 @@ "COPY_TO_CLIPPBOARD": "复制到剪贴板", "ERR_COMPRESSION": "压缩界面出错", "PERSISTENCE_DISALLOWED": "数据将不会永久保存。请注意,这可能会导致数据丢失!!", - "RUNNING_X": "运行“{{str}}”。" + "RUNNING_X": "运行“{{str}}”。", + "SHORTCUT_WARN_OPEN_BOOKMARKS_FROM_TAG": " 按下了{{keyCombo}} ,但是打开书签快捷方式仅在项目上下文中可用。", + "SHORTCUT_WARN_OPEN_NOTES_FROM_TAG": " 按下了{{keyCombo}} ,但是仅在项目上下文中,打开注释快捷方式才可用。" }, "GPB": { "ASSETS": "正在加载数据...", diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 8c0bca92c..c1376e3fa 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -2,5 +2,6 @@ import { version } from '../../package.json'; export const environment = { production: true, + stage: false, version, }; diff --git a/src/environments/environment.stage.ts b/src/environments/environment.stage.ts new file mode 100644 index 000000000..9d0818615 --- /dev/null +++ b/src/environments/environment.stage.ts @@ -0,0 +1,7 @@ +import { version } from '../../package.json'; + +export const environment = { + production: false, + stage: true, + version, +}; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index dcc040558..0261c87a9 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -5,6 +5,7 @@ import { version } from '../../package.json'; export const environment = { production: false, + stage: false, version, }; diff --git a/src/main.ts b/src/main.ts index 81c932c0e..1d9684e80 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,13 +7,13 @@ import { IS_ELECTRON } from './app/app.constants'; import { IS_ANDROID_WEB_VIEW } from './app/util/is-android-web-view'; import { androidInterface } from './app/core/android/android-interface'; -if (environment.production) { +if (environment.production || environment.stage) { enableProdMode(); } platformBrowserDynamic().bootstrapModule(AppModule).then(() => { // TODO make asset caching work for electron - if ('serviceWorker' in navigator && environment.production && !IS_ELECTRON) { + if ('serviceWorker' in navigator && (environment.production || environment.stage) && !IS_ELECTRON) { console.log('Registering Service worker'); return navigator.serviceWorker.register('ngsw-worker.js'); } @@ -27,7 +27,7 @@ platformBrowserDynamic().bootstrapModule(AppModule).then(() => { window.addEventListener('touchmove', () => { }); -if (!environment.production && IS_ANDROID_WEB_VIEW) { +if (!(environment.production || environment.stage) && IS_ANDROID_WEB_VIEW) { setTimeout(() => { androidInterface.showToast('Android DEV works'); console.log(androidInterface); diff --git a/yarn.lock b/yarn.lock index 9173a6787..4732c3ee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4552,10 +4552,10 @@ electron-window-state@^5.0.3: jsonfile "^4.0.0" mkdirp "^0.5.1" -electron@^10.1.0: - version "10.1.0" - resolved "https://registry.yarnpkg.com/electron/-/electron-10.1.0.tgz#3559182545dc76e20d9764d183d555415d55c250" - integrity sha512-DyS6WhQ59+ZXQsI1EkpsYkOXFt0Xbp+mbxPTJS9A7O21r3JDzaTC+1Jxz7g6J+Sbi9Y7UFdRs0tn/vqhHJx2gA== +electron@^10.1.2: + version "10.1.2" + resolved "https://registry.yarnpkg.com/electron/-/electron-10.1.2.tgz#30b6fd7669f8daf08c56219a61dfa053fa2b0c70" + integrity sha512-SvN8DcKCmPZ0UcQSNAJBfaUu+LGACqtRhUn1rW0UBLHgdbbDM76L0GU5/XGQEllH5pu5bwlCZwax3srzIl+Aeg== dependencies: "@electron/get" "^1.0.1" "@types/node" "^12.0.12" @@ -6256,7 +6256,7 @@ icss-utils@^4.0.0, icss-utils@^4.1.1: dependencies: postcss "^7.0.14" -idb@^5.0.3: +idb@^5.0.6: version "5.0.6" resolved "https://registry.yarnpkg.com/idb/-/idb-5.0.6.tgz#8c94624f5a8a026abe3bef3c7166a5febd1cadc1" integrity sha512-/PFvOWPzRcEPmlDt5jEvzVZVs0wyd/EvGvkDIcbBpGuMMLQKrTPG0TxvE2UJtgZtCQCmOtM2QD7yQJBVEjKGOw== @@ -6907,7 +6907,7 @@ jake@^10.6.1: filelist "^1.0.1" minimatch "^3.0.4" -jasmine-core@^3.6.0, jasmine-core@~3.6.0: +jasmine-core@^3.4.0, jasmine-core@^3.6.0, jasmine-core@~3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-3.6.0.tgz#491f3bb23941799c353ceb7a45b38a950ebc5a20" integrity sha512-8uQYa7zJN8hq9z+g8z1bqCfdC8eoDAeVnM5sfqs7KHv9/ifoJ500m018fpFc7RDaO6SWCLCXwo/wPSNcdYTgcw==