mirror of
https://github.com/johannesjo/super-productivity.git
synced 2026-01-23 02:36:05 +00:00
Merge branch 'master' into patch-1
This commit is contained in:
commit
6b23abb86a
66 changed files with 3628 additions and 286 deletions
2
.github/workflows/build-android.yml
vendored
2
.github/workflows/build-android.yml
vendored
|
|
@ -16,7 +16,7 @@ jobs:
|
|||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Setup Java
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ jobs:
|
|||
if: '!github.event.release.prerelease'
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
# required because setting via env.TZ does not work on windows
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
if: '!github.event.release.prerelease'
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Check out Git repository
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Check out Git repository
|
||||
|
|
@ -28,7 +28,7 @@ jobs:
|
|||
ssh://git@github.com/
|
||||
|
||||
- name: Install Node.js, NPM and Yarn
|
||||
uses: actions/setup-node@v3
|
||||
uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
|
||||
|
|
|
|||
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
|
|
@ -14,7 +14,7 @@ jobs:
|
|||
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Check out Git repository
|
||||
|
|
@ -103,7 +103,7 @@ jobs:
|
|||
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Echo is Release
|
||||
|
|
@ -195,7 +195,7 @@ jobs:
|
|||
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
# required because setting via env.TZ does not work on windows
|
||||
|
|
|
|||
2
.github/workflows/lighthouse-ci.yml
vendored
2
.github/workflows/lighthouse-ci.yml
vendored
|
|
@ -18,7 +18,7 @@ jobs:
|
|||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
|
|
|
|||
2
.github/workflows/lint-and-test-pr.yml
vendored
2
.github/workflows/lint-and-test-pr.yml
vendored
|
|
@ -12,7 +12,7 @@ jobs:
|
|||
UNSPLASH_KEY: ${{ secrets.UNSPLASH_KEY }}
|
||||
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Check out Git repository
|
||||
|
|
|
|||
4
.github/workflows/manual-build.yml
vendored
4
.github/workflows/manual-build.yml
vendored
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
# required because setting via env.TZ does not work on windows
|
||||
|
|
@ -70,7 +70,7 @@ jobs:
|
|||
UNSPLASH_CLIENT_ID: ${{ secrets.UNSPLASH_CLIENT_ID }}
|
||||
|
||||
steps:
|
||||
- uses: actions/setup-node@v3
|
||||
- uses: actions/setup-node@v5
|
||||
with:
|
||||
node-version: 20
|
||||
- name: Echo is Release
|
||||
|
|
|
|||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
|
|
@ -7,7 +7,7 @@ jobs:
|
|||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v9
|
||||
- uses: actions/stale@v10
|
||||
with:
|
||||
days-before-stale: 180
|
||||
days-before-close: 14
|
||||
|
|
|
|||
48
CHANGELOG.md
48
CHANGELOG.md
|
|
@ -1,3 +1,51 @@
|
|||
## [15.1.3-rc.0](https://github.com/johannesjo/super-productivity/compare/v15.1.2-rc.0...v15.1.3-rc.0) (2025-10-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **webdav:** add fallback for missing Last-Modified and ETag in download method ([58a2e3a](https://github.com/johannesjo/super-productivity/commit/58a2e3a73896bf14b96a63290a493b4b47b23ce0))
|
||||
|
||||
## [15.1.2-rc.0](https://github.com/johannesjo/super-productivity/compare/v15.1.1...v15.1.2-rc.0) (2025-10-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- typing issues ([746745c](https://github.com/johannesjo/super-productivity/commit/746745cc705eaa87d2387d19dd2843e47200b6e8))
|
||||
|
||||
### Features
|
||||
|
||||
- **int:** translate month calendar headers ([2f5c398](https://github.com/johannesjo/super-productivity/commit/2f5c39853a005db095b77ff4a45495a292a2a2bc))
|
||||
- remove faulty wayland fallbacks [#5235](https://github.com/johannesjo/super-productivity/issues/5235) ([4c563b1](https://github.com/johannesjo/super-productivity/commit/4c563b117e28767e46f878f7a3f3c3c8a714f00a))
|
||||
|
||||
## [15.1.1](https://github.com/johannesjo/super-productivity/compare/v15.1.0...v15.1.1) (2025-10-08)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- full screen notes editor for mac ([80aec12](https://github.com/johannesjo/super-productivity/commit/80aec12c7008651bfb16e74b72f5596c0077c321)), closes [#4190](https://github.com/johannesjo/super-productivity/issues/4190) [#5230](https://github.com/johannesjo/super-productivity/issues/5230)
|
||||
- mac overlapping with window controls ([738375a](https://github.com/johannesjo/super-productivity/commit/738375ade0e92b4e4e2978a1d330b1b25ce38a85)), closes [#5224](https://github.com/johannesjo/super-productivity/issues/5224)
|
||||
- **task:** improve project name parsing from task input ([dc5960e](https://github.com/johannesjo/super-productivity/commit/dc5960eb986035a3181c62272db31670b82cc080))
|
||||
- typing issues ([07f32bf](https://github.com/johannesjo/super-productivity/commit/07f32bf51f97780d34f98e669df35a7a54197815))
|
||||
|
||||
### Features
|
||||
|
||||
- **projectFolders:** add touch delay for drag and drop ([25cecb9](https://github.com/johannesjo/super-productivity/commit/25cecb9629a4e79978340bff8e3e5f88a02ad58f))
|
||||
|
||||
# [15.1.0](https://github.com/johannesjo/super-productivity/compare/v15.1.0-rc.0...v15.1.0) (2025-10-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **boards:** not correctly navigating to the newly created board [#5211](https://github.com/johannesjo/super-productivity/issues/5211) ([6dc1922](https://github.com/johannesjo/super-productivity/commit/6dc1922fb9ee3c941985049e443a4a9749d93c80))
|
||||
- day of week header missing [#5168](https://github.com/johannesjo/super-productivity/issues/5168) ([699e244](https://github.com/johannesjo/super-productivity/commit/699e24402638547255a7fa9438c410da60958618))
|
||||
- **focusMode:** issue link styling ([eeb8faf](https://github.com/johannesjo/super-productivity/commit/eeb8faf365e74d9d6afe9cc346038820220fe11f))
|
||||
- snacks showing forever ([239dec5](https://github.com/johannesjo/super-productivity/commit/239dec5b194fddf8d488bb8b75275d9f7731def2))
|
||||
|
||||
### Features
|
||||
|
||||
- **projectFolders:** add data validation and repair ([beb29db](https://github.com/johannesjo/super-productivity/commit/beb29db05f23061d621013fbe7fca3c2ffa3fa15))
|
||||
- **projectFolders:** make it also work for tags ([5c0c944](https://github.com/johannesjo/super-productivity/commit/5c0c944f17eddce908f767bf10631636bca8c76f))
|
||||
- **projectFolders:** prepare to make it work for tags too ([3e98ca9](https://github.com/johannesjo/super-productivity/commit/3e98ca93b336c4a86a796bf7e478eb974cf814a5))
|
||||
- **projectFolders:** reduce size ([aa17eac](https://github.com/johannesjo/super-productivity/commit/aa17eac846a5ba75f886db999ef0cf2a680b3e19))
|
||||
- **schedule:** improve styling ([fa1482f](https://github.com/johannesjo/super-productivity/commit/fa1482ffe408f0f29fcae19b6a6b90b863a7e105))
|
||||
- show done tasks section only if there are done tasks ([61d03a8](https://github.com/johannesjo/super-productivity/commit/61d03a8ebf0196483ea3dafd2dcb08d3230244d9))
|
||||
|
||||
# [15.1.0-rc.0](https://github.com/johannesjo/super-productivity/compare/v15.0.3...v15.1.0-rc.0) (2025-09-20)
|
||||
|
||||
### Bug Fixes
|
||||
|
|
|
|||
|
|
@ -20,8 +20,8 @@ android {
|
|||
minSdkVersion 24
|
||||
targetSdkVersion 35
|
||||
compileSdk 35
|
||||
versionCode 15_00_03_0000
|
||||
versionName "15.0.3"
|
||||
versionCode 15_01_01_0000
|
||||
versionName "15.1.1"
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
manifestPlaceholders = [
|
||||
hostName : "app.super-productivity.com",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* **boards:** not correctly navigating to the newly created board #5211
|
||||
* day of week header missing #5168
|
||||
* **focusMode:** issue link styling
|
||||
* snacks showing forever
|
||||
### Features
|
||||
|
||||
* **projectFolders:** add data validation and repair
|
||||
* **projectFolders:** make it also work for tags
|
||||
* **projectFolders:** prepare to make it work for tags too
|
||||
* **projectFolders:** reduce size
|
||||
* **schedule:** improve styling
|
||||
* show done tasks section only if there are done tasks
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
### Bug Fixes
|
||||
|
||||
* full screen notes editor for mac (80aec12), closes #4190 #5230
|
||||
* mac overlapping with window controls (738375a), closes #5224
|
||||
* **task:** improve project name parsing from task input
|
||||
* typing issues
|
||||
### Features
|
||||
|
||||
* **projectFolders:** add touch delay for drag and drop
|
||||
|
|
@ -60,3 +60,4 @@
|
|||
* https://www.producthunt.com/products/super-productivity
|
||||
* https://play.google.com/store/apps/details?id=com.superproductivity.superproductivity
|
||||
* https://www.pling.com/p/1352584/
|
||||
* https://alternativeto.net/software/super-productivity/about/
|
||||
|
|
|
|||
|
|
@ -39,7 +39,6 @@ let customUrl: string;
|
|||
let isDisableTray = false;
|
||||
let forceDarkTray = false;
|
||||
let wasUserDataDirSet = false;
|
||||
let forceX11 = false;
|
||||
|
||||
if (IS_DEV) {
|
||||
log('Starting in DEV Mode!!!');
|
||||
|
|
@ -78,30 +77,6 @@ export const startApp = (): void => {
|
|||
// https://github.com/electron/electron/issues/46538#issuecomment-2808806722
|
||||
app.commandLine.appendSwitch('gtk-version', '3');
|
||||
|
||||
// Wayland compatibility fixes
|
||||
// Force X11 backend on Wayland to avoid rendering issues
|
||||
if (process.platform === 'linux') {
|
||||
// Check if running on Wayland or if X11 is forced
|
||||
const isWayland =
|
||||
process.env.WAYLAND_DISPLAY || process.env.XDG_SESSION_TYPE === 'wayland';
|
||||
|
||||
if (isWayland || forceX11) {
|
||||
log('Applying X11/Wayland compatibility fixes');
|
||||
// Force Ozone platform to X11
|
||||
app.commandLine.appendSwitch('ozone-platform', 'x11');
|
||||
|
||||
// Disable GPU vsync to fix GetVSyncParametersIfAvailable() errors
|
||||
app.commandLine.appendSwitch('disable-gpu-vsync');
|
||||
|
||||
// Additional flags to improve compatibility
|
||||
app.commandLine.appendSwitch('disable-features', 'UseOzonePlatform');
|
||||
app.commandLine.appendSwitch('enable-features', 'UseSkiaRenderer');
|
||||
|
||||
// Set GDK backend to X11 which is needed for idle handling to work it seems
|
||||
process.env.GDK_BACKEND = 'x11';
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: needs to be executed before everything else
|
||||
process.argv.forEach((val) => {
|
||||
if (val && val.includes('--disable-tray')) {
|
||||
|
|
@ -129,11 +104,6 @@ export const startApp = (): void => {
|
|||
if (val && val.includes('--dev-tools')) {
|
||||
isShowDevTools = true;
|
||||
}
|
||||
|
||||
if (val && val.includes('--force-x11')) {
|
||||
forceX11 = true;
|
||||
log('Forcing X11 mode');
|
||||
}
|
||||
});
|
||||
|
||||
// TODO remove at one point in the future and only leave the directory setting part
|
||||
|
|
|
|||
41
package-lock.json
generated
41
package-lock.json
generated
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "superProductivity",
|
||||
"version": "15.1.0-rc.0",
|
||||
"version": "15.1.3-rc.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "superProductivity",
|
||||
"version": "15.1.0-rc.0",
|
||||
"version": "15.1.3-rc.0",
|
||||
"license": "MIT",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
@ -16,7 +16,7 @@
|
|||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-log": "^5.4.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fs-extra": "^11.3.0",
|
||||
"fs-extra": "^11.3.2",
|
||||
"node-fetch": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -89,7 +89,7 @@
|
|||
"core-js": "^3.39.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"detect-it": "^4.0.1",
|
||||
"electron": "^38.1.0",
|
||||
"electron": "38.2.2",
|
||||
"electron-builder": "^26.0.12",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
|
|
@ -142,7 +142,7 @@
|
|||
"@lmdb/lmdb-win32-x64": "^3.2.0",
|
||||
"@rollup/rollup-darwin-x64": "4.27.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.27.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.44.1"
|
||||
"@rollup/rollup-win32-x64-msvc": "4.52.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@algolia/client-abtesting": {
|
||||
|
|
@ -8531,12 +8531,13 @@
|
|||
]
|
||||
},
|
||||
"node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz",
|
||||
"integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==",
|
||||
"version": "4.52.3",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.3.tgz",
|
||||
"integrity": "sha512-zGIbEVVXVtauFgl3MRwGWEN36P5ZGenHRMgNw88X5wEhEBpq0XrMEZwOn07+ICrwM17XO5xfMZqh0OldCH5VTA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
|
|
@ -13897,9 +13898,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/electron": {
|
||||
"version": "38.1.0",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-38.1.0.tgz",
|
||||
"integrity": "sha512-ypA8GF8RU4HD5pA1sa0/2U8k+92EPP2c7pX+3XbgB760F7OmqrFXtYkOilVw6HfV4+lk88XxqigmsUKTACQYoQ==",
|
||||
"version": "38.2.2",
|
||||
"resolved": "https://registry.npmjs.org/electron/-/electron-38.2.2.tgz",
|
||||
"integrity": "sha512-OXSaVNXDlonXDjMRsFNQo1j5tzTKwKXh5/m46IjAFccBcZJZMISI+EjSI07oexIuhvKM8AZLuFuihVn4YjWWrA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
|
|
@ -15829,7 +15830,9 @@
|
|||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "11.3.0",
|
||||
"version": "11.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz",
|
||||
"integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"graceful-fs": "^4.2.0",
|
||||
|
|
@ -22767,6 +22770,20 @@
|
|||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/rollup/node_modules/@rollup/rollup-win32-x64-msvc": {
|
||||
"version": "4.44.1",
|
||||
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.44.1.tgz",
|
||||
"integrity": "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/roughjs": {
|
||||
"version": "4.6.6",
|
||||
"dev": true,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "superProductivity",
|
||||
"version": "15.1.0-rc.0",
|
||||
"version": "15.1.3-rc.0",
|
||||
"description": "ToDo list and Time Tracking",
|
||||
"keywords": [
|
||||
"ToDo",
|
||||
|
|
@ -135,7 +135,7 @@
|
|||
"electron-localshortcut": "^3.2.1",
|
||||
"electron-log": "^5.4.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"fs-extra": "^11.3.0",
|
||||
"fs-extra": "^11.3.2",
|
||||
"node-fetch": "^2.7.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
|
@ -208,7 +208,7 @@
|
|||
"core-js": "^3.39.0",
|
||||
"cross-env": "^7.0.3",
|
||||
"detect-it": "^4.0.1",
|
||||
"electron": "^38.1.0",
|
||||
"electron": "38.2.2",
|
||||
"electron-builder": "^26.0.12",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-config-prettier": "^10.1.5",
|
||||
|
|
@ -266,7 +266,7 @@
|
|||
"@lmdb/lmdb-win32-x64": "^3.2.0",
|
||||
"@rollup/rollup-darwin-x64": "4.27.4",
|
||||
"@rollup/rollup-linux-x64-gnu": "4.27.4",
|
||||
"@rollup/rollup-win32-x64-msvc": "4.44.1"
|
||||
"@rollup/rollup-win32-x64-msvc": "4.52.3"
|
||||
},
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
|
|
|
|||
|
|
@ -7,8 +7,7 @@ export const IS_WEB_EXTENSION_REQUIRED_FOR_JIRA = !IS_ELECTRON && !IS_ANDROID_WE
|
|||
export const TRACKING_INTERVAL = 1000;
|
||||
export const TIME_TRACKING_TO_DB_INTERVAL = 15000;
|
||||
|
||||
export const DRAG_DELAY_FOR_TOUCH = 75;
|
||||
export const DRAG_DELAY_FOR_TOUCH_LONGER = 150;
|
||||
export const DRAG_DELAY_FOR_TOUCH = 100;
|
||||
|
||||
// TODO use
|
||||
// const CORS_SKIP_EXTRA_HEADER_PROP = 'sp_cors_skip' as const;
|
||||
|
|
|
|||
|
|
@ -9,7 +9,10 @@ import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
|||
import { T } from '../../t.const';
|
||||
import { DialogPromptComponent } from '../../ui/dialog-prompt/dialog-prompt.component';
|
||||
import { MenuTreeService } from '../../features/menu-tree/menu-tree.service';
|
||||
import { MenuTreeFolderNode } from '../../features/menu-tree/store/menu-tree.model';
|
||||
import {
|
||||
MenuTreeFolderNode,
|
||||
MenuTreeKind,
|
||||
} from '../../features/menu-tree/store/menu-tree.model';
|
||||
|
||||
@Component({
|
||||
selector: 'folder-context-menu',
|
||||
|
|
@ -25,7 +28,7 @@ export class FolderContextMenuComponent {
|
|||
private readonly _menuTreeService = inject(MenuTreeService);
|
||||
|
||||
@Input() folderId!: string;
|
||||
@Input() treeType: 'project' | 'tag' = 'project';
|
||||
@Input() treeKind: MenuTreeKind = MenuTreeKind.PROJECT;
|
||||
|
||||
readonly T = T;
|
||||
|
||||
|
|
@ -33,32 +36,35 @@ export class FolderContextMenuComponent {
|
|||
const folder = this._loadFolder(this.folderId);
|
||||
if (!folder) return;
|
||||
|
||||
const folderNs =
|
||||
this.treeKind === MenuTreeKind.PROJECT ? T.F.PROJECT_FOLDER : T.F.TAG_FOLDER;
|
||||
|
||||
const dialogRef = this._matDialog.open(DialogPromptComponent, {
|
||||
restoreFocus: true,
|
||||
data: {
|
||||
txtLabel: this._translateService.instant(T.F.PROJECT_FOLDER.DIALOG.NAME_LABEL),
|
||||
txtLabel: this._translateService.instant(folderNs.DIALOG.NAME_LABEL),
|
||||
txtValue: folder.name,
|
||||
placeholder: this._translateService.instant(
|
||||
T.F.PROJECT_FOLDER.DIALOG.NAME_PLACEHOLDER,
|
||||
),
|
||||
placeholder: this._translateService.instant(folderNs.DIALOG.NAME_PLACEHOLDER),
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef
|
||||
.afterClosed()
|
||||
.pipe(take(1))
|
||||
.subscribe((result: string) => {
|
||||
if (result && result.trim() !== folder.name) {
|
||||
// Extract folder ID (remove the "folder-" prefix if present)
|
||||
const cleanId = this.folderId.startsWith('folder-')
|
||||
? this.folderId.substring(7)
|
||||
: this.folderId;
|
||||
.subscribe((result: string | null) => {
|
||||
const trimmed = result?.trim();
|
||||
if (!trimmed || trimmed === folder.name) {
|
||||
return;
|
||||
}
|
||||
// Extract folder ID (remove the "folder-" prefix if present)
|
||||
const cleanId = this.folderId.startsWith('folder-')
|
||||
? this.folderId.substring(7)
|
||||
: this.folderId;
|
||||
|
||||
if (this.treeType === 'project') {
|
||||
this._menuTreeService.updateFolderInProject(cleanId, result.trim());
|
||||
} else {
|
||||
this._menuTreeService.updateFolderInTag(cleanId, result.trim());
|
||||
}
|
||||
if (this.treeKind === MenuTreeKind.PROJECT) {
|
||||
this._menuTreeService.updateFolderInProject(cleanId, trimmed);
|
||||
} else {
|
||||
this._menuTreeService.updateFolderInTag(cleanId, trimmed);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -67,7 +73,12 @@ export class FolderContextMenuComponent {
|
|||
const folder = this._loadFolder(this.folderId);
|
||||
if (!folder) return;
|
||||
|
||||
const message = this._translateService.instant(T.F.PROJECT_FOLDER.CONFIRM_DELETE, {
|
||||
const confirmKey =
|
||||
this.treeKind === MenuTreeKind.PROJECT
|
||||
? T.F.PROJECT_FOLDER.CONFIRM_DELETE
|
||||
: T.F.TAG_FOLDER.CONFIRM_DELETE;
|
||||
|
||||
const message = this._translateService.instant(confirmKey, {
|
||||
title: folder.name,
|
||||
});
|
||||
|
||||
|
|
@ -86,7 +97,7 @@ export class FolderContextMenuComponent {
|
|||
? this.folderId.substring(7)
|
||||
: this.folderId;
|
||||
|
||||
if (this.treeType === 'project') {
|
||||
if (this.treeKind === MenuTreeKind.PROJECT) {
|
||||
this._menuTreeService.deleteFolderFromProject(cleanId);
|
||||
} else {
|
||||
this._menuTreeService.deleteFolderFromTag(cleanId);
|
||||
|
|
@ -103,8 +114,8 @@ export class FolderContextMenuComponent {
|
|||
const tagTree = this._menuTreeService.tagTree();
|
||||
|
||||
// Search in the appropriate tree first, then fallback to the other
|
||||
const primaryTree = this.treeType === 'project' ? projectTree : tagTree;
|
||||
const secondaryTree = this.treeType === 'project' ? tagTree : projectTree;
|
||||
const primaryTree = this.treeKind === MenuTreeKind.PROJECT ? projectTree : tagTree;
|
||||
const secondaryTree = this.treeKind === MenuTreeKind.PROJECT ? tagTree : projectTree;
|
||||
|
||||
return (
|
||||
this._menuTreeService.findFolderInTree(cleanId, primaryTree) ||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,10 @@ import { NavConfig, NavItem } from './magic-side-nav.model';
|
|||
import { PluginBridgeService } from '../../plugins/plugin-bridge.service';
|
||||
import { lsGetBoolean, lsSetItem } from '../../util/ls-util';
|
||||
import { MenuTreeService } from '../../features/menu-tree/menu-tree.service';
|
||||
import { MenuTreeViewNode } from '../../features/menu-tree/store/menu-tree.model';
|
||||
import {
|
||||
MenuTreeKind,
|
||||
MenuTreeViewNode,
|
||||
} from '../../features/menu-tree/store/menu-tree.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
|
|
@ -133,11 +136,12 @@ export class MagicNavConfigService {
|
|||
id: 'projects',
|
||||
label: T.MH.PROJECTS,
|
||||
icon: 'expand_more',
|
||||
treeKind: MenuTreeKind.PROJECT,
|
||||
tree:
|
||||
this._projectNavTree().length > 0
|
||||
? this._projectNavTree()
|
||||
: this._visibleProjects().map((project) => ({
|
||||
kind: 'project',
|
||||
k: MenuTreeKind.PROJECT,
|
||||
project,
|
||||
})),
|
||||
action: () => this._toggleProjectsExpanded(),
|
||||
|
|
@ -169,14 +173,29 @@ export class MagicNavConfigService {
|
|||
id: 'tags',
|
||||
label: T.MH.TAGS,
|
||||
icon: 'expand_more',
|
||||
treeKind: MenuTreeKind.TAG,
|
||||
tree:
|
||||
this._tagNavTree().length > 0
|
||||
? this._tagNavTree()
|
||||
: this._tags().map((tag) => ({
|
||||
kind: 'tag',
|
||||
k: MenuTreeKind.TAG,
|
||||
tag,
|
||||
})),
|
||||
action: () => this._toggleTagsExpanded(),
|
||||
additionalButtons: [
|
||||
{
|
||||
id: 'add-tag-folder',
|
||||
icon: 'create_new_folder',
|
||||
tooltip: T.F.TAG_FOLDER.TOOLTIP_CREATE,
|
||||
action: () => this._openCreateTagFolder(),
|
||||
},
|
||||
{
|
||||
id: 'add-tag',
|
||||
icon: 'add',
|
||||
tooltip: T.MH.CREATE_TAG,
|
||||
action: () => this._createNewTag(),
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Separator
|
||||
|
|
@ -397,6 +416,27 @@ export class MagicNavConfigService {
|
|||
});
|
||||
}
|
||||
|
||||
private _openCreateTagFolder(): void {
|
||||
this._matDialog
|
||||
.open(DialogPromptComponent, {
|
||||
restoreFocus: true,
|
||||
data: {
|
||||
placeholder: T.F.TAG_FOLDER.DIALOG.NAME_PLACEHOLDER,
|
||||
},
|
||||
})
|
||||
.afterClosed()
|
||||
.subscribe((title) => {
|
||||
if (!title) {
|
||||
return;
|
||||
}
|
||||
const trimmed = title.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
this._menuTreeService.createTagFolder(trimmed);
|
||||
});
|
||||
}
|
||||
|
||||
private _createNewTag(): void {
|
||||
this._matDialog
|
||||
.open(DialogPromptComponent, {
|
||||
|
|
|
|||
|
|
@ -153,9 +153,10 @@
|
|||
|
||||
// Sidebar toggle button (desktop)
|
||||
.mode-toggle {
|
||||
--top: 8px;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
top: var(--top);
|
||||
right: 8px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--sidenav-border-color);
|
||||
|
|
@ -165,6 +166,10 @@
|
|||
transition: var(--transition-standard);
|
||||
color: var(--sidenav-text-secondary);
|
||||
|
||||
:host-context(.isMac.isElectron) & {
|
||||
top: calc(var(--top) + var(--mac-title-bar-padding, 0px));
|
||||
}
|
||||
|
||||
&.visible {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
@ -180,10 +185,11 @@
|
|||
|
||||
// Navigation list
|
||||
.nav-list {
|
||||
--pt: 48px;
|
||||
list-style: none;
|
||||
//padding: 60px var(--s) var(--s2);
|
||||
padding: 0;
|
||||
padding-top: 48px;
|
||||
padding-top: var(--pt);
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
|
|
@ -193,6 +199,10 @@
|
|||
@media (max-width: 600px) {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:host-context(.isMac.isElectron) & {
|
||||
padding-top: calc(var(--pt) + var(--mac-title-bar-padding, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
|
|
|
|||
|
|
@ -2,7 +2,10 @@ import {
|
|||
WorkContextCommon,
|
||||
WorkContextType,
|
||||
} from '../../features/work-context/work-context.model';
|
||||
import { MenuTreeViewNode } from '../../features/menu-tree/store/menu-tree.model';
|
||||
import {
|
||||
MenuTreeKind,
|
||||
MenuTreeViewNode,
|
||||
} from '../../features/menu-tree/store/menu-tree.model';
|
||||
|
||||
export type NavItem =
|
||||
| NavSeparatorItem
|
||||
|
|
@ -70,6 +73,7 @@ export interface NavTreeItem extends NavBaseItem {
|
|||
type: 'tree';
|
||||
label: string;
|
||||
icon: string;
|
||||
treeKind: MenuTreeKind;
|
||||
additionalButtons?: NavAdditionalButton[];
|
||||
contextMenuItems?: NavContextItem[];
|
||||
action?: () => void; // optional external toggle logic
|
||||
|
|
|
|||
|
|
@ -96,7 +96,10 @@
|
|||
></context-menu>
|
||||
|
||||
<ng-template #folderContextMenu>
|
||||
<folder-context-menu [folderId]="folderId()!"></folder-context-menu>
|
||||
<folder-context-menu
|
||||
[folderId]="folderId()!"
|
||||
[treeKind]="treeKind()"
|
||||
></folder-context-menu>
|
||||
</ng-template>
|
||||
} @else {
|
||||
<!-- Container + presentational row unified here -->
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ import { selectAllDoneIds } from '../../../features/tasks/store/task.selectors';
|
|||
import { Store } from '@ngrx/store';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { isSingleEmoji } from '../../../util/extract-first-emoji';
|
||||
import { MenuTreeKind } from '../../../features/menu-tree/store/menu-tree.model';
|
||||
|
||||
@Component({
|
||||
selector: 'nav-item',
|
||||
|
|
@ -79,6 +80,7 @@ export class NavItemComponent {
|
|||
// Folder inputs
|
||||
|
||||
folderId = input<string | null>(null);
|
||||
treeKind = input<MenuTreeKind>(MenuTreeKind.PROJECT);
|
||||
// Variant styling to integrate into magic-side-nav without deep selectors
|
||||
showMoreButton = input<boolean>(true);
|
||||
|
||||
|
|
|
|||
|
|
@ -66,11 +66,14 @@
|
|||
<nav-item
|
||||
[mode]="'folder'"
|
||||
[expanded]="expanded"
|
||||
[label]="node.data?.kind === 'folder' ? node.data.name : node.data.label"
|
||||
[label]="
|
||||
node.data?.k === MenuTreeKind.FOLDER ? node.data.name : node.data.label
|
||||
"
|
||||
[variant]="'nav'"
|
||||
[showLabels]="showLabels()"
|
||||
(clicked)="onChildClick(node)"
|
||||
[folderId]="node.id"
|
||||
[treeKind]="treeKind()"
|
||||
></nav-item>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
|
@ -82,14 +85,14 @@
|
|||
>
|
||||
<div
|
||||
class="nav-child-item"
|
||||
[class.draggable]="node.data?.kind !== 'tag'"
|
||||
[class.draggable]="node.data?.k !== MenuTreeKind.TAG"
|
||||
[attr.data-project-id]="
|
||||
node.data?.kind === 'project' ? node.data.project.id : null
|
||||
node.data?.k === MenuTreeKind.PROJECT ? node.data.project.id : null
|
||||
"
|
||||
[attr.data-tag-id]="node.data?.kind === 'tag' ? node.data.tag.id : null"
|
||||
[attr.data-tag-id]="node.data?.k === MenuTreeKind.TAG ? node.data.tag.id : null"
|
||||
>
|
||||
@switch (node.data?.kind) {
|
||||
@case ('project') {
|
||||
@switch (node.data?.k) {
|
||||
@case (MenuTreeKind.PROJECT) {
|
||||
<nav-item
|
||||
[workContext]="node.data.project"
|
||||
[type]="WorkContextType.PROJECT"
|
||||
|
|
@ -100,7 +103,7 @@
|
|||
(clicked)="onChildClick(node)"
|
||||
></nav-item>
|
||||
}
|
||||
@case ('tag') {
|
||||
@case (MenuTreeKind.TAG) {
|
||||
<nav-item
|
||||
[workContext]="node.data.tag"
|
||||
[type]="WorkContextType.TAG"
|
||||
|
|
@ -142,9 +145,9 @@
|
|||
{{ data.isFolder ? 'folder' : 'assignment' }}
|
||||
</mat-icon>
|
||||
<span>{{
|
||||
data.node.data?.kind === 'folder'
|
||||
data.node.data?.k === MenuTreeKind.FOLDER
|
||||
? data.node.data.name
|
||||
: data.node.data?.kind === 'project'
|
||||
: data.node.data?.k === MenuTreeKind.PROJECT
|
||||
? data.node.data.project.title
|
||||
: data.node.data?.tag.title
|
||||
}}</span>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
effect,
|
||||
inject,
|
||||
input,
|
||||
|
|
@ -20,6 +21,7 @@ import { NavItem, NavTreeItem } from '../magic-side-nav.model';
|
|||
import { MagicNavConfigService } from '../magic-nav-config.service';
|
||||
import { T } from '../../../t.const';
|
||||
import {
|
||||
MenuTreeKind,
|
||||
MenuTreeViewFolderNode,
|
||||
MenuTreeViewNode,
|
||||
MenuTreeViewProjectNode,
|
||||
|
|
@ -63,11 +65,13 @@ export class NavListTreeComponent {
|
|||
readonly T = T;
|
||||
readonly WorkContextType = WorkContextType;
|
||||
readonly DEFAULT_PROJECT_ICON = DEFAULT_PROJECT_ICON;
|
||||
readonly MenuTreeKind = MenuTreeKind;
|
||||
|
||||
// Access to service methods and data for visibility menu
|
||||
readonly allProjectsExceptInbox = this._navConfigService.allProjectsExceptInbox;
|
||||
|
||||
readonly treeNodes = signal<TreeNode<MenuTreeViewNode>[]>([]);
|
||||
readonly treeKind = computed<MenuTreeKind>(() => this.item().treeKind);
|
||||
|
||||
constructor() {
|
||||
effect(() => {
|
||||
|
|
@ -84,17 +88,17 @@ export class NavListTreeComponent {
|
|||
const data = node.data;
|
||||
if (!data) return;
|
||||
|
||||
if (data.kind === 'folder') {
|
||||
if (data.k === MenuTreeKind.FOLDER) {
|
||||
this._toggleFolder(node.id);
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.kind === 'project') {
|
||||
if (data.k === MenuTreeKind.PROJECT) {
|
||||
this.itemClick.emit(this._toProjectNavItem(data));
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.kind === 'tag') {
|
||||
if (data.k === MenuTreeKind.TAG) {
|
||||
this.itemClick.emit(this._toTagNavItem(data));
|
||||
}
|
||||
}
|
||||
|
|
@ -127,7 +131,7 @@ export class NavListTreeComponent {
|
|||
// TODO: Implement folder context menu
|
||||
console.log(
|
||||
'Folder context menu for:',
|
||||
node.data?.kind === 'folder' ? node.data.name : 'unknown',
|
||||
node.data?.k === MenuTreeKind.FOLDER ? node.data.name : 'unknown',
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -138,7 +142,10 @@ export class NavListTreeComponent {
|
|||
return {
|
||||
...node,
|
||||
expanded: isExpanded,
|
||||
data: node.data?.kind === 'folder' ? { ...node.data, isExpanded } : node.data,
|
||||
data:
|
||||
node.data?.k === MenuTreeKind.FOLDER
|
||||
? { ...node.data, isExpanded }
|
||||
: node.data,
|
||||
};
|
||||
}
|
||||
return node;
|
||||
|
|
@ -157,7 +164,7 @@ export class NavListTreeComponent {
|
|||
}
|
||||
|
||||
private _toTreeNode(node: MenuTreeViewNode): TreeNode<MenuTreeViewNode> {
|
||||
if (node.kind === 'folder') {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
const children = node.children.map((child) => this._toTreeNode(child));
|
||||
// Always expand empty folders
|
||||
const shouldExpand = children.length === 0 ? true : node.isExpanded;
|
||||
|
|
@ -170,7 +177,7 @@ export class NavListTreeComponent {
|
|||
children,
|
||||
} satisfies TreeNode<MenuTreeViewNode>;
|
||||
}
|
||||
if (node.kind === 'project') {
|
||||
if (node.k === MenuTreeKind.PROJECT) {
|
||||
return {
|
||||
id: `project-${node.project.id}`,
|
||||
isFolder: false,
|
||||
|
|
@ -191,16 +198,16 @@ export class NavListTreeComponent {
|
|||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
if (data.kind === 'folder') {
|
||||
if (data.k === MenuTreeKind.FOLDER) {
|
||||
return {
|
||||
kind: 'folder',
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
isExpanded: node.expanded ?? data.isExpanded,
|
||||
children: this._treeNodesToViewNodes(node.children ?? []),
|
||||
} satisfies MenuTreeViewFolderNode;
|
||||
}
|
||||
if (data.kind === 'project') {
|
||||
if (data.k === MenuTreeKind.PROJECT) {
|
||||
return data satisfies MenuTreeViewProjectNode;
|
||||
}
|
||||
return data satisfies MenuTreeViewTagNode;
|
||||
|
|
|
|||
|
|
@ -67,8 +67,7 @@ export class SnackService {
|
|||
|
||||
const cfg = {
|
||||
...DEFAULT_SNACK_CFG,
|
||||
// duration: type === 'ERROR' ? 8000 : DEFAULT_SNACK_CFG.duration,
|
||||
duration: 2222222222222,
|
||||
duration: type === 'ERROR' ? 8000 : DEFAULT_SNACK_CFG.duration,
|
||||
...config,
|
||||
data: {
|
||||
...params,
|
||||
|
|
|
|||
|
|
@ -65,7 +65,10 @@ export class BoardsComponent {
|
|||
// NOTE: since the index number does not change (the add tab index is at the same index as the newly added tab) we need to do this in two steps
|
||||
const newIndex = (this.boards()?.length || 1) - 1;
|
||||
setTimeout(() => {
|
||||
this.selectedTabIndex.set(newIndex);
|
||||
this.selectedTabIndex.set(newIndex + 1);
|
||||
setTimeout(() => {
|
||||
this.selectedTabIndex.set(newIndex);
|
||||
}, 10);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -144,6 +144,7 @@ breathing-dot {
|
|||
justify-content: center;
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
a,
|
||||
|
|
|
|||
|
|
@ -120,10 +120,7 @@
|
|||
</div>
|
||||
}
|
||||
@if (!isCollapsedIssueComments()) {
|
||||
@for (
|
||||
comment of issue?.comments | sort: 'created_at';
|
||||
track trackByIndex($index, comment)
|
||||
) {
|
||||
@for (comment of sortedComments; track trackByIndex($index, comment)) {
|
||||
<div class="comment">
|
||||
<!--<img [src]="comment.author.avatarUrl"-->
|
||||
<!--class="author-avatar">-->
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ import { MatChipListbox, MatChipOption } from '@angular/material/chips';
|
|||
import { MarkdownComponent, MarkdownPipe } from 'ngx-markdown';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { AsyncPipe, DatePipe } from '@angular/common';
|
||||
import { SortPipe } from '../../../../../ui/pipes/sort.pipe';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
|
|
@ -34,7 +33,6 @@ import { TranslatePipe } from '@ngx-translate/core';
|
|||
MatAnchor,
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
SortPipe,
|
||||
TranslatePipe,
|
||||
MarkdownPipe,
|
||||
],
|
||||
|
|
@ -58,6 +56,15 @@ export class GithubIssueContentComponent {
|
|||
this.issue.comments[this.issue.comments?.length - 1]) as GithubComment;
|
||||
}
|
||||
|
||||
get sortedComments(): readonly GithubComment[] {
|
||||
if (!this.issue?.comments) {
|
||||
return [];
|
||||
}
|
||||
return [...this.issue.comments].sort((a, b) =>
|
||||
a.created_at.localeCompare(b.created_at),
|
||||
);
|
||||
}
|
||||
|
||||
isCollapsedIssueSummary(): boolean {
|
||||
if (this.issue) {
|
||||
return this.isCollapsedIssueComments() && this.issue.body?.length > 200;
|
||||
|
|
@ -83,7 +90,7 @@ export class GithubIssueContentComponent {
|
|||
this._taskService.markIssueUpdatesAsRead(task.id);
|
||||
}
|
||||
|
||||
trackByIndex(i: number, p: any): number {
|
||||
trackByIndex(i: number, p: GithubComment): number {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -74,10 +74,7 @@
|
|||
|
||||
@if (issue?.comments) {
|
||||
<div>
|
||||
@for (
|
||||
comment of issue?.comments | sort: 'created_at';
|
||||
track trackByIndex($index, comment)
|
||||
) {
|
||||
@for (comment of sortedComments; track trackByIndex($index, comment)) {
|
||||
<div class="comment">
|
||||
<div class="name-and-comment-content">
|
||||
<div>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, input, inject } from '@angular/core';
|
||||
import { TaskWithSubTasks } from '../../../../tasks/task.model';
|
||||
import { GitlabIssue } from '../gitlab-issue.model';
|
||||
import { GitlabComment, GitlabIssue } from '../gitlab-issue.model';
|
||||
import { expandAnimation } from '../../../../../ui/animations/expand.ani';
|
||||
import { T } from '../../../../../t.const';
|
||||
import { TaskService } from '../../../../tasks/task.service';
|
||||
|
|
@ -9,7 +9,6 @@ import { MatChipListbox, MatChipOption } from '@angular/material/chips';
|
|||
import { MarkdownComponent, MarkdownPipe } from 'ngx-markdown';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { AsyncPipe, DatePipe } from '@angular/common';
|
||||
import { SortPipe } from '../../../../../ui/pipes/sort.pipe';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
|
|
@ -27,7 +26,6 @@ import { TranslatePipe } from '@ngx-translate/core';
|
|||
MatIcon,
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
SortPipe,
|
||||
TranslatePipe,
|
||||
MarkdownPipe,
|
||||
],
|
||||
|
|
@ -54,7 +52,16 @@ export class GitlabIssueContentComponent {
|
|||
this._taskService.markIssueUpdatesAsRead(task.id);
|
||||
}
|
||||
|
||||
trackByIndex(i: number, p: any): number {
|
||||
trackByIndex(i: number, p: GitlabComment): number {
|
||||
return i;
|
||||
}
|
||||
|
||||
get sortedComments(): GitlabComment[] {
|
||||
if (!this.issue?.comments) {
|
||||
return [];
|
||||
}
|
||||
return [...this.issue.comments].sort((a, b) =>
|
||||
a.created_at.localeCompare(b.created_at),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -150,10 +150,7 @@
|
|||
<tr>
|
||||
<th>{{ T.F.JIRA.ISSUE_CONTENT.COMMENTS | translate }}</th>
|
||||
<td>
|
||||
@for (
|
||||
comment of issue?.comments | sort: 'created';
|
||||
track trackByIndex($index, comment)
|
||||
) {
|
||||
@for (comment of sortedComments; track trackByIndex($index, comment)) {
|
||||
<div class="comment">
|
||||
<img
|
||||
[src]="comment.author.avatarUrl"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
import { ChangeDetectionStrategy, Component, Input, inject } from '@angular/core';
|
||||
import { TaskWithSubTasks } from '../../../../tasks/task.model';
|
||||
import { JiraIssue, JiraRelatedIssue, JiraSubtask } from '../jira-issue.model';
|
||||
import {
|
||||
JiraComment,
|
||||
JiraIssue,
|
||||
JiraRelatedIssue,
|
||||
JiraSubtask,
|
||||
} from '../jira-issue.model';
|
||||
import { expandAnimation } from '../../../../../ui/animations/expand.ani';
|
||||
import { TaskAttachment } from '../../../../tasks/task-attachment/task-attachment.model';
|
||||
import { T } from '../../../../../t.const';
|
||||
|
|
@ -27,7 +32,6 @@ import { MatIcon } from '@angular/material/icon';
|
|||
import { AsyncPipe, DatePipe } from '@angular/common';
|
||||
import { JiraToMarkdownPipe } from '../../../../../ui/pipes/jira-to-markdown.pipe';
|
||||
import { MsToStringPipe } from '../../../../../ui/duration/ms-to-string.pipe';
|
||||
import { SortPipe } from '../../../../../ui/pipes/sort.pipe';
|
||||
import { TranslatePipe } from '@ngx-translate/core';
|
||||
import { SnackService } from '../../../../../core/snack/snack.service';
|
||||
import { IssueLog } from '../../../../../core/log';
|
||||
|
|
@ -53,7 +57,6 @@ interface JiraSubtaskWithUrl extends JiraSubtask {
|
|||
DatePipe,
|
||||
JiraToMarkdownPipe,
|
||||
MsToStringPipe,
|
||||
SortPipe,
|
||||
TranslatePipe,
|
||||
MarkdownPipe,
|
||||
],
|
||||
|
|
@ -171,7 +174,14 @@ export class JiraIssueContentComponent {
|
|||
this._taskService.markIssueUpdatesAsRead(this.task.id);
|
||||
}
|
||||
|
||||
trackByIndex(i: number, p: any): number {
|
||||
trackByIndex(i: number, p: JiraComment): number {
|
||||
return i;
|
||||
}
|
||||
|
||||
get sortedComments(): JiraComment[] {
|
||||
if (!this.issue?.comments) {
|
||||
return [];
|
||||
}
|
||||
return [...this.issue.comments].sort((a, b) => a.created.localeCompare(b.created));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { Project } from '../project/project.model';
|
|||
import { Tag } from '../tag/tag.model';
|
||||
import {
|
||||
MenuTreeFolderNode,
|
||||
MenuTreeKind,
|
||||
MenuTreeProjectNode,
|
||||
MenuTreeTagNode,
|
||||
MenuTreeTreeNode,
|
||||
|
|
@ -56,7 +57,7 @@ export class MenuTreeService {
|
|||
|
||||
initializeProjectTree(projects: Project[]): void {
|
||||
const tree = projects.map<MenuTreeProjectNode>((project) => ({
|
||||
kind: 'project',
|
||||
k: MenuTreeKind.PROJECT,
|
||||
id: project.id,
|
||||
projectId: project.id,
|
||||
}));
|
||||
|
|
@ -65,7 +66,7 @@ export class MenuTreeService {
|
|||
|
||||
initializeTagTree(tags: Tag[]): void {
|
||||
const tree = tags.map<MenuTreeTagNode>((tag) => ({
|
||||
kind: 'tag',
|
||||
k: MenuTreeKind.TAG,
|
||||
id: tag.id,
|
||||
tagId: tag.id,
|
||||
}));
|
||||
|
|
@ -78,10 +79,10 @@ export class MenuTreeService {
|
|||
items: projects,
|
||||
getId: (project) => project.id,
|
||||
createViewNode: (project): MenuTreeViewProjectNode => ({
|
||||
kind: 'project',
|
||||
k: MenuTreeKind.PROJECT,
|
||||
project,
|
||||
}),
|
||||
itemType: 'project',
|
||||
itemType: MenuTreeKind.PROJECT,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -91,10 +92,10 @@ export class MenuTreeService {
|
|||
items: tags,
|
||||
getId: (tag) => tag.id,
|
||||
createViewNode: (tag): MenuTreeViewTagNode => ({
|
||||
kind: 'tag',
|
||||
k: MenuTreeKind.TAG,
|
||||
tag,
|
||||
}),
|
||||
itemType: 'tag',
|
||||
itemType: MenuTreeKind.TAG,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -107,52 +108,47 @@ export class MenuTreeService {
|
|||
}
|
||||
|
||||
persistProjectViewTree(viewNodes: MenuTreeViewNode[]): void {
|
||||
const stored = this._viewToStoredTree(viewNodes, 'project');
|
||||
const stored = this._viewToStoredTree(viewNodes, MenuTreeKind.PROJECT);
|
||||
this.setProjectTree(stored);
|
||||
}
|
||||
|
||||
persistTagViewTree(viewNodes: MenuTreeViewNode[]): void {
|
||||
const stored = this._viewToStoredTree(viewNodes, 'tag');
|
||||
const stored = this._viewToStoredTree(viewNodes, MenuTreeKind.TAG);
|
||||
this.setTagTree(stored);
|
||||
}
|
||||
|
||||
createProjectFolder(name: string, parentFolderId?: string | null): void {
|
||||
const trimmed = name.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
this._createFolder({
|
||||
name,
|
||||
parentFolderId: parentFolderId ?? null,
|
||||
treeKind: MenuTreeKind.PROJECT,
|
||||
});
|
||||
}
|
||||
|
||||
const newFolder: MenuTreeFolderNode = {
|
||||
kind: 'folder',
|
||||
id: this._createFolderId(),
|
||||
name: trimmed,
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
|
||||
const currentTree = this.projectTree();
|
||||
const nextTree = this._insertFolderNode(
|
||||
currentTree,
|
||||
newFolder,
|
||||
parentFolderId ?? null,
|
||||
);
|
||||
this.setProjectTree(nextTree);
|
||||
createTagFolder(name: string, parentFolderId?: string | null): void {
|
||||
this._createFolder({
|
||||
name,
|
||||
parentFolderId: parentFolderId ?? null,
|
||||
treeKind: MenuTreeKind.TAG,
|
||||
});
|
||||
}
|
||||
|
||||
deleteFolderFromProject(folderId: string): void {
|
||||
this._store.dispatch(deleteFolder({ folderId, treeType: 'project' }));
|
||||
this._store.dispatch(deleteFolder({ folderId, treeType: MenuTreeKind.PROJECT }));
|
||||
}
|
||||
|
||||
deleteFolderFromTag(folderId: string): void {
|
||||
this._store.dispatch(deleteFolder({ folderId, treeType: 'tag' }));
|
||||
this._store.dispatch(deleteFolder({ folderId, treeType: MenuTreeKind.TAG }));
|
||||
}
|
||||
|
||||
updateFolderInProject(folderId: string, name: string): void {
|
||||
this._store.dispatch(updateFolder({ folderId, name, treeType: 'project' }));
|
||||
this._store.dispatch(
|
||||
updateFolder({ folderId, name, treeType: MenuTreeKind.PROJECT }),
|
||||
);
|
||||
}
|
||||
|
||||
updateFolderInTag(folderId: string, name: string): void {
|
||||
this._store.dispatch(updateFolder({ folderId, name, treeType: 'tag' }));
|
||||
this._store.dispatch(updateFolder({ folderId, name, treeType: MenuTreeKind.TAG }));
|
||||
}
|
||||
|
||||
findFolderInTree(
|
||||
|
|
@ -160,10 +156,10 @@ export class MenuTreeService {
|
|||
tree: MenuTreeTreeNode[],
|
||||
): MenuTreeFolderNode | null {
|
||||
for (const node of tree) {
|
||||
if (node.id === folderId && node.kind === 'folder') {
|
||||
if (node.id === folderId && node.k === MenuTreeKind.FOLDER) {
|
||||
return node;
|
||||
}
|
||||
if (node.kind === 'folder') {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
const found = this.findFolderInTree(folderId, node.children);
|
||||
if (found) {
|
||||
return found;
|
||||
|
|
@ -178,19 +174,19 @@ export class MenuTreeService {
|
|||
items: T[];
|
||||
getId: (item: T) => string;
|
||||
createViewNode: (item: T) => MenuTreeViewNode;
|
||||
itemType: 'project' | 'tag';
|
||||
itemType: MenuTreeKind;
|
||||
}): MenuTreeViewNode[] {
|
||||
const { storedTree, items, getId, createViewNode, itemType } = options;
|
||||
const itemMap = new Map(items.map((item) => [getId(item), item]));
|
||||
const usedIds = new Set<string>();
|
||||
|
||||
const mapNode = (node: MenuTreeTreeNode): MenuTreeViewNode | null => {
|
||||
if (node.kind === 'folder') {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
const children = node.children
|
||||
.map((child) => mapNode(child))
|
||||
.filter((child): child is MenuTreeViewNode => child !== null);
|
||||
return {
|
||||
kind: 'folder',
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
isExpanded: node.isExpanded ?? true,
|
||||
|
|
@ -198,7 +194,7 @@ export class MenuTreeService {
|
|||
} satisfies MenuTreeViewFolderNode;
|
||||
}
|
||||
|
||||
if (itemType === 'project' && node.kind === 'project') {
|
||||
if (itemType === MenuTreeKind.PROJECT && node.k === MenuTreeKind.PROJECT) {
|
||||
const project = itemMap.get(node.id);
|
||||
if (!project) {
|
||||
return null;
|
||||
|
|
@ -207,7 +203,7 @@ export class MenuTreeService {
|
|||
return createViewNode(project) as MenuTreeViewProjectNode;
|
||||
}
|
||||
|
||||
if (itemType === 'tag' && node.kind === 'tag') {
|
||||
if (itemType === MenuTreeKind.TAG && node.k === MenuTreeKind.TAG) {
|
||||
const tag = itemMap.get(node.id);
|
||||
if (!tag) {
|
||||
return null;
|
||||
|
|
@ -236,15 +232,15 @@ export class MenuTreeService {
|
|||
|
||||
private _viewToStoredTree(
|
||||
nodes: MenuTreeViewNode[],
|
||||
itemType: 'project' | 'tag',
|
||||
itemType: MenuTreeKind,
|
||||
): MenuTreeTreeNode[] {
|
||||
const mapNode = (node: MenuTreeViewNode): MenuTreeTreeNode | null => {
|
||||
if (node.kind === 'folder') {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
const children = node.children
|
||||
.map((child) => mapNode(child))
|
||||
.filter((child): child is MenuTreeTreeNode => child !== null);
|
||||
return {
|
||||
kind: 'folder',
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
isExpanded: node.isExpanded,
|
||||
|
|
@ -252,16 +248,16 @@ export class MenuTreeService {
|
|||
} satisfies MenuTreeFolderNode;
|
||||
}
|
||||
|
||||
if (itemType === 'project' && node.kind === 'project') {
|
||||
if (itemType === MenuTreeKind.PROJECT && node.k === MenuTreeKind.PROJECT) {
|
||||
return {
|
||||
kind: 'project',
|
||||
k: MenuTreeKind.PROJECT,
|
||||
id: node.project.id,
|
||||
} satisfies MenuTreeProjectNode;
|
||||
}
|
||||
|
||||
if (itemType === 'tag' && node.kind === 'tag') {
|
||||
if (itemType === MenuTreeKind.TAG && node.k === MenuTreeKind.TAG) {
|
||||
return {
|
||||
kind: 'tag',
|
||||
k: MenuTreeKind.TAG,
|
||||
id: node.tag.id,
|
||||
} satisfies MenuTreeTagNode;
|
||||
}
|
||||
|
|
@ -280,7 +276,7 @@ export class MenuTreeService {
|
|||
const result: Array<{ id: string; name: string }> = [];
|
||||
const walk = (list: MenuTreeTreeNode[]): void => {
|
||||
list.forEach((node) => {
|
||||
if (node.kind === 'folder') {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
result.push({ id: node.id, name: node.name });
|
||||
walk(node.children);
|
||||
}
|
||||
|
|
@ -312,9 +308,9 @@ export class MenuTreeService {
|
|||
|
||||
private _cloneTree(tree: MenuTreeTreeNode[]): MenuTreeTreeNode[] {
|
||||
return tree.map((node) =>
|
||||
node.kind === 'folder'
|
||||
node.k === MenuTreeKind.FOLDER
|
||||
? {
|
||||
kind: 'folder',
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: node.id,
|
||||
name: node.name,
|
||||
isExpanded: node.isExpanded,
|
||||
|
|
@ -326,7 +322,7 @@ export class MenuTreeService {
|
|||
|
||||
private _findFolder(tree: MenuTreeTreeNode[], id: string): MenuTreeFolderNode | null {
|
||||
for (const node of tree) {
|
||||
if (node.kind === 'folder') {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
if (node.id === id) {
|
||||
return node;
|
||||
}
|
||||
|
|
@ -345,4 +341,37 @@ export class MenuTreeService {
|
|||
}
|
||||
return `folder-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
private _createFolder(options: {
|
||||
name: string;
|
||||
parentFolderId: string | null;
|
||||
treeKind: MenuTreeKind;
|
||||
}): void {
|
||||
const trimmed = options.name.trim();
|
||||
if (!trimmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newFolder: MenuTreeFolderNode = {
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: this._createFolderId(),
|
||||
name: trimmed,
|
||||
isExpanded: true,
|
||||
children: [],
|
||||
};
|
||||
|
||||
const currentTree =
|
||||
options.treeKind === MenuTreeKind.PROJECT ? this.projectTree() : this.tagTree();
|
||||
const nextTree = this._insertFolderNode(
|
||||
currentTree,
|
||||
newFolder,
|
||||
options.parentFolderId,
|
||||
);
|
||||
|
||||
if (options.treeKind === MenuTreeKind.PROJECT) {
|
||||
this.setProjectTree(nextTree);
|
||||
} else {
|
||||
this.setTagTree(nextTree);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { createAction, props } from '@ngrx/store';
|
||||
import { MenuTreeTreeNode } from './menu-tree.model';
|
||||
import { MenuTreeKind, MenuTreeTreeNode } from './menu-tree.model';
|
||||
|
||||
type MenuTreeItemKind = MenuTreeKind.PROJECT | MenuTreeKind.TAG;
|
||||
|
||||
export const updateProjectTree = createAction(
|
||||
'[MenuTree] Update Project Tree',
|
||||
|
|
@ -13,10 +15,10 @@ export const updateTagTree = createAction(
|
|||
|
||||
export const deleteFolder = createAction(
|
||||
'[MenuTree] Delete Folder',
|
||||
props<{ folderId: string; treeType: 'project' | 'tag' }>(),
|
||||
props<{ folderId: string; treeType: MenuTreeItemKind }>(),
|
||||
);
|
||||
|
||||
export const updateFolder = createAction(
|
||||
'[MenuTree] Update Folder',
|
||||
props<{ folderId: string; name: string; treeType: 'project' | 'tag' }>(),
|
||||
props<{ folderId: string; name: string; treeType: MenuTreeItemKind }>(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
import { Project } from '../../project/project.model';
|
||||
import { Tag } from '../../tag/tag.model';
|
||||
|
||||
export type MenuTreeNodeKind = 'folder' | 'project' | 'tag';
|
||||
export enum MenuTreeKind {
|
||||
FOLDER = 'f',
|
||||
PROJECT = 'p',
|
||||
TAG = 't',
|
||||
}
|
||||
|
||||
export type MenuTreeNodeKind =
|
||||
| MenuTreeKind.FOLDER
|
||||
| MenuTreeKind.PROJECT
|
||||
| MenuTreeKind.TAG;
|
||||
|
||||
export interface MenuTreeState {
|
||||
projectTree: MenuTreeTreeNode[];
|
||||
|
|
@ -10,19 +19,19 @@ export interface MenuTreeState {
|
|||
|
||||
interface MenuTreeBaseNode {
|
||||
id: string;
|
||||
kind: MenuTreeNodeKind;
|
||||
k: MenuTreeNodeKind;
|
||||
}
|
||||
|
||||
export interface MenuTreeProjectNode extends MenuTreeBaseNode {
|
||||
kind: 'project';
|
||||
k: MenuTreeKind.PROJECT;
|
||||
}
|
||||
|
||||
export interface MenuTreeTagNode extends MenuTreeBaseNode {
|
||||
kind: 'tag';
|
||||
k: MenuTreeKind.TAG;
|
||||
}
|
||||
|
||||
export interface MenuTreeFolderNode extends MenuTreeBaseNode {
|
||||
kind: 'folder';
|
||||
k: MenuTreeKind.FOLDER;
|
||||
children: MenuTreeTreeNode[];
|
||||
name: string;
|
||||
isExpanded?: boolean;
|
||||
|
|
@ -39,7 +48,7 @@ export type MenuTreeViewNode =
|
|||
| MenuTreeViewTagNode;
|
||||
|
||||
export interface MenuTreeViewFolderNode {
|
||||
kind: 'folder';
|
||||
k: MenuTreeKind.FOLDER;
|
||||
id: string;
|
||||
name: string;
|
||||
isExpanded: boolean;
|
||||
|
|
@ -47,11 +56,11 @@ export interface MenuTreeViewFolderNode {
|
|||
}
|
||||
|
||||
export interface MenuTreeViewProjectNode {
|
||||
kind: 'project';
|
||||
k: MenuTreeKind.PROJECT;
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export interface MenuTreeViewTagNode {
|
||||
kind: 'tag';
|
||||
k: MenuTreeKind.TAG;
|
||||
tag: Tag;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { createReducer, on } from '@ngrx/store';
|
||||
import { MenuTreeState, MenuTreeTreeNode } from './menu-tree.model';
|
||||
import { MenuTreeKind, MenuTreeState, MenuTreeTreeNode } from './menu-tree.model';
|
||||
import { loadAllData } from '../../../root-store/meta/load-all-data.action';
|
||||
import {
|
||||
updateProjectTree,
|
||||
|
|
@ -25,7 +25,7 @@ const _deleteFolderFromTree = (
|
|||
return tree
|
||||
.filter((node) => node.id !== folderId)
|
||||
.map((node) => {
|
||||
if (node.kind === 'folder') {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
return {
|
||||
...node,
|
||||
children: _deleteFolderFromTree(node.children, folderId),
|
||||
|
|
@ -41,13 +41,13 @@ const _updateFolderInTree = (
|
|||
name: string,
|
||||
): MenuTreeTreeNode[] => {
|
||||
return tree.map((node) => {
|
||||
if (node.id === folderId && node.kind === 'folder') {
|
||||
if (node.id === folderId && node.k === MenuTreeKind.FOLDER) {
|
||||
return {
|
||||
...node,
|
||||
name,
|
||||
};
|
||||
}
|
||||
if (node.kind === 'folder') {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
return {
|
||||
...node,
|
||||
children: _updateFolderInTree(node.children, folderId, name),
|
||||
|
|
@ -85,20 +85,22 @@ export const menuTreeReducer = createReducer(
|
|||
on(deleteFolder, (state, { folderId, treeType }) => ({
|
||||
...state,
|
||||
projectTree:
|
||||
treeType === 'project'
|
||||
treeType === MenuTreeKind.PROJECT
|
||||
? _deleteFolderFromTree(state.projectTree, folderId)
|
||||
: state.projectTree,
|
||||
tagTree:
|
||||
treeType === 'tag' ? _deleteFolderFromTree(state.tagTree, folderId) : state.tagTree,
|
||||
treeType === MenuTreeKind.TAG
|
||||
? _deleteFolderFromTree(state.tagTree, folderId)
|
||||
: state.tagTree,
|
||||
})),
|
||||
on(updateFolder, (state, { folderId, name, treeType }) => ({
|
||||
...state,
|
||||
projectTree:
|
||||
treeType === 'project'
|
||||
treeType === MenuTreeKind.PROJECT
|
||||
? _updateFolderInTree(state.projectTree, folderId, name)
|
||||
: state.projectTree,
|
||||
tagTree:
|
||||
treeType === 'tag'
|
||||
treeType === MenuTreeKind.TAG
|
||||
? _updateFolderInTree(state.tagTree, folderId, name)
|
||||
: state.tagTree,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -1,16 +1,12 @@
|
|||
<div class="month-grid-container">
|
||||
<div class="weekday-header">Sun</div>
|
||||
<div class="weekday-header">Mon</div>
|
||||
<div class="weekday-header">Tue</div>
|
||||
<div class="weekday-header">Wed</div>
|
||||
<div class="weekday-header">Thu</div>
|
||||
<div class="weekday-header">Fri</div>
|
||||
<div class="weekday-header">Sat</div>
|
||||
@for (header of weekdayHeaders(); track $index) {
|
||||
<div class="weekday-header">{{ header }}</div>
|
||||
}
|
||||
|
||||
@for (day of daysToShow; track $index) {
|
||||
@for (day of daysToShow(); track $index) {
|
||||
@let weekIndex = getWeekIndex($index);
|
||||
@let dayIndex = getDayIndex($index);
|
||||
@if (weekIndex < weeksToShow) {
|
||||
@if (weekIndex < weeksToShow()) {
|
||||
<div
|
||||
class="month-day-cell {{ getDayClass(day) }}"
|
||||
[attr.data-day]="day"
|
||||
|
|
|
|||
|
|
@ -1,9 +1,15 @@
|
|||
import { ChangeDetectionStrategy, Component, inject, Input } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
computed,
|
||||
inject,
|
||||
input,
|
||||
LOCALE_ID,
|
||||
} from '@angular/core';
|
||||
import { ScheduleEvent } from '../schedule.model';
|
||||
import { ScheduleEventComponent } from '../schedule-event/schedule-event.component';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { DatePipe, formatDate } from '@angular/common';
|
||||
import { T } from '../../../t.const';
|
||||
import { DateService } from '../../../core/date/date.service';
|
||||
import { ScheduleService } from '../schedule.service';
|
||||
|
||||
@Component({
|
||||
|
|
@ -15,12 +21,33 @@ import { ScheduleService } from '../schedule.service';
|
|||
standalone: true,
|
||||
})
|
||||
export class ScheduleMonthComponent {
|
||||
private _dateService = inject(DateService);
|
||||
private _scheduleService = inject(ScheduleService);
|
||||
private _locale = inject(LOCALE_ID);
|
||||
|
||||
@Input() events: ScheduleEvent[] | null = [];
|
||||
@Input() daysToShow: string[] = [];
|
||||
@Input() weeksToShow: number = 6;
|
||||
readonly events = input<ScheduleEvent[] | null>([]);
|
||||
readonly daysToShow = input<string[]>([]);
|
||||
readonly weeksToShow = input<number>(6);
|
||||
readonly firstDayOfWeek = input<number>(1);
|
||||
|
||||
// Generate weekday headers based on firstDayOfWeek setting
|
||||
readonly weekdayHeaders = computed(() => {
|
||||
const firstDay = this.firstDayOfWeek();
|
||||
const headers: string[] = [];
|
||||
|
||||
// Create a date for each day of week (using a week starting on Sunday)
|
||||
// January 2, 2000 was a Sunday
|
||||
const sundayDate = new Date(2000, 0, 2);
|
||||
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const dayIndex = (firstDay + i) % 7;
|
||||
const date = new Date(sundayDate);
|
||||
date.setDate(sundayDate.getDate() + dayIndex);
|
||||
// 'EEE' format gives abbreviated day name (e.g., 'Mon', 'Tue')
|
||||
headers.push(formatDate(date, 'EEE', this._locale));
|
||||
}
|
||||
|
||||
return headers;
|
||||
});
|
||||
|
||||
T: typeof T = T;
|
||||
|
||||
|
|
@ -37,11 +64,11 @@ export class ScheduleMonthComponent {
|
|||
}
|
||||
|
||||
hasEventsForDay(day: string): boolean {
|
||||
return this._scheduleService.hasEventsForDay(day, this.events || []);
|
||||
return this._scheduleService.hasEventsForDay(day, this.events() || []);
|
||||
}
|
||||
|
||||
getEventsForDay(day: string): ScheduleEvent[] {
|
||||
return this._scheduleService.getEventsForDay(day, this.events || []);
|
||||
return this._scheduleService.getEventsForDay(day, this.events() || []);
|
||||
}
|
||||
|
||||
getEventDayStr(ev: ScheduleEvent): string | null {
|
||||
|
|
|
|||
109
src/app/features/schedule/schedule.service.spec.ts
Normal file
109
src/app/features/schedule/schedule.service.spec.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { ScheduleService } from './schedule.service';
|
||||
import { DateService } from '../../core/date/date.service';
|
||||
|
||||
describe('ScheduleService', () => {
|
||||
let service: ScheduleService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [ScheduleService, DateService],
|
||||
});
|
||||
service = TestBed.inject(ScheduleService);
|
||||
});
|
||||
|
||||
it('should be created', () => {
|
||||
expect(service).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('getMonthDaysToShow', () => {
|
||||
it('should return correct number of days', () => {
|
||||
const numberOfWeeks = 5;
|
||||
const firstDayOfWeek = 1; // Monday
|
||||
const result = service.getMonthDaysToShow(numberOfWeeks, firstDayOfWeek);
|
||||
expect(result.length).toBe(numberOfWeeks * 7);
|
||||
});
|
||||
|
||||
it('should start with the configured first day of week when firstDayOfWeek is Monday (1)', () => {
|
||||
const numberOfWeeks = 5;
|
||||
const firstDayOfWeek = 1; // Monday
|
||||
|
||||
// Mock the current date to a known value for testing
|
||||
const testDate = new Date(2025, 0, 15); // January 15, 2025 (Wednesday)
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(testDate);
|
||||
|
||||
const result = service.getMonthDaysToShow(numberOfWeeks, firstDayOfWeek);
|
||||
|
||||
// January 2025 starts on Wednesday (day 3)
|
||||
// With Monday as first day of week, the calendar should start from Dec 30, 2024 (Monday)
|
||||
// Parse the date string in local timezone by using the Date constructor with year, month, day
|
||||
const [year, month, day] = result[0].split('-').map(Number);
|
||||
const firstDayDate = new Date(year, month - 1, day);
|
||||
expect(firstDayDate.getDay()).toBe(1); // Monday
|
||||
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should start with the configured first day of week when firstDayOfWeek is Sunday (0)', () => {
|
||||
const numberOfWeeks = 5;
|
||||
const firstDayOfWeek = 0; // Sunday
|
||||
|
||||
// Mock the current date to a known value for testing
|
||||
const testDate = new Date(2025, 0, 15); // January 15, 2025 (Wednesday)
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(testDate);
|
||||
|
||||
const result = service.getMonthDaysToShow(numberOfWeeks, firstDayOfWeek);
|
||||
|
||||
// January 2025 starts on Wednesday (day 3)
|
||||
// With Sunday as first day of week, the calendar should start from Dec 29, 2024 (Sunday)
|
||||
// Parse the date string in local timezone by using the Date constructor with year, month, day
|
||||
const [year, month, day] = result[0].split('-').map(Number);
|
||||
const firstDayDate = new Date(year, month - 1, day);
|
||||
expect(firstDayDate.getDay()).toBe(0); // Sunday
|
||||
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should start with the configured first day of week when firstDayOfWeek is Saturday (6)', () => {
|
||||
const numberOfWeeks = 5;
|
||||
const firstDayOfWeek = 6; // Saturday
|
||||
|
||||
// Mock the current date to a known value for testing
|
||||
const testDate = new Date(2025, 0, 15); // January 15, 2025 (Wednesday)
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(testDate);
|
||||
|
||||
const result = service.getMonthDaysToShow(numberOfWeeks, firstDayOfWeek);
|
||||
|
||||
// January 2025 starts on Wednesday (day 3)
|
||||
// With Saturday as first day of week, the calendar should start from Dec 28, 2024 (Saturday)
|
||||
// Parse the date string in local timezone by using the Date constructor with year, month, day
|
||||
const [year, month, day] = result[0].split('-').map(Number);
|
||||
const firstDayDate = new Date(year, month - 1, day);
|
||||
expect(firstDayDate.getDay()).toBe(6); // Saturday
|
||||
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
|
||||
it('should default to Sunday (0) when no firstDayOfWeek is provided', () => {
|
||||
const numberOfWeeks = 5;
|
||||
|
||||
// Mock the current date to a known value for testing
|
||||
const testDate = new Date(2025, 0, 15); // January 15, 2025 (Wednesday)
|
||||
jasmine.clock().install();
|
||||
jasmine.clock().mockDate(testDate);
|
||||
|
||||
const result = service.getMonthDaysToShow(numberOfWeeks);
|
||||
|
||||
// Should default to Sunday as first day
|
||||
// Parse the date string in local timezone by using the Date constructor with year, month, day
|
||||
const [year, month, day] = result[0].split('-').map(Number);
|
||||
const firstDayDate = new Date(year, month - 1, day);
|
||||
expect(firstDayDate.getDay()).toBe(0); // Sunday
|
||||
|
||||
jasmine.clock().uninstall();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -19,18 +19,24 @@ export class ScheduleService {
|
|||
return daysToShow;
|
||||
}
|
||||
|
||||
getMonthDaysToShow(numberOfWeeks: number): string[] {
|
||||
getMonthDaysToShow(numberOfWeeks: number, firstDayOfWeek: number = 0): string[] {
|
||||
const today = new Date();
|
||||
const firstDayOfMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
|
||||
const firstSunday = new Date(firstDayOfMonth);
|
||||
firstSunday.setDate(firstDayOfMonth.getDate() - firstDayOfMonth.getDay());
|
||||
// Calculate the first day to show based on firstDayOfWeek setting
|
||||
// firstDayOfWeek: 0=Sunday, 1=Monday, 2=Tuesday, etc.
|
||||
const firstDayToShow = new Date(firstDayOfMonth);
|
||||
const monthStartDay = firstDayOfMonth.getDay(); // 0=Sunday, 1=Monday, etc.
|
||||
|
||||
// Calculate how many days to go back from the first of the month
|
||||
const daysToGoBack = (monthStartDay - firstDayOfWeek + 7) % 7;
|
||||
firstDayToShow.setDate(firstDayOfMonth.getDate() - daysToGoBack);
|
||||
|
||||
const totalDays = numberOfWeeks * 7;
|
||||
const daysToShow: string[] = [];
|
||||
for (let i = 0; i < totalDays; i++) {
|
||||
const currentDate = new Date(firstSunday);
|
||||
currentDate.setDate(firstSunday.getDate() + i);
|
||||
const currentDate = new Date(firstDayToShow);
|
||||
currentDate.setDate(firstDayToShow.getDate() + i);
|
||||
daysToShow.push(this._dateService.todayStr(currentDate.getTime()));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -65,6 +65,7 @@
|
|||
[events]="events()"
|
||||
[daysToShow]="daysToShow()"
|
||||
[weeksToShow]="weeksToShow()"
|
||||
[firstDayOfWeek]="firstDayOfWeek()"
|
||||
></schedule-month>
|
||||
} @else {
|
||||
<schedule-week
|
||||
|
|
|
|||
|
|
@ -10,10 +10,11 @@
|
|||
|
||||
// we need this to prevent problems with route ani and scroll behavior
|
||||
.scroll-wrapper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
header {
|
||||
|
|
@ -29,6 +30,8 @@ header {
|
|||
z-index: 10;
|
||||
color: var(--text-color);
|
||||
background: var(--bg-lighter);
|
||||
// to account for scrollbar
|
||||
padding-right: 11px;
|
||||
}
|
||||
|
||||
.main-controls {
|
||||
|
|
@ -39,7 +42,6 @@ header {
|
|||
}
|
||||
|
||||
.days {
|
||||
grid-auto-flow: column;
|
||||
display: grid;
|
||||
place-content: center;
|
||||
text-align: center;
|
||||
|
|
@ -74,19 +76,17 @@ header {
|
|||
.days.month-view {
|
||||
display: block;
|
||||
grid-template-columns: none;
|
||||
padding-bottom: 6px;
|
||||
|
||||
.month-header {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 8px 16px;
|
||||
padding: 6px 16px;
|
||||
|
||||
.month-title {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 6px;
|
||||
|
||||
@include mq(xs, max) {
|
||||
font-size: 16px;
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import { GlobalTrackingIntervalService } from '../../../core/global-tracking-int
|
|||
import {
|
||||
selectTimelineConfig,
|
||||
selectTimelineWorkStartEndHours,
|
||||
selectMiscConfig,
|
||||
} from '../../config/store/global-config.reducer';
|
||||
import { FH } from '../schedule.const';
|
||||
import { mapToScheduleDays } from '../map-schedule-data/map-to-schedule-days';
|
||||
|
|
@ -106,22 +107,30 @@ export class ScheduleComponent implements AfterViewInit {
|
|||
daysToShow = computed(() => {
|
||||
const count = this._daysToShowCount();
|
||||
const selectedView = this._currentTimeViewMode();
|
||||
const miscConfig = this._miscConfig();
|
||||
// Trigger re-computation when today changes
|
||||
this._todayDateStr();
|
||||
|
||||
if (selectedView === 'month') {
|
||||
return this.scheduleService.getMonthDaysToShow(count);
|
||||
const firstDayOfWeek = miscConfig?.firstDayOfWeek ?? 1; // Default to Monday
|
||||
return this.scheduleService.getMonthDaysToShow(count, firstDayOfWeek);
|
||||
}
|
||||
return this.scheduleService.getDaysToShow(count);
|
||||
});
|
||||
|
||||
weeksToShow = computed(() => Math.ceil(this.daysToShow().length / 7));
|
||||
|
||||
firstDayOfWeek = computed(() => {
|
||||
const miscConfig = this._miscConfig();
|
||||
return miscConfig?.firstDayOfWeek ?? 1; // Default to Monday
|
||||
});
|
||||
|
||||
private _timelineTasks = toSignal(this._store.pipe(select(selectTimelineTasks)));
|
||||
private _taskRepeatCfgs = toSignal(
|
||||
this._store.pipe(select(selectTaskRepeatCfgsWithAndWithoutStartTime)),
|
||||
);
|
||||
private _timelineConfig = toSignal(this._store.pipe(select(selectTimelineConfig)));
|
||||
private _miscConfig = toSignal(this._store.pipe(select(selectMiscConfig)));
|
||||
private _icalEvents = toSignal(this._calendarIntegrationService.icalEvents$, {
|
||||
initialValue: [],
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1021,6 +1021,35 @@ describe('shortSyntax', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('projects using special delimiters', () => {
|
||||
const taskTemplates = [
|
||||
'Task *',
|
||||
'Task * 10m',
|
||||
'Task * 1h / 2d',
|
||||
'Task * @tomorrow',
|
||||
'Task * @in 1 day',
|
||||
'Task * #A',
|
||||
];
|
||||
|
||||
const projects = ['a+b', '10 contracts', 'c++', 'my@email.com', 'issue#123'].map(
|
||||
(title) => ({ id: title, title }) as Project,
|
||||
);
|
||||
|
||||
for (const taskTemplate of taskTemplates) {
|
||||
for (const project of projects) {
|
||||
const taskTitle = taskTemplate.replaceAll('*', `+${project.title}`);
|
||||
it(`should parse project "${project.title}" from "${taskTitle}"`, () => {
|
||||
const task = {
|
||||
...TASK,
|
||||
title: taskTitle,
|
||||
};
|
||||
const result = shortSyntax(task, CONFIG, ALL_TAGS, projects);
|
||||
expect(result?.projectId).toBe(project.id);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// This group of tests address Chrono's parsing the format "<date> <month> <yy}>" as year
|
||||
// This will cause unintended parsing result when the date syntax is used together with the time estimate syntax
|
||||
// https://github.com/johannesjo/super-productivity/issues/4194
|
||||
|
|
|
|||
|
|
@ -66,7 +66,13 @@ customDateParser.refiners.push({
|
|||
},
|
||||
});
|
||||
|
||||
const SHORT_SYNTAX_PROJECT_REG_EX = new RegExp(`\\${CH_PRO}[^${ALL_SPECIAL}]+`, 'gi');
|
||||
// The following project name extraction pattern attempts to improve on the
|
||||
// previous version by not immediately terminating upon encountering a short
|
||||
// syntax delimiting character and looks ahead to consider usage context
|
||||
const SHORT_SYNTAX_PROJECT_REG_EX = new RegExp(
|
||||
`\\${CH_PRO}((?:(?!\\s+(?:\\${CH_TAG}|\\${CH_DUE}|t?\\d+[mh]\\b)).)+)`,
|
||||
'i',
|
||||
);
|
||||
const SHORT_SYNTAX_TAGS_REG_EX = new RegExp(`\\${CH_TAG}[^${ALL_SPECIAL}|\\s]+`, 'gi');
|
||||
|
||||
// Literal notation: /\@[^\+|\#|\@]/gi
|
||||
|
|
@ -198,7 +204,10 @@ const parseProjectChanges = (
|
|||
|
||||
if (existingProject) {
|
||||
return {
|
||||
title: task.title?.replace(`${CH_PRO}${projectTitle}`, '').trim(),
|
||||
title: task.title
|
||||
?.replace(`${CH_PRO}${projectTitle}`, '')
|
||||
.trim()
|
||||
.replace(' ', ' '),
|
||||
projectId: existingProject.id,
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,12 +173,17 @@
|
|||
@if (customizedUndoneTasks(); as customized) {
|
||||
@if (customized.grouped) {
|
||||
@for (group of customized.grouped | keyvalue; track group.key) {
|
||||
<h3>{{ group.key }}</h3>
|
||||
<task-list
|
||||
[tasks]="group.value"
|
||||
listId="PARENT"
|
||||
listModelId="UNDONE"
|
||||
></task-list>
|
||||
<collapsible
|
||||
[title]="group.key"
|
||||
[isIconBefore]="true"
|
||||
[isExpanded]="true"
|
||||
>
|
||||
<task-list
|
||||
[tasks]="group.value"
|
||||
listId="PARENT"
|
||||
listModelId="UNDONE"
|
||||
></task-list>
|
||||
</collapsible>
|
||||
}
|
||||
} @else {
|
||||
<task-list
|
||||
|
|
@ -260,7 +265,7 @@
|
|||
</div>
|
||||
}
|
||||
|
||||
@if (!isPlanningMode() || hasDoneTasks()) {
|
||||
@if (hasDoneTasks()) {
|
||||
@let nrOfDoneArchived =
|
||||
doneTasks()?.length === 0
|
||||
? (workContextService.doneTodayArchived$ | async)
|
||||
|
|
|
|||
|
|
@ -91,14 +91,35 @@ export class WebdavApi {
|
|||
);
|
||||
|
||||
// Get revision from Last-Modified
|
||||
const lastModified =
|
||||
let lastModified =
|
||||
response.headers['last-modified'] || response.headers['Last-Modified'];
|
||||
|
||||
// Get ETag for legacy compatibility
|
||||
const etag = response.headers['etag'] || response.headers['ETag'];
|
||||
const legacyRev = etag ? this._cleanRev(etag) : undefined;
|
||||
let etag = response.headers['etag'] || response.headers['ETag'];
|
||||
let legacyRev = etag ? this._cleanRev(etag) : undefined;
|
||||
|
||||
const rev = lastModified || '';
|
||||
let rev = lastModified || '';
|
||||
|
||||
// Fallback: If Last-Modified and ETag are both missing, use PROPFIND to retrive them as it is defined by webDAV protocol
|
||||
// Some servers (like OpenList/Alist) may not return these headers on GET
|
||||
if (!lastModified && !etag) {
|
||||
PFLog.verbose(
|
||||
`${WebdavApi.L}.download() missing Last-Modified/ETag, trying PROPFIND fallback for ${path}`,
|
||||
);
|
||||
try {
|
||||
const meta = await this.getFileMeta(path, null, false);
|
||||
if (!lastModified && meta.lastmod) {
|
||||
lastModified = meta.lastmod;
|
||||
rev = lastModified;
|
||||
}
|
||||
if (!etag && meta.etag) {
|
||||
etag = meta.etag;
|
||||
legacyRev = this._cleanRev(etag);
|
||||
}
|
||||
} catch (e) {
|
||||
PFLog.warn(`${WebdavApi.L}.download() PROPFIND fallback failed for ${path}`, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (!rev) {
|
||||
PFLog.err(`${WebdavApi.L}.download() no Last-Modified found for ${path}`);
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { INBOX_PROJECT } from '../../features/project/project.const';
|
|||
import { autoFixTypiaErrors } from './auto-fix-typia-errors';
|
||||
import { IValidation } from 'typia';
|
||||
import { PFLog } from '../../core/log';
|
||||
import { repairMenuTree } from './repair-menu-tree';
|
||||
|
||||
// TODO improve later
|
||||
const ENTITY_STATE_KEYS: (keyof AppDataCompleteLegacy)[] = ALL_ENTITY_MODEL_KEYS;
|
||||
|
|
@ -73,6 +74,7 @@ export const dataRepair = (
|
|||
dataOut = _removeNonExistentProjectIdsFromTasks(dataOut);
|
||||
dataOut = _removeNonExistentTagsFromTasks(dataOut);
|
||||
dataOut = _addInboxProjectIdIfNecessary(dataOut);
|
||||
dataOut = _repairMenuTree(dataOut);
|
||||
dataOut = autoFixTypiaErrors(dataOut, errors);
|
||||
|
||||
// console.timeEnd('dataRepair');
|
||||
|
|
@ -805,3 +807,16 @@ const _cleanupOrphanedSubTasks = (data: AppDataCompleteNew): AppDataCompleteNew
|
|||
|
||||
return data;
|
||||
};
|
||||
|
||||
const _repairMenuTree = (data: AppDataCompleteNew): AppDataCompleteNew => {
|
||||
if (!data.menuTree) {
|
||||
return data;
|
||||
}
|
||||
|
||||
const validProjectIds = new Set<string>(data.project.ids as string[]);
|
||||
const validTagIds = new Set<string>(data.tag.ids as string[]);
|
||||
|
||||
data.menuTree = repairMenuTree(data.menuTree, validProjectIds, validTagIds);
|
||||
|
||||
return data;
|
||||
};
|
||||
|
|
|
|||
191
src/app/pfapi/repair/repair-menu-tree.spec.ts
Normal file
191
src/app/pfapi/repair/repair-menu-tree.spec.ts
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
import { repairMenuTree } from './repair-menu-tree';
|
||||
import {
|
||||
MenuTreeKind,
|
||||
MenuTreeState,
|
||||
} from '../../features/menu-tree/store/menu-tree.model';
|
||||
|
||||
describe('repairMenuTree', () => {
|
||||
it('should remove orphaned project references from projectTree', () => {
|
||||
const validProjectIds = new Set(['project1', 'project2']);
|
||||
const validTagIds = new Set<string>();
|
||||
|
||||
const menuTree: MenuTreeState = {
|
||||
projectTree: [
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project1' },
|
||||
{ k: MenuTreeKind.PROJECT, id: 'orphaned-project' },
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project2' },
|
||||
],
|
||||
tagTree: [],
|
||||
};
|
||||
|
||||
const result = repairMenuTree(menuTree, validProjectIds, validTagIds);
|
||||
|
||||
expect(result.projectTree.length).toBe(2);
|
||||
expect(result.projectTree).toEqual([
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project1' },
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should remove orphaned tag references from tagTree', () => {
|
||||
const validProjectIds = new Set<string>();
|
||||
const validTagIds = new Set(['tag1', 'tag2']);
|
||||
|
||||
const menuTree: MenuTreeState = {
|
||||
projectTree: [],
|
||||
tagTree: [
|
||||
{ k: MenuTreeKind.TAG, id: 'tag1' },
|
||||
{ k: MenuTreeKind.TAG, id: 'orphaned-tag' },
|
||||
{ k: MenuTreeKind.TAG, id: 'tag2' },
|
||||
],
|
||||
};
|
||||
|
||||
const result = repairMenuTree(menuTree, validProjectIds, validTagIds);
|
||||
|
||||
expect(result.tagTree.length).toBe(2);
|
||||
expect(result.tagTree).toEqual([
|
||||
{ k: MenuTreeKind.TAG, id: 'tag1' },
|
||||
{ k: MenuTreeKind.TAG, id: 'tag2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should keep folders even if they end up empty', () => {
|
||||
const validProjectIds = new Set(['project1']);
|
||||
const validTagIds = new Set<string>();
|
||||
|
||||
const menuTree: MenuTreeState = {
|
||||
projectTree: [
|
||||
{
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: 'folder1',
|
||||
name: 'Folder 1',
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project1' },
|
||||
{ k: MenuTreeKind.PROJECT, id: 'orphaned-project' },
|
||||
],
|
||||
},
|
||||
],
|
||||
tagTree: [],
|
||||
};
|
||||
|
||||
const result = repairMenuTree(menuTree, validProjectIds, validTagIds);
|
||||
|
||||
expect(result.projectTree.length).toBe(1);
|
||||
expect(result.projectTree[0].k).toBe(MenuTreeKind.FOLDER);
|
||||
if (result.projectTree[0].k === MenuTreeKind.FOLDER) {
|
||||
expect(result.projectTree[0].id).toBe('folder1');
|
||||
expect(result.projectTree[0].children.length).toBe(1);
|
||||
expect(result.projectTree[0].children[0]).toEqual({
|
||||
k: MenuTreeKind.PROJECT,
|
||||
id: 'project1',
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it('should handle nested folders correctly', () => {
|
||||
const validProjectIds = new Set(['project1', 'project2']);
|
||||
const validTagIds = new Set<string>();
|
||||
|
||||
const menuTree: MenuTreeState = {
|
||||
projectTree: [
|
||||
{
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: 'parent-folder',
|
||||
name: 'Parent',
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project1' },
|
||||
{
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: 'nested-folder',
|
||||
name: 'Nested',
|
||||
isExpanded: false,
|
||||
children: [
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project2' },
|
||||
{ k: MenuTreeKind.PROJECT, id: 'orphaned-nested-project' },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
tagTree: [],
|
||||
};
|
||||
|
||||
const result = repairMenuTree(menuTree, validProjectIds, validTagIds);
|
||||
|
||||
expect(result.projectTree.length).toBe(1);
|
||||
if (result.projectTree[0].k === MenuTreeKind.FOLDER) {
|
||||
expect(result.projectTree[0].children.length).toBe(2);
|
||||
const nestedFolder = result.projectTree[0].children[1];
|
||||
if (nestedFolder.k === MenuTreeKind.FOLDER) {
|
||||
expect(nestedFolder.children.length).toBe(1);
|
||||
expect(nestedFolder.children[0]).toEqual({
|
||||
k: MenuTreeKind.PROJECT,
|
||||
id: 'project2',
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('should return empty arrays for invalid tree structures', () => {
|
||||
const validProjectIds = new Set(['project1']);
|
||||
const validTagIds = new Set<string>();
|
||||
|
||||
const menuTree: MenuTreeState = {
|
||||
projectTree: null as any,
|
||||
tagTree: undefined as any,
|
||||
};
|
||||
|
||||
const result = repairMenuTree(menuTree, validProjectIds, validTagIds);
|
||||
|
||||
expect(result.projectTree).toEqual([]);
|
||||
expect(result.tagTree).toEqual([]);
|
||||
});
|
||||
|
||||
it('should preserve valid folder structure', () => {
|
||||
const validProjectIds = new Set(['project1', 'project2']);
|
||||
const validTagIds = new Set<string>();
|
||||
|
||||
const menuTree: MenuTreeState = {
|
||||
projectTree: [
|
||||
{
|
||||
k: MenuTreeKind.FOLDER,
|
||||
id: 'folder1',
|
||||
name: 'Work Projects',
|
||||
isExpanded: true,
|
||||
children: [
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project1' },
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
tagTree: [],
|
||||
};
|
||||
|
||||
const result = repairMenuTree(menuTree, validProjectIds, validTagIds);
|
||||
|
||||
expect(result.projectTree).toEqual(menuTree.projectTree);
|
||||
});
|
||||
|
||||
it('should remove mismatched node kinds', () => {
|
||||
const validProjectIds = new Set(['project1']);
|
||||
const validTagIds = new Set(['tag1']);
|
||||
|
||||
const menuTree: MenuTreeState = {
|
||||
projectTree: [
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project1' },
|
||||
{ k: MenuTreeKind.TAG, id: 'tag1' } as any,
|
||||
],
|
||||
tagTree: [
|
||||
{ k: MenuTreeKind.TAG, id: 'tag1' },
|
||||
{ k: MenuTreeKind.PROJECT, id: 'project1' } as any,
|
||||
],
|
||||
};
|
||||
|
||||
const result = repairMenuTree(menuTree, validProjectIds, validTagIds);
|
||||
|
||||
expect(result.projectTree).toEqual([{ k: MenuTreeKind.PROJECT, id: 'project1' }]);
|
||||
expect(result.tagTree).toEqual([{ k: MenuTreeKind.TAG, id: 'tag1' }]);
|
||||
});
|
||||
});
|
||||
74
src/app/pfapi/repair/repair-menu-tree.ts
Normal file
74
src/app/pfapi/repair/repair-menu-tree.ts
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
import {
|
||||
MenuTreeKind,
|
||||
MenuTreeState,
|
||||
MenuTreeTreeNode,
|
||||
} from '../../features/menu-tree/store/menu-tree.model';
|
||||
import { PFLog } from '../../core/log';
|
||||
|
||||
/**
|
||||
* Repairs menuTree by removing orphaned project/tag references
|
||||
* @param menuTree The menuTree state to repair
|
||||
* @param validProjectIds Set of valid project IDs
|
||||
* @param validTagIds Set of valid tag IDs
|
||||
* @returns Repaired menuTree state
|
||||
*/
|
||||
export const repairMenuTree = (
|
||||
menuTree: MenuTreeState,
|
||||
validProjectIds: Set<string>,
|
||||
validTagIds: Set<string>,
|
||||
): MenuTreeState => {
|
||||
PFLog.log('Repairing menuTree - removing orphaned references');
|
||||
|
||||
/**
|
||||
* Recursively filters tree nodes, removing orphaned project/tag references
|
||||
* and empty folders
|
||||
*/
|
||||
const filterTreeNodes = (
|
||||
nodes: MenuTreeTreeNode[],
|
||||
treeType: 'projectTree' | 'tagTree',
|
||||
): MenuTreeTreeNode[] => {
|
||||
const filtered: MenuTreeTreeNode[] = [];
|
||||
|
||||
for (const node of nodes) {
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
const filteredChildren = filterTreeNodes(node.children, treeType);
|
||||
filtered.push({
|
||||
...node,
|
||||
children: filteredChildren,
|
||||
});
|
||||
} else if (treeType === 'projectTree' && node.k === MenuTreeKind.PROJECT) {
|
||||
// Keep project only if it exists
|
||||
if (validProjectIds.has(node.id)) {
|
||||
filtered.push(node);
|
||||
} else {
|
||||
PFLog.log(`Removing orphaned project reference ${node.id} from ${treeType}`);
|
||||
}
|
||||
} else if (treeType === 'tagTree' && node.k === MenuTreeKind.TAG) {
|
||||
// Keep tag only if it exists
|
||||
if (validTagIds.has(node.id)) {
|
||||
filtered.push(node);
|
||||
} else {
|
||||
PFLog.log(`Removing orphaned tag reference ${node.id} from ${treeType}`);
|
||||
}
|
||||
} else {
|
||||
// kind mismatch or unknown
|
||||
PFLog.warn(`Removing invalid node from ${treeType}:`, node);
|
||||
}
|
||||
}
|
||||
|
||||
return filtered;
|
||||
};
|
||||
|
||||
const repairedProjectTree = Array.isArray(menuTree.projectTree)
|
||||
? filterTreeNodes(menuTree.projectTree, 'projectTree')
|
||||
: [];
|
||||
|
||||
const repairedTagTree = Array.isArray(menuTree.tagTree)
|
||||
? filterTreeNodes(menuTree.tagTree, 'tagTree')
|
||||
: [];
|
||||
|
||||
return {
|
||||
projectTree: repairedProjectTree,
|
||||
tagTree: repairedTagTree,
|
||||
};
|
||||
};
|
||||
|
|
@ -2,6 +2,7 @@ import { devError } from '../../util/dev-error';
|
|||
import { environment } from '../../../environments/environment';
|
||||
import { AppDataCompleteNew } from '../pfapi-config';
|
||||
import { PFLog } from '../../core/log';
|
||||
import { MenuTreeKind } from '../../features/menu-tree/store/menu-tree.model';
|
||||
|
||||
let errorCount = 0;
|
||||
let lastValidityError: string;
|
||||
|
|
@ -44,6 +45,11 @@ export const isRelatedModelDataValid = (d: AppDataCompleteNew): boolean => {
|
|||
return false;
|
||||
}
|
||||
|
||||
// Validate menuTree
|
||||
if (!validateMenuTree(d, projectIds, tagIds)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
|
|
@ -357,3 +363,102 @@ const validateReminders = (d: AppDataCompleteNew): boolean => {
|
|||
|
||||
return true;
|
||||
};
|
||||
|
||||
const validateMenuTree = (
|
||||
d: AppDataCompleteNew,
|
||||
projectIds: Set<string>,
|
||||
tagIds: Set<string>,
|
||||
): boolean => {
|
||||
// Recursive function to validate tree nodes
|
||||
const validateTreeNodes = (
|
||||
nodes: any[],
|
||||
treeType: 'projectTree' | 'tagTree',
|
||||
): boolean => {
|
||||
for (const node of nodes) {
|
||||
if (!node || typeof node !== 'object') {
|
||||
_validityError(`Invalid menuTree node in ${treeType}`, { node, d });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (node.k === MenuTreeKind.FOLDER) {
|
||||
// Validate folder structure
|
||||
if (!node.id || !node.name) {
|
||||
_validityError(`Invalid folder node in ${treeType} - missing id or name`, {
|
||||
node,
|
||||
d,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(node.children)) {
|
||||
_validityError(`Invalid folder node in ${treeType} - children not array`, {
|
||||
node,
|
||||
d,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
// Recursively validate children
|
||||
if (!validateTreeNodes(node.children, treeType)) {
|
||||
return false;
|
||||
}
|
||||
} else if (treeType === 'projectTree' && node.k === MenuTreeKind.PROJECT) {
|
||||
// Validate project reference
|
||||
if (!node.id) {
|
||||
_validityError(`Project node in menuTree missing id`, { node, d });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!projectIds.has(node.id)) {
|
||||
_validityError(
|
||||
`Orphaned project reference in menuTree - project ${node.id} doesn't exist`,
|
||||
{ node, treeType, d },
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else if (treeType === 'tagTree' && node.k === MenuTreeKind.TAG) {
|
||||
// Validate tag reference
|
||||
if (!node.id) {
|
||||
_validityError(`Tag node in menuTree missing id`, { node, d });
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tagIds.has(node.id)) {
|
||||
_validityError(
|
||||
`Orphaned tag reference in menuTree - tag ${node.id} doesn't exist`,
|
||||
{ node, treeType, d },
|
||||
);
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
_validityError(`Invalid node kind in ${treeType}: ${node.k}`, { node, d });
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// Validate projectTree
|
||||
if (d.menuTree?.projectTree) {
|
||||
if (!Array.isArray(d.menuTree.projectTree)) {
|
||||
_validityError('menuTree.projectTree is not an array', { d });
|
||||
return false;
|
||||
}
|
||||
if (!validateTreeNodes(d.menuTree.projectTree, 'projectTree')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate tagTree
|
||||
if (d.menuTree?.tagTree) {
|
||||
if (!Array.isArray(d.menuTree.tagTree)) {
|
||||
_validityError('menuTree.tagTree is not an array', { d });
|
||||
return false;
|
||||
}
|
||||
if (!validateTreeNodes(d.menuTree.tagTree, 'tagTree')) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -879,6 +879,24 @@ const T = {
|
|||
TOOLTIP_CREATE: 'F.PROJECT_FOLDER.TOOLTIP_CREATE',
|
||||
TOOLTIP_VISIBILITY: 'F.PROJECT_FOLDER.TOOLTIP_VISIBILITY',
|
||||
},
|
||||
TAG_FOLDER: {
|
||||
DIALOG: {
|
||||
CREATE_TITLE: 'F.TAG_FOLDER.DIALOG.CREATE_TITLE',
|
||||
EDIT_TITLE: 'F.TAG_FOLDER.DIALOG.EDIT_TITLE',
|
||||
NAME_LABEL: 'F.TAG_FOLDER.DIALOG.NAME_LABEL',
|
||||
NAME_PLACEHOLDER: 'F.TAG_FOLDER.DIALOG.NAME_PLACEHOLDER',
|
||||
NAME_REQUIRED: 'F.TAG_FOLDER.DIALOG.NAME_REQUIRED',
|
||||
PARENT_LABEL: 'F.TAG_FOLDER.DIALOG.PARENT_LABEL',
|
||||
NO_PARENT: 'F.TAG_FOLDER.DIALOG.NO_PARENT',
|
||||
},
|
||||
SELECT: {
|
||||
LABEL: 'F.TAG_FOLDER.SELECT.LABEL',
|
||||
PLACEHOLDER: 'F.TAG_FOLDER.SELECT.PLACEHOLDER',
|
||||
NO_PARENT: 'F.TAG_FOLDER.SELECT.NO_PARENT',
|
||||
},
|
||||
CONFIRM_DELETE: 'F.TAG_FOLDER.CONFIRM_DELETE',
|
||||
TOOLTIP_CREATE: 'F.TAG_FOLDER.TOOLTIP_CREATE',
|
||||
},
|
||||
QUICK_HISTORY: {
|
||||
NO_DATA: 'F.QUICK_HISTORY.NO_DATA',
|
||||
PAGE_TITLE: 'F.QUICK_HISTORY.PAGE_TITLE',
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
:host-context(.isMac.isElectron) {
|
||||
padding-top: var(--mac-title-bar-padding);
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
|
|
|
|||
|
|
@ -1,26 +1,40 @@
|
|||
import { Pipe, PipeTransform } from '@angular/core';
|
||||
|
||||
@Pipe({ name: 'sort' })
|
||||
@Pipe({ name: 'sort', standalone: true })
|
||||
export class SortPipe implements PipeTransform {
|
||||
transform<T extends Record<string, unknown>>(
|
||||
array: T[],
|
||||
transform<T>(
|
||||
array: readonly T[] | null | undefined,
|
||||
field: keyof T,
|
||||
reverse: boolean = false,
|
||||
): T[] {
|
||||
const f = reverse ? -1 : 1;
|
||||
|
||||
if (!Array.isArray(array)) {
|
||||
return array;
|
||||
if (!Array.isArray(array) || array.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const factor = reverse ? -1 : 1;
|
||||
const arr = [...array];
|
||||
arr.sort((a: T, b: T) => {
|
||||
if (a[field] < b[field]) {
|
||||
return -1 * f;
|
||||
} else if (a[field] > b[field]) {
|
||||
return 1 * f;
|
||||
} else {
|
||||
|
||||
arr.sort((a, b) => {
|
||||
const aValue = a[field] as unknown;
|
||||
const bValue = b[field] as unknown;
|
||||
|
||||
if (aValue == null && bValue == null) {
|
||||
return 0;
|
||||
}
|
||||
if (aValue == null) {
|
||||
return -1 * factor;
|
||||
}
|
||||
if (bValue == null) {
|
||||
return 1 * factor;
|
||||
}
|
||||
|
||||
if (aValue < bValue) {
|
||||
return -1 * factor;
|
||||
}
|
||||
if (aValue > bValue) {
|
||||
return 1 * factor;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return arr;
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@
|
|||
class="item"
|
||||
cdkDrag
|
||||
[cdkDragData]="node.id"
|
||||
[cdkDragStartDelay]="IS_TOUCH_PRIMARY ? DRAG_DELAY_FOR_TOUCH : 0"
|
||||
[ngClass]="{
|
||||
'is-folder': folder,
|
||||
'is-dragging': draggingId() === node.id,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ import { TreeIndicatorService } from './tree-indicator.service';
|
|||
import { TREE_CONSTANTS } from './tree-constants';
|
||||
import { assertTreeId } from './tree-guards';
|
||||
import { expandCollapseAni } from './tree.animations';
|
||||
import { DRAG_DELAY_FOR_TOUCH } from '../../app.constants';
|
||||
import { IS_TOUCH_PRIMARY } from '../../util/is-mouse-primary';
|
||||
|
||||
@Component({
|
||||
selector: 'tree-dnd',
|
||||
|
|
@ -72,6 +74,8 @@ export class TreeDndComponent<TData = unknown> {
|
|||
readonly isDragInvalid = signal<boolean>(false);
|
||||
readonly isRootOver = signal<boolean>(false);
|
||||
readonly indicatorStyle = this._indicatorService.indicatorStyle;
|
||||
protected readonly DRAG_DELAY_FOR_TOUCH = DRAG_DELAY_FOR_TOUCH;
|
||||
protected readonly IS_TOUCH_PRIMARY = IS_TOUCH_PRIMARY;
|
||||
|
||||
// === PRIVATE STATE ===
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -17,10 +17,18 @@
|
|||
"MSG": "Möchten Sie Super Productivity als PWA installieren?"
|
||||
},
|
||||
"B_OFFLINE": "Sie sind vom Internet getrennt. Das Synchronisieren und Anfordern von Daten des Issue-Providers funktioniert nicht.",
|
||||
"CONTEXT_MENU": {
|
||||
"CHANGE_BACKGROUND": "Hintergrund ändern"
|
||||
},
|
||||
"UPDATE_MAIN_MODEL": "Super Productivity hat ein großes Update bekommen! Einige Migrationen für Ihre Daten ist erforderlich. Bitte beachten Sie, dass Ihre Daten dadurch nicht mit älteren Versionen der App kompatibel sind.",
|
||||
"UPDATE_MAIN_MODEL_NO_UPDATE": "Kein Modellupdate ausgewählt. Bitte beachten Sie, dass Sie ein Downgrade auf die letzte Version durchführen müssen, wenn Sie das Modell-Upgrade nicht durchführen möchten.",
|
||||
"UPDATE_WEB_APP": "Neue Version verfügbar. Neue Version laden?"
|
||||
},
|
||||
"BN": {
|
||||
"SHOW_ISSUE_PANEL": "Vorgangspanel anzeigen",
|
||||
"SHOW_NOTES": "Projekt-Notizen anzeigen",
|
||||
"SHOW_TASK_VIEW_CUSTOMIZER_PANEL": "Filter-/Gruppen-/Sortierfenster anzeigen"
|
||||
},
|
||||
"BL": {
|
||||
"NO_TASKS": "Derzeit befinden sich keine Aufgaben in Ihrem Backlog"
|
||||
},
|
||||
|
|
@ -38,6 +46,13 @@
|
|||
"PRESS_ENTER_AGAIN": "Drücken Sie zum Speichern erneut die Eingabetaste",
|
||||
"TOMORROW": "Morgen"
|
||||
},
|
||||
"DIALOG_UNSPLASH_PICKER": {
|
||||
"TITLE": "Hintergrundbild von Unsplash auswählen",
|
||||
"SEARCH_LABEL": "Bilder suchen",
|
||||
"SEARCH_PLACEHOLDER": "z.B., Natur, Berge, abstrakt",
|
||||
"NO_RESULTS": "Keine Bilder gefunden. Versuchen Sie einen anderen Suchbegriff.",
|
||||
"BY": "von"
|
||||
},
|
||||
"F": {
|
||||
"ATTACHMENT": {
|
||||
"DIALOG_EDIT": {
|
||||
|
|
@ -147,6 +162,10 @@
|
|||
"TXT_PAST": "<strong>{{title}}</strong> startete um <strong>{{start}}</strong> !",
|
||||
"TXT_PAST_MULTIPLE": "<strong>{{title}}</strong> gestartet um <strong>{{start}}</strong> !<br> (und {{nrOfOtherBanners}} weitere Ereignisse sind fällig)"
|
||||
},
|
||||
"EVENT_STRINGS": {
|
||||
"EVENT_STR": "Ereignis",
|
||||
"EVENTS_STR": "Ereignisse"
|
||||
},
|
||||
"S": {
|
||||
"CAL_PROVIDER_ERROR": "Fehler beim Kalender-Provider: {{errTxt}}"
|
||||
}
|
||||
|
|
@ -192,6 +211,9 @@
|
|||
"FOCUS_MODE": {
|
||||
"B": {
|
||||
"SESSION_RUNNING": "Fokussierungssitzung läuft",
|
||||
"POMODORO_SESSION_RUNNING": "Pomodoro Sitzung #{{cycleNr}} läuft",
|
||||
"BREAK_RUNNING": "Die Pause läuft",
|
||||
"POMODORO_BREAK_RUNNING": "Die Pause #{{cycleNr}} läuft",
|
||||
"TO_FOCUS_OVERLAY": "Zur Fokussierungsüberlagerung"
|
||||
},
|
||||
"BACK_TO_PLANNING": "Zurück zur Planung",
|
||||
|
|
@ -216,11 +238,23 @@
|
|||
"SELECT_ANOTHER_TASK": "Wählen Sie eine andere Aufgabe aus",
|
||||
"SELECT_TASK": "Wählen Sie die Aufgabe aus, auf die Sie sich konzentrieren möchten",
|
||||
"SESSION_COMPLETED": "Fokussierungssitzung abgeschlossen!",
|
||||
"POMODORO_SESSION_COMPLETED": "Pomodoro Session Completed!",
|
||||
"SET_FOCUS_SESSION_DURATION": "Legen Sie die Dauer der Fokussitzung fest",
|
||||
"SHOW_HIDE_NOTES_AND_ATTACHMENTS": "Aufgabennotizen und Anhänge ein-/ausblenden",
|
||||
"START_FOCUS_SESSION": "Starten Sie die Fokussitzung",
|
||||
"START_NEXT_FOCUS_SESSION": "Starten Sie die nächste Fokussitzung",
|
||||
"WORKED_FOR": "Sie arbeiten bereits seit"
|
||||
"WORKED_FOR": "Sie arbeiten bereits seit",
|
||||
"BREAK_RELAX_MSG": "Nehmen Sie sich einen Moment Zeit zum Entspannen",
|
||||
"SKIP_BREAK": "Pause überspringen",
|
||||
"CONTINUE_TO_NEXT_SESSION": "Weiter zur nächsten Sitzung",
|
||||
"FOCUS_TIME_TOOLTIP": "Fokus Zeit",
|
||||
"CURRENT_SESSION_TIME_TOOLTIP": "Aktuelle Sitzungszeit",
|
||||
"COMPLETE_SESSION": "Vollständige Sitzung",
|
||||
"CONTINUE_SESSION": "Sitzung fortsetzen",
|
||||
"LONG_BREAK": "Lange Pause",
|
||||
"SHORT_BREAK": "Kurze Pause",
|
||||
"LONG_BREAK_TITLE": "Lange Pause - Zyklus {{cycle}}",
|
||||
"SHORT_BREAK_TITLE": "Kurze Pause - Zyklus {{cycle}}"
|
||||
},
|
||||
"GITEA": {
|
||||
"DIALOG_INITIAL": {
|
||||
|
|
@ -831,6 +865,24 @@
|
|||
"TOOLTIP_CREATE": "Projektordner erstellen",
|
||||
"TOOLTIP_VISIBILITY": "Projekte anzeigen/ausblenden"
|
||||
},
|
||||
"TAG_FOLDER": {
|
||||
"DIALOG": {
|
||||
"CREATE_TITLE": "Ordner erstellen",
|
||||
"EDIT_TITLE": "Ordner bearbeiten",
|
||||
"NAME_LABEL": "Ordnername",
|
||||
"NAME_PLACEHOLDER": "Ordnernamen eingeben",
|
||||
"NAME_REQUIRED": "Ordnername ist erforderlich",
|
||||
"PARENT_LABEL": "Übergeordneter Ordner",
|
||||
"NO_PARENT": "Kein übergeordneter Ordner (Root-Ebene)"
|
||||
},
|
||||
"SELECT": {
|
||||
"LABEL": "Ordner",
|
||||
"PLACEHOLDER": "Ordner wählen",
|
||||
"NO_PARENT": "Kein Ordner (Root-Ebene)"
|
||||
},
|
||||
"CONFIRM_DELETE": "Möchtest du den Ordner \"{{title}}\" wirklich löschen? Alle Tags in diesem Ordner werden auf die oberste Ebene verschoben.",
|
||||
"TOOLTIP_CREATE": "Tag-Ordner erstellen"
|
||||
},
|
||||
"QUICK_HISTORY": {
|
||||
"NO_DATA": "Keine Daten für das laufende Jahr",
|
||||
"PAGE_TITLE": "Schnellverlauf",
|
||||
|
|
@ -883,12 +935,14 @@
|
|||
},
|
||||
"END": "Arbeitsende",
|
||||
"LUNCH_BREAK": "Mittagspause",
|
||||
"MONTH": "Monat",
|
||||
"NO_TASKS": "Derzeit gibt es keine Aufgaben. Bitte fügen Sie einige Aufgaben über die Schaltfläche + in der oberen Leiste hinzu.",
|
||||
"NOW": "Jetzt",
|
||||
"PLAN_END_DAY": "Plan am Ende des Tages",
|
||||
"PLAN_START_DAY": "Plan zu Beginn des Tages",
|
||||
"START": "Arbeitsbeginn",
|
||||
"TASK_PROJECTION_INFO": "Zukunftsprojektion einer geplanten wiederkehrenden Aufgabe"
|
||||
"TASK_PROJECTION_INFO": "Zukunftsprojektion einer geplanten wiederkehrenden Aufgabe",
|
||||
"WEEK": "Woche"
|
||||
},
|
||||
"SEARCH_BAR": {
|
||||
"INFO": "Klicken Sie auf das Listensymbol, um nach archivierten Aufgaben zu suchen",
|
||||
|
|
@ -937,6 +991,7 @@
|
|||
"POSSIBLE_LEGACY_DATA": "Super Productivity hat die Synchronisierung verbessert, indem jetzt zwei separate Dateien statt einer einzigen verwendet werden, was eine wesentlich geringere Datenübertragung ermöglicht. Es wird empfohlen, alle Instanzen von Super Productivity zu aktualisieren und zuerst die Daten von der App-Instanz zu synchronisieren, in der die Daten am neuesten sind. Wenn dies die Daten von Ihrem lokalen Gerät sind, ignorieren Sie bitte diese Warnung und fahren Sie einfach mit dem Hochladen fort, indem Sie den nächsten Dialog bestätigen.",
|
||||
"REMOTE_MODEL_VERSION_NEWER": "Die Remote-Modellversion ist neuer als die lokale. Bitte aktualisieren Sie Ihre lokale App auf die neueste Version!"
|
||||
},
|
||||
"BTN_SYNC_NOW": "Jetzt Synchronisieren",
|
||||
"C": {
|
||||
"EMPTY_SYNC": "Sie versuchen, ein leeres Datenobjekt zu synchronisieren. Wenn Sie versuchen, Synchronisation von einer neuen App-Instanz zu aktivieren, klicken Sie einfach \"OK\" um die Daten vom Server zu laden. Andernfalls überprüfen Sie bitte Ihre Daten.",
|
||||
"FORCE_UPLOAD": "Lokale Daten trotzdem hochladen?",
|
||||
|
|
@ -953,17 +1008,34 @@
|
|||
"TITLE": "Login: {{provider}}"
|
||||
},
|
||||
"D_CONFLICT": {
|
||||
"COMPARISON_RESULT": "Ergebnis des Vergleichs",
|
||||
"LAMPORT_CLOCK": "Revision",
|
||||
"LAST_CHANGE": "Letzte Änderung:",
|
||||
"LAST_SYNC": "letzte Synchronisierung:",
|
||||
"LAST_SYNCED": "zuletzt Synchronisiert",
|
||||
"LAST_WRITE": "letztes Schreiben",
|
||||
"LOCAL": "Lokal",
|
||||
"LOCAL_REMOTE": "lokal -> remote",
|
||||
"NEVER": "Nie",
|
||||
"REMOTE": "Remote",
|
||||
"TEXT": "<p>Update von Dropbox. Sowohl lokale als auch remote Daten scheinen geändert zu sein.</p>",
|
||||
"TIMESTAMP": "Zeitstempel",
|
||||
"TITLE": "Dropbox: Datenkonflikte",
|
||||
"USE_LOCAL": "Verwenden Sie die lokale Datei",
|
||||
"USE_REMOTE": "Verwenden Sie die remote Datei"
|
||||
"USE_REMOTE": "Verwenden Sie die remote Datei",
|
||||
"ADDITIONAL_INFO": "Zusätzliche Info",
|
||||
"VECTOR_COMPARISON_CONCURRENT": "Konkurrent (Echter Konflikt)",
|
||||
"VECTOR_COMPARISON_EQUAL": "Gleich",
|
||||
"VECTOR_COMPARISON_LOCAL_GREATER": "Lokal > Remote",
|
||||
"VECTOR_COMPARISON_LOCAL_LESS": "Lokal < Remote",
|
||||
"DATE": "Datum",
|
||||
"TIME": "Zeit",
|
||||
"VECTOR_CLOCK": "Vektor-Uhr",
|
||||
"RESULT": "Ergebnis",
|
||||
"CHANGES": "Änderungen",
|
||||
"CHANGES_SINCE_LAST_SYNC": "Änderungen seit der letzten Synchronisierung",
|
||||
"VECTOR_CLOCK_HEADING": "Vektor-Uhr",
|
||||
"OVERWRITE_WARNING": "WARNUNG: Die {{targetName}} Daten enthalten ungefähr {{targetChanges}} Änderungen, während die {{sourceName}} Daten nur {{sourceChanges}} Änderungen enthalten. Sind Sie sicher, dass Sie die {{ZielName}} Daten mit der {{QuellName}} Version überschreiben wollen? Diese Aktion kann nicht rückgängig gemacht werden."
|
||||
},
|
||||
"D_DECRYPT_ERROR": {
|
||||
"BTN_OVER_WRITE_REMOTE": "Ändern und Remote überschreiben",
|
||||
|
|
@ -1020,6 +1092,7 @@
|
|||
"TITLE": "Sync",
|
||||
"WEB_DAV": {
|
||||
"CORS_INFO": "<p><strong>Experimental!!</strong> <strong>Damit dies für Mobil oder den Browser funktioniert, müssen Sie Super Productivity für CORS-Anfragen bei Ihrer Nextcloud-Instanz auf die Whitelist setzen</strong>, dies kann negative Auswirkungen auf die Sicherheit haben! Bitte beziehen Sie sich <a href='https://github.com/nextcloud/server/issues/3131'>auf diesen Thread</a> für weitere Informationen. Ein Ansatz, um dies auch mobil zu ermöglichen, ist das Whitelisting \"https://app.super-productivity.com\" über die Nextcloud-App <a href='https://apps.nextcloud.com/apps/webapppassword'>webapppassword<a>. Benutzung auf eigene Gefahr!",
|
||||
"INFO": "Leider unterscheiden sich WebDAV-Implementierungen stark voneinander. Super Productivity funktioniert bekanntermaßen gut mit Nextcloud, <strong>aber möglicherweise nicht mit Ihrem Anbieter</strong>.",
|
||||
"L_BASE_URL": "Basis-URL",
|
||||
"L_PASSWORD": "Passwort",
|
||||
"L_SYNC_FOLDER_PATH": "Sync-Ordnerpfad",
|
||||
|
|
@ -1031,6 +1104,7 @@
|
|||
"ALREADY_IN_SYNC_NO_LOCAL_CHANGES": "Keine lokalen Änderungen – bereits synchronisiert",
|
||||
"BTN_CONFIGURE": "Konfigurieren",
|
||||
"BTN_FORCE_OVERWRITE": "Zwangsüberschreibung",
|
||||
"ERROR_CORS": "WebDAV-Synchronisierungsfehler: Netzwerkanforderung fehlgeschlagen.\n\nDies könnte ein CORS-Problem sein. Bitte stellen Sie sicher:\n• Ihr WebDAV-Server erlaubt Cross-Origin-Anfragen\n• Die Server-URL ist korrekt und zugänglich\n• Sie haben eine funktionierende Internetverbindung",
|
||||
"ERROR_DATA_IS_CURRENTLY_WRITTEN": "Remote-Daten werden derzeit geschrieben",
|
||||
"ERROR_FALLBACK_TO_BACKUP": "Beim Importieren der Daten ist ein Fehler aufgetreten. Zurückgreifen auf lokales Backup.",
|
||||
"ERROR_INVALID_DATA": "Fehler beim Synchronisieren. Ungültige Daten",
|
||||
|
|
@ -1044,6 +1118,40 @@
|
|||
"SUCCESS_VIA_BUTTON": "Daten erfolgreich synchronisiert",
|
||||
"UNKNOWN_ERROR": "Unbekannter Fehler beim Synchronisieren. Bitte überprüfen Sie die Konsole.",
|
||||
"UPLOAD_ERROR": "Unbekannter Upload-Fehler (Einstellungen korrekt?): {{err}}"
|
||||
},
|
||||
"SAFETY_BACKUP": {
|
||||
"TITLE": "Sicherheits-Backups bei Synchronisierung",
|
||||
"DESCRIPTION": "Vor dem Herunterladen von Remote gespeicherten Daten, werden bei der Synchronisierung automatisch Backups erstellt. Die Backups sind in 4 intelligente Slots organisiert: 2 aktuellste, 1 erstes von heute und 1 erstes von vorgestern.",
|
||||
"BTN_CREATE_MANUAL": "Erzeuge manuelles Backup",
|
||||
"BTN_REFRESH": "Aktualisieren",
|
||||
"BTN_CLEAR_ALL": "Alle löschen",
|
||||
"BTN_RESTORE": "Wiederherstellen",
|
||||
"BTN_DELETE": "Löschen",
|
||||
"LOADING": "Laden...",
|
||||
"NO_BACKUPS": "Es sind noch keine Sicherheits-Backups verfügbar. Backups werden automatisch vor der Synchronisierung erstellt.",
|
||||
"TOOLTIP_CREATE_MANUAL": "Erstellen Sie ein manuelles Backup all Ihrer Daten",
|
||||
"TOOLTIP_REFRESH": "Aktualisieren der Backup-Liste",
|
||||
"TOOLTIP_CLEAR_ALL": "Alle Backups löschen",
|
||||
"TOOLTIP_RESTORE": "Dieses Backup wiederherstellen (ersetzt alle aktuellen Daten)",
|
||||
"TOOLTIP_DELETE": "Dieses Backup löschen",
|
||||
"SLOT_RECENT": "Aktuelles Backup",
|
||||
"SLOT_TODAY": "Erstes Backup von heute",
|
||||
"SLOT_BEFORE_TODAY": "Erstes Backup vor heute",
|
||||
"REASON_MANUAL": "Manuelles Backup",
|
||||
"REASON_BEFORE_UPDATE": "Automatisches Backup vor der Synchronisierung",
|
||||
"LAST_CHANGE_PREFIX": "Letzte Änderung:",
|
||||
"RESTORE_CONFIRM_TITLE": "Backup wiederherstellen",
|
||||
"RESTORE_CONFIRM_MSG": "Sind Sie sicher, dass Sie das Backup vom {{Zeitstempel}} wiederherstellen möchten?\n\nDamit werden alle Ihre aktuellen Daten VOLLSTÄNDIG ERSETZT!\n\nGrund: {{Grund}}\n\nKlicken Sie auf OK, um fortzufahren oder auf Abbrechen, um abzubrechen.",
|
||||
"BACKUP_NOT_FOUND": "Backup mit ID {{backupId}} nicht gefunden",
|
||||
"RESTORE_FAILED": "Wiederherstellung des Backup fehlgeschlagen: {{error}}",
|
||||
"INVALID_ID_ERROR": "Ungültige Backup-ID erzeugt",
|
||||
"CREATED_SUCCESS": "Manuelles Backup erfolgreich erstellt",
|
||||
"RESTORED_SUCCESS": "Backup erfolgreich wiederhergestellt",
|
||||
"DELETED_SUCCESS": "Backup erfolgreich gelöscht",
|
||||
"CLEARED_SUCCESS": "Alle Backups wurden erfolgreich gelöscht",
|
||||
"CREATE_FAILED": "Backup konnte nicht erstellt werden",
|
||||
"DELETE_FAILED": "Backup konnte nicht gelöscht werden",
|
||||
"CLEAR_FAILED": "Backups konnten nicht gelöscht werden"
|
||||
}
|
||||
},
|
||||
"TAG": {
|
||||
|
|
@ -1084,7 +1192,28 @@
|
|||
"EXAMPLE": "Beispiel: \"Aufgabentitel + Projektname #EinSchlüsselwort #EinAnderesSchlüssenwort 10m/3h\"",
|
||||
"START": "Drücken Sie die Eingabetaste noch einmal, um zu beginnen",
|
||||
"TOGGLE_ADD_TO_BACKLOG_TODAY": "„Aufgabe zum Backlog/zur heutigen Liste hinzufügen“ umschalten.",
|
||||
"TOGGLE_ADD_TOP_OR_BOTTOM": "Umschalten zwischen Hinzufügen von Aufgaben am Anfang und Ende der Liste"
|
||||
"TOGGLE_ADD_TOP_OR_BOTTOM": "Umschalten zwischen Hinzufügen von Aufgaben am Anfang und Ende der Liste",
|
||||
"PLACEHOLDER_SEARCH": "Vorhandene Aufgabe oder Vorgänge hinzufügen...",
|
||||
"PLACEHOLDER_CREATE": "Ein Aufgabentitel #Stichwort @16:00",
|
||||
"TOOLTIP_ADD_TASK": "Aufgabe hinzufügen",
|
||||
"TOOLTIP_ADD_TO_TOP": "am Anfang hinzufügen (Ctrl+1)",
|
||||
"TOOLTIP_ADD_TO_BOTTOM": "am Ende hinzufügen (Ctrl+1)",
|
||||
"TOOLTIP_ADD_TO_TODAY": "Zu Heute hinzufügen",
|
||||
"TOOLTIP_ADD_TO_BACKLOG": "Zum Backlog hinzufügen",
|
||||
"TOOLTIP_ENABLE_SEARCH": "Vorgangssuche aktivieren (Ctrl+2)",
|
||||
"TOOLTIP_DISABLE_SEARCH": "Vorgangssuche deaktivieren (Ctrl+2)",
|
||||
"SEARCH_INFO_TEXT": "Suchen und Hinzufügen von Vorgängen und Aufgaben aus dem Archiv und anderen Projekten",
|
||||
"DUE_BUTTON": "fällig",
|
||||
"TAGS_BUTTON": "Stichworte",
|
||||
"ESTIMATE_BUTTON": "Schätzung",
|
||||
"TOOLTIP_CLEAR_DATE": "Datum löschen",
|
||||
"TOOLTIP_CLEAR_TAGS": "Stichworte löschen",
|
||||
"TOOLTIP_CLEAR_ESTIMATE": "Schätzung löschen",
|
||||
"NO_DATE": "Kein Datum",
|
||||
"NO_TIME": "Keine Zeit",
|
||||
"TODAY": "Heute",
|
||||
"TOMORROW": "Morgen",
|
||||
"CREATE_NEW_TAGS": "Neue Stichworte erstellen"
|
||||
},
|
||||
"ADDITIONAL_INFO": {
|
||||
"ADD_ATTACHMENT": "Anhang hinzufügen",
|
||||
|
|
@ -1114,6 +1243,7 @@
|
|||
"DELETE": "Aufgabe löschen",
|
||||
"DELETE_REPEAT_INSTANCE": "Instanz der wiederkehrenden Aufgabe löschen",
|
||||
"DROP_ATTACHMENT": "Hier ablegen und an \"{{title}}\" anhängen",
|
||||
"DUPLICATE": "Duplizieren",
|
||||
"EDIT_SCHEDULED": "Erinnerung bearbeiten",
|
||||
"EDIT_TAGS": "Stichworte bearbeiten",
|
||||
"EDIT_TASK_TITLE": "Titel bearbeiten",
|
||||
|
|
@ -1264,6 +1394,10 @@
|
|||
"MSG": "Durch das Entfernen der Wiederholungskonfiguration werden alle vorherigen Instanzen dieser Aufgabe in normale Aufgaben konvertiert. Sind Sie sicher, dass Sie fortfahren möchten",
|
||||
"OK": "Vollständig entfernen"
|
||||
},
|
||||
"D_DELETE_INSTANCE": {
|
||||
"MSG": "Die Instanz der wiederkehrenden Aufgabe am {{Datum}} entfernen? Dadurch wird verhindert, dass die Aufgabe nur an diesem Datum erstellt wird.",
|
||||
"OK": "Instanz entfernen"
|
||||
},
|
||||
"D_CONFIRM_UPDATE_INSTANCES": {
|
||||
"CANCEL": "Nur zukünftige Aufgaben",
|
||||
"MSG": "Es gibt {{tasksNr}} Instanzen, die für diese wiederkehrende Aufgabe erstellt wurden. Möchten Sie alle mit den neuen Vorgaben aktualisieren oder nur zukünftige Aufgaben?",
|
||||
|
|
@ -1310,7 +1444,13 @@
|
|||
"THURSDAY": "Donnerstag",
|
||||
"TITLE": "Titel für die Aufgabe",
|
||||
"TUESDAY": "Dienstag",
|
||||
"WEDNESDAY": "Mittwoch"
|
||||
"WEDNESDAY": "Mittwoch",
|
||||
"INHERIT_SUBTASKS": "Unteraufgaben vererben",
|
||||
"INHERIT_SUBTASKS_DESCRIPTION": "Wenn diese Option aktiviert ist, werden die Unteraufgaben der letzten Aufgabeninstanz mit der wiederkehrenden Aufgabe neu erstellt.",
|
||||
"REMOVE_INSTANCE": "Heute entfernen",
|
||||
"REMOVE_FOR_DATE": "Entfernen für {{date}}",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS": "Autom. Aktualisierung von Unteraufgaben deaktivieren",
|
||||
"DISABLE_AUTO_UPDATE_SUBTASKS_DESCRIPTION": "Vererbte Unteraufgaben nicht automatisch aktualisieren (Stand beibehalten), wenn sich die neueste Instanz ändert"
|
||||
}
|
||||
},
|
||||
"TASK_VIEW": {
|
||||
|
|
@ -1359,7 +1499,8 @@
|
|||
},
|
||||
"B_TTR": {
|
||||
"ADD_TO_TASK": "Zur Aufgabe hinzufügen",
|
||||
"MSG": "Sie haben die Zeit für {{time}} nicht erfasst"
|
||||
"MSG": "Sie haben die Zeit für {{time}} nicht erfasst",
|
||||
"MSG_WITHOUT_TIME": "Sie haben die Zeit nicht erfasst"
|
||||
},
|
||||
"D_IDLE": {
|
||||
"ADD_ENTRY": "Eintrag für Erfassung hinzufügen",
|
||||
|
|
@ -1471,10 +1612,12 @@
|
|||
"ADVANCED_CFG": "Erweiterte Konfiguration",
|
||||
"CANCEL": "Abbrechen",
|
||||
"CLOSE": "Schließen",
|
||||
"COMPLETE": "Vollständig",
|
||||
"CONFIRM": "Bestätigen",
|
||||
"DELETE": "Löschen",
|
||||
"DISMISS": "Verwerfen",
|
||||
"DO_IT": "Tu es!",
|
||||
"DONT_SHOW_AGAIN": "Nicht mehr anzeigen",
|
||||
"DURATION_DESCRIPTION": "z. B. \"5h 23m\" was in 5 Stunden und 23 Minuten resultiert",
|
||||
"EDIT": "Bearbeiten",
|
||||
"ENABLED": "Aktiviert",
|
||||
|
|
@ -1596,7 +1739,8 @@
|
|||
"TRIGGER_SYNC": "Trigger-Synchronisation (falls konfiguriert)",
|
||||
"ZOOM_DEFAULT": "Zoom Standard (nur Desktop)",
|
||||
"ZOOM_IN": "Vergrößern (nur Desktop)",
|
||||
"ZOOM_OUT": "Verkleinern (nur Desktop)"
|
||||
"ZOOM_OUT": "Verkleinern (nur Desktop)",
|
||||
"PLUGIN_SHORTCUTS": "Plugin Tastenkürzel"
|
||||
},
|
||||
"LANG": {
|
||||
"AR": "عربى",
|
||||
|
|
@ -1622,7 +1766,22 @@
|
|||
"TR": "Türkçe",
|
||||
"UK": "Українська",
|
||||
"ZH": "中文(简体)",
|
||||
"ZH_TW": "中文(繁體)"
|
||||
"ZH_TW": "中文(繁體)",
|
||||
"TIME_LOCALE": "Zeitformat Gebietsschema",
|
||||
"TIME_LOCALE_DESCRIPTION": "Wählen Sie ein Gebietsschema für die Zeitformatierung. Verschiedene Gebietsschemata verwenden unterschiedliche Konventionen (12h vs. 24h Format)",
|
||||
"TIME_LOCALE_AUTO": "Systemvorgabe",
|
||||
"TIME_LOCALE_EN_US": "Englisch (US) - 12 Stunden AM/PM",
|
||||
"TIME_LOCALE_EN_GB": "Englisch (UK) - 24 Stunden",
|
||||
"TIME_LOCALE_TR_TR": "Türkisch - 24 Stunden",
|
||||
"TIME_LOCALE_DE_DE": "Deutsch - 24 Stunden",
|
||||
"TIME_LOCALE_FR_FR": "Französisch - 24 Stunden",
|
||||
"TIME_LOCALE_ES_ES": "Spanisch - 24 Stunden",
|
||||
"TIME_LOCALE_IT_IT": "Italienisch - 24 Stunden",
|
||||
"TIME_LOCALE_PT_BR": "Portugiesisch (Brasilien) - 24 Stunden",
|
||||
"TIME_LOCALE_RU_RU": "Russisch - 24 Stunden",
|
||||
"TIME_LOCALE_ZH_CN": "Chinesisch (vereinfacht) - 24 Stunden",
|
||||
"TIME_LOCALE_JA_JP": "Japanisch - 24 Stunden",
|
||||
"TIME_LOCALE_KO_KR": "Koreanisch - 12 Stunden AM/PM"
|
||||
},
|
||||
"MISC": {
|
||||
"DEFAULT_PROJECT": "Standardprojekt für Aufgaben, wenn keines angegeben ist",
|
||||
|
|
@ -1637,14 +1796,23 @@
|
|||
"IS_HIDE_NAV": "Navigation verbergen, bis die Hauptüberschrift angezeigt wird (nur Desktop)",
|
||||
"IS_MINIMIZE_TO_TRAY": "Anwendung als Trayicon minimieren (nur Deskop)",
|
||||
"IS_SHOW_TIP_LONGER": "Zeige den Produktivitätstipp beim Start der App etwas länger an",
|
||||
"IS_DISABLE_PRODUCTIVITY_TIPS": "Produktivitätstipps beim Start der Anwendung deaktivieren",
|
||||
"IS_TRAY_SHOW_CURRENT_COUNTDOWN": "Aktuellen Countdown im Tray / Statusmenü anzeigen (nur Desktop Mac)",
|
||||
"IS_TRAY_SHOW_CURRENT_TASK": "Aktuelle Aufgabe im Tray / Status-Menu zeigen (nur Desktop)",
|
||||
"IS_OVERLAY_INDICATOR_ENABLED": "Enable overlay indicator window (desktop linux/gnome)",
|
||||
"IS_TURN_OFF_MARKDOWN": "Deaktivieren Sie das Markdown-Parsing für Notizen",
|
||||
"IS_USE_MINIMAL_SIDE_NAV": "Minimale Navigationsleiste verwenden (nur Icons anzeigen)",
|
||||
"START_OF_NEXT_DAY": "Startzeit des nächsten Tages",
|
||||
"START_OF_NEXT_DAY_HINT": "Ab wann (in Stunden) der nächste Tag beginnen. Der Standardwert ist Mitternacht, also 0.",
|
||||
"TASK_NOTES_TPL": "Aufgabenbeschreibungsvorlage",
|
||||
"TITLE": "Verschiedene Einstellungen"
|
||||
"TITLE": "Verschiedene Einstellungen",
|
||||
"DARK_MODE": "Dark Mode",
|
||||
"DARK_MODE_SYSTEM": "System",
|
||||
"DARK_MODE_DARK": "Dunkel",
|
||||
"DARK_MODE_LIGHT": "Hell",
|
||||
"DARK_MODE_ARIA_LABEL": "Auswahl des Dunkelmodus",
|
||||
"THEME": "Thema",
|
||||
"THEME_EXPERIMENTAL": "Thema (experimentell)",
|
||||
"THEME_SELECT_LABEL": "Thema auswählen"
|
||||
},
|
||||
"POMODORO": {
|
||||
"BREAK_DURATION": "Dauer der kurzen Pausen",
|
||||
|
|
@ -1656,6 +1824,7 @@
|
|||
"IS_MANUAL_CONTINUE_BREAK": "Beginn der nächsten Pause manuell bestätigen",
|
||||
"IS_PLAY_SOUND": "Ton abspielen, wenn die Sitzung beendet ist",
|
||||
"IS_PLAY_SOUND_AFTER_BREAK": "Ton abspielen, wenn die Pause beendet ist",
|
||||
"IS_DISABLE_AUTO_START_AFTER_BREAK": "Automatischer Start der Sitzung nach einer Pause deaktivieren",
|
||||
"IS_PLAY_TICK": "Spielen Sie jede Sekunde einen Tick-Sound ab",
|
||||
"IS_STOP_TRACKING_ON_BREAK": "Stoppen Sie die Zeiterfassung für die Aufgabe in der Pause",
|
||||
"LONGER_BREAK_DURATION": "Dauer längerer Pausen",
|
||||
|
|
@ -1676,7 +1845,9 @@
|
|||
"L_WORK_START": "Beginn des Arbeitstages",
|
||||
"LUNCH_BREAK_START_END_DESCRIPTION": "z.B. 13:00",
|
||||
"TITLE": "Zeitleiste",
|
||||
"WORK_START_END_DESCRIPTION": "z. B. 17:00"
|
||||
"WORK_START_END_DESCRIPTION": "z. B. 17:00",
|
||||
"WEEK": "Woche",
|
||||
"MONTH": "Monat"
|
||||
},
|
||||
"SHORT_SYNTAX": {
|
||||
"HELP": "<p>Hier können Sie kurze Syntaxoptionen beim Erstellen einer Aufgabe festlegen</p>",
|
||||
|
|
@ -1819,7 +1990,8 @@
|
|||
"TOGGLE_SHOW_NOTES": "Projektnotizen ein- / ausblenden",
|
||||
"TOGGLE_TRACK_TIME": "Tracking-Zeit starten / stoppen",
|
||||
"TRIGGER_SYNC": "Synchronisierung manuell auslösen",
|
||||
"WORKLOG": "Worklog"
|
||||
"WORKLOG": "Worklog",
|
||||
"SIDE_PANEL_MENU": "Seitenleiste Menü"
|
||||
},
|
||||
"MIGRATE": {
|
||||
"C_DOWNLOAD_BACKUP": "Möchten Sie ein Backup Ihrer alten Daten herunterladen (kann mit älteren Versionen von Super Productivity verwendet werden)?",
|
||||
|
|
@ -1870,15 +2042,120 @@
|
|||
"PS": {
|
||||
"GLOBAL_SETTINGS": "Globale Einstellungen",
|
||||
"ISSUE_INTEGRATION": "Problemintegration",
|
||||
"NO_PLUGINS_INSTALLED": "Derzeit sind keine Plugins installiert",
|
||||
"PLUGINS": "Plugins",
|
||||
"PRIVACY_POLICY": "Datenschutzerklärung",
|
||||
"PRODUCTIVITY_HELPER": "Produktivitäts-Helfer",
|
||||
"PROJECT_SETTINGS": "Projektspezifische Einstellungen",
|
||||
"PROVIDE_FEEDBACK": "Rückmeldung geben",
|
||||
"RELOAD": "Neu laden",
|
||||
"SYNC_EXPORT": "Synchronisieren und exportieren",
|
||||
"TAG_SETTINGS": "Stichwort-spezifische Einstellungen",
|
||||
"TOGGLE_DARK_MODE": "Schaltet den Dunklen Modus um"
|
||||
},
|
||||
"PLUGINS": {
|
||||
"INSTALL_PLUGIN": "Plugin installieren",
|
||||
"UPLOAD_PLUGIN_INSTRUCTION": "Laden Sie eine Plugin-ZIP-Datei hoch, um sie zu installieren:",
|
||||
"CHOOSE_PLUGIN_FILE": "Plugin-Datei auswählen",
|
||||
"INSTALLING": "Installieren...",
|
||||
"CLEAR_PLUGIN_CACHE": "Plugin-Cache löschen",
|
||||
"REMOVE": "entfernen",
|
||||
"HOOKS": "Hooks/Auslöser",
|
||||
"PERMISSIONS": "Berechtigungen",
|
||||
"ID": "ID:",
|
||||
"MIN_VERSION": "Min. Version:",
|
||||
"NODE_EXECUTION_REQUIRED": "Dieses Plugin erfordert die Ausführung von Node.js, das nur in der Desktop-App verfügbar ist.",
|
||||
"TOGGLE_PLUGIN": "Umschalten auf {{pluginName}}",
|
||||
"EXPERIMENTAL_WARNING_TITLE": "Experimentelle Funktion - Sicherheitswarnung",
|
||||
"EXPERIMENTAL_WARNING": "Das Plugin-System befindet sich in einem experimentellen Stadium und sollte mit äußerster Vorsicht verwendet werden.",
|
||||
"SECURITY_WARNING": "Plugins haben erheblichen Zugriff auf Ihre Daten und Ihr System. Die Installation von nicht vertrauenswürdigen Plugins birgt ernsthafte Sicherheitsrisiken:",
|
||||
"RISK_DATA_ACCESS": "Plugins können ALLE Ihre Aufgaben, Projekte und persönlichen Daten lesen, ändern und löschen",
|
||||
"RISK_MALICIOUS_CODE": "Bösartige Plugins könnten vertrauliche Informationen stehlen oder Ihre Daten beschädigen",
|
||||
"RISK_SYSTEM_ACCESS": "Desktop-Plugins mit Node.js-Berechtigungen können Systembefehle ausführen",
|
||||
"RISK_PERFORMANCE": "Schlecht geschriebene Plugins können Leistungsprobleme oder Abstürze verursachen",
|
||||
"RECOMMENDATION": "Installieren Sie nur Plugins aus vertrauenswürdigen Quellen und überprüfen Sie deren Code, wenn möglich. Sichern Sie immer Ihre Daten, bevor Sie neue Plugins installieren.",
|
||||
"CONFIGURATION": "Konfiguration",
|
||||
"CONFIGURE": "Konfigurieren",
|
||||
"INSTALL_WARNING": "Bevor Sie ein Plugin installieren, vergewissern Sie sich, dass Sie der Quelle vertrauen und die erforderlichen Berechtigungen kennen.",
|
||||
"SYSTEM_ACCESS_REQUEST_TITLE": "Antrag auf Systemzugang",
|
||||
"SYSTEM_ACCESS_REQUEST_DESC": "Dieses Plugin bittet um die Erlaubnis, Node.js-Code auf Ihrem System auszuführen. Dies wird es erlauben:",
|
||||
"GRANT_PERMISSION": "Berechtigung erteilen",
|
||||
"CANCEL": "Abbrechen",
|
||||
"REMEMBER_CHOICE": "meine Wahl für dieses Plugin merken",
|
||||
"TRUST_WARNING": "Erlauben Sie nur, wenn Sie diesem Plugin vertrauen",
|
||||
"CAPABILITIES": {
|
||||
"ACCESS_FILES": "Zugriff und Änderung von Dateien auf Ihrem System",
|
||||
"RUN_COMMANDS": "Ausführen von Systembefehlen",
|
||||
"USE_NODE_APIS": "Node.js-APIs und -Module verwenden"
|
||||
},
|
||||
"ERROR": "Error",
|
||||
"LOADING_PLUGIN": "Laden...",
|
||||
"ENABLED": "Aktiviert",
|
||||
"DISABLED": "Deaktiviert",
|
||||
"PLEASE_SELECT_ZIP_FILE": "Bitte wählen Sie eine ZIP-Datei",
|
||||
"FILE_TOO_LARGE": "Datei zu groß ({{fileSize}}MB). Maximale Größe ist {{maxSize}}MB",
|
||||
"FAILED_TO_INSTALL": "Plugin konnte nicht installiert werden",
|
||||
"FAILED_TO_CLEAR_CACHE": "Plugin-Cache konnte nicht gelöscht werden",
|
||||
"CONFIRM_REMOVE": "Sind Sie sicher, dass Sie das Plugin entfernen möchten \"{{name}}\"?",
|
||||
"FAILED_TO_REMOVE": "Plugin konnte nicht entfernt werden",
|
||||
"TYPE": "Typ: {{type}}",
|
||||
"NO_ADDITIONAL_INFO": "Keine weiteren Informationen verfügbar",
|
||||
"NO_PLUGIN_ID_PROVIDED_FOR_HTML": "Keine Plugin-ID für HTML-Inhalte vorhanden",
|
||||
"PROJECT_NOT_FOUND": "Projekt '{{contextId}}' nicht gefunden",
|
||||
"TASKS_NOT_IN_PROJECT": "Eine oder mehrere Aufgaben sind nicht im Projekt '{{contextId}}'",
|
||||
"PARENT_TASK_NOT_FOUND": "Übergeordnete Aufgabe nicht im Kontext gefunden '{{contextId}}'",
|
||||
"TASK_NOT_FOUND": "Aufgabe mit ID '{{taskId}}' nicht gefunden",
|
||||
"TASKS_NOT_SUBTASKS": "Eine oder mehrere Aufgaben sind keine Unteraufgaben von '{{contextId}}'",
|
||||
"NO_PLUGIN_CONTEXT_PERSISTENCE": "Das Plugin hat keine Berechtigung, Daten dauerhaft zu speichern.",
|
||||
"UNABLE_TO_PERSIST_DATA": "Daten können nicht in den Plugin-Speicher übernommen werden",
|
||||
"NO_PLUGIN_CONTEXT_LOADING": "Plugin hat keinen Kontext für das Laden von Daten",
|
||||
"NO_PLUGIN_CONTEXT_SYNC": "Plugin hat keine Berechtigung zur Verwendung der Synchronisierung",
|
||||
"NO_PLUGIN_CONTEXT_HEADER_BUTTON": "Das Plugin hat keine Berechtigung, Header-Schaltflächen hinzuzufügen.",
|
||||
"NO_PLUGIN_CONTEXT_MENU_ENTRY": "Plugin hat nicht die Berechtigung, Menüeinträge hinzuzufügen",
|
||||
"MENU_ENTRY_LABEL_REQUIRED": "Menüeintragsbezeichnung ist erforderlich",
|
||||
"MENU_ENTRY_ONCLICK_REQUIRED": "Menüeintrag onClick-Handler erforderlich",
|
||||
"MENU_ENTRY_ICON_STRING": "Das Symbol für den Menüeintrag muss ein String sein",
|
||||
"NO_PLUGIN_CONTEXT_SIDE_PANEL": "Plugin hat keine Berechtigung zum Hinzufügen von Schaltflächen im Seitenbereich",
|
||||
"SIDE_PANEL_LABEL_REQUIRED": "Beschriftung der Schaltfläche im Seitenbereich ist erforderlich",
|
||||
"SIDE_PANEL_ONCLICK_REQUIRED": "Seitenpanel-Schaltfläche onClick-Handler ist erforderlich",
|
||||
"NO_PLUGIN_CONTEXT_SHORTCUT": "Plugin hat nicht die Berechtigung, Tastaturkürzel zu registrieren",
|
||||
"PROJECT_DOES_NOT_EXIST": "Das Projekt existiert nicht",
|
||||
"TAGS_DO_NOT_EXIST": "Ein oder mehrere Stichworte existieren nicht.",
|
||||
"PARENT_TASK_DOES_NOT_EXIST": "Die übergeordnete Aufgabe existiert nicht",
|
||||
"VALIDATION_FAILED": "Validierung fehlgeschlagen",
|
||||
"NO_PLUGIN_CONTEXT_ACTION": "Plugin hat keine Berechtigung zum Ausführen von Aktionen",
|
||||
"ACTION_TYPE_NOT_ALLOWED": "Aktionstyp '{{Typ}}' ist nicht erlaubt",
|
||||
"NODE_ONLY_DESKTOP": "Node.js-Ausführung ist nur in der Desktop-Anwendung verfügbar",
|
||||
"NO_PLUGIN_CONTEXT_NODE": "Plugin hat keine Berechtigung zur Ausführung von Node.js-Code",
|
||||
"NO_PLUGIN_MANIFEST_NODE": "Das Plugin-Manifest enthält kein Node.js-Modul",
|
||||
"ELECTRON_API_NOT_AVAILABLE": "Electron API ist nicht verfügbar",
|
||||
"FAILED_TO_EXECUTE_SCRIPT": "Node.js-Skript kann nicht ausgeführt werden",
|
||||
"ALREADY_INITIALIZED": "Plugin ist bereits initialisiert",
|
||||
"USER_DECLINED_NODE_PERMISSION": "Benutzer hat Node.js-Ausführungserlaubnis abgelehnt",
|
||||
"FAILED_TO_EXTRACT_ZIP": "Plugin-ZIP-Datei konnte nicht entpackt werden",
|
||||
"MANIFEST_NOT_FOUND": "Plugin-Manifest (manifest.json) nicht in ZIP-Datei gefunden",
|
||||
"MANIFEST_TOO_LARGE": "Plugin-Manifest ist zu groß (max {{maxSize}}KB)",
|
||||
"PLUGIN_JS_NOT_FOUND": "Plugin-JavaScript-Datei (plugin.js) nicht in ZIP-Datei gefunden",
|
||||
"CODE_TOO_LARGE": "Plugin-Code ist zu groß (max {{maxSize}}MB)",
|
||||
"UNKNOWN_ERROR": "Ein unbekannter Fehler ist aufgetreten",
|
||||
"PLUGIN_DIALOG_TITLE": "Plugin-Meldung",
|
||||
"NO_CONTENT_PROVIDED": "Kein Inhalt vorhanden",
|
||||
"OK": "OK",
|
||||
"LOADING_INTERFACE": "Lade Plugin-Schnittstelle...",
|
||||
"ERROR_LOADING_PLUGIN": "Fehler beim Laden des Plugins",
|
||||
"GO_BACK": "Zurückgehen",
|
||||
"FAILED_TO_LOAD": "Plugin konnte nicht geladen werden",
|
||||
"FAILED_TO_LOAD_CONFIG": "Plugin-Konfiguration konnte nicht geladen werden",
|
||||
"FAILED_TO_SAVE_CONFIG": "Plugin-Konfiguration konnte nicht gespeichert werden",
|
||||
"PLUGIN_ID_NOT_PROVIDED": "Plugin-ID nicht angegeben",
|
||||
"PLUGIN_SYSTEM_FAILED_INIT": "Plugin-System konnte nicht initialisiert werden",
|
||||
"PLUGIN_NOT_FOUND": "Plugin nicht gefunden",
|
||||
"PLUGIN_DOES_NOT_SUPPORT_IFRAME": "Dieses Plugin unterstützt keine Iframe-Schnittstelle",
|
||||
"INDEX_HTML_NOT_LOADED": "Plugin index.html nicht geladen"
|
||||
},
|
||||
"SCHEDULE": {
|
||||
"LAST": "Zuletzt:",
|
||||
"NEXT": "Weiter",
|
||||
"NO_REPEATABLE_TASKS": "Es gibt aktuell keine wiederkehrenden Aufgaben. Sie können eine Aufgabe planen indem Sie auf \"Aufgabe wiederholen\" im Aufgaben Seitenpanel klicken. Um sie zu öffnen klicken Sie auf das Symbol ganz rechts das erscheint, wenn man über eine Aufgabe fährt (oder tippen Sie auf die Aufgabe auf Mobilgeräten).",
|
||||
"NO_SCHEDULED": "Derzeit sind keine Aufgaben geplant. Sie können eine Aufgabe planen, indem Sie im Kontextmenü der Aufgabe \"Aufgabe planen\" auswählen. Zum Öffnen klicken Sie auf die 3 kleinen Punkte rechts neben einer Aufgabe.",
|
||||
"NO_SCHEDULED_TITLE": "Geplante Aufgaben für den Tag",
|
||||
|
|
@ -1923,6 +2200,7 @@
|
|||
"DONE_TASKS": "Erledigte Aufgaben",
|
||||
"DONE_TASKS_IN_ARCHIVE": "Derzeit sind hier keine erledigten Aufgaben, aber es gibt bereits einige archivierte.",
|
||||
"ESTIMATE_REMAINING": "Schätzung verbleibend:",
|
||||
"LATER_TODAY": "Heute später",
|
||||
"FINISH_DAY": "Tag beenden",
|
||||
"FINISH_DAY_FOR_PROJECT": "Tag für dieses Projekt beenden",
|
||||
"FINISH_DAY_FOR_TAG": "Tag für dieses Schlüsselwort beenden",
|
||||
|
|
|
|||
|
|
@ -865,6 +865,24 @@
|
|||
"TOOLTIP_CREATE": "Create project folder",
|
||||
"TOOLTIP_VISIBILITY": "Show/hide projects"
|
||||
},
|
||||
"TAG_FOLDER": {
|
||||
"DIALOG": {
|
||||
"CREATE_TITLE": "Create folder",
|
||||
"EDIT_TITLE": "Edit folder",
|
||||
"NAME_LABEL": "Folder name",
|
||||
"NAME_PLACEHOLDER": "Enter folder name",
|
||||
"NAME_REQUIRED": "Folder name is required",
|
||||
"PARENT_LABEL": "Parent folder",
|
||||
"NO_PARENT": "No parent (root level)"
|
||||
},
|
||||
"SELECT": {
|
||||
"LABEL": "Folder",
|
||||
"PLACEHOLDER": "Select folder",
|
||||
"NO_PARENT": "No folder (root level)"
|
||||
},
|
||||
"CONFIRM_DELETE": "Are you sure you want to delete the folder \"{{title}}\"? All tags in this folder will be moved to the root level.",
|
||||
"TOOLTIP_CREATE": "Create tag folder"
|
||||
},
|
||||
"QUICK_HISTORY": {
|
||||
"NO_DATA": "No data for current year",
|
||||
"PAGE_TITLE": "Quick History",
|
||||
|
|
|
|||
2202
src/assets/i18n/fi.json
Normal file
2202
src/assets/i18n/fi.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -1354,34 +1354,34 @@
|
|||
"TIME_TRACKING": {
|
||||
"B": {
|
||||
"ALREADY_DID": "Я уже сделал",
|
||||
"SNOOZE": "Отложить {{time}}"
|
||||
"SNOOZE": "Отложить на {{time}}"
|
||||
},
|
||||
"B_TTR": {
|
||||
"ADD_TO_TASK": "Добавить в задачу",
|
||||
"MSG": "Вы не отслеживали время для {{time}}"
|
||||
"MSG": "Вы не отслеживали время {{time}}"
|
||||
},
|
||||
"D_IDLE": {
|
||||
"ADD_ENTRY": "Добавить запись для отслеживания",
|
||||
"BREAK": "Перерыв",
|
||||
"CREATE_AND_TRACK": "<em>Создать</em> и отследить до:",
|
||||
"CREATE_AND_TRACK": "<em>Создать</em> и отследить в:",
|
||||
"IDLE_FOR": "Вы были без дела:",
|
||||
"RESET_BREAK_REMINDER_TIMER": "Сбросить таймер перерыва",
|
||||
"SIMPLE_CONFIRM_COUNTER_CANCEL": "Пропустить",
|
||||
"SIMPLE_CONFIRM_COUNTER_OK": "Отследить",
|
||||
"SIMPLE_COUNTER_CONFIRM_TXT": "Вы выбрали \"Пропустить\", но активировали кнопки простого счетчика ({{nr}}). Хотите отследить в него время простоя?",
|
||||
"SIMPLE_COUNTER_TOOLTIP": "Нажмите, чтобы отслеживать до {{title}}",
|
||||
"SIMPLE_COUNTER_TOOLTIP_DISABLE": "Нажмите, чтобы НЕ отслеживать до {{title}}",
|
||||
"SIMPLE_COUNTER_TOOLTIP": "Нажмите, чтобы отслеживать в {{title}}",
|
||||
"SIMPLE_COUNTER_TOOLTIP_DISABLE": "Нажмите, чтобы НЕ отслеживать в {{title}}",
|
||||
"SKIP": "Пропустить",
|
||||
"SPLIT_TIME": "Разделить время на несколько задач и/или перерывов",
|
||||
"TASK": "Задача",
|
||||
"TRACK_TO": "Отследить до:"
|
||||
"TRACK_TO": "Отследить в:"
|
||||
},
|
||||
"D_TRACKING_REMINDER": {
|
||||
"CREATE_AND_TRACK": "<em>Создать</em> и отслеживать",
|
||||
"IDLE_FOR": "Вы были без дела:",
|
||||
"NOTIFICATION_TITLE": "Следите за своим временем!",
|
||||
"TASK": "Задача",
|
||||
"TRACK_TO": "Отследить до:",
|
||||
"TRACK_TO": "Отследить в:",
|
||||
"UNTRACKED_TIME": "Не отслеженное время:"
|
||||
}
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// this file is automatically generated by git.version.ts script
|
||||
export const versions = {
|
||||
version: '15.1.0-rc.0',
|
||||
version: '15.1.3-rc.0',
|
||||
revision: 'NO_REV',
|
||||
branch: 'NO_BRANCH',
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
@use '@angular/material' as mat;
|
||||
@use 'angular-material-css-vars' as mat-css-vars;
|
||||
|
||||
//$custom-typography: mat.m2-define-typography-config(
|
||||
// $font-family: null,
|
||||
//);
|
||||
//@include mat-css-vars.init-material-css-vars($typography-config: $custom-typography);
|
||||
@include mat-css-vars.init-material-css-vars();
|
||||
$custom-typography: mat.m2-define-typography-config(
|
||||
$font-family: 'Open Sans, sans-serif',
|
||||
);
|
||||
@include mat-css-vars.init-material-css-vars($typography-config: $custom-typography);
|
||||
|
||||
.bg-card {
|
||||
background: var(--card-bg);
|
||||
|
|
|
|||
|
|
@ -7,7 +7,14 @@
|
|||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"files": ["main.ts", "polyfills.ts"],
|
||||
"include": ["**/*.d.ts"],
|
||||
"include": ["app/**/*.ts"],
|
||||
"exclude": [
|
||||
"**/*.spec.ts",
|
||||
"**/*.worker.ts",
|
||||
"**/__mocks__/**",
|
||||
"**/test-utils.ts",
|
||||
"app/util/**/*.ts"
|
||||
],
|
||||
"angularCompilerOptions": {
|
||||
"fullTemplateTypeCheck": true,
|
||||
"strictInjectionParameters": true
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue