forked from vikunja/frontend
Compare commits
210 Commits
feature/re
...
main
Author | SHA1 | Date |
---|---|---|
renovate | e0c00b306e | |
renovate | 10eaacc552 | |
renovate | 4b1465955a | |
renovate | 1711318212 | |
renovate | b042547aaa | |
renovate | ed0db956eb | |
renovate | a66f8a6484 | |
renovate | 47e895149e | |
renovate | 8e00014feb | |
renovate | 6146340034 | |
renovate | c7b761b0eb | |
renovate | a1e84b3460 | |
renovate | 038debaa22 | |
renovate | 88faf04251 | |
renovate | 04be2b9745 | |
renovate | 815e8cce0e | |
renovate | d12f9247ff | |
renovate | 85e7a17934 | |
renovate | 59c5d43348 | |
renovate | c011f9aa52 | |
renovate | b9f5319a4f | |
renovate | f120ba4169 | |
renovate | b2b70f4a9d | |
renovate | 9facffe3e9 | |
renovate | c31aff1d88 | |
renovate | 60dea80462 | |
renovate | cd10ccfbc0 | |
renovate | 8647402038 | |
renovate | 990fd46302 | |
renovate | cf0aafd9e6 | |
renovate | 70d2535e93 | |
Frederick [Bot] | 0c6f1a4083 | |
renovate | 29eb42932a | |
renovate | 736e9051d8 | |
renovate | 4a4c401558 | |
renovate | 9198abe24d | |
Dominik Pschenitschni | 97c8970dd6 | |
renovate | 5303b6bc97 | |
renovate | 24a0a8f5eb | |
Dominik Pschenitschni | d07ad495e2 | |
renovate | 8465afe421 | |
kolaente | d40729cbe7 | |
kolaente | fa0e46a399 | |
renovate | b78481f9f6 | |
renovate | cbc9cf6f7f | |
renovate | 62fd9a656e | |
renovate | 85269b4524 | |
renovate | 536d709961 | |
renovate | 59d6d7e786 | |
renovate | ae86d0d42a | |
renovate | 9a20b7a853 | |
renovate | 5687b66ea5 | |
renovate | 1da411e1f6 | |
renovate | e8a6d3f31b | |
renovate | a25a795276 | |
renovate | 57f6abd99f | |
renovate | 84d205f90b | |
renovate | de91e7c9ae | |
renovate | 2cf9c35acb | |
konrad | db525db6eb | |
konrad | 88525ae7c8 | |
renovate | 957bfdc8f1 | |
renovate | c52ae83b75 | |
renovate | df40c4e475 | |
renovate | 3f41e9a3a6 | |
renovate | 1da510b5dd | |
renovate | 536db3fd46 | |
kolaente | cefa5250c5 | |
kolaente | f697640636 | |
renovate | 09b7595b68 | |
renovate | 6b7f73f724 | |
Dominik Pschenitschni | d6b55c7570 | |
Yurii Vlasov | 3f4b08b8be | |
kolaente | 791c61cabb | |
konrad | e3dd4ef78a | |
renovate | 830d0887b9 | |
Dominik Pschenitschni | e8db2c2b45 | |
renovate | 706a13242e | |
renovate | 13fab10584 | |
renovate | 4b0c8aa66b | |
renovate | bfaf9401f4 | |
renovate | 13607124a6 | |
renovate | 9fc3d0a965 | |
renovate | 4d6286451e | |
renovate | 0479d17e69 | |
renovate | 5ca272959d | |
Dominik Pschenitschni | c502f9b840 | |
renovate | a3a313a21f | |
renovate | c58d1ffd2e | |
David Angel | 99dc5cf34f | |
David Angel | 3604cb3ec7 | |
David Angel | aa01a92278 | |
Dominik Pschenitschni | 7b96397e3b | |
renovate | b45a4e1aaf | |
Frederick [Bot] | d3365d6add | |
renovate | 49cb2b9e6f | |
renovate | d4ce10e79a | |
renovate | 345c5e3588 | |
renovate | 7ff84bcd29 | |
renovate | d1633ef622 | |
renovate | 7e92bc63ac | |
renovate | be076b65cf | |
Frederick [Bot] | 65b90cbee0 | |
renovate | 74aac1b245 | |
renovate | ade791ed43 | |
renovate | 55b008c67c | |
Frederick [Bot] | 1f088cca18 | |
renovate | 6fad1e4969 | |
Dominik Pschenitschni | eaeddda4e4 | |
kolaente | 7cbf0acac5 | |
Dominik Pschenitschni | 3db5ea45d7 | |
RoboMagus | dcd5c3fd6a | |
renovate | 61fff44764 | |
renovate | ecdae4e03e | |
kolaente | b26ea45fe0 | |
kolaente | 7cb0cd293d | |
konrad | 6572f75e5d | |
Jef Oliver | af55992057 | |
Jef Oliver | e92559dc00 | |
renovate | 3dbf02fd7a | |
Dominik Pschenitschni | 81a4f2d977 | |
renovate | 2972d0d400 | |
renovate | c11ebc44c4 | |
renovate | 144f90c5f7 | |
renovate | 913879604a | |
renovate | 1589ed5739 | |
renovate | a991c537ac | |
renovate | 69b57aa23a | |
renovate | 1a1939963a | |
renovate | 3d62c9789c | |
renovate | c18df8687c | |
renovate | d83ba0c158 | |
kolaente | cea31d1da7 | |
renovate | 12509a7e0f | |
renovate | dd43057a08 | |
renovate | 19d3cf01cd | |
renovate | 80012bf035 | |
renovate | 899d9e1cb7 | |
renovate | 56830ddadc | |
kolaente | 1749d6ba0a | |
renovate | b29008d304 | |
renovate | 8ae3054b1a | |
renovate | f9dad79b23 | |
renovate | 30f5cb0656 | |
renovate | 3f58c983da | |
kolaente | 8fa8b03aa6 | |
Yurii Vlasov | e4499f44b7 | |
kolaente | b799233bca | |
renovate | be0ae4bc29 | |
renovate | 60d99f3bba | |
renovate | fa666d2817 | |
renovate | 9312aa14fa | |
renovate | 68e4f776b9 | |
renovate | 2d137d564e | |
Frederick [Bot] | fc8824d942 | |
renovate | 6d4ca57601 | |
renovate | d2bf4e38b1 | |
renovate | a5f6857a40 | |
renovate | ed3d79fa4c | |
Frederick [Bot] | 81c5c54aed | |
renovate | 793e06c6ac | |
Nikola Sivkov v2 | 7eb07e92f8 | |
renovate | 2a15878b81 | |
renovate | ebd2b1e8c0 | |
renovate | d11fcfa072 | |
Frederick [Bot] | 8e6e976867 | |
kolaente | 9adf1aba89 | |
kolaente | e67088fdb7 | |
kolaente | da241d21f3 | |
kolaente | 97133010af | |
kolaente | 4576da0dd3 | |
renovate | fd4a68daf0 | |
renovate | 6f02d43801 | |
konrad | 2be784766f | |
Dominik Pschenitschni | 13a39be3de | |
renovate | d2e07efc7d | |
renovate | a44299e786 | |
renovate | 221f73c347 | |
renovate | 9b170d0d81 | |
kolaente | 16e61a8492 | |
kolaente | a95f1090d7 | |
kolaente | c6026107fa | |
renovate | e07e6bf677 | |
Dominik Pschenitschni | c6ed925424 | |
Dominik Pschenitschni | 7ed1a37de5 | |
renovate | 1a2e9af88f | |
renovate | f5e90067f6 | |
renovate | 188ae57dc0 | |
renovate | 3e4bbd58a3 | |
renovate | bb8ee15a2d | |
renovate | 4c46ae5b2f | |
renovate | ff27030e1c | |
renovate | 639e5e3d23 | |
Frederick [Bot] | e49e9352e5 | |
renovate | 705afa0272 | |
renovate | 83864a6bac | |
renovate | 664a39b70d | |
Dominik Pschenitschni | 9922fcba65 | |
Dominik Pschenitschni | 489014944a | |
Frederick [Bot] | bb44beb4ba | |
renovate | d996f24028 | |
renovate | 59cac0eb38 | |
Frederick [Bot] | 7adc5ceb9f | |
renovate | a3d9cb5324 | |
Frederick [Bot] | 6c192b6f59 | |
konrad | 8ff1b3006b | |
renovate | 0414352b02 | |
Dominik Pschenitschni | 2a2c27af92 | |
Dominik Pschenitschni | e1b35ff023 | |
renovate | 95b2bcf5fb |
10
.drone.yml
10
.drone.yml
|
@ -149,8 +149,10 @@ steps:
|
|||
# Override the default api url used for preview
|
||||
- sed -i 's|http://localhost:3456|https://try.vikunja.io|g' dist-preview/index.html
|
||||
- apk add --no-cache perl-utils
|
||||
- shasum -a 384 -c ./scripts/deploy-preview-netlify.js.sha384
|
||||
- node ./scripts/deploy-preview-netlify.js
|
||||
# create via:
|
||||
# `shasum -a 384 ./scripts/deploy-preview-netlify.mjs > ./scripts/deploy-preview-netlify.mjs.sha384`
|
||||
- shasum -a 384 -c ./scripts/deploy-preview-netlify.mjs.sha384
|
||||
- node ./scripts/deploy-preview-netlify.mjs
|
||||
depends_on:
|
||||
- build-prod
|
||||
when:
|
||||
|
@ -391,6 +393,7 @@ steps:
|
|||
environment:
|
||||
DOCKER_AUTOTAG_VERSION: ${DRONE_TAG}
|
||||
DOCKER_AUTOTAG_EXTRA_TAGS: latest
|
||||
DOCKER_AUTOTAG_OUTPUT_FILE: .tags
|
||||
depends_on: [ fetch-tags ]
|
||||
when:
|
||||
ref:
|
||||
|
@ -406,7 +409,6 @@ steps:
|
|||
password:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/frontend
|
||||
auto_tag: true
|
||||
build_args:
|
||||
- USE_RELEASE=true
|
||||
- RELEASE_VERSION=${DRONE_TAG##v}
|
||||
|
@ -519,6 +521,6 @@ steps:
|
|||
from_secret: crowdin_key
|
||||
---
|
||||
kind: signature
|
||||
hmac: 0a65a032008f2d1911866dcb0d9536b26c1d6b105943ed5328ac13abc99c423f
|
||||
hmac: 971875b90c7bb1649d1b00d022d0b594ba9b68f927bf8f0dbe840190816d676b
|
||||
|
||||
...
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
# (1) Duplicate this file and remove the '.example' suffix.
|
||||
# Naming this file '.env.local' is a Vite convention to prevent accidentally
|
||||
# submitting to git.
|
||||
# For more info see: https://vitejs.dev/guide/env-and-mode.html#env-files
|
||||
|
||||
# (2) Comment in and adjust the values as needed.
|
||||
|
||||
# VITE_IS_ONLINE=true
|
||||
# VITE_WORKBOX_DEBUG=false
|
||||
# SENTRY_AUTH_TOKEN=YOUR_TOKEN
|
||||
# SENTRY_ORG=vikunja
|
||||
# SENTRY_PROJECT=frontend-oss
|
||||
# VIKUNJA_FRONTEND_BASE=/custom-subpath
|
|
@ -1,2 +1,3 @@
|
|||
github: kolaente
|
||||
custom: https://www.buymeacoffee.com/kolaente
|
||||
open_collective: vikunja
|
||||
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]
|
||||
|
|
167
CHANGELOG.md
167
CHANGELOG.md
|
@ -9,6 +9,173 @@ All releases can be found on https://code.vikunja.io/frontend/releases.
|
|||
|
||||
The releases aim at the api versions which is why there are missing versions.
|
||||
|
||||
## [0.20.3] - 2023-01-24
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(BaseButton)* Prop type
|
||||
* *(ci)* Make sure the i18n sync cron job actually runs
|
||||
* *(ci)* Sign drone config
|
||||
* *(ci)* Sign drone config
|
||||
* *(ci)* Tagging logic for release docker images
|
||||
* *(ci)* Sign drone config
|
||||
* *(cypress)* Use ts for updateUserSettings
|
||||
* *(cypress)* Use env for API_URL (#2925)
|
||||
* *(drone)* Use correct property value (#2920)
|
||||
* *(drone)* Pnpm cache folder path (#2932)
|
||||
* *(faker)* Remove mock types (#2921)
|
||||
* *(i18n)* Incorrect translation string
|
||||
* *(migration)* Actually pass migration oauth code from query param
|
||||
* *(quick add magic)* Make sure assignees which don't exist are not removed from task title
|
||||
* *(task)* Update task description when switching between related tasks
|
||||
* *(task)* Don't show the list color on the task when only viewing the list (#2975)
|
||||
* *(useOnline)* Only log if actually faking state (#2924)
|
||||
* Close button hover for sidebar (#2981) ([9922fcb](9922fcba65c8dc2c46c4f085813c2fbc0d0a7df6))
|
||||
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update dependency vite to v4.0.2 (#2861)
|
||||
* *(deps)* Update dependency netlify-cli to v12.4.0 (#2862)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.47.0 (#2864)
|
||||
* *(deps)* Update dependency esbuild to v0.16.10 (#2865)
|
||||
* *(deps)* Update dependency sass to v1.57.1 (#2866)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.16 (#2867)
|
||||
* *(deps)* Update dependency codemirror to v5.65.11
|
||||
* *(deps)* Update dependency @vueuse/core to v9.8.0
|
||||
* *(deps)* Update dependency vitest to v0.26.1
|
||||
* *(deps)* Update dependency @vueuse/core to v9.8.1 (#2870)
|
||||
* *(deps)* Update dependency @vueuse/core to v9.8.2
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.28.0
|
||||
* *(deps)* Update dependency cypress to v12.2.0 (#2873)
|
||||
* *(deps)* Update dependency vitest to v0.26.2 (#2874)
|
||||
* *(deps)* Update dependency vite to v4.0.3 (#2876)
|
||||
* *(deps)* Update pnpm to v7.19.0 (#2875)
|
||||
* *(deps)* Update dependency rollup to v3.8.0 (#2877)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.28.1 (#2878)
|
||||
* *(deps)* Update dependency @vueuse/core to v9.9.0 (#2881)
|
||||
* *(deps)* Update dependency rollup to v3.8.1 (#2879)
|
||||
* *(deps)* Update dependency vite-svg-loader to v4 (#2882)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.17 (#2883)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001441 (#2884)
|
||||
* *(deps)* Update dependency netlify-cli to v12.5.0 (#2886)
|
||||
* *(deps)* Update pnpm to v7.20.0 (#2887)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.18 (#2888)
|
||||
* *(deps)* Update dependency happy-dom to v8.1.1 (#2885)
|
||||
* *(deps)* Update dependency @types/node to v18.11.18 (#2889)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.47.1 (#2890)
|
||||
* *(deps)* Update dependency esbuild to v0.16.11
|
||||
* *(deps)* Update dependency esbuild to v0.16.12 (#2893)
|
||||
* *(deps)* Update dependency rollup to v3.9.0 (#2894)
|
||||
* *(deps)* Update dependency rollup-plugin-visualizer to v5.9.0 (#2896)
|
||||
* *(deps)* Update dependency marked to v4.2.5 (#2880)
|
||||
* *(deps)* Update pnpm to v7.21.0 (#2895)
|
||||
* *(deps)* Update dependency eslint to v8.31.0
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.19
|
||||
* *(deps)* Update dependency @types/codemirror to v5.60.6
|
||||
* *(deps)* Update dependency rollup to v3.9.1
|
||||
* *(deps)* Update dependency vitest to v0.26.3
|
||||
* *(deps)* Update dependency vite-plugin-pwa to v0.14.1 (#2909)
|
||||
* *(deps)* Update dependency esbuild to v0.16.13 (#2907)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.48.0 (#2906)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.20
|
||||
* *(deps)* Update dependency cypress to v12.3.0
|
||||
* *(deps)* Update dependency @vueuse/core to v9.10.0 (#2911)
|
||||
* *(deps)* Update pnpm to v7.22.0 (#2910)
|
||||
* *(deps)* Update dependency @vue/test-utils to v2.2.7 (#2914)
|
||||
* *(deps)* Update dependency vite to v4.0.4 (#2908)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.29.0 (#2915)
|
||||
* *(deps)* Update dependency esbuild to v0.16.14
|
||||
* *(deps)* Update dependency axios to v1
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.21
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.22
|
||||
* *(deps)* Update dependency dompurify to v2.4.2
|
||||
* *(deps)* Update dependency dompurify to v2.4.3 (#2931)
|
||||
* *(deps)* Update dependency postcss to v8.4.21 (#2933)
|
||||
* *(deps)* Update dependency esbuild to v0.16.15 (#2934)
|
||||
* *(deps)* Update dependency vue-tsc to v1.0.24
|
||||
* *(deps)* Update pnpm to v7.23.0 (#2940)
|
||||
* *(deps)* Update dependency happy-dom to v8.1.3 (#2939)
|
||||
* *(deps)* Update dependency esbuild to v0.16.16 (#2937)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001442 (#2938)
|
||||
* *(deps)* Update dependency vitest to v0.27.0 (#2941)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.48.1 (#2942)
|
||||
* *(deps)* Update pnpm to v7.24.2 (#2944)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.30.0 (#2945)
|
||||
* *(deps)* Update pnpm to v7.24.3 (#2946)
|
||||
* *(deps)* Update dependency vitest to v0.27.1 (#2947)
|
||||
* *(deps)* Update dependency esbuild to v0.16.17 (#2948)
|
||||
* *(deps)* Update dependency rollup to v3.10.0 (#2949)
|
||||
* *(deps)* Update dependency eslint-plugin-vue to v9.9.0 (#2950)
|
||||
* *(deps)* Update pnpm to v7.25.0 (#2951)
|
||||
* *(deps)* Update dependency marked to v4.2.12 (#2952)
|
||||
* *(deps)* Update dependency esbuild to v0.17.0 (#2953)
|
||||
* *(deps)* Update dependency eslint to v8.32.0 (#2954)
|
||||
* *(deps)* Update dependency vue-advanced-cropper to v2.8.8 (#2955)
|
||||
* *(deps)* Update dependency pinia to v2.0.29 (#2956)
|
||||
* *(deps)* Update dependency @kyvg/vue3-notification to v2.8.0 (#2957)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001445 (#2958)
|
||||
* *(deps)* Update dependency happy-dom to v8.1.4 (#2959)
|
||||
* *(deps)* Update dependency netlify-cli to v12.7.2 (#2960)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.31.0
|
||||
* *(deps)* Update dependency esbuild to v0.17.1 (#2963)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.48.2 (#2962)
|
||||
* *(deps)* Update dependency esbuild to v0.17.2 (#2965)
|
||||
* *(deps)* Update dependency vitest to v0.27.2 (#2966)
|
||||
* *(deps)* Update dependency @vueuse/core to v9.11.0 (#2967)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.31.1 (#2973)
|
||||
* *(deps)* Update dependency axios to v1.2.3 (#2974)
|
||||
* *(deps)* Update dependency esbuild to v0.17.3 (#2976)
|
||||
* *(deps)* Update pnpm to v7.25.1 (#2977)
|
||||
* *(deps)* Update dependency @vueuse/core to v9.11.1
|
||||
* *(deps)* Update dependency rollup to v3.10.1
|
||||
* *(deps)* Update dependency vite-plugin-inject-preload to v1.2.0 (#2983)
|
||||
* *(deps)* Update dependency vitest to v0.27.3 (#2984)
|
||||
* *(deps)* Update dependency esbuild to v0.17.4 (#2985)
|
||||
* *(deps)* Update dependency caniuse-lite to v1.0.30001447 (#2986)
|
||||
* *(deps)* Update dependency happy-dom to v8.1.5 (#2987)
|
||||
* *(deps)* Update dependency netlify-cli to v12.9.1 (#2988)
|
||||
* *(deps)* Update sentry-javascript monorepo to v7.32.1 (#2991)
|
||||
* *(deps)* Update dependency vitest to v0.28.1 (#2990)
|
||||
* *(deps)* Update dependency @types/codemirror to v5.60.7 (#2993)
|
||||
* *(deps)* Update typescript-eslint monorepo to v5.49.0 (#2994)
|
||||
* *(deps)* Update dependency start-server-and-test to v1.15.3
|
||||
* *(deps)* Update dependency @fortawesome/vue-fontawesome to v3.0.3 (#3003)
|
||||
|
||||
### Features
|
||||
|
||||
* *(cypress)* Remove getSettled
|
||||
* *(cypress)* Use cy.session
|
||||
* *(i18n)* Add Norwegian translation
|
||||
* *(netlify)* Abstract createSlug helper function (#2923)
|
||||
* *(postcss)* Mock plugin types (#2930)
|
||||
* Enable ts for rollup-plugin-visualizer (#2897) ([09d1352](09d13520b060e47be18640865befde44f59332e3))
|
||||
* Remove date-fns formatISO (#2899) ([1f25386](1f25386f54f376357722e1e589d3a8bd8288a033))
|
||||
* Add-task usability improvements (#2767) ([4be53b0](4be53b098ca909194aefb464a93b6dae99f4b9ab))
|
||||
* Remove formatISO from list-view-gantt.spec (#2922) ([a29131e](a29131e7d4be2c83c3e9046549924d1f7692c95e))
|
||||
* Add histoire ([7be8e89](7be8e892e2480f17cb5de6a69d35287906151c0f))
|
||||
* Add XButton story ([ccc85b9](ccc85b9a828488dc849758f1e89f3ba3f75967d1))
|
||||
* Add card story ([35cfb2f](35cfb2f3ca42ac83a9b943fc59818c978ee95fcc))
|
||||
* Add histoire (#2724) ([a4424e0](a4424e089cdfadb4ab3b753e6fdca818bbe82dc4))
|
||||
* Add describe project better in package.json (#2971) ([14466bf](14466bf9b7b8a3fc455c0d601205abbaf8cba4f5))
|
||||
* Add .env.local.example (#2972) ([e1b35ff](e1b35ff023679a7cb8448a06e9edeb8eccc2f727))
|
||||
* Fix broken font preloading (#2980) ([4890149](489014944a1544846875910d7d5e17e3d71b7e2d))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(config)* Remove unused URL_PREFIX const (#2926)
|
||||
* *(package)* Use pnpm commands (#2919)
|
||||
* *(tests)* Fix macos cypress and align with create vite (#2898)
|
||||
* Improve migrate title (#2968) ([56fd25e](56fd25e888cae8343f64a4c14ac5a3a760bdc7be))
|
||||
* Add has content="false" to gantt charts (#2969) ([903e9a9](903e9a9904c18ced59962fc03b4c36e5ac8cd688))
|
||||
* Use es6 imports for deploy-preview-netlify (#2970) ([2a2c27a](2a2c27af9226f441ec80d9d4f560b55cd357126c))
|
||||
|
||||
### Other
|
||||
|
||||
* *(other)* [skip ci] Updated translations via Crowdin
|
||||
* *(other)* Redirect to oidc provider if configured correctly (#2805)
|
||||
|
||||
|
||||
## [0.20.2] - 2022-12-18
|
||||
|
||||
### Bug Fixes
|
||||
|
|
89
Dockerfile
89
Dockerfile
|
@ -1,49 +1,70 @@
|
|||
# Stage 1: Build application
|
||||
FROM --platform=$BUILDPLATFORM node:18-alpine AS compile-image
|
||||
# syntax=docker/dockerfile:1
|
||||
# ┬─┐┬ ┐o┬ ┬─┐
|
||||
# │─││ │││ │ │
|
||||
# ┘─┘┘─┘┘┘─┘┘─┘
|
||||
|
||||
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ARG USE_RELEASE=false
|
||||
ARG RELEASE_VERSION=main
|
||||
|
||||
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
||||
ADD . ./
|
||||
|
||||
RUN \
|
||||
if [ $USE_RELEASE = true ]; then \
|
||||
wget https://dl.vikunja.io/frontend/vikunja-frontend-$RELEASE_VERSION.zip -O frontend-release.zip && \
|
||||
unzip frontend-release.zip -d dist/ && \
|
||||
exit 0; \
|
||||
fi && \
|
||||
# https://pnpm.io/installation#using-corepack
|
||||
corepack enable && \
|
||||
# we don't use corepack prepare here by intend since
|
||||
# we have renovate to keep our dependencies up to date
|
||||
# Build the frontend
|
||||
pnpm install && \
|
||||
apk add --no-cache git && \
|
||||
echo '{"VERSION": "'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'"}' > src/version.json && \
|
||||
pnpm run build
|
||||
COPY package.json ./
|
||||
COPY pnpm-lock.yaml ./
|
||||
|
||||
# Stage 2: copy
|
||||
FROM nginx:alpine
|
||||
RUN if [ "$USE_RELEASE" != true ]; then \
|
||||
# https://pnpm.io/installation#using-corepack
|
||||
corepack enable && \
|
||||
pnpm install; \
|
||||
fi
|
||||
|
||||
COPY nginx.conf /etc/nginx/nginx.conf
|
||||
COPY scripts/run.sh /run.sh
|
||||
COPY . ./
|
||||
|
||||
# copy compiled files from stage 1
|
||||
COPY --from=compile-image /build/dist /usr/share/nginx/html
|
||||
RUN if [ "$USE_RELEASE" != true ]; then \
|
||||
apk add --no-cache --virtual .build-deps git jq && \
|
||||
git describe --tags --always --abbrev=10 | sed 's/-/+/; s/^v//; s/-g/-/' | \
|
||||
xargs -0 -I{} jq -Mcnr --arg version {} '{VERSION:$version}' | \
|
||||
tee src/version.json && \
|
||||
apk del .build-deps; \
|
||||
fi
|
||||
|
||||
# Unprivileged user
|
||||
ENV PUID 1000
|
||||
ENV PGID 1000
|
||||
RUN if [ "$USE_RELEASE" = true ]; then \
|
||||
wget "https://dl.vikunja.io/frontend/vikunja-frontend-${RELEASE_VERSION}.zip" -O frontend-release.zip && \
|
||||
unzip frontend-release.zip -d dist/; \
|
||||
else \
|
||||
# we don't use corepack prepare here by intend since
|
||||
# we have renovate to keep our dependencies up to date
|
||||
# Build the frontend
|
||||
pnpm run build; \
|
||||
fi
|
||||
|
||||
# ┌┐┐┌─┐o┌┐┐┐ │
|
||||
# ││││ ┬││││┌┼┘
|
||||
# ┘└┘┘─┘┘┘└┘┘ └
|
||||
|
||||
FROM nginx:stable-alpine AS runner
|
||||
WORKDIR /usr/share/nginx/html
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
|
||||
RUN apk add --no-cache \
|
||||
# for sh file
|
||||
bash \
|
||||
# installs usermod and groupmod
|
||||
shadow
|
||||
ENV VIKUNJA_HTTP_PORT 80
|
||||
ENV VIKUNJA_HTTP2_PORT 81
|
||||
ENV VIKUNJA_LOG_FORMAT main
|
||||
ENV VIKUNJA_API_URL /api/v1
|
||||
ENV VIKUNJA_SENTRY_ENABLED false
|
||||
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
|
||||
|
||||
CMD "/run.sh"
|
||||
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
|
||||
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh
|
||||
COPY docker/nginx.conf /etc/nginx/nginx.conf
|
||||
COPY docker/templates/. /etc/nginx/templates/
|
||||
# copy compiled files from stage 1
|
||||
COPY --from=builder /build/dist ./
|
||||
# manage permissions
|
||||
RUN chmod 0755 /docker-entrypoint.d/*.sh /etc/nginx/templates && \
|
||||
chmod -R 0644 /etc/nginx/nginx.conf && \
|
||||
chown -R nginx:nginx ./ /etc/nginx/conf.d /etc/nginx/templates && \
|
||||
rm -f /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
|
||||
# unprivileged user
|
||||
USER nginx
|
||||
|
|
13
README.md
13
README.md
|
@ -4,7 +4,7 @@
|
|||
|
||||
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
|
||||
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
|
||||
[![Download](https://img.shields.io/badge/download-v0.20.2-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Download](https://img.shields.io/badge/download-v0.20.3-brightgreen.svg)](https://dl.vikunja.io)
|
||||
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
|
||||
|
||||
This is the web frontend for Vikunja, written in Vue.js.
|
||||
|
@ -18,6 +18,14 @@ If you find any security-related issues you don't want to disclose publicly, ple
|
|||
## Docker
|
||||
|
||||
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
|
||||
In order to build it from sources run the command below. (Docker >= v19.03)
|
||||
|
||||
```shell
|
||||
export DOCKER_BUILDKIT=1
|
||||
docker build -t vikunja/frontend .
|
||||
```
|
||||
|
||||
Refer to Refer [to multi-platform documentation](https://docs.docker.com/build/building/multi-platform/) in order to build for the different platform.
|
||||
|
||||
## Project setup
|
||||
|
||||
|
@ -43,6 +51,3 @@ pnpm run build
|
|||
pnpm run lint
|
||||
```
|
||||
|
||||
## Sponsors
|
||||
|
||||
[![Relm](https://vikunja.io/images/sponsors/relm.png)](https://relm.us)
|
||||
|
|
|
@ -14,9 +14,9 @@ describe('List View List', () => {
|
|||
cy.visit('/lists/1')
|
||||
cy.url()
|
||||
.should('contain', '/lists/1/list')
|
||||
cy.get('.list-title h1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'First List')
|
||||
cy.get('.list-title .dropdown')
|
||||
cy.get('.list-title-dropdown')
|
||||
.should('exist')
|
||||
cy.get('p')
|
||||
.contains('This list is currently empty.')
|
||||
|
@ -62,7 +62,7 @@ describe('List View List', () => {
|
|||
})
|
||||
cy.visit(`/lists/${lists[1].id}/`)
|
||||
|
||||
cy.get('.list-title .icon')
|
||||
cy.get('.list-title-wrapper .icon')
|
||||
.should('not.exist')
|
||||
cy.get('input.input[placeholder="Add a new task..."')
|
||||
.should('not.exist')
|
||||
|
|
|
@ -30,7 +30,7 @@ describe('Lists', () => {
|
|||
.should('contain', 'Success')
|
||||
cy.url()
|
||||
.should('contain', '/lists/')
|
||||
cy.get('.list-title h1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'New List')
|
||||
})
|
||||
|
||||
|
@ -51,7 +51,7 @@ describe('Lists', () => {
|
|||
const newListName = 'New list name'
|
||||
|
||||
cy.visit('/lists/1')
|
||||
cy.get('.list-title h1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', 'First List')
|
||||
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
|
||||
|
@ -67,7 +67,7 @@ describe('Lists', () => {
|
|||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.list-title h1')
|
||||
cy.get('.list-title')
|
||||
.should('contain', newListName)
|
||||
.should('not.contain', lists[0].title)
|
||||
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child')
|
||||
|
@ -104,9 +104,9 @@ describe('Lists', () => {
|
|||
it('Should archive a list', () => {
|
||||
cy.visit(`/lists/${lists[0].id}`)
|
||||
|
||||
cy.get('.list-title .dropdown')
|
||||
cy.get('.list-title-dropdown')
|
||||
.click()
|
||||
cy.get('.list-title .dropdown .dropdown-menu .dropdown-item')
|
||||
cy.get('.list-title-dropdown .dropdown-menu .dropdown-item')
|
||||
.contains('Archive')
|
||||
.click()
|
||||
cy.get('.modal-content')
|
||||
|
|
|
@ -11,7 +11,7 @@ export function createLists() {
|
|||
return lists
|
||||
}
|
||||
|
||||
export function prepareLists(setLists = () => {}) {
|
||||
export function prepareLists(setLists = (...args: any[]) => {}) {
|
||||
beforeEach(() => {
|
||||
const lists = createLists()
|
||||
setLists(lists)
|
||||
|
|
|
@ -2,9 +2,9 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
|
|||
import {createLists} from '../list/prepareLists'
|
||||
|
||||
function logout() {
|
||||
cy.get('.navbar .user .username')
|
||||
cy.get('.navbar .username-dropdown-trigger')
|
||||
.click()
|
||||
cy.get('.navbar .user .dropdown-menu .dropdown-item')
|
||||
cy.get('.navbar .dropdown-item')
|
||||
.contains('Logout')
|
||||
.click()
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ describe('User Settings', () => {
|
|||
|
||||
cy.get('.global-notification')
|
||||
.should('contain', 'Success')
|
||||
cy.get('.navbar .user .username')
|
||||
cy.get('.navbar .username-dropdown-trigger .username')
|
||||
.should('contain', 'Lorem Ipsum')
|
||||
})
|
||||
})
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
echo "info: API URL is $VIKUNJA_API_URL"
|
||||
echo "info: Sentry enabled: $VIKUNJA_SENTRY_ENABLED"
|
||||
|
||||
# Escape the variable to prevent sed from complaining
|
||||
VIKUNJA_API_URL="$(echo "$VIKUNJA_API_URL" | sed -r 's/([:;])/\\\1/g')"
|
||||
VIKUNJA_SENTRY_DSN="$(echo "$VIKUNJA_SENTRY_DSN" | sed -r 's/([:;])/\\\1/g')"
|
||||
|
||||
sed -ri "s:^(\s*window.API_URL\s*=)\s*.+:\1 '${VIKUNJA_API_URL}':g" /usr/share/nginx/html/index.html
|
||||
sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g" /usr/share/nginx/html/index.html
|
||||
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
|
||||
|
||||
date -uIseconds | xargs echo 'info: started at'
|
|
@ -0,0 +1,19 @@
|
|||
#!/usr/bin/env sh
|
||||
set -e
|
||||
|
||||
if [ ! -f "/proc/net/if_inet6" ]; then
|
||||
echo "info: IPv6 is not available! Removing IPv6 listen configuration"
|
||||
find /etc/nginx/conf.d -name '*.conf' -type f | \
|
||||
while IFS= read -r CONFIG; do
|
||||
sed -r '/^\s*listen\s*\[::\]:.+$/d' "$CONFIG" > "$CONFIG.temp"
|
||||
if ! diff -U 5 "$CONFIG" "$CONFIG.temp" > "$CONFIG.diff"; then
|
||||
echo "info: Removing IPv6 lines from $CONFIG" | \
|
||||
cat - "$CONFIG.diff"
|
||||
echo "# IPv6 is disabled because /proc/net/if_inet6 was not found" | \
|
||||
cat - "$CONFIG.temp" > "$CONFIG"
|
||||
else
|
||||
echo "info: Skipping $CONFIG because it does not have IPv6 listen"
|
||||
fi
|
||||
rm -f "$CONFIG.temp" "$CONFIG.diff"
|
||||
done
|
||||
fi
|
|
@ -0,0 +1,112 @@
|
|||
# Generated by nginxconfig.io
|
||||
# https://www.digitalocean.com/community/tools/nginx?domains.0.server.domain=localhost&domains.0.server.documentRoot=%2Fusr%2Fshare%2Fnginx%2Fhtml&domains.0.server.cdnSubdomain=true&domains.0.https.https=false&domains.0.php.php=false&domains.0.routing.index=index.html&domains.0.routing.fallbackHtml=true&domains.0.routing.fallbackPhp=false&global.performance.assetsExpiration=1d&global.performance.mediaExpiration=1d&global.performance.svgExpiration=1d&global.performance.fontsExpiration=1d&global.logging.accessLog=%2Fdev%2Fstdout&global.logging.errorLog=%2Fdev%2Fstderr%20warn&global.logging.logNotFound=true&global.nginx.user=nginx&global.nginx.pid=%2Fvar%2Frun%2Fnginx.pid&global.nginx.clientMaxBodySize=50&global.docker.dockerfile=true&global.tools.modularizedStructure=false&global.tools.symlinkVhost=false
|
||||
# and then edited manually ;)
|
||||
|
||||
pid /tmp/nginx.pid;
|
||||
worker_processes auto;
|
||||
worker_rlimit_nofile 65535;
|
||||
|
||||
events {
|
||||
multi_accept on;
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
charset utf-8;
|
||||
sendfile on;
|
||||
tcp_nopush on;
|
||||
tcp_nodelay on;
|
||||
server_tokens off;
|
||||
types_hash_max_size 2048;
|
||||
types_hash_bucket_size 64;
|
||||
|
||||
# rootless
|
||||
client_body_temp_path /tmp/client_temp;
|
||||
proxy_temp_path /tmp/proxy_temp_path;
|
||||
fastcgi_temp_path /tmp/fastcgi_temp;
|
||||
uwsgi_temp_path /tmp/uwsgi_temp;
|
||||
scgi_temp_path /tmp/scgi_temp;
|
||||
|
||||
# MIME
|
||||
include mime.types;
|
||||
default_type application/octet-stream;
|
||||
types {
|
||||
application/manifest+json webmanifest;
|
||||
}
|
||||
|
||||
# Logging
|
||||
log_format json escape=json
|
||||
'{'
|
||||
'"bytes_sent": "$bytes_sent",'
|
||||
'"http_user_agent": "$http_user_agent",'
|
||||
'"nginx_version": "$nginx_version",'
|
||||
'"query_string": "$query_string",'
|
||||
'"realip_remote_addr": "$realip_remote_addr",'
|
||||
'"remote_addr": "$remote_addr",'
|
||||
'"remote_user": "$remote_user",'
|
||||
'"request_length": "$request_length",'
|
||||
'"request_method": "$request_method",'
|
||||
'"request_time": "$request_time",'
|
||||
'"server_addr": "$server_addr",'
|
||||
'"server_port": "$server_port",'
|
||||
'"server_protocol": "$server_protocol",'
|
||||
'"status": "$status",'
|
||||
'"time_local": "$time_local",'
|
||||
'"uri": "$uri"'
|
||||
'}';
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /dev/stdout main;
|
||||
error_log /dev/stderr warn;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
# compression
|
||||
gzip on;
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
application/json
|
||||
application/x-javascript
|
||||
application/javascript
|
||||
text/xml
|
||||
application/xml
|
||||
application/xml+rss
|
||||
text/javascript
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
font/opentype
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
audio/wav;
|
||||
|
||||
map_hash_max_size 128;
|
||||
map_hash_bucket_size 128;
|
||||
|
||||
map $sent_http_content_type $expires {
|
||||
default off;
|
||||
text/css max;
|
||||
application/javascript max;
|
||||
text/javascript max;
|
||||
application/vnd.ms-fontobject max;
|
||||
application/x-font-ttf max;
|
||||
font/opentype max;
|
||||
font/woff2 max;
|
||||
image/svg+xml max;
|
||||
image/x-icon max;
|
||||
audio/wav max;
|
||||
~images/ max;
|
||||
~font/ max;
|
||||
}
|
||||
|
||||
include /etc/nginx/conf.d/*.conf;
|
||||
}
|
|
@ -0,0 +1,71 @@
|
|||
server {
|
||||
listen ${VIKUNJA_HTTP_PORT};
|
||||
listen [::]:${VIKUNJA_HTTP_PORT};
|
||||
## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
||||
listen ${VIKUNJA_HTTP2_PORT} default_server http2 proxy_protocol;
|
||||
listen [::]:${VIKUNJA_HTTP2_PORT} default_server http2 proxy_protocol;
|
||||
|
||||
server_name _;
|
||||
expires $expires;
|
||||
root /usr/share/nginx/html;
|
||||
access_log /dev/stdout ${VIKUNJA_LOG_FORMAT};
|
||||
# security headers
|
||||
add_header X-XSS-Protection "1; mode=block" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
add_header Referrer-Policy "no-referrer-when-downgrade" always;
|
||||
add_header Content-Security-Policy "default-src 'self' http: https: ws: wss: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always;
|
||||
add_header Permissions-Policy "interest-cohort=()" always;
|
||||
|
||||
# . files
|
||||
location ~ /\.(?!well-known) {
|
||||
deny all;
|
||||
}
|
||||
|
||||
# assume that everything else is handled by the application router, by injecting the index.html.
|
||||
location / {
|
||||
autoindex off;
|
||||
expires off;
|
||||
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
# favicon.ico
|
||||
location = /favicon.ico {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# robots.txt
|
||||
location = /robots.txt {
|
||||
log_not_found off;
|
||||
access_log off;
|
||||
expires -1; # no-cache
|
||||
}
|
||||
|
||||
location = /ready {
|
||||
return 200 "";
|
||||
access_log off;
|
||||
expires -1; # no-cache
|
||||
}
|
||||
|
||||
# all assets contain hash in filename, cache forever
|
||||
location ^~ /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# all workbox scripts are compiled with hash in filename, cache forever3
|
||||
location ^~ /workbox- {
|
||||
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# assets, media
|
||||
location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html { }
|
||||
|
||||
}
|
|
@ -9,9 +9,7 @@
|
|||
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="apple-touch-icon" href="/images/icons/apple-touch-icon-180x180.png"/>
|
||||
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/OpenSans[wght].woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/OpenSans-Italic[wght].woff2" as="font">
|
||||
<link rel="preload" crossorigin="anonymous" href="/src/assets/fonts/Quicksand[wght].woff2" as="font">
|
||||
<!--__vite-plugin-inject-preload__-->
|
||||
</head>
|
||||
<body>
|
||||
<noscript>
|
||||
|
|
115
nginx.conf
115
nginx.conf
|
@ -1,115 +0,0 @@
|
|||
user nginx;
|
||||
worker_processes 1;
|
||||
|
||||
error_log /var/log/nginx/error.log warn;
|
||||
pid /var/run/nginx.pid;
|
||||
|
||||
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
|
||||
types {
|
||||
application/manifest+json webmanifest;
|
||||
}
|
||||
|
||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
||||
'$status $body_bytes_sent "$http_referer" '
|
||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
||||
|
||||
access_log /var/log/nginx/access.log main;
|
||||
|
||||
sendfile on;
|
||||
#tcp_nopush on;
|
||||
|
||||
keepalive_timeout 65;
|
||||
|
||||
gzip on;
|
||||
|
||||
gzip_vary on;
|
||||
gzip_proxied any;
|
||||
gzip_comp_level 6;
|
||||
gzip_buffers 16 8k;
|
||||
gzip_http_version 1.1;
|
||||
gzip_min_length 256;
|
||||
gzip_types
|
||||
text/plain
|
||||
text/css
|
||||
application/json
|
||||
application/x-javascript
|
||||
application/javascript
|
||||
text/xml
|
||||
application/xml
|
||||
application/xml+rss
|
||||
text/javascript
|
||||
application/vnd.ms-fontobject
|
||||
application/x-font-ttf
|
||||
font/opentype
|
||||
image/svg+xml
|
||||
image/x-icon
|
||||
audio/wav;
|
||||
|
||||
map_hash_max_size 128;
|
||||
map_hash_bucket_size 128;
|
||||
|
||||
# Expires map
|
||||
map $sent_http_content_type $expires {
|
||||
default off;
|
||||
text/css max;
|
||||
application/javascript max;
|
||||
text/javascript max;
|
||||
application/vnd.ms-fontobject max;
|
||||
application/x-font-ttf max;
|
||||
font/opentype max;
|
||||
font/woff2 max;
|
||||
image/svg+xml max;
|
||||
image/x-icon max;
|
||||
audio/wav max;
|
||||
~images/ max;
|
||||
~font/ max;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
listen 81 default_server http2 proxy_protocol; ## Needed when behind HAProxy with SSL termination + HTTP/2 support
|
||||
|
||||
server_name _;
|
||||
|
||||
expires $expires;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
# all assets contain hash in filename, cache forever
|
||||
location ^~ /assets/ {
|
||||
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# all workbox scripts are compiled with hash in filename, cache forever3
|
||||
location ^~ /workbox- {
|
||||
add_header Cache-Control "public, max-age=31536000, s-maxage=31536000, immutable";
|
||||
try_files $uri =404;
|
||||
}
|
||||
|
||||
# assume that everything else is handled by the application router, by injecting the index.html.
|
||||
location / {
|
||||
autoindex off;
|
||||
expires off;
|
||||
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||
try_files $uri /index.html =404;
|
||||
}
|
||||
|
||||
location ~* .(txt|webmanifest|css|js|mjs|map|svg|jpg|jpeg|png|ico|ttf|woff|woff2|wav)$ {
|
||||
try_files $uri $uri/ =404;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
}
|
||||
}
|
||||
}
|
98
package.json
98
package.json
|
@ -13,7 +13,7 @@
|
|||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@7.25.0",
|
||||
"packageManager": "pnpm@7.27.1",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
|
@ -34,7 +34,7 @@
|
|||
"test:e2e-record": "start-server-and-test preview http://127.0.0.1:4173 'cypress run --e2e --browser chrome --record'",
|
||||
"test:e2e-dev-dev": "start-server-and-test preview:dev http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:e2e-dev": "start-server-and-test preview http://127.0.0.1:4173 'cypress open --e2e'",
|
||||
"test:unit": "vitest",
|
||||
"test:unit": "vitest --dir ./src",
|
||||
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
|
||||
"browserslist:update": "pnpm dlx browserslist@latest --update-db",
|
||||
"fonts:update": "pnpm fonts:download && pnpm fonts:subset",
|
||||
|
@ -45,28 +45,28 @@
|
|||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.2.1",
|
||||
"@fortawesome/free-regular-svg-icons": "6.2.1",
|
||||
"@fortawesome/free-solid-svg-icons": "6.2.1",
|
||||
"@fortawesome/vue-fontawesome": "3.0.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.3.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.3.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.3.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.3",
|
||||
"@github/hotkey": "2.0.1",
|
||||
"@infectoone/vue-ganttastic": "2.1.3",
|
||||
"@intlify/unplugin-vue-i18n": "0.8.1",
|
||||
"@kyvg/vue3-notification": "2.8.0",
|
||||
"@sentry/tracing": "7.31.1",
|
||||
"@sentry/vue": "7.31.1",
|
||||
"@infectoone/vue-ganttastic": "2.1.4",
|
||||
"@intlify/unplugin-vue-i18n": "0.8.2",
|
||||
"@kyvg/vue3-notification": "2.9.0",
|
||||
"@sentry/tracing": "7.38.0",
|
||||
"@sentry/vue": "7.38.0",
|
||||
"@types/is-touch-device": "1.0.0",
|
||||
"@types/lodash.clonedeep": "4.5.7",
|
||||
"@types/sortablejs": "1.15.0",
|
||||
"@vueuse/core": "9.11.0",
|
||||
"axios": "1.2.2",
|
||||
"blurhash": "2.0.4",
|
||||
"@vueuse/core": "9.13.0",
|
||||
"axios": "1.3.3",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"camel-case": "4.1.2",
|
||||
"codemirror": "5.65.11",
|
||||
"codemirror": "5.65.12",
|
||||
"date-fns": "2.29.3",
|
||||
"dayjs": "1.11.7",
|
||||
"dompurify": "2.4.3",
|
||||
"dompurify": "3.0.0",
|
||||
"easymde": "2.18.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
|
@ -75,18 +75,17 @@
|
|||
"focus-within": "3.0.2",
|
||||
"highlight.js": "11.7.0",
|
||||
"is-touch-device": "1.0.1",
|
||||
"lodash.clonedeep": "4.5.0",
|
||||
"klona": "2.0.6",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"marked": "4.2.12",
|
||||
"minimist": "1.2.7",
|
||||
"pinia": "2.0.29",
|
||||
"pinia": "2.0.32",
|
||||
"register-service-worker": "1.7.2",
|
||||
"snake-case": "3.0.4",
|
||||
"sortablejs": "1.15.0",
|
||||
"ufo": "1.0.1",
|
||||
"vue": "3.2.45",
|
||||
"ufo": "1.1.0",
|
||||
"vue": "3.2.47",
|
||||
"vue-advanced-cropper": "2.8.8",
|
||||
"vue-flatpickr-component": "11.0.1",
|
||||
"vue-flatpickr-component": "11.0.2",
|
||||
"vue-i18n": "9.2.2",
|
||||
"vue-router": "4.1.6",
|
||||
"workbox-precaching": "6.5.4",
|
||||
|
@ -94,52 +93,53 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.3",
|
||||
"@cypress/vite-dev-server": "5.0.2",
|
||||
"@cypress/vue": "5.0.3",
|
||||
"@cypress/vite-dev-server": "5.0.3",
|
||||
"@cypress/vue": "5.0.4",
|
||||
"@faker-js/faker": "7.6.0",
|
||||
"@histoire/plugin-screenshot": "0.12.4",
|
||||
"@histoire/plugin-vue": "0.12.4",
|
||||
"@histoire/plugin-screenshot": "0.15.8",
|
||||
"@histoire/plugin-vue": "0.15.8",
|
||||
"@rushstack/eslint-patch": "1.2.0",
|
||||
"@types/codemirror": "5.60.6",
|
||||
"@types/codemirror": "5.60.7",
|
||||
"@types/dompurify": "2.4.0",
|
||||
"@types/flexsearch": "0.7.3",
|
||||
"@types/focus-within": "1.0.1",
|
||||
"@types/lodash.debounce": "4.0.7",
|
||||
"@types/marked": "4.0.8",
|
||||
"@types/node": "18.11.18",
|
||||
"@types/node": "18.14.0",
|
||||
"@types/postcss-preset-env": "7.7.0",
|
||||
"@typescript-eslint/eslint-plugin": "5.48.2",
|
||||
"@typescript-eslint/parser": "5.48.2",
|
||||
"@vitejs/plugin-legacy": "3.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.53.0",
|
||||
"@typescript-eslint/parser": "5.53.0",
|
||||
"@vitejs/plugin-legacy": "4.0.1",
|
||||
"@vitejs/plugin-vue": "4.0.0",
|
||||
"@vue/eslint-config-typescript": "11.0.2",
|
||||
"@vue/test-utils": "2.2.7",
|
||||
"@vue/test-utils": "2.3.0",
|
||||
"@vue/tsconfig": "0.1.3",
|
||||
"autoprefixer": "10.4.13",
|
||||
"browserslist": "4.21.4",
|
||||
"caniuse-lite": "1.0.30001445",
|
||||
"browserslist": "4.21.5",
|
||||
"caniuse-lite": "1.0.30001457",
|
||||
"csstype": "3.1.1",
|
||||
"cypress": "12.3.0",
|
||||
"esbuild": "0.17.2",
|
||||
"eslint": "8.32.0",
|
||||
"cypress": "12.6.0",
|
||||
"esbuild": "0.17.10",
|
||||
"eslint": "8.34.0",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"happy-dom": "8.1.4",
|
||||
"histoire": "0.12.4",
|
||||
"netlify-cli": "12.7.2",
|
||||
"happy-dom": "8.6.0",
|
||||
"histoire": "0.15.8",
|
||||
"netlify-cli": "12.13.2",
|
||||
"postcss": "8.4.21",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "3.0.1",
|
||||
"postcss-preset-env": "7.8.3",
|
||||
"rollup": "3.10.0",
|
||||
"postcss-preset-env": "8.0.1",
|
||||
"rollup": "3.17.2",
|
||||
"rollup-plugin-visualizer": "5.9.0",
|
||||
"sass": "1.57.1",
|
||||
"start-server-and-test": "1.15.2",
|
||||
"typescript": "4.9.4",
|
||||
"vite": "4.0.4",
|
||||
"vite-plugin-pwa": "0.14.1",
|
||||
"sass": "1.58.3",
|
||||
"start-server-and-test": "1.15.4",
|
||||
"typescript": "4.9.5",
|
||||
"vite": "4.1.3",
|
||||
"vite-plugin-inject-preload": "1.3.0",
|
||||
"vite-plugin-pwa": "0.14.4",
|
||||
"vite-svg-loader": "4.0.0",
|
||||
"vitest": "0.27.2",
|
||||
"vue-tsc": "1.0.24",
|
||||
"vitest": "0.28.5",
|
||||
"vue-tsc": "1.1.5",
|
||||
"wait-on": "7.0.1",
|
||||
"workbox-cli": "6.5.4"
|
||||
}
|
||||
|
|
3698
pnpm-lock.yaml
3698
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -2,7 +2,7 @@
|
|||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"labels": ["dependencies"],
|
||||
"extends": [
|
||||
"config:base"
|
||||
"config:js-app"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
|
@ -20,6 +20,13 @@
|
|||
"@vueuse/"
|
||||
]
|
||||
},
|
||||
{
|
||||
"groupName": "histoire",
|
||||
"matchPackagePrefixes": [
|
||||
"@histoire/",
|
||||
"histoire"
|
||||
]
|
||||
},
|
||||
{
|
||||
"matchDepTypes": ["devDependencies"],
|
||||
"automerge": true,
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
24df869e7a9282c76c9e1883071a39c0b11a53a57da68b37f2b918df25b1ae0f1b403e38a29c9cb694575bb9a7b52b6e ./scripts/deploy-preview-netlify.js
|
|
@ -1,4 +1,4 @@
|
|||
const { exec } = require('child_process')
|
||||
import { exec } from 'node:child_process'
|
||||
|
||||
function createSlug(string) {
|
||||
return String(string)
|
|
@ -0,0 +1 @@
|
|||
57af69409e66bc87f4f2fc5822dd8d3c2eb47c601f81af1ac4a56f3e2d80837b1a2de06f4ff57695ec379b7c15b881e3 ./scripts/deploy-preview-netlify.mjs
|
|
@ -1,28 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
# This shell script sets the api url based on an environment variable and starts nginx in foreground.
|
||||
|
||||
VIKUNJA_API_URL="${VIKUNJA_API_URL:-"/api/v1"}"
|
||||
VIKUNJA_SENTRY_ENABLED="${VIKUNJA_SENTRY_ENABLED:-"false"}"
|
||||
VIKUNJA_SENTRY_DSN="${VIKUNJA_SENTRY_DSN:-"https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480"}"
|
||||
VIKUNJA_HTTP_PORT="${VIKUNJA_HTTP_PORT:-80}"
|
||||
VIKUNJA_HTTPS_PORT="${VIKUNJA_HTTPS_PORT:-443}"
|
||||
|
||||
echo "Using $VIKUNJA_API_URL as default api url"
|
||||
|
||||
# Escape the variable to prevent sed from complaining
|
||||
VIKUNJA_API_URL=$(echo $VIKUNJA_API_URL |sed 's/\//\\\//g')
|
||||
|
||||
sed -i "s/http\:\/\/localhost\:3456//g" /usr/share/nginx/html/index.html # replacing in two steps to make sure api urls from releases are properly replaced as well
|
||||
sed -i "s/'\/api\/v1/'$VIKUNJA_API_URL/g" /usr/share/nginx/html/index.html
|
||||
sed -i "s/\.SENTRY_ENABLED = false/\.SENTRY_ENABLED = $VIKUNJA_SENTRY_ENABLED/g" /usr/share/nginx/html/index.html
|
||||
sed -i "s|\.SENTRY_DSN = '.*'|\.SENTRY_DSN = '$VIKUNJA_SENTRY_DSN'|g" /usr/share/nginx/html/index.html
|
||||
|
||||
sed -i "s/listen 80/listen $VIKUNJA_HTTP_PORT/g" /etc/nginx/nginx.conf
|
||||
sed -i "s/listen 443/listen $VIKUNJA_HTTPS_PORT/g" /etc/nginx/nginx.conf
|
||||
|
||||
# Set the uid and gid of the nginx run user
|
||||
usermod --non-unique --uid ${PUID} nginx
|
||||
groupmod --non-unique --gid ${PGID} nginx
|
||||
|
||||
nginx -g "daemon off;"
|
23
src/App.vue
23
src/App.vue
|
@ -8,9 +8,13 @@
|
|||
<no-auth-wrapper v-else>
|
||||
<router-view/>
|
||||
</no-auth-wrapper>
|
||||
<Notification/>
|
||||
|
||||
|
||||
<keyboard-shortcuts v-if="keyboardShortcutsActive"/>
|
||||
|
||||
<Teleport to="body">
|
||||
<UpdateNotification/>
|
||||
<Notification/>
|
||||
</Teleport>
|
||||
</ready>
|
||||
</template>
|
||||
|
||||
|
@ -19,23 +23,26 @@ import {computed, watch} from 'vue'
|
|||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import isTouchDevice from 'is-touch-device'
|
||||
import {success} from '@/message'
|
||||
|
||||
import Notification from '@/components/misc/notification.vue'
|
||||
import KeyboardShortcuts from './components/misc/keyboard-shortcuts/index.vue'
|
||||
import UpdateNotification from '@/components/home/UpdateNotification.vue'
|
||||
import KeyboardShortcuts from '@/components/misc/keyboard-shortcuts/index.vue'
|
||||
|
||||
import TheNavigation from '@/components/home/TheNavigation.vue'
|
||||
import ContentAuth from './components/home/contentAuth.vue'
|
||||
import ContentLinkShare from './components/home/contentLinkShare.vue'
|
||||
import ContentAuth from '@/components/home/contentAuth.vue'
|
||||
import ContentLinkShare from '@/components/home/contentLinkShare.vue'
|
||||
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
|
||||
import Ready from '@/components/misc/ready.vue'
|
||||
|
||||
import {setLanguage} from './i18n'
|
||||
import {setLanguage} from '@/i18n'
|
||||
import AccountDeleteService from '@/services/accountDelete'
|
||||
import {success} from '@/message'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import {useColorScheme} from '@/composables/useColorScheme'
|
||||
import {useBodyClass} from '@/composables/useBodyClass'
|
||||
import {useAuthStore} from './stores/auth'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const authStore = useAuthStore()
|
||||
|
|
|
@ -2,100 +2,93 @@
|
|||
<header
|
||||
:class="{'has-background': background, 'menu-active': menuActive}"
|
||||
aria-label="main navigation"
|
||||
class="navbar main-theme is-fixed-top d-print-none"
|
||||
class="navbar d-print-none"
|
||||
>
|
||||
<router-link :to="{name: 'home'}" class="logo-link">
|
||||
<Logo width="164" height="48"/>
|
||||
</router-link>
|
||||
|
||||
<MenuButton class="menu-button"/>
|
||||
<div class="list-title" ref="listTitle" v-show="currentList.id">
|
||||
<template v-if="currentList.id">
|
||||
<h1
|
||||
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
|
||||
class="title">
|
||||
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
|
||||
</h1>
|
||||
|
||||
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="info-button">
|
||||
<icon icon="circle-info"/>
|
||||
</BaseButton>
|
||||
<div
|
||||
v-if="currentList.id"
|
||||
class="list-title-wrapper"
|
||||
>
|
||||
<h1 class="list-title">{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}</h1>
|
||||
|
||||
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="list-title-button">
|
||||
<icon icon="circle-info"/>
|
||||
</BaseButton>
|
||||
|
||||
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
|
||||
</template>
|
||||
<list-settings-dropdown
|
||||
v-if="canWriteCurrentList && currentList.id !== -1"
|
||||
class="list-title-dropdown"
|
||||
:list="currentList"
|
||||
>
|
||||
<template #trigger="{toggleOpen}">
|
||||
<BaseButton class="list-title-button" @click="toggleOpen">
|
||||
<icon icon="ellipsis-h" class="icon"/>
|
||||
</BaseButton>
|
||||
</template>
|
||||
</list-settings-dropdown>
|
||||
</div>
|
||||
|
||||
<div class="navbar-end">
|
||||
<update/>
|
||||
<BaseButton
|
||||
@click="openQuickActions"
|
||||
class="trigger-button pr-0"
|
||||
class="trigger-button"
|
||||
v-shortcut="'Control+k'"
|
||||
:title="$t('keyboardShortcuts.quickSearch')"
|
||||
>
|
||||
<icon icon="search"/>
|
||||
</BaseButton>
|
||||
<notifications/>
|
||||
<div class="user">
|
||||
<dropdown class="is-right" ref="usernameDropdown">
|
||||
<template #trigger="{toggleOpen}">
|
||||
<x-button
|
||||
class="username-dropdown-trigger"
|
||||
@click="toggleOpen()"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
|
||||
<span class="username">{{ authStore.userDisplayName }}</span>
|
||||
<span class="icon is-small">
|
||||
<icon icon="chevron-down"/>
|
||||
</span>
|
||||
</x-button>
|
||||
</template>
|
||||
<Notifications />
|
||||
<dropdown>
|
||||
<template #trigger="{toggleOpen, open}">
|
||||
<BaseButton
|
||||
class="username-dropdown-trigger"
|
||||
@click="toggleOpen"
|
||||
variant="secondary"
|
||||
:shadow="false"
|
||||
>
|
||||
<img :src="authStore.avatarUrl" alt="" class="avatar" width="40" height="40"/>
|
||||
<span class="username">{{ authStore.userDisplayName }}</span>
|
||||
<span class="icon is-small" :style="{
|
||||
transform: open ? 'rotate(180deg)' : 'rotate(0)',
|
||||
}">
|
||||
<icon icon="chevron-down"/>
|
||||
</span>
|
||||
</BaseButton>
|
||||
</template>
|
||||
|
||||
<dropdown-item
|
||||
:to="{name: 'user.settings'}"
|
||||
>
|
||||
{{ $t('user.settings.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="imprintUrl"
|
||||
:href="imprintUrl"
|
||||
>
|
||||
{{ $t('navigation.imprint') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
v-if="privacyPolicyUrl"
|
||||
:href="privacyPolicyUrl"
|
||||
>
|
||||
{{ $t('navigation.privacy') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click="baseStore.setKeyboardShortcutsActive(true)"
|
||||
>
|
||||
{{ $t('keyboardShortcuts.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
:to="{name: 'about'}"
|
||||
>
|
||||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item
|
||||
@click="authStore.logout()"
|
||||
>
|
||||
{{ $t('user.auth.logout') }}
|
||||
</dropdown-item>
|
||||
</dropdown>
|
||||
</div>
|
||||
<dropdown-item :to="{name: 'user.settings'}">
|
||||
{{ $t('user.settings.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item v-if="imprintUrl" :href="imprintUrl">
|
||||
{{ $t('navigation.imprint') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item v-if="privacyPolicyUrl" :href="privacyPolicyUrl">
|
||||
{{ $t('navigation.privacy') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item @click="baseStore.setKeyboardShortcutsActive(true)">
|
||||
{{ $t('keyboardShortcuts.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item :to="{name: 'about'}">
|
||||
{{ $t('about.title') }}
|
||||
</dropdown-item>
|
||||
<dropdown-item @click="authStore.logout()">
|
||||
{{ $t('user.auth.logout') }}
|
||||
</dropdown-item>
|
||||
</dropdown>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, nextTick} from 'vue'
|
||||
import {computed} from 'vue'
|
||||
|
||||
import {RIGHTS as Rights} from '@/constants/rights'
|
||||
|
||||
import Update from '@/components/home/update.vue'
|
||||
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
|
||||
import Dropdown from '@/components/misc/dropdown.vue'
|
||||
import DropdownItem from '@/components/misc/dropdown-item.vue'
|
||||
|
@ -122,182 +115,152 @@ const configStore = useConfigStore()
|
|||
const imprintUrl = computed(() => configStore.legal.imprintUrl)
|
||||
const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl)
|
||||
|
||||
const usernameDropdown = ref()
|
||||
const listTitle = ref()
|
||||
onMounted(async () => {
|
||||
await nextTick()
|
||||
if (typeof usernameDropdown.value === 'undefined' || typeof listTitle.value === 'undefined') {
|
||||
return
|
||||
}
|
||||
|
||||
const usernameWidth = usernameDropdown.value.$el.clientWidth
|
||||
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
|
||||
})
|
||||
|
||||
function openQuickActions() {
|
||||
baseStore.setQuickActionsActive(true)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
$vikunja-nav-logo-full-width: 164px;
|
||||
$user-dropdown-width-mobile: 5rem;
|
||||
|
||||
$hamburger-menu-icon-spacing: 1rem;
|
||||
$hamburger-menu-icon-width: 28px;
|
||||
.navbar {
|
||||
--navbar-button-min-width: 40px;
|
||||
--navbar-gap-width: 1rem;
|
||||
--navbar-icon-size: 1.25rem;
|
||||
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: var(--navbar-gap-width);
|
||||
|
||||
background: var(--site-background);
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
padding-right: .5rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
padding-left: 2rem;
|
||||
padding-right: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
&.menu-active {
|
||||
@media screen and (max-width: $tablet) {
|
||||
z-index: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// FIXME: notifications should provide a slot for the icon instead, so that we can style it as we want
|
||||
:deep() {
|
||||
.trigger-button {
|
||||
color: var(--grey-400);
|
||||
font-size: var(--navbar-icon-size);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.logo-link {
|
||||
display: none;
|
||||
padding: 0.5rem 0.75rem;
|
||||
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
align-self: stretch;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: 2rem;
|
||||
margin-right: 1.5rem;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-button {
|
||||
align-self: stretch;
|
||||
margin-right: auto;
|
||||
align-self: stretch;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin-left: $hamburger-menu-icon-spacing;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar.main-theme {
|
||||
background: var(--site-background);
|
||||
justify-content: space-between;
|
||||
.list-title-wrapper {
|
||||
margin-inline: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@media screen and (max-width: $desktop) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
// this makes the truncated text of the list title work
|
||||
// inside the flexbox parent
|
||||
min-width: 0;
|
||||
|
||||
.title {
|
||||
margin: 0;
|
||||
font-size: 1.75rem;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.navbar-end {
|
||||
margin-left: 0;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
&.menu-active {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.user {
|
||||
width: $user-dropdown-width-mobile;
|
||||
|
||||
.username-dropdown-trigger {
|
||||
line-height: 1;
|
||||
padding: 0 0.25rem;
|
||||
height: 1rem;
|
||||
|
||||
.icon {
|
||||
width: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.navbar {
|
||||
// FIXME: notifications should provide a slot for the icon instead, so that we can style it as we want
|
||||
:deep() {
|
||||
.trigger-button {
|
||||
cursor: pointer;
|
||||
color: var(--grey-400);
|
||||
padding: .5rem;
|
||||
font-size: 1.25rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
> * > .trigger-button {
|
||||
width: $navbar-icon-width;
|
||||
}
|
||||
}
|
||||
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
span {
|
||||
font-family: $vikunja-font;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 100%;
|
||||
vertical-align: middle;
|
||||
height: 40px;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
|
||||
.username-dropdown-trigger {
|
||||
background: none;
|
||||
|
||||
&:focus:not(:active), &:active {
|
||||
outline: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
@media screen and (min-width: $tablet) {
|
||||
padding-inline: var(--navbar-gap-width);
|
||||
}
|
||||
}
|
||||
|
||||
.list-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
$edit-icon-width: 1rem;
|
||||
font-size: 1rem;
|
||||
// We need the following for overflowing ellipsis to work
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
// We need a fixed width for overflowing ellipsis to work
|
||||
--nav-username-width: 0;
|
||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width} - #{$vikunja-nav-logo-full-width} - var(--nav-username-width));
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-dropdown {
|
||||
align-self: stretch;
|
||||
|
||||
.list-title-button {
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.list-title-button {
|
||||
align-self: stretch;
|
||||
min-width: var(--navbar-button-min-width);
|
||||
display: flex;
|
||||
place-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--navbar-icon-size);
|
||||
color: var(--grey-400);
|
||||
}
|
||||
|
||||
.navbar-end {
|
||||
margin-left: auto;
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
|
||||
> * {
|
||||
min-width: var(--navbar-button-min-width);
|
||||
}
|
||||
}
|
||||
|
||||
.username-dropdown-trigger {
|
||||
padding-left: 1rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
text-transform: uppercase;
|
||||
font-size: .85rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.username {
|
||||
font-family: $vikunja-font;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
// We need a fixed width for overflowing ellipsis to work
|
||||
width: calc(100vw - #{$user-dropdown-width-mobile} - #{2 * $hamburger-menu-icon-spacing} - #{$hamburger-menu-icon-width} - #{$edit-icon-width} - #{2 * $navbar-icon-width});
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
:deep(.dropdown-trigger) {
|
||||
color: var(--grey-400);
|
||||
margin-left: .5rem;
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
cursor: pointer;
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.info-button {
|
||||
text-align: center;
|
||||
height: 1.25rem;
|
||||
line-height: 1.25rem;
|
||||
width: 2rem;
|
||||
margin-top: .25rem;
|
||||
padding: 0 .5rem;
|
||||
color: var(--grey-400);
|
||||
margin-left: .5rem;
|
||||
.avatar {
|
||||
border-radius: 100%;
|
||||
vertical-align: middle;
|
||||
height: 40px;
|
||||
margin-right: .5rem;
|
||||
}
|
||||
</style>
|
|
@ -1,7 +1,11 @@
|
|||
<template>
|
||||
<div class="update-notification" v-if="updateAvailable">
|
||||
<p>{{ $t('update.available') }}</p>
|
||||
<x-button @click="refreshApp()" :shadow="false" class="has-no-text-wrap">
|
||||
<p class="update-notification__message">{{ $t('update.available') }}</p>
|
||||
<x-button
|
||||
@click="refreshApp()"
|
||||
:shadow="false"
|
||||
:wrap="false"
|
||||
>
|
||||
{{ $t('update.do') }}
|
||||
</x-button>
|
||||
</div>
|
||||
|
@ -16,15 +20,13 @@ const refreshing = ref(false)
|
|||
|
||||
document.addEventListener('swUpdated', showRefreshUI, {once: true})
|
||||
|
||||
if (navigator && navigator.serviceWorker) {
|
||||
navigator.serviceWorker.addEventListener(
|
||||
'controllerchange', () => {
|
||||
if (refreshing.value) return
|
||||
refreshing.value = true
|
||||
window.location.reload()
|
||||
},
|
||||
)
|
||||
}
|
||||
navigator?.serviceWorker?.addEventListener(
|
||||
'controllerchange', () => {
|
||||
if (refreshing.value) return
|
||||
refreshing.value = true
|
||||
window.location.reload()
|
||||
},
|
||||
)
|
||||
|
||||
function showRefreshUI(e: Event) {
|
||||
console.log('recieved refresh event', e)
|
||||
|
@ -33,6 +35,7 @@ function showRefreshUI(e: Event) {
|
|||
}
|
||||
|
||||
function refreshApp() {
|
||||
updateAvailable.value = false
|
||||
if (!registration.value || !registration.value.waiting) {
|
||||
return
|
||||
}
|
||||
|
@ -43,39 +46,30 @@ function refreshApp() {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.update-notification {
|
||||
position: fixed;
|
||||
// FIXME: We should prevent usage of z-index or
|
||||
// at least define it centrally
|
||||
// the highest z-index of a modal is .hint-modal with 4500
|
||||
z-index: 5000;
|
||||
bottom: 1rem;
|
||||
inset-inline: 1rem;
|
||||
max-width: max-content;
|
||||
margin-inline: auto;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: $warning;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
padding: .5rem;
|
||||
background: $warning;
|
||||
border-radius: $radius;
|
||||
font-size: .9rem;
|
||||
color: var(--grey-900);
|
||||
justify-content: space-between;
|
||||
|
||||
position: fixed;
|
||||
bottom: 1rem;
|
||||
width: 450px;
|
||||
left: calc(50vw - 225px);
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
position: fixed;
|
||||
left: 1rem;
|
||||
right: 1rem;
|
||||
bottom: 1rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
p {
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> * + * {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .update-notification {
|
||||
color: var(--grey-200);
|
||||
.update-notification__message {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
}
|
||||
</style>
|
|
@ -1,16 +1,16 @@
|
|||
<template>
|
||||
<div class="content-auth">
|
||||
<BaseButton
|
||||
v-if="menuActive"
|
||||
v-show="menuActive"
|
||||
@click="baseStore.setMenuActive(false)"
|
||||
class="menu-hide-button d-print-none"
|
||||
>
|
||||
<icon icon="times"/>
|
||||
</BaseButton>
|
||||
<div
|
||||
class="app-container"
|
||||
:class="{'has-background': background || blurHash}"
|
||||
:style="{'background-image': blurHash && `url(${blurHash})`}"
|
||||
class="app-container"
|
||||
>
|
||||
<div
|
||||
:class="{'is-visible': background}"
|
||||
|
@ -18,14 +18,14 @@
|
|||
:style="{'background-image': background && `url(${background})`}"></div>
|
||||
<navigation class="d-print-none"/>
|
||||
<main
|
||||
class="app-content"
|
||||
:class="[
|
||||
{ 'is-menu-enabled': menuActive },
|
||||
$route.name,
|
||||
]"
|
||||
class="app-content"
|
||||
>
|
||||
<BaseButton
|
||||
v-if="menuActive"
|
||||
v-show="menuActive"
|
||||
@click="baseStore.setMenuActive(false)"
|
||||
class="mobile-overlay d-print-none"
|
||||
/>
|
||||
|
@ -86,9 +86,6 @@ function showKeyboardShortcuts() {
|
|||
|
||||
const route = useRoute()
|
||||
|
||||
// hide menu on mobile
|
||||
watch(() => route.fullPath, () => window.innerWidth < 769 && baseStore.setMenuActive(false))
|
||||
|
||||
// FIXME: this is really error prone
|
||||
// Reset the current list highlight in menu if the current route is not list related.
|
||||
watch(() => route.name as string, (routeName) => {
|
||||
|
@ -143,7 +140,6 @@ labelStore.loadAllLabels()
|
|||
|
||||
&:hover,
|
||||
&:focus {
|
||||
height: 1rem;
|
||||
color: var(--grey-600);
|
||||
}
|
||||
}
|
||||
|
@ -225,9 +221,4 @@ labelStore.loadAllLabels()
|
|||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.is-touch .content-auth,
|
||||
.content-auth.z-unset {
|
||||
z-index: unset;
|
||||
}
|
||||
</style>
|
|
@ -146,7 +146,7 @@
|
|||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, onMounted, onBeforeMount} from 'vue'
|
||||
import {ref, computed, onBeforeMount} from 'vue'
|
||||
import draggable from 'zhyswan-vuedraggable'
|
||||
import type {SortableEvent} from 'sortablejs'
|
||||
|
||||
|
@ -159,7 +159,6 @@ import Logo from '@/components/home/Logo.vue'
|
|||
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
|
||||
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
|
||||
import {getListTitle} from '@/helpers/getListTitle'
|
||||
import {useEventListener} from '@vueuse/core'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {INamespace} from '@/modelTypes/INamespace'
|
||||
import ColorBubble from '@/components/misc/colorBubble.vue'
|
||||
|
@ -200,17 +199,8 @@ const namespaceListsCount = computed(() => {
|
|||
return namespaces.value.map((_, index) => activeLists.value[index]?.length ?? 0)
|
||||
})
|
||||
|
||||
|
||||
useEventListener('resize', resize)
|
||||
onMounted(() => resize())
|
||||
|
||||
const listStore = useListStore()
|
||||
|
||||
function resize() {
|
||||
// Hide the menu by default on mobile
|
||||
baseStore.setMenuActive(window.innerWidth >= 770)
|
||||
}
|
||||
|
||||
function toggleLists(namespaceId: INamespace['id']) {
|
||||
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
|
||||
}
|
||||
|
|
|
@ -8,17 +8,20 @@
|
|||
'has-no-shadow': !shadow || variant === 'tertiary',
|
||||
}
|
||||
]"
|
||||
:style="{
|
||||
'--button-white-space': wrap ? 'break-spaces' : 'nowrap',
|
||||
}"
|
||||
>
|
||||
<template v-if="icon">
|
||||
<icon
|
||||
v-if="showIconOnly"
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||
:style="{'color': iconColor !== '' ? iconColor : undefined}"
|
||||
/>
|
||||
<span class="icon is-small" v-else>
|
||||
<icon
|
||||
:icon="icon"
|
||||
:style="{'color': iconColor !== '' ? iconColor : false}"
|
||||
:style="{'color': iconColor !== '' ? iconColor : undefined}"
|
||||
/>
|
||||
</span>
|
||||
</template>
|
||||
|
@ -50,6 +53,7 @@ export interface ButtonProps extends BaseButtonProps {
|
|||
iconColor?: string
|
||||
loading?: boolean
|
||||
shadow?: boolean
|
||||
wrap?: boolean
|
||||
}
|
||||
|
||||
const {
|
||||
|
@ -58,6 +62,7 @@ const {
|
|||
iconColor = '',
|
||||
loading = false,
|
||||
shadow = true,
|
||||
wrap = true,
|
||||
} = defineProps<ButtonProps>()
|
||||
|
||||
const variantClass = computed(() => BUTTON_TYPES_MAP[variant])
|
||||
|
@ -77,7 +82,7 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
|
|||
min-height: $button-height;
|
||||
box-shadow: var(--shadow-sm);
|
||||
display: inline-flex;
|
||||
white-space: break-spaces;
|
||||
white-space: var(--button-white-space);
|
||||
|
||||
&:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
|
@ -99,7 +104,6 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
|
|||
&.is-primary.is-outlined:hover {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.is-small {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="dropdown" ref="dropdown">
|
||||
<slot name="trigger" :close="close" :toggleOpen="toggleOpen">
|
||||
<slot name="trigger" :close="close" :toggleOpen="toggleOpen" :open="open">
|
||||
<BaseButton class="dropdown-trigger is-flex" @click="toggleOpen">
|
||||
<icon :icon="triggerIcon" class="icon"/>
|
||||
</BaseButton>
|
||||
|
@ -56,7 +56,6 @@ onClickOutside(dropdown, (e: Event) => {
|
|||
.dropdown {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div class="notifications">
|
||||
<div class="is-flex is-justify-content-center">
|
||||
<BaseButton @click.stop="showNotifications = !showNotifications" class="trigger-button">
|
||||
<slot name="trigger" toggleOpen="() => showNotifications = !showNotifications" :has-unread-notifications="unreadNotifications > 0">
|
||||
<BaseButton class="trigger-button" @click.stop="showNotifications = !showNotifications">
|
||||
<span class="unread-indicator" v-if="unreadNotifications > 0"></span>
|
||||
<icon icon="bell"/>
|
||||
</BaseButton>
|
||||
</div>
|
||||
</slot>
|
||||
|
||||
<CustomTransition name="fade">
|
||||
<div class="notifications-list" v-if="showNotifications" ref="popup">
|
||||
|
@ -141,7 +141,11 @@ function to(n, index) {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.notifications {
|
||||
width: $navbar-icon-width;
|
||||
display: flex;
|
||||
|
||||
.trigger-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.unread-indicator {
|
||||
position: absolute;
|
||||
|
@ -156,9 +160,9 @@ function to(n, index) {
|
|||
}
|
||||
|
||||
.notifications-list {
|
||||
position: fixed;
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
margin-top: 1rem;
|
||||
top: calc(100% + 1rem);
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
|
||||
|
|
|
@ -546,7 +546,7 @@ function select(parentIndex: number, index: number) {
|
|||
}
|
||||
let elems = resultRefs.value[parentIndex][index]
|
||||
if (results.value[parentIndex].items.length === index) {
|
||||
elems = resultRefs.value[parentIndex + 1][0]
|
||||
elems = resultRefs.value[parentIndex + 1] ? resultRefs.value[parentIndex + 1][0] : undefined
|
||||
}
|
||||
if (
|
||||
typeof elems === 'undefined'
|
||||
|
@ -576,6 +576,8 @@ function reset() {
|
|||
|
||||
<style lang="scss" scoped>
|
||||
.quick-actions {
|
||||
overflow: hidden;
|
||||
|
||||
// FIXME: changed position should be an option of the modal
|
||||
:deep(.modal-content) {
|
||||
top: 3rem;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
/>
|
||||
|
||||
<ColorBubble
|
||||
v-if="showListColor && listColor !== ''"
|
||||
v-if="showListColor && listColor !== '' && currentList.id !== task.listId"
|
||||
:color="listColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import {ref, watch, readonly} from 'vue'
|
||||
import {useLocalStorage, useMediaQuery} from '@vueuse/core'
|
||||
|
||||
const BULMA_MOBILE_BREAKPOINT = 768
|
||||
|
||||
export function useMenuActive() {
|
||||
const isMobile = useMediaQuery(`(max-width: ${BULMA_MOBILE_BREAKPOINT}px)`)
|
||||
|
||||
const desktopPreference = useLocalStorage(
|
||||
'menuActiveDesktopPreference',
|
||||
true,
|
||||
// If we have two tabs open we want to be able to have the menu open in one window
|
||||
// and closed in the other. The last changed value will be the new preference
|
||||
{listenToStorageChanges: false},
|
||||
)
|
||||
|
||||
const menuActive = ref(false)
|
||||
|
||||
// set to prefered value
|
||||
watch(isMobile, (current) => {
|
||||
menuActive.value = current
|
||||
// On mobile we don't show the menu in an expanded state
|
||||
// because that would hide the main content
|
||||
? false
|
||||
: desktopPreference.value
|
||||
}, {immediate: true})
|
||||
|
||||
watch(menuActive, (current) => {
|
||||
if (!isMobile.value) {
|
||||
desktopPreference.value = current
|
||||
}
|
||||
})
|
||||
|
||||
function setMenuActive(newMenuActive: boolean) {
|
||||
menuActive.value = newMenuActive
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
menuActive.value = menuActive.value = !menuActive.value
|
||||
}
|
||||
|
||||
return {
|
||||
menuActive: readonly(menuActive),
|
||||
setMenuActive,
|
||||
toggleMenu,
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@ import {useRouter} from 'vue-router'
|
|||
import {useEventListener} from '@vueuse/core'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {MILLISECONDS_A_HOUR, SECONDS_A_HOUR} from '@/constants/date'
|
||||
import {MILLISECONDS_A_SECOND, SECONDS_A_HOUR} from '@/constants/date'
|
||||
|
||||
const SECONDS_TOKEN_VALID = 60 * SECONDS_A_HOUR
|
||||
|
||||
|
@ -24,11 +24,14 @@ export function useRenewTokenOnFocus() {
|
|||
return
|
||||
}
|
||||
|
||||
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - new Date().valueOf() / MILLISECONDS_A_HOUR
|
||||
const nowInSeconds = new Date().getTime() / MILLISECONDS_A_SECOND
|
||||
const expiresIn = userInfo.value !== null
|
||||
? userInfo.value.exp - nowInSeconds
|
||||
: 0
|
||||
|
||||
// If the token expiry is negative, it is already expired and we have no choice but to redirect
|
||||
// the user to the login page
|
||||
if (expiresIn < 0) {
|
||||
if (expiresIn <= 0) {
|
||||
await authStore.checkAuth()
|
||||
await router.push({name: 'user.login'})
|
||||
return
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
/**
|
||||
* Get full BASE_URL
|
||||
* - including path
|
||||
* - will always end with a trailing slash
|
||||
*/
|
||||
export function getFullBaseUrl() {
|
||||
// (1) The injected BASE_URL is declared from the `resolvedBase` that might miss a trailing slash...
|
||||
// see: https://github.com/vitejs/vite/blob/b35fe883fdc699ac1450882562872095abe9959b/packages/vite/src/node/config.ts#LL614C25-L614C25
|
||||
const rawBase = import.meta.env.BASE_URL
|
||||
// (2) so we readd a slash like done here
|
||||
// https://github.com/vitejs/vite/blob/b35fe883fdc699ac1450882562872095abe9959b/packages/vite/src/node/config.ts#L643
|
||||
// See this comment: https://github.com/vitejs/vite/pull/10723#issuecomment-1303627478
|
||||
return rawBase.endsWith('/') ? rawBase : rawBase + '/'
|
||||
}
|
|
@ -233,7 +233,7 @@ export const getDateFromTextIn = (text: string, now: Date = new Date()) => {
|
|||
}
|
||||
|
||||
const getDateFromWeekday = (text: string): dateFoundResult => {
|
||||
const matcher = / (next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g
|
||||
const matcher = /(^| )(next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g
|
||||
const results: string[] | null = matcher.exec(text.toLowerCase()) // The i modifier does not seem to work.
|
||||
if (results === null) {
|
||||
return {
|
||||
|
@ -246,7 +246,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
|
|||
const currentDay: number = date.getDay()
|
||||
let day = 0
|
||||
|
||||
switch (results[2]) {
|
||||
switch (results[3]) {
|
||||
case 'mon':
|
||||
case 'monday':
|
||||
day = 1
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Import your data into Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrace z jiných služeb do Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Importujte svá data z {name} do Vikunja",
|
||||
"import": "Importujte svá data do Vikunja",
|
||||
"description": "Chcete-li začít, klikněte na logo jedné ze služeb třetích stran.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "K dispozici je aktualizace pro Vikunja!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Aktualizovat nyní"
|
||||
},
|
||||
"menu": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Von einem anderen Dienst zu Vikunja migrieren",
|
||||
"title": "Aus anderen Diensten importieren",
|
||||
"titleService": "Importiere deine Daten von {name} in Vikunja",
|
||||
"import": "Importiere deine Daten in Vikunja",
|
||||
"description": "Klicke auf das Logo eines der unten aufgeführten Drittanbieterdienste, um loszulegen.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Es ist ein Aktualisierung für Vikunja verfügbar!",
|
||||
"available": "Es ist ein Update verfügbar!",
|
||||
"do": "Jetzt aktualisieren"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Vomene andere Dienst zu Vikunja migriere",
|
||||
"title": "Aus anderen Diensten importieren",
|
||||
"titleService": "Dini Date vo {name} in Vikunja importiere",
|
||||
"import": "Dini Date in Vikunja importiere",
|
||||
"description": "Klick ufs Logo une vo eine vo de Drittabüüter um aahzfange.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Es het es Update für Vikiunja!",
|
||||
"available": "Es ist ein Update verfügbar!",
|
||||
"do": "Jetzt aktualisierä"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -915,7 +915,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Import your data into Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "¡Hay una actualización de Vikunja disponible!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Actualizar Ahora"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrer d’autres services vers Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Importe tes données depuis {name} dans Vikunja",
|
||||
"import": "Importer tes données dans Vikunja",
|
||||
"description": "Clique sur le logo d’un des services tiers ci-dessous pour commencer.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Il y a une mise à jour pour Vikunja disponible !",
|
||||
"available": "There is an update available!",
|
||||
"do": "Mettre à jour maintenant"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migra da altri servizi a Vikunja",
|
||||
"title": "Importa da altri servizi",
|
||||
"titleService": "Importa i tuoi dati da {name} in Vikunja",
|
||||
"import": "Importa i tuoi dati in Vikunja",
|
||||
"description": "Clicca sul logo di uno dei servizi esterni qui sotto per iniziare.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "È disponibile un aggiornamento per Vikunja!",
|
||||
"available": "È disponibile un aggiornamento!",
|
||||
"do": "Aggiorna Adesso"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migreer van andere diensten naar Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Importeer je gegevens van {name} naar Vikunja",
|
||||
"import": "Importeer je data in Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Er is een update voor Vikunja beschikbaar!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Nu bijwerken"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"home": {
|
||||
"welcomeNight": "God Morgen {username}!",
|
||||
"welcomeNight": "God Natt {username}!",
|
||||
"welcomeMorning": "God Morgen {username}!",
|
||||
"welcomeDay": "Hei {username}!",
|
||||
"welcomeEvening": "God Morgen {username}!",
|
||||
|
@ -91,7 +91,7 @@
|
|||
},
|
||||
"totp": {
|
||||
"title": "To-faktor-autentisering",
|
||||
"enroll": "Delta",
|
||||
"enroll": "Registrere",
|
||||
"finishSetupPart1": "For å fullføre oppsettet, bruk denne appen (Google Authenticator eller lignende):",
|
||||
"finishSetupPart2": "Etter det, skriv inn en kode fra appen under.",
|
||||
"scanQR": "Alternativt kan du skanne denne QR-koden:",
|
||||
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrere fra andre tjenester til Vikunja",
|
||||
"title": "Importer fra andre tjenester",
|
||||
"titleService": "Importer dine data fra {name} til Vikunja",
|
||||
"import": "Importer dine data til Vikunja",
|
||||
"description": "Klikk på logoen til en av tjenester fra tredjeparter nedenfor for å starte.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Det er en oppdatering for Vikunja tilgjengelig!",
|
||||
"available": "Det er en oppdatering tilgjengelig!",
|
||||
"do": "Oppdater Nå"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migruj z innych usług do Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Zaimportuj swoje dane z {name} do Vikunja",
|
||||
"import": "Zaimportuj swoje dane do Vikunja",
|
||||
"description": "Aby rozpocząć, kliknij logo jednej z usług zewnętrznych.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Dostępna jest aktualizacja Vikunji!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Aktualizuj teraz"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Importe seus dados para o Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Atualizar agora"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrar de outros serviços para o Vikunja",
|
||||
"title": "Importar de outros serviços",
|
||||
"titleService": "Importar os teus dados de {name} para o Vikunja",
|
||||
"import": "Importar os teus dados para o Vikunja",
|
||||
"description": "Clica no logótipo de um dos serviços de terceiros abaixo para começar.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Há uma atualização para o Vikunja disponível!",
|
||||
"available": "Existe uma atualização disponível!",
|
||||
"do": "Atualizar Agora"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Import your data into Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Import your data into Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Import your data into Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Import your data into Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Di chuyển từ các dịch vụ khác đến Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Nhập dữ liệu của bạn từ {name} vào Vikunja",
|
||||
"import": "Nhập dữ liệu của bạn vào Vikunja",
|
||||
"description": "Nhấp vào Logo của một trong các dịch vụ bên dưới để bắt đầu.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Đã có bản cập nhật cho Vikunja!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Cập nhật bây giờ"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "从其他服务迁移到 Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "从 {name} 导入您的数据到 Vikunja",
|
||||
"import": "导入数据到 Vikunja",
|
||||
"description": "点击下面的第三方服务的徽标开始操作。",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Vikunja 有可用的更新!",
|
||||
"available": "There is an update available!",
|
||||
"do": "立即更新"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -417,7 +417,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "Migrate from other services to Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "Import your data from {name} into Vikunja",
|
||||
"import": "Import your data into Vikunja",
|
||||
"description": "Click on the logo of one of the third-party services below to get started.",
|
||||
|
@ -911,7 +911,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -1,38 +1,27 @@
|
|||
import {i18n} from '@/i18n'
|
||||
import {notify} from '@kyvg/vue3-notification'
|
||||
|
||||
export const getErrorText = (r) => {
|
||||
export function getErrorText(r): string {
|
||||
const data = r?.reason?.response?.data || r?.response?.data
|
||||
|
||||
if (r.response && r.response.data) {
|
||||
if(r.response.data.code) {
|
||||
const path = `error.${r.response.data.code}`
|
||||
const message = i18n.global.t(path)
|
||||
if (data?.code) {
|
||||
const path = `error.${data.code}`
|
||||
const message = i18n.global.t(path)
|
||||
|
||||
// If message and path are equal no translation exists for that error code
|
||||
if (path !== message) {
|
||||
return [
|
||||
r.message,
|
||||
message,
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
if (r.response.data.message) {
|
||||
return [
|
||||
r.message,
|
||||
r.response.data.message,
|
||||
]
|
||||
// If message and path are equal no translation exists for that error code
|
||||
if (path !== message) {
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
return [r.message]
|
||||
return data?.message || r.message
|
||||
}
|
||||
|
||||
export function error(e, actions = []) {
|
||||
notify({
|
||||
type: 'error',
|
||||
title: i18n.global.t('error.error'),
|
||||
text: getErrorText(e),
|
||||
text: [getErrorText(e)],
|
||||
actions: actions,
|
||||
})
|
||||
}
|
||||
|
@ -41,7 +30,7 @@ export function success(e, actions = []) {
|
|||
notify({
|
||||
type: 'success',
|
||||
title: i18n.global.t('error.success'),
|
||||
text: getErrorText(e),
|
||||
text: [getErrorText(e)],
|
||||
data: {
|
||||
actions: actions,
|
||||
},
|
||||
|
|
|
@ -124,6 +124,18 @@ describe('Parse Task Text', () => {
|
|||
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||
})
|
||||
it('should recognize next monday on the beginning of the sentence', () => {
|
||||
const result = parseTaskText('next monday Lorem Ipsum')
|
||||
|
||||
const untilNextMonday = calculateDayInterval('nextMonday')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextMonday = new Date()
|
||||
nextMonday.setDate(nextMonday.getDate() + untilNextMonday)
|
||||
expect(result?.date?.getFullYear()).toBe(nextMonday.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(nextMonday.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(nextMonday.getDate())
|
||||
})
|
||||
it('should recognize next monday and ignore casing', () => {
|
||||
const result = parseTaskText('Lorem Ipsum nExt Monday')
|
||||
|
||||
|
@ -216,46 +228,7 @@ describe('Parse Task Text', () => {
|
|||
expect(result?.date?.getDate()).toBe(date.getDate())
|
||||
})
|
||||
|
||||
const cases = {
|
||||
'monday': 1,
|
||||
'Monday': 1,
|
||||
'mon': 1,
|
||||
'Mon': 1,
|
||||
'tuesday': 2,
|
||||
'Tuesday': 2,
|
||||
'tue': 2,
|
||||
'Tue': 2,
|
||||
'wednesday': 3,
|
||||
'Wednesday': 3,
|
||||
'wed': 3,
|
||||
'Wed': 3,
|
||||
'thursday': 4,
|
||||
'Thursday': 4,
|
||||
'thu': 4,
|
||||
'Thu': 4,
|
||||
'friday': 5,
|
||||
'Friday': 5,
|
||||
'fri': 5,
|
||||
'Fri': 5,
|
||||
'saturday': 6,
|
||||
'Saturday': 6,
|
||||
'sat': 6,
|
||||
'Sat': 6,
|
||||
'sunday': 7,
|
||||
'Sunday': 7,
|
||||
'sun': 7,
|
||||
'Sun': 7,
|
||||
} as Record<string, number>
|
||||
for (const c in cases) {
|
||||
it(`should recognize ${c} as weekday`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum ${c}`)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const nextDate = new Date()
|
||||
nextDate.setDate(nextDate.getDate() + ((cases[c] + 7 - nextDate.getDay()) % 7))
|
||||
expect(`${result?.date?.getFullYear()}-${result?.date?.getMonth()}-${result?.date?.getDate()}`).toBe(`${nextDate.getFullYear()}-${nextDate.getMonth()}-${nextDate.getDate()}`)
|
||||
})
|
||||
}
|
||||
it('should recognize weekdays with time', () => {
|
||||
const result = parseTaskText('Lorem Ipsum thu at 14:00')
|
||||
|
||||
|
@ -369,20 +342,34 @@ describe('Parse Task Text', () => {
|
|||
describe('Parse weekdays', () => {
|
||||
|
||||
const days = {
|
||||
'mon': 1,
|
||||
'monday': 1,
|
||||
'tue': 2,
|
||||
'Monday': 1,
|
||||
'mon': 1,
|
||||
'Mon': 1,
|
||||
'tuesday': 2,
|
||||
'wed': 3,
|
||||
'Tuesday': 2,
|
||||
'tue': 2,
|
||||
'Tue': 2,
|
||||
'wednesday': 3,
|
||||
'thu': 4,
|
||||
'Wednesday': 3,
|
||||
'wed': 3,
|
||||
'Wed': 3,
|
||||
'thursday': 4,
|
||||
'fri': 5,
|
||||
'Thursday': 4,
|
||||
'thu': 4,
|
||||
'Thu': 4,
|
||||
'friday': 5,
|
||||
'sat': 6,
|
||||
'Friday': 5,
|
||||
'fri': 5,
|
||||
'Fri': 5,
|
||||
'saturday': 6,
|
||||
'sun': 7,
|
||||
'Saturday': 6,
|
||||
'sat': 6,
|
||||
'Sat': 6,
|
||||
'sunday': 7,
|
||||
'Sunday': 7,
|
||||
'sun': 7,
|
||||
'Sun': 7,
|
||||
} as Record<string, number>
|
||||
|
||||
const prefix = [
|
||||
|
@ -399,6 +386,18 @@ describe('Parse Task Text', () => {
|
|||
const distance = (days[d] + 7 - next.getDay()) % 7
|
||||
next.setDate(next.getDate() + distance)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result?.date?.getFullYear()).toBe(next.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(next.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(next.getDate())
|
||||
})
|
||||
it(`should recognize ${p}${d} at the beginning of the text`, () => {
|
||||
const result = parseTaskText(`${p}${d} Lorem Ipsum`)
|
||||
|
||||
const next = new Date()
|
||||
const distance = (days[d] + 7 - next.getDay()) % 7
|
||||
next.setDate(next.getDate() + distance)
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
expect(result?.date?.getFullYear()).toBe(next.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(next.getMonth())
|
||||
|
|
|
@ -2,8 +2,10 @@
|
|||
|
||||
import {register} from 'register-service-worker'
|
||||
|
||||
import {getFullBaseUrl} from './helpers/getFullBaseUrl'
|
||||
|
||||
if (import.meta.env.PROD) {
|
||||
register('/sw.js', {
|
||||
register(getFullBaseUrl() + 'sw.js', {
|
||||
ready() {
|
||||
console.log('App is being served from cache by a service worker.')
|
||||
},
|
||||
|
|
|
@ -81,7 +81,7 @@ const EditTeamComponent = () => import('@/views/teams/EditTeam.vue')
|
|||
const NewTeamComponent = () => import('@/views/teams/NewTeam.vue')
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(),
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
scrollBehavior(to, from, savedPosition) {
|
||||
// If the user is using their forward/backward keys to navigate, we want to restore the scroll view
|
||||
if (savedPosition) {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {readonly, ref} from 'vue'
|
||||
import { readonly, ref} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
|
||||
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
|
||||
|
@ -7,6 +7,8 @@ import ListModel from '@/models/list'
|
|||
import ListService from '../services/list'
|
||||
import {checkAndSetApiUrl} from '@/helpers/checkAndSetApiUrl'
|
||||
|
||||
import {useMenuActive} from '@/composables/useMenuActive'
|
||||
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
|
||||
|
@ -23,7 +25,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
const blurHash = ref('')
|
||||
|
||||
const hasTasks = ref(false)
|
||||
const menuActive = ref(true)
|
||||
const keyboardShortcutsActive = ref(false)
|
||||
const quickActionsActive = ref(false)
|
||||
const logoVisible = ref(true)
|
||||
|
@ -53,14 +54,6 @@ export const useBaseStore = defineStore('base', () => {
|
|||
hasTasks.value = newHasTasks
|
||||
}
|
||||
|
||||
function setMenuActive(newMenuActive: boolean) {
|
||||
menuActive.value = newMenuActive
|
||||
}
|
||||
|
||||
function toggleMenu() {
|
||||
menuActive.value = !menuActive.value
|
||||
}
|
||||
|
||||
function setKeyboardShortcutsActive(value: boolean) {
|
||||
keyboardShortcutsActive.value = value
|
||||
}
|
||||
|
@ -139,25 +132,24 @@ export const useBaseStore = defineStore('base', () => {
|
|||
background: readonly(background),
|
||||
blurHash: readonly(blurHash),
|
||||
hasTasks: readonly(hasTasks),
|
||||
menuActive: readonly(menuActive),
|
||||
keyboardShortcutsActive: readonly(keyboardShortcutsActive),
|
||||
quickActionsActive: readonly(quickActionsActive),
|
||||
logoVisible: readonly(logoVisible),
|
||||
|
||||
setLoading,
|
||||
setReady,
|
||||
setCurrentList,
|
||||
setHasTasks,
|
||||
setMenuActive,
|
||||
toggleMenu,
|
||||
setKeyboardShortcutsActive,
|
||||
setQuickActionsActive,
|
||||
setBackground,
|
||||
setBlurHash,
|
||||
setLogoVisible,
|
||||
setReady,
|
||||
|
||||
handleSetCurrentList,
|
||||
loadApp,
|
||||
|
||||
...useMenuActive(),
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {computed, readonly, ref} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import {klona} from 'klona/lite'
|
||||
|
||||
import {findById, findIndexById} from '@/helpers/utils'
|
||||
import {i18n} from '@/i18n'
|
||||
|
@ -333,7 +333,7 @@ export const useKanbanStore = defineStore('kanban', () => {
|
|||
const cancel = setModuleLoading(setIsLoading)
|
||||
|
||||
const bucketIndex = findIndexById(buckets.value, updatedBucketData.id)
|
||||
const oldBucket = cloneDeep(buckets.value[bucketIndex])
|
||||
const oldBucket = klona(buckets.value[bucketIndex])
|
||||
|
||||
const updatedBucket = {
|
||||
...oldBucket,
|
||||
|
|
|
@ -124,10 +124,10 @@ export const useListStore = defineStore('list', () => {
|
|||
...list,
|
||||
namespaceId: FavoriteListsNamespace,
|
||||
}
|
||||
|
||||
namespaceStore.removeListFromNamespaceById(newList)
|
||||
if (list.isFavorite) {
|
||||
namespaceStore.addListToNamespace(newList)
|
||||
} else {
|
||||
namespaceStore.removeListFromNamespaceById(newList)
|
||||
}
|
||||
namespaceStore.loadNamespacesIfFavoritesDontExist()
|
||||
namespaceStore.removeFavoritesNamespaceIfEmpty()
|
||||
|
|
|
@ -32,5 +32,4 @@ $button-height: 34px;
|
|||
$switch-view-height: 2.69rem;
|
||||
|
||||
$navbar-height: 4rem;
|
||||
$navbar-width: 300px;
|
||||
$navbar-icon-width: 40px;
|
||||
$navbar-width: 300px;
|
|
@ -256,8 +256,8 @@
|
|||
--card-border-color: var(--grey-200);
|
||||
--logo-text-color: hsl(180, 1%, 15%);
|
||||
|
||||
@media screen {
|
||||
&.dark {
|
||||
&.dark {
|
||||
@media screen {
|
||||
// Light mode colours reversed for dark mode
|
||||
--grey-900-hsl: 210, 20%, 98%;
|
||||
--grey-900: hsl(var(--grey-900-hsl));
|
||||
|
|
|
@ -2,8 +2,4 @@
|
|||
@media print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.has-no-text-wrap {
|
||||
white-space: nowrap !important;
|
||||
}
|
||||
}
|
12
src/sw.ts
12
src/sw.ts
|
@ -1,10 +1,16 @@
|
|||
/* eslint-disable no-console */
|
||||
/* eslint-disable no-undef */
|
||||
|
||||
import {getFullBaseUrl} from './helpers/getFullBaseUrl'
|
||||
|
||||
declare let self: ServiceWorkerGlobalScope
|
||||
|
||||
const fullBaseUrl = getFullBaseUrl()
|
||||
const workboxVersion = 'v6.5.4'
|
||||
importScripts( `/workbox-${workboxVersion}/workbox-sw.js`)
|
||||
|
||||
importScripts(`${fullBaseUrl}workbox-${workboxVersion}/workbox-sw.js`)
|
||||
workbox.setConfig({
|
||||
modulePathPrefix: `/workbox-${workboxVersion}`,
|
||||
modulePathPrefix: `${fullBaseUrl}workbox-${workboxVersion}`,
|
||||
debug: Boolean(import.meta.env.VITE_WORKBOX_DEBUG),
|
||||
})
|
||||
|
||||
|
@ -47,7 +53,7 @@ self.addEventListener('notificationclick', function (event) {
|
|||
|
||||
switch (event.action) {
|
||||
case 'show-task':
|
||||
clients.openWindow(`/tasks/${taskId}`)
|
||||
clients.openWindow(`${fullBaseUrl}tasks/${taskId}`)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
|
|
@ -227,7 +227,7 @@
|
|||
import {computed, nextTick, ref, watch, type PropType} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import draggable from 'zhyswan-vuedraggable'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import {klona} from 'klona/lite'
|
||||
|
||||
import {RIGHTS as Rights} from '@/constants/rights'
|
||||
import BucketModel from '@/models/bucket'
|
||||
|
@ -419,7 +419,7 @@ async function updateTaskPosition(e) {
|
|||
const taskAfter = newBucket.tasks[newTaskIndex + 1] ?? null
|
||||
taskUpdating.value[task.id] = true
|
||||
|
||||
const newTask = cloneDeep(task) // cloning the task to avoid pinia store manipulation
|
||||
const newTask = klona(task) // cloning the task to avoid pinia store manipulation
|
||||
newTask.bucketId = newBucket.id
|
||||
newTask.kanbanPosition = calculateItemPosition(
|
||||
taskBefore !== null ? taskBefore.kanbanPosition : null,
|
||||
|
@ -432,7 +432,7 @@ async function updateTaskPosition(e) {
|
|||
// Make sure the first and second task don't both get position 0 assigned
|
||||
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
|
||||
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
|
||||
const newTaskAfter = cloneDeep(taskAfter) // cloning the task to avoid pinia store manipulation
|
||||
const newTaskAfter = klona(taskAfter) // cloning the task to avoid pinia store manipulation
|
||||
newTaskAfter.bucketId = newBucket.id
|
||||
newTaskAfter.kanbanPosition = calculateItemPosition(
|
||||
0,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {computed, ref, shallowReactive, watch, type Ref} from 'vue'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import {klona} from 'klona/lite'
|
||||
|
||||
import type {Filters} from '@/composables/useRouteFilters'
|
||||
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
|
||||
|
@ -64,7 +64,7 @@ export function useGanttTaskList<F extends Filters>(
|
|||
}
|
||||
|
||||
async function updateTask(task: ITaskPartialWithId) {
|
||||
const oldTask = cloneDeep(tasks.value.get(task.id))
|
||||
const oldTask = klona(tasks.value.get(task.id))
|
||||
|
||||
if (!oldTask) return
|
||||
|
||||
|
|
|
@ -449,7 +449,7 @@ import {ref, reactive, toRef, shallowReactive, computed, watch, nextTick, type P
|
|||
import {useRouter, type RouteLocation} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {unrefElement} from '@vueuse/core'
|
||||
import cloneDeep from 'lodash.clonedeep'
|
||||
import {klona} from 'klona/lite'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel, {TASK_DEFAULT_COLOR} from '@/models/task'
|
||||
|
@ -703,7 +703,7 @@ async function saveTask(args?: {
|
|||
undoCallback,
|
||||
} = {
|
||||
...{
|
||||
task: cloneDeep(task),
|
||||
task: klona(task),
|
||||
},
|
||||
...args,
|
||||
}
|
||||
|
|
|
@ -193,8 +193,7 @@ async function submit() {
|
|||
return
|
||||
}
|
||||
|
||||
const err = getErrorText(e)
|
||||
errorMessage.value = typeof err[1] !== 'undefined' ? err[1] : err[0]
|
||||
errorMessage.value = getErrorText(e)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -76,8 +76,7 @@ async function authenticateWithCode() {
|
|||
})
|
||||
redirectIfSaved()
|
||||
} catch(e) {
|
||||
const err = getErrorText(e)
|
||||
errorMessage.value = typeof err[1] !== 'undefined' ? err[1] : err[0]
|
||||
errorMessage.value = getErrorText(e)
|
||||
} finally {
|
||||
localStorage.removeItem('authenticating')
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
<nav class="navigation">
|
||||
<ul>
|
||||
<li v-for="({routeName, title }, index) in navigationItems" :key="index">
|
||||
<router-link :to="{name: routeName}">
|
||||
<router-link class="navigation-link" :to="{name: routeName}">
|
||||
{{ title }}
|
||||
</router-link>
|
||||
</li>
|
||||
|
@ -90,39 +90,42 @@ const navigationItems = computed(() => {
|
|||
.user-settings {
|
||||
display: flex;
|
||||
|
||||
.navigation {
|
||||
width: 25%;
|
||||
padding-right: 1rem;
|
||||
|
||||
a {
|
||||
display: block;
|
||||
padding: .5rem;
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&:hover, &.router-link-active {
|
||||
background: var(--white);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.view {
|
||||
width: 75%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation, .view {
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
}
|
||||
.navigation {
|
||||
width: 25%;
|
||||
padding-right: 1rem;
|
||||
|
||||
.view {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
@media screen and (max-width: $tablet) {
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-link {
|
||||
display: block;
|
||||
padding: .5rem;
|
||||
color: var(--text);
|
||||
width: 100%;
|
||||
border-left: 3px solid transparent;
|
||||
|
||||
&:hover,
|
||||
&.router-link-active {
|
||||
background: var(--white);
|
||||
border-color: var(--primary);
|
||||
}
|
||||
}
|
||||
|
||||
.view {
|
||||
width: 75%;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
width: 100%;
|
||||
padding-left: 0;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"baseUrl": ".",
|
||||
"lib": ["ESNext"],
|
||||
"lib": ["ESNext", "DOM", "WebWorker"],
|
||||
|
||||
"importHelpers": true,
|
||||
"sourceMap": true,
|
||||
|
|
296
vite.config.ts
296
vite.config.ts
|
@ -1,13 +1,14 @@
|
|||
/// <reference types="vitest" />
|
||||
import {defineConfig, type PluginOption} from 'vite'
|
||||
import {defineConfig, type PluginOption, loadEnv} from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import legacyFn from '@vitejs/plugin-legacy'
|
||||
import { URL, fileURLToPath } from 'node:url'
|
||||
import { dirname, resolve } from 'node:path'
|
||||
import {URL, fileURLToPath} from 'node:url'
|
||||
import {dirname, resolve} from 'node:path'
|
||||
|
||||
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
|
||||
import {VitePWA} from 'vite-plugin-pwa'
|
||||
import {visualizer} from 'rollup-plugin-visualizer'
|
||||
import {VitePWA} from 'vite-plugin-pwa'
|
||||
import VitePluginInjectPreload from 'vite-plugin-inject-preload'
|
||||
import {visualizer} from 'rollup-plugin-visualizer'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import postcssPresetEnv from 'postcss-preset-env'
|
||||
import postcssEasings from 'postcss-easings'
|
||||
|
@ -33,139 +34,168 @@ console.log(isModernBuild
|
|||
: 'Building "legacy" build with "@vitejs/plugin-legacy"',
|
||||
)
|
||||
|
||||
/**
|
||||
* @param fontNames Array of the file names of the fonts without axis and hash suffixes
|
||||
*/
|
||||
function createFontMatcher(fontNames: string[]) {
|
||||
// The `match` option for the files of VitePluginInjectPreload
|
||||
// matches the _output_ files.
|
||||
// Since we only want to mach variable fonts, we exploit here the fact
|
||||
// that we added the `wght` term to indicate the variable weight axis.
|
||||
// The format is something like:
|
||||
// `/assets/OpenSans-Italic_wght__c9a8fe68-5f21f1e7.woff2`
|
||||
// see: https://regex101.com/r/UgUWr1/1
|
||||
return new RegExp(`^.+\\/(${fontNames.join('|')})_wght__[a-z1-9]{8}-[a-z1-9]{8}\\.woff2$`)
|
||||
}
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
// https://vitest.dev/config/
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: PREFIXED_SCSS_STYLES,
|
||||
charset: false, // fixes "@charset" must be the first rule in the file" warnings
|
||||
export default defineConfig(({mode}) => {
|
||||
// Load env file based on `mode` in the current working directory.
|
||||
// Set the third parameter to '' to load all env regardless of the `VITE_` prefix.
|
||||
// https://vitejs.dev/config/#environment-variables
|
||||
const env = loadEnv(mode, process.cwd(), '')
|
||||
|
||||
return {
|
||||
base: env.VIKUNJA_FRONTEND_BASE,
|
||||
// https://vitest.dev/config/
|
||||
test: {
|
||||
environment: 'happy-dom',
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
scss: {
|
||||
additionalData: PREFIXED_SCSS_STYLES,
|
||||
charset: false, // fixes "@charset" must be the first rule in the file" warnings
|
||||
},
|
||||
},
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
postcssEasings(),
|
||||
postcssEasingGradients(),
|
||||
postcssPresetEnv({
|
||||
// These plugins are enabled by default but require
|
||||
// a polyfill that we don't include
|
||||
// see also './src/polyfills.ts'
|
||||
features: {
|
||||
'blank-pseudo-class': false,
|
||||
'focus-visible-pseudo-class': false,
|
||||
'has-pseudo-class': false,
|
||||
'prefers-color-scheme-query': false,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue({
|
||||
reactivityTransform: true,
|
||||
}),
|
||||
legacy,
|
||||
svgLoader({
|
||||
// Since the svgs are already manually optimized via https://jakearchibald.github.io/svgomg/
|
||||
// we don't need to optimize them again.
|
||||
svgo: false,
|
||||
}),
|
||||
VueI18nPlugin({
|
||||
// TODO: only install needed stuff
|
||||
// Whether to install the full set of APIs, components, etc. provided by Vue I18n.
|
||||
// By default, all of them will be installed.
|
||||
fullInstall: true,
|
||||
include: resolve(dirname(pathSrc), './src/i18n/lang/**'),
|
||||
}),
|
||||
VitePWA({
|
||||
srcDir: 'src',
|
||||
filename: 'sw.ts',
|
||||
base: '/',
|
||||
strategies: 'injectManifest',
|
||||
injectRegister: false,
|
||||
manifest: {
|
||||
name: 'Vikunja',
|
||||
short_name: 'Vikunja',
|
||||
theme_color: '#1973ff',
|
||||
icons: [
|
||||
{
|
||||
src: './images/icons/android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: './images/icons/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: './images/icons/icon-maskable.png',
|
||||
sizes: '1024x1024',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable',
|
||||
},
|
||||
],
|
||||
start_url: '.',
|
||||
display: 'standalone',
|
||||
background_color: '#000000',
|
||||
shortcuts: [
|
||||
{
|
||||
name: 'Overview',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
name: 'Namespaces And Lists Overview',
|
||||
short_name: 'Namespaces & Lists',
|
||||
url: '/namespaces',
|
||||
},
|
||||
{
|
||||
name: 'Tasks Next Week',
|
||||
short_name: 'Next Week',
|
||||
url: '/tasks/by/week',
|
||||
},
|
||||
{
|
||||
name: 'Tasks Next Month',
|
||||
short_name: 'Next Month',
|
||||
url: '/tasks/by/month',
|
||||
},
|
||||
{
|
||||
name: 'Teams Overview',
|
||||
short_name: 'Teams',
|
||||
url: '/teams',
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
postcssEasings(),
|
||||
postcssEasingGradients(),
|
||||
postcssPresetEnv({
|
||||
// Since postcss-preset-env v8.0.0 the 'enableClientSidePolyfills' option is disabled by default.
|
||||
// This is the list of features that require a client side library:
|
||||
// https://github.com/csstools/postcss-plugins/tree/main/plugin-packs/postcss-preset-env#plugins-that-need-client-library
|
||||
// Since we only use 'focus-within-pseudo-class' we have to force enable
|
||||
// that plugin now manually in order to keep the browser support as it was.
|
||||
// See also './src/polyfills.ts'
|
||||
features: {
|
||||
'focus-within-pseudo-class': true,
|
||||
},
|
||||
}),
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: '@',
|
||||
replacement: pathSrc,
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
vue({
|
||||
reactivityTransform: true,
|
||||
}),
|
||||
legacy,
|
||||
svgLoader({
|
||||
// Since the svgs are already manually optimized via https://jakearchibald.github.io/svgomg/
|
||||
// we don't need to optimize them again.
|
||||
svgo: false,
|
||||
}),
|
||||
VueI18nPlugin({
|
||||
// TODO: only install needed stuff
|
||||
// Whether to install the full set of APIs, components, etc. provided by Vue I18n.
|
||||
// By default, all of them will be installed.
|
||||
fullInstall: true,
|
||||
include: resolve(dirname(pathSrc), './src/i18n/lang/**'),
|
||||
}),
|
||||
// https://github.com/Applelo/vite-plugin-inject-preload
|
||||
VitePluginInjectPreload({
|
||||
files: [{
|
||||
match: createFontMatcher(['Quicksand', 'OpenSans', 'OpenSans-Italic']),
|
||||
attributes: {crossorigin: 'anonymous'},
|
||||
}],
|
||||
injectTo: 'custom',
|
||||
}),
|
||||
VitePWA({
|
||||
srcDir: 'src',
|
||||
filename: 'sw.ts',
|
||||
strategies: 'injectManifest',
|
||||
injectRegister: false,
|
||||
manifest: {
|
||||
name: 'Vikunja',
|
||||
short_name: 'Vikunja',
|
||||
theme_color: '#1973ff',
|
||||
icons: [
|
||||
{
|
||||
src: './images/icons/android-chrome-192x192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: './images/icons/android-chrome-512x512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
},
|
||||
{
|
||||
src: './images/icons/icon-maskable.png',
|
||||
sizes: '1024x1024',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable',
|
||||
},
|
||||
],
|
||||
start_url: '.',
|
||||
display: 'standalone',
|
||||
background_color: '#000000',
|
||||
shortcuts: [
|
||||
{
|
||||
name: 'Overview',
|
||||
url: '/',
|
||||
},
|
||||
{
|
||||
name: 'Namespaces And Lists Overview',
|
||||
short_name: 'Namespaces & Lists',
|
||||
url: '/namespaces',
|
||||
},
|
||||
{
|
||||
name: 'Tasks Next Week',
|
||||
short_name: 'Next Week',
|
||||
url: '/tasks/by/week',
|
||||
},
|
||||
{
|
||||
name: 'Tasks Next Month',
|
||||
short_name: 'Next Month',
|
||||
url: '/tasks/by/month',
|
||||
},
|
||||
{
|
||||
name: 'Teams Overview',
|
||||
short_name: 'Teams',
|
||||
url: '/teams',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
],
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
server: {
|
||||
host: '127.0.0.1', // see: https://github.com/vitejs/vite/pull/8543
|
||||
port: 4173,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
visualizer({
|
||||
filename: 'stats.html',
|
||||
gzipSize: true,
|
||||
// template: 'sunburst',
|
||||
// brotliSize: true,
|
||||
}) as PluginOption,
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: '@',
|
||||
replacement: pathSrc,
|
||||
},
|
||||
],
|
||||
extensions: ['.mjs', '.js', '.ts', '.jsx', '.tsx', '.json', '.vue'],
|
||||
},
|
||||
},
|
||||
server: {
|
||||
host: '127.0.0.1', // see: https://github.com/vitejs/vite/pull/8543
|
||||
port: 4173,
|
||||
strictPort: true,
|
||||
},
|
||||
build: {
|
||||
target: 'esnext',
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
visualizer({
|
||||
filename: 'stats.html',
|
||||
gzipSize: true,
|
||||
// template: 'sunburst',
|
||||
// brotliSize: true,
|
||||
}) as PluginOption,
|
||||
],
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Reference in New Issue