forked from vikunja/frontend
Compare commits
238 Commits
feature/re
...
main
Author | SHA1 | Date |
---|---|---|
renovate | 1392d7f101 | |
renovate | e5758e21c7 | |
Dominik Pschenitschni | e0f06999be | |
renovate | 3b72acff27 | |
renovate | df1c44aabe | |
kolaente | fe764a46e9 | |
renovate | 000e3080a5 | |
renovate | f4c568e961 | |
renovate | ef70ead3f0 | |
renovate | 7a326d6e03 | |
renovate | afb6383a85 | |
renovate | e49969dcad | |
renovate | 81e1d70847 | |
renovate | 5226517954 | |
renovate | d5d0f9a8e2 | |
renovate | 2337b6c9f3 | |
renovate | 289802b13d | |
renovate | 5de9a2880f | |
Frederick [Bot] | 62f6895950 | |
renovate | c198b9a164 | |
renovate | d5f5e2a412 | |
kolaente | cabee68bbb | |
kolaente | 2fd2214a2e | |
kolaente | 64735e0c3d | |
kolaente | 1f40b68108 | |
renovate | 4033c28a67 | |
renovate | be20a01dd6 | |
renovate | 8f5a628e54 | |
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"]
|
||||
|
|
|
@ -18,6 +18,12 @@
|
|||
"javascriptreact",
|
||||
"vue"
|
||||
],
|
||||
|
||||
"volar.completion.preferredTagNameCase": "pascal",
|
||||
|
||||
// disable vetur in case it is installed
|
||||
"vetur.validation.template": false,
|
||||
|
||||
// i18n ally
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/i18n/lang"
|
||||
|
|
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,85 @@
|
|||
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;
|
||||
}
|
||||
|
||||
# Disable caching for sw
|
||||
location = /sw.js {
|
||||
autoindex off;
|
||||
expires off;
|
||||
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||
}
|
||||
|
||||
# Disable caching for webmanifest
|
||||
location = /manifest.webmanifest {
|
||||
autoindex off;
|
||||
expires off;
|
||||
add_header Cache-Control "public, max-age=0, s-maxage=0, must-revalidate" always;
|
||||
}
|
||||
|
||||
# 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.28.0",
|
||||
"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.39.0",
|
||||
"@sentry/vue": "7.39.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.4",
|
||||
"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.1",
|
||||
"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.4",
|
||||
"@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.2",
|
||||
"@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.54.0",
|
||||
"@typescript-eslint/parser": "5.54.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.30001458",
|
||||
"csstype": "3.1.1",
|
||||
"cypress": "12.3.0",
|
||||
"esbuild": "0.17.2",
|
||||
"eslint": "8.32.0",
|
||||
"cypress": "12.7.0",
|
||||
"esbuild": "0.17.10",
|
||||
"eslint": "8.35.0",
|
||||
"eslint-plugin-vue": "9.9.0",
|
||||
"happy-dom": "8.1.4",
|
||||
"histoire": "0.12.4",
|
||||
"netlify-cli": "12.7.2",
|
||||
"happy-dom": "8.9.0",
|
||||
"histoire": "0.15.8",
|
||||
"netlify-cli": "13.0.0",
|
||||
"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.3",
|
||||
"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": "2.0.0",
|
||||
"typescript": "4.9.5",
|
||||
"vite": "4.1.4",
|
||||
"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.29.1",
|
||||
"vue-tsc": "1.2.0",
|
||||
"wait-on": "7.0.1",
|
||||
"workbox-cli": "6.5.4"
|
||||
}
|
||||
|
|
3726
pnpm-lock.yaml
3726
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>
|
|
@ -122,6 +122,7 @@
|
|||
<span class="list-menu-title">{{ getListTitle(l) }}</span>
|
||||
</BaseButton>
|
||||
<BaseButton
|
||||
v-if="l.id > 0"
|
||||
class="favorite"
|
||||
:class="{'is-favorite': l.isFavorite}"
|
||||
@click="listStore.toggleListFavorite(l)"
|
||||
|
@ -146,7 +147,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 +160,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 +200,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]
|
||||
}
|
||||
|
@ -421,12 +412,16 @@ $vikunja-nav-selected-width: 0.4rem;
|
|||
opacity: 0;
|
||||
transition: opacity $transition;
|
||||
margin-right: .25rem;
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
&:hover .handle {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
&:not(.dragging-disabled) .handle {
|
||||
cursor: grab;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
@change="(event: Event) => updateData((event.target as HTMLInputElement).checked)"
|
||||
type="checkbox"
|
||||
/>
|
||||
<label :for="checkBoxId" class="check">
|
||||
<label :for="checkBoxId" class="check" @click.prevent="check">
|
||||
<svg height="18px" viewBox="0 0 18 18" width="18px">
|
||||
<path
|
||||
d="M1,9 L1,3.5 C1,2 2,1 3.5,1 L14.5,1 C16,1 17,2 17,3.5 L17,14.5 C17,16 16,17 14.5,17 L3.5,17 C2,17 1,16 1,14.5 L1,9 Z"></path>
|
||||
|
@ -56,6 +56,11 @@ function updateData(newChecked: boolean) {
|
|||
emit('update:modelValue', newChecked)
|
||||
emit('change', newChecked)
|
||||
}
|
||||
|
||||
function check() {
|
||||
checked.value = !checked.value
|
||||
updateData(checked.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
<template>
|
||||
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
|
||||
<router-link
|
||||
:to="taskDetailRoute"
|
||||
:class="{'is-loading': taskService.loading}"
|
||||
class="task loader-container"
|
||||
>
|
||||
<fancycheckbox
|
||||
:disabled="(isArchived || disabled) && !canMarkAsDone"
|
||||
@change="markAsDone"
|
||||
|
@ -7,13 +11,12 @@
|
|||
/>
|
||||
|
||||
<ColorBubble
|
||||
v-if="showListColor && listColor !== ''"
|
||||
v-if="showListColor && listColor !== '' && currentList.id !== task.listId"
|
||||
:color="listColor"
|
||||
class="mr-1"
|
||||
/>
|
||||
|
||||
<router-link
|
||||
:to="taskDetailRoute"
|
||||
<div
|
||||
:class="{ 'done': task.done, 'show-list': showList && taskList !== null}"
|
||||
class="tasktext"
|
||||
>
|
||||
|
@ -93,7 +96,7 @@
|
|||
</span>
|
||||
|
||||
<checklist-summary :task="task"/>
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<progress
|
||||
class="progress is-small"
|
||||
|
@ -114,14 +117,14 @@
|
|||
|
||||
<BaseButton
|
||||
:class="{'is-favorite': task.isFavorite}"
|
||||
@click="toggleFavorite"
|
||||
@click.prevent="toggleFavorite"
|
||||
class="favorite"
|
||||
>
|
||||
<icon icon="star" v-if="task.isFavorite"/>
|
||||
<icon :icon="['far', 'star']" v-else/>
|
||||
</BaseButton>
|
||||
<slot />
|
||||
</div>
|
||||
</router-link>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
@ -284,8 +287,12 @@ function hideDeferDueDatePopup(e) {
|
|||
cursor: pointer;
|
||||
border-radius: $radius;
|
||||
border: 2px solid transparent;
|
||||
|
||||
color: var(--text);
|
||||
transition: color ease $transition-duration;
|
||||
|
||||
&:hover {
|
||||
color: var(--grey-900);
|
||||
background-color: var(--grey-100);
|
||||
}
|
||||
|
||||
|
@ -331,15 +338,6 @@ function hideDeferDueDatePopup(e) {
|
|||
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--text);
|
||||
transition: color ease $transition-duration;
|
||||
|
||||
&:hover {
|
||||
color: var(--grey-900);
|
||||
}
|
||||
}
|
||||
|
||||
.favorite {
|
||||
opacity: 0;
|
||||
text-align: center;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Nový uložený filtr",
|
||||
"description": "Uložený filtr je virtuální seznam, který se počítá ze sady filtrů pokaždé, když je přístupný. Jakmile bude vytvořen, objeví se ve speciálním prostoru.",
|
||||
"action": "Vytvořit uložený filtr"
|
||||
"action": "Vytvořit uložený filtr",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Smazat tento uložený filtr",
|
||||
|
@ -417,7 +418,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 +912,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
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Neuer gespeicherter Filter",
|
||||
"description": "Ein gespeicherter Filter ist eine virtuelle Liste, die bei jedem Zugriff aus einem Satz von Filtern errechnet wird. Einmal erstellt, erscheint diese in einem speziellen Namespace.",
|
||||
"action": "Neuen gespeicherten Filter erstellen"
|
||||
"action": "Neuen gespeicherten Filter erstellen",
|
||||
"titleRequired": "Bitte gib den Titel für den Filter an."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Diesen gespeicherten Filter löschen",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Es ist ein Aktualisierung für Vikunja verfügbar!",
|
||||
"available": "Es ist ein Update verfügbar!",
|
||||
"do": "Jetzt aktualisieren"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Neuer gespeicherter Filter",
|
||||
"description": "En gspeicherete Filter isch e virtuelli Liste, welche vomene Satz a Filter zemmegsetzt wird, sobald me uf sie zuegriift. Wenn sie mal erstellt worde isch, erhaltet si ihren eigene Namensruum.",
|
||||
"action": "Neue gspeicherete Filter erstelle"
|
||||
"action": "Neue gspeicherete Filter erstelle",
|
||||
"titleRequired": "Bitte gib den Titel für den Filter an."
|
||||
},
|
||||
"delete": {
|
||||
"header": "De g'speicheret Filter chüble",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Es het es Update für Vikiunja!",
|
||||
"available": "Es ist ein Update verfügbar!",
|
||||
"do": "Jetzt aktualisierä"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -405,7 +405,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -915,7 +916,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "¡Hay una actualización de Vikunja disponible!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Actualizar Ahora"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Nouveau filtre enregistré",
|
||||
"description": "Un filtre enregistré est une liste virtuelle qui est calculée à partir d’un ensemble de filtres à chaque fois qu’on y accède. Une fois créé, il apparaît dans un espace de noms spécial.",
|
||||
"action": "Créer un nouveau filtre enregistré"
|
||||
"action": "Créer un nouveau filtre enregistré",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Supprimer ce filtre enregistré",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Il y a une mise à jour pour Vikunja disponible !",
|
||||
"available": "There is an update available!",
|
||||
"do": "Mettre à jour maintenant"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Nuovo Filtro Salvato",
|
||||
"description": "Un filtro salvato è una lista virtuale che viene calcolata da un insieme di filtri di volta in volta. Una volta creato, apparirà in un namespace speciale.",
|
||||
"action": "Crea nuovo filtro salvato"
|
||||
"action": "Crea nuovo filtro salvato",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Elimina questo filtro salvato",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "È disponibile un aggiornamento per Vikunja!",
|
||||
"available": "È disponibile un aggiornamento!",
|
||||
"do": "Aggiorna Adesso"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,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:",
|
||||
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Nytt lagret filter",
|
||||
"description": "Et lagret filter er en virtuell liste som beregnes fra et sett med filtre hver gang det åpnes. Når du er opprettet, vil det vises i et eget navneområde.",
|
||||
"action": "Opprett nytt filter"
|
||||
"action": "Opprett nytt filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Slett dette lagrede filteret",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Det er en oppdatering for Vikunja tilgjengelig!",
|
||||
"available": "Det er en oppdatering tilgjengelig!",
|
||||
"do": "Oppdater Nå"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Nowy filtr stały",
|
||||
"description": "Filtr stały to wirtualna lista, która jest kalkulowana na podstawie zestawu filtrów przy każdym wejściu w nią. Po utworzeniu pojawi się w specjalnej sekcji.",
|
||||
"action": "Utwórz nowy filtr stały"
|
||||
"action": "Utwórz nowy filtr stały",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Usuń ten filtr stały",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Dostępna jest aktualizacja Vikunji!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Aktualizuj teraz"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Novo filtro salvo",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Atualizar agora"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Novo Filtro Memorizado",
|
||||
"description": "Um filtro memorizado é uma lista virtual que é compilada a partir de um conjunto de filtros de cada vez que é acedido. Uma vez criado, irá aparecer num espaço especial.",
|
||||
"action": "Criar novo filtro memorizado"
|
||||
"action": "Criar novo filtro memorizado",
|
||||
"titleRequired": "Por favor, insere um título para o filtro."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Eliminar este filtro memorizado",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Há uma atualização para o Vikunja disponível!",
|
||||
"available": "Existe uma atualização disponível!",
|
||||
"do": "Atualizar Agora"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,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
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "There is an update for Vikunja available!",
|
||||
"available": "There is an update available!",
|
||||
"do": "Update Now"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "Bộ lọc đã lưu mới",
|
||||
"description": "Bộ lọc sẵn là một danh sách ảo được chọn từ một tập hợp các bộ lọc. Sau khi được tạo, nó sẽ xuất hiện trong một không gian làm việc đặc biệt.",
|
||||
"action": "Tạo thêm bộ lọc sẵn"
|
||||
"action": "Tạo thêm bộ lọc sẵn",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Xóa bộ lọc sẵn này",
|
||||
|
@ -417,7 +418,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 +912,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": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "新保存的过滤器",
|
||||
"description": "保存的过滤器是一个虚拟列表,在每次访问时从一组过滤器中计算出来。 创建后,它将出现在一个特殊的命名空间里。",
|
||||
"action": "创建新保存的过滤器"
|
||||
"action": "创建新保存的过滤器",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "删除此保存的过滤器",
|
||||
|
@ -417,7 +418,7 @@
|
|||
}
|
||||
},
|
||||
"migrate": {
|
||||
"title": "从其他服务迁移到 Vikunja",
|
||||
"title": "Import from other services",
|
||||
"titleService": "从 {name} 导入您的数据到 Vikunja",
|
||||
"import": "导入数据到 Vikunja",
|
||||
"description": "点击下面的第三方服务的徽标开始操作。",
|
||||
|
@ -911,7 +912,7 @@
|
|||
}
|
||||
},
|
||||
"update": {
|
||||
"available": "Vikunja 有可用的更新!",
|
||||
"available": "There is an update available!",
|
||||
"do": "立即更新"
|
||||
},
|
||||
"menu": {
|
||||
|
|
|
@ -404,7 +404,8 @@
|
|||
"create": {
|
||||
"title": "New Saved Filter",
|
||||
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
|
||||
"action": "Create new saved filter"
|
||||
"action": "Create new saved filter",
|
||||
"titleRequired": "Please provide a title for the filter."
|
||||
},
|
||||
"delete": {
|
||||
"header": "Delete this saved filter",
|
||||
|
@ -417,7 +418,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 +912,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) {
|
||||
|
|
|
@ -2,6 +2,7 @@ import {computed, ref, shallowReactive, unref, watch} from 'vue'
|
|||
import {useRouter} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import type {MaybeRef} from '@vueuse/core'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
|
||||
import type {IList} from '@/modelTypes/IList'
|
||||
import type {ISavedFilter} from '@/modelTypes/ISavedFilter'
|
||||
|
@ -133,14 +134,38 @@ export function useSavedFilter(listId?: MaybeRef<IList['id']>) {
|
|||
router.push({name: 'namespaces.index'})
|
||||
}
|
||||
|
||||
const titleValid = ref(true)
|
||||
const validateTitleField = useDebounceFn(() => {
|
||||
titleValid.value = filter.value.title !== ''
|
||||
}, 100)
|
||||
|
||||
async function createFilterWithValidation() {
|
||||
if (!titleValid.value) {
|
||||
return
|
||||
}
|
||||
return createFilter()
|
||||
}
|
||||
|
||||
async function saveFilterWithValidation() {
|
||||
if (!titleValid.value) {
|
||||
return
|
||||
}
|
||||
return saveFilter()
|
||||
}
|
||||
|
||||
return {
|
||||
createFilter,
|
||||
createFilterWithValidation,
|
||||
saveFilter,
|
||||
saveFilterWithValidation,
|
||||
deleteFilter,
|
||||
|
||||
filter,
|
||||
filters,
|
||||
|
||||
filterService,
|
||||
|
||||
titleValid,
|
||||
validateTitleField,
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
})
|
||||
|
|
|
@ -3,25 +3,27 @@
|
|||
:title="$t('filters.edit.title')"
|
||||
primary-icon=""
|
||||
:primary-label="$t('misc.save')"
|
||||
@primary="saveFilter"
|
||||
@primary="saveFilterWithValidation"
|
||||
:tertiary="$t('misc.delete')"
|
||||
@tertiary="$router.push({ name: 'filter.settings.delete', params: { id: listId } })"
|
||||
>
|
||||
<form @submit.prevent="saveFilter()">
|
||||
<form @submit.prevent="saveFilterWithValidation()">
|
||||
<div class="field">
|
||||
<label class="label" for="title">{{ $t('filters.attributes.title') }}</label>
|
||||
<div class="control">
|
||||
<input
|
||||
:class="{ 'disabled': filterService.loading}"
|
||||
v-model="filter.title"
|
||||
:class="{ 'disabled': filterService.loading, 'is-danger': !titleValid }"
|
||||
:disabled="filterService.loading || undefined"
|
||||
@keyup.enter="saveFilter"
|
||||
class="input"
|
||||
id="title"
|
||||
id="Title"
|
||||
:placeholder="$t('filters.attributes.titlePlaceholder')"
|
||||
type="text"
|
||||
v-focus
|
||||
v-model="filter.title"/>
|
||||
@focusout="validateTitleField"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="!titleValid">{{ $t('filters.create.titleRequired') }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="description">{{ $t('filters.attributes.description') }}</label>
|
||||
|
@ -65,9 +67,11 @@ import type {IList} from '@/modelTypes/IList'
|
|||
const props = defineProps<{ listId: IList['id'] }>()
|
||||
|
||||
const {
|
||||
saveFilter,
|
||||
saveFilterWithValidation,
|
||||
filter,
|
||||
filters,
|
||||
filterService,
|
||||
titleValid,
|
||||
validateTitleField,
|
||||
} = useSavedFilter(toRef(props, 'listId'))
|
||||
</script>
|
||||
|
|
|
@ -12,15 +12,17 @@
|
|||
<div class="control">
|
||||
<input
|
||||
v-model="filter.title"
|
||||
:class="{ 'disabled': filterService.loading}"
|
||||
:class="{ 'disabled': filterService.loading, 'is-danger': !titleValid }"
|
||||
:disabled="filterService.loading || undefined"
|
||||
class="input"
|
||||
id="Title"
|
||||
:placeholder="$t('filters.attributes.titlePlaceholder')"
|
||||
type="text"
|
||||
v-focus
|
||||
@focusout="validateTitleField"
|
||||
/>
|
||||
</div>
|
||||
<p class="help is-danger" v-if="!titleValid">{{ $t('filters.create.titleRequired') }}</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label class="label" for="description">{{ $t('filters.attributes.description') }}</label>
|
||||
|
@ -51,8 +53,8 @@
|
|||
<template #footer>
|
||||
<x-button
|
||||
:loading="filterService.loading"
|
||||
:disabled="filterService.loading"
|
||||
@click="createFilter()"
|
||||
:disabled="filterService.loading || !titleValid"
|
||||
@click="createFilterWithValidation()"
|
||||
class="is-fullwidth"
|
||||
>
|
||||
{{ $t('filters.create.action') }}
|
||||
|
@ -71,7 +73,9 @@ import {useSavedFilter} from '@/services/savedFilter'
|
|||
const {
|
||||
filter,
|
||||
filters,
|
||||
createFilter,
|
||||
createFilterWithValidation,
|
||||
filterService,
|
||||
titleValid,
|
||||
validateTitleField,
|
||||
} = useSavedFilter()
|
||||
</script>
|
||||
|
|
|
@ -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