Compare commits

...

224 Commits

Author SHA1 Message Date
danstewart 7b6f76d1b4 fix: stop revealing elements on hover if hover is not supported (#3191)
Resolves #3162

Co-authored-by: Dan Stewart <git@mail.danstewart.dev>
Reviewed-on: vikunja/frontend#3191
Reviewed-by: konrad <k@knt.li>
Co-authored-by: danstewart <danstewart@noreply.kolaente.de>
Co-committed-by: danstewart <danstewart@noreply.kolaente.de>
2023-03-04 16:13:31 +00:00
renovate ad0029789d chore(deps): update dependency esbuild to v0.17.11 2023-03-03 23:04:25 +00:00
renovate e13f57c30a chore(deps): update dependency @types/node to v18.14.6 2023-03-03 22:04:16 +00:00
WofWca 6a3518dace chore(refactor): improve `stores/config` types (#3190)
Reviewed-on: vikunja/frontend#3190
Reviewed-by: konrad <k@knt.li>
Co-authored-by: WofWca <wofwca@protonmail.com>
Co-committed-by: WofWca <wofwca@protonmail.com>
2023-03-03 14:36:59 +00:00
renovate f1ec554d09 chore(deps): update dependency @types/node to v18.14.5 2023-03-03 06:04:12 +00:00
WofWca 6aa02e29b1
chore(services): let `getAll`: always return `Model[]` 2023-03-02 16:44:01 +01:00
WofWca 5f9485414b
chore(services): add examples for some functions 2023-03-02 16:43:46 +01:00
WofWca 149ceaf2e5 fix(quick-actions): nothing happening on team click (#3186)
Reviewed-on: vikunja/frontend#3186
Reviewed-by: konrad <k@knt.li>
Co-authored-by: WofWca <wofwca@protonmail.com>
Co-committed-by: WofWca <wofwca@protonmail.com>
2023-03-02 15:28:43 +00:00
renovate 9b3e185dd4 chore(deps): update dependency @types/node to v18.14.4 2023-03-02 09:04:10 +00:00
renovate 779fe3e323 fix(deps): update sentry-javascript monorepo to v7.40.0 2023-03-02 08:02:54 +00:00
renovate a27b77f24e fix(deps): update dependency dompurify to v3.0.1 2023-03-02 08:02:13 +00:00
renovate 41f22a1035 chore(deps): update dependency rollup to v3.18.0 2023-03-01 23:04:23 +00:00
renovate 28d01c5ba0 chore(deps): update dependency vitest to v0.29.2 2023-03-01 19:04:08 +00:00
Frederick [Bot] e272dd8e64 [skip ci] Updated translations via Crowdin 2023-03-01 00:06:13 +00:00
kolaente c002275e7f
fix(table view): correctly load sort order from local storage
Resolves https://community.vikunja.io/t/table-view-sort-by-due-date-doesnt-persist-after-page-refresh/1198
2023-02-28 11:56:05 +01:00
renovate 1392d7f101 fix(deps): update dependency ufo to v1.1.1 2023-02-27 21:43:42 +00:00
renovate e5758e21c7 chore(deps): update typescript-eslint monorepo to v5.54.0 2023-02-27 18:04:10 +00:00
Dominik Pschenitschni e0f06999be
feat: improve recommended vscode settings 2023-02-27 16:52:48 +01:00
renovate 3b72acff27 fix(deps): update sentry-javascript monorepo to v7.39.0 2023-02-27 15:49:34 +00:00
renovate df1c44aabe chore(deps): update dependency @types/node to v18.14.2 2023-02-27 15:04:16 +00:00
kolaente fe764a46e9
fix(task): allow clicking on the whole task to open the task detail view
Resolves #3172
2023-02-27 16:00:08 +01:00
renovate 000e3080a5 chore(deps): update dependency start-server-and-test to v2 2023-02-27 13:05:45 +00:00
renovate f4c568e961 chore(deps): update dependency start-server-and-test to v1.15.5 2023-02-27 08:55:40 +00:00
renovate ef70ead3f0 chore(deps): update dependency caniuse-lite to v1.0.30001458 2023-02-27 08:55:03 +00:00
renovate 7a326d6e03 chore(deps): update dependency happy-dom to v8.9.0 2023-02-27 08:54:32 +00:00
renovate afb6383a85 chore(deps): update dependency netlify-cli to v13 2023-02-27 01:06:23 +00:00
renovate e49969dcad chore(deps): update dependency rollup to v3.17.3 2023-02-26 13:26:09 +00:00
renovate 81e1d70847 chore(deps): update dependency eslint to v8.35.0 2023-02-26 10:04:19 +00:00
renovate 5226517954 chore(deps): update pnpm to v7.28.0 2023-02-25 17:03:54 +00:00
renovate d5d0f9a8e2 chore(deps): update dependency vitest to v0.29.1 2023-02-25 11:04:09 +00:00
renovate 2337b6c9f3 chore(deps): update dependency vue-tsc to v1.2.0 2023-02-25 10:18:01 +00:00
renovate 289802b13d chore(deps): update dependency cypress to v12.7.0 2023-02-25 03:04:10 +00:00
renovate 5de9a2880f chore(deps): update dependency @cypress/vite-dev-server to v5.0.4 2023-02-24 04:04:04 +00:00
Frederick [Bot] 62f6895950 [skip ci] Updated translations via Crowdin 2023-02-24 00:06:10 +00:00
renovate c198b9a164 chore(deps): update dependency @types/node to v18.14.1 2023-02-23 12:04:39 +00:00
renovate d5f5e2a412 fix(deps): update dependency axios to v1.3.4 2023-02-22 22:04:25 +00:00
kolaente cabee68bbb
fix(docker): make sure the service worker and webmanifest are never cached 2023-02-22 12:18:46 +01:00
kolaente 2fd2214a2e
fix(menu): don't show drag handle for not draggable menu items 2023-02-22 12:17:33 +01:00
kolaente 64735e0c3d
fix(filter): don't allow marking a filter as favorite
Resolves https://community.vikunja.io/t/error-favouriting-filters-lists/1161/1
2023-02-22 12:13:48 +01:00
kolaente 1f40b68108
fix(filter): validate title before creating or editing a filter
Resolves #3152
2023-02-22 11:04:31 +01:00
renovate 4033c28a67 chore(deps): update dependency vue-tsc to v1.1.7 2023-02-22 04:04:05 +00:00
renovate be20a01dd6 chore(deps): update dependency vite to v4.1.4 2023-02-21 20:04:08 +00:00
renovate 8f5a628e54 chore(deps): update node.js to v18.14.2 2023-02-21 19:03:24 +00:00
renovate e0c00b306e fix(deps): update dependency pinia to v2.0.32 2023-02-21 08:04:09 +00:00
renovate 10eaacc552 chore(deps): update dependency vue-tsc to v1.1.5 2023-02-20 20:04:05 +00:00
renovate 4b1465955a chore(deps): update typescript-eslint monorepo to v5.53.0 2023-02-20 19:16:38 +00:00
renovate 1711318212 chore(deps): update dependency esbuild to v0.17.10 2023-02-20 18:04:33 +00:00
renovate b042547aaa chore(deps): update dependency netlify-cli to v12.13.2 2023-02-20 12:29:07 +00:00
renovate ed0db956eb chore(deps): update dependency happy-dom to v8.6.0 2023-02-20 12:04:07 +00:00
renovate a66f8a6484 chore(deps): update dependency rollup to v3.17.2 2023-02-20 11:34:43 +00:00
renovate 47e895149e chore(deps): update dependency vue-tsc to v1.1.4 2023-02-20 11:34:01 +00:00
renovate 8e00014feb fix(deps): update dependency pinia to v2.0.31 2023-02-20 11:33:30 +00:00
renovate 6146340034 fix(deps): update dependency codemirror to v5.65.12 2023-02-20 11:04:16 +00:00
renovate c7b761b0eb chore(deps): update dependency caniuse-lite to v1.0.30001457 2023-02-20 09:32:51 +00:00
renovate a1e84b3460 chore(deps): update dependency @vue/test-utils to v2.3.0 2023-02-20 09:32:12 +00:00
renovate 038debaa22 chore(deps): update dependency vite to v4.1.3 2023-02-20 09:04:17 +00:00
renovate 88faf04251 chore(deps): update dependency esbuild to v0.17.9 2023-02-19 18:04:07 +00:00
renovate 04be2b9745 chore(deps): update dependency rollup to v3.17.1 2023-02-18 20:04:05 +00:00
renovate 815e8cce0e chore(deps): update dependency sass to v1.58.3 2023-02-18 13:04:11 +00:00
renovate d12f9247ff chore(deps): update dependency vue-tsc to v1.1.3 2023-02-18 12:38:10 +00:00
renovate 85e7a17934 chore(deps): update pnpm to v7.27.1 2023-02-18 12:37:43 +00:00
renovate 59c5d43348 chore(deps): update dependency rollup to v3.17.0 2023-02-18 12:37:10 +00:00
renovate c011f9aa52 fix(deps): update dependency @vueuse/core to v9.13.0 2023-02-18 12:36:29 +00:00
renovate b9f5319a4f chore(deps): update histoire to v0.15.8 2023-02-18 12:04:22 +00:00
renovate f120ba4169 chore(deps): update dependency @types/node to v18.14.0 2023-02-17 21:04:08 +00:00
renovate b2b70f4a9d chore(deps): update dependency @cypress/vite-dev-server to v5.0.3 2023-02-17 15:04:01 +00:00
renovate 9facffe3e9 fix(deps): update dependency blurhash to v2.0.5 2023-02-17 14:38:30 +00:00
renovate c31aff1d88 chore(deps): update histoire to v0.15.7 2023-02-17 14:23:27 +00:00
renovate 60dea80462 chore(deps): update dependency rollup to v3.16.0 2023-02-17 14:20:21 +00:00
renovate cd10ccfbc0 fix(deps): update sentry-javascript monorepo to v7.38.0 2023-02-17 14:04:40 +00:00
renovate 8647402038 chore(deps): update dependency vite to v4.1.2 2023-02-17 11:04:08 +00:00
renovate 990fd46302 chore(deps): update node.js to v18.14.1 2023-02-17 10:17:25 +00:00
renovate cf0aafd9e6 fix(deps): update dependency ufo to v1.1.0 2023-02-17 09:15:10 +00:00
renovate 70d2535e93 chore(deps): update dependency sass to v1.58.2 2023-02-17 02:04:07 +00:00
Frederick [Bot] 0c6f1a4083 [skip ci] Updated translations via Crowdin 2023-02-17 00:06:13 +00:00
renovate 29eb42932a chore(deps): update dependency vue-tsc to v1.1.2 2023-02-16 17:04:00 +00:00
renovate 736e9051d8 chore(deps): update histoire to v0.15.4 2023-02-16 10:04:01 +00:00
renovate 4a4c401558 chore(deps): update dependency cypress to v12.6.0 (#3115)
Reviewed-on: vikunja/frontend#3115
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-15 22:45:51 +00:00
renovate 9198abe24d chore(deps): pin node.js to 18.14.0 2023-02-15 22:02:55 +00:00
Dominik Pschenitschni 97c8970dd6 feat: use renovate js-app as preset (#3087)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#3087
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2023-02-15 21:58:35 +00:00
renovate 5303b6bc97 chore(deps): update dependency vue-tsc to v1.1.0 2023-02-15 20:04:24 +00:00
renovate 24a0a8f5eb chore(deps): update histoire to v0.15.3 2023-02-15 19:03:46 +00:00
Dominik Pschenitschni d07ad495e2 fix(postcss-preset-env): client side polyfills (#3051)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#3051
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2023-02-15 15:40:00 +00:00
renovate 8465afe421 chore(deps): update histoire to v0.15.1 2023-02-15 15:03:47 +00:00
kolaente d40729cbe7
fix: button styles
Partially reverts eaeddda4e4
2023-02-15 11:28:25 +01:00
kolaente fa0e46a399
chore: remove sponsor 2023-02-15 11:12:00 +01:00
renovate b78481f9f6 fix(deps): update dependency @kyvg/vue3-notification to v2.9.0 (#3113)
Reviewed-on: vikunja/frontend#3113
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-14 15:16:03 +00:00
renovate cbc9cf6f7f fix(deps): update dependency vue-flatpickr-component to v11.0.2 (#3112)
Reviewed-on: vikunja/frontend#3112
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-14 10:28:55 +00:00
renovate 62fd9a656e chore(deps): update dependency sass to v1.58.1 2023-02-14 01:04:11 +00:00
renovate 85269b4524 chore(deps): update dependency start-server-and-test to v1.15.4 (#3109)
Reviewed-on: vikunja/frontend#3109
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-13 20:15:41 +00:00
renovate 536d709961 fix(deps): update dependency axios to v1.3.3 2023-02-13 19:04:37 +00:00
renovate 59d6d7e786 chore(deps): update typescript-eslint monorepo to v5.52.0 2023-02-13 18:04:07 +00:00
renovate ae86d0d42a fix(deps): update dependency dompurify to v3 (#3107)
Reviewed-on: vikunja/frontend#3107
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-13 17:39:29 +00:00
renovate 9a20b7a853 fix(deps): update sentry-javascript monorepo to v7.37.2 2023-02-13 16:04:08 +00:00
renovate 5687b66ea5 chore(deps): update dependency vitest to v0.28.5 2023-02-13 13:04:04 +00:00
renovate 1da411e1f6 chore(deps): update dependency vite-plugin-inject-preload to v1.3.0 2023-02-13 09:48:27 +00:00
renovate e8a6d3f31b chore(deps): update dependency caniuse-lite to v1.0.30001451 2023-02-13 09:47:50 +00:00
renovate a25a795276 chore(deps): update dependency netlify-cli to v12.12.0 2023-02-13 09:45:48 +00:00
renovate 57f6abd99f chore(deps): update dependency esbuild to v0.17.8 2023-02-13 07:04:03 +00:00
renovate 84d205f90b chore(deps): update dependency vite-plugin-pwa to v0.14.4 2023-02-11 10:04:05 +00:00
renovate de91e7c9ae chore(deps): update histoire to v0.14.2 2023-02-11 09:04:37 +00:00
renovate 2cf9c35acb chore(deps): update dependency eslint to v8.34.0 2023-02-11 08:04:56 +00:00
konrad db525db6eb fix(deps): histoire renovate group 2023-02-11 08:04:20 +00:00
konrad 88525ae7c8 chore(deps): include histoire main package in histoire renovate group 2023-02-11 08:03:36 +00:00
renovate 957bfdc8f1 chore(deps): update dependency histoire to v0.14.2 2023-02-11 00:05:13 +00:00
renovate c52ae83b75 fix(deps): update sentry-javascript monorepo to v7.37.1 2023-02-10 16:03:55 +00:00
renovate df40c4e475
chore(deps): update dependency histoire to v0.14.0 2023-02-10 14:29:39 +01:00
renovate 3f41e9a3a6 chore(deps): update dependency @histoire/plugin-vue to v0.14.0 2023-02-10 13:04:04 +00:00
renovate 1da510b5dd
chore(deps): update dependency @histoire/plugin-screenshot to v0.14.0 2023-02-10 13:35:01 +01:00
renovate 536db3fd46 chore(deps): update dependency @histoire/plugin-vue to v0.14.0 2023-02-10 12:26:56 +00:00
kolaente cefa5250c5
chore(deps): create a group for all histoire dependencies 2023-02-10 13:10:46 +01:00
kolaente f697640636
chore: remove minimist dependency (not used anywhere) 2023-02-10 12:57:48 +01:00
renovate 09b7595b68 chore(deps): update dependency rollup to v3.15.0 2023-02-10 07:08:20 +00:00
renovate 6b7f73f724 chore(deps): update dependency esbuild to v0.17.7 2023-02-09 23:04:14 +00:00
Dominik Pschenitschni d6b55c7570 feat: fix calculation of token invalidation (#3077)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#3077
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2023-02-09 21:45:18 +00:00
Yurii Vlasov 3f4b08b8be Added ipv6 control script 2023-02-09 21:43:32 +00:00
kolaente 791c61cabb
fix(docker): default api url 2023-02-09 22:30:36 +01:00
konrad e3dd4ef78a feat: persistent menuActive state with Local Storage (#3011)
Reviewed-on: vikunja/frontend#3011
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2023-02-09 21:14:49 +00:00
renovate 830d0887b9 fix(deps): update sentry-javascript monorepo to v7.37.0 2023-02-09 17:04:06 +00:00
Dominik Pschenitschni e8db2c2b45
feat: header improvements 2023-02-09 15:19:33 +01:00
renovate 706a13242e fix(deps): update dependency @intlify/unplugin-vue-i18n to v0.8.2 2023-02-08 23:34:16 +00:00
renovate 13fab10584 chore(deps): update dependency histoire to v0.13.2 2023-02-08 23:05:01 +00:00
renovate 4b0c8aa66b chore(deps): update dependency @histoire/plugin-vue to v0.13.2 2023-02-08 22:04:14 +00:00
renovate bfaf9401f4 chore(deps): update dependency @histoire/plugin-screenshot to v0.13.2 2023-02-08 21:07:12 +00:00
renovate 13607124a6
chore(deps): update dependency histoire to v0.13.1 2023-02-08 17:34:13 +01:00
renovate 9fc3d0a965 chore(deps): update dependency vite-plugin-pwa to v0.14.3 2023-02-08 16:27:37 +00:00
renovate 4d6286451e chore(deps): update dependency @histoire/plugin-vue to v0.13.1 2023-02-08 16:04:15 +00:00
renovate 0479d17e69 chore(deps): update dependency @histoire/plugin-screenshot to v0.13.1 2023-02-08 15:04:13 +00:00
renovate 5ca272959d chore(deps): update pnpm to v7.27.0 2023-02-08 14:03:48 +00:00
Dominik Pschenitschni c502f9b840
feat: refactor to composable
- using useMediaQuery and useLocalStorage
- remove watcher in contentAuth
2023-02-08 12:56:32 +01:00
renovate a3a313a21f fix(deps): update font awesome to v6.3.0 2023-02-07 20:04:47 +00:00
renovate c58d1ffd2e chore(deps): update dependency vite-plugin-pwa to v0.14.2 2023-02-07 17:04:16 +00:00
David Angel 99dc5cf34f
Refactor to only used local storage value when on desktop viewport widths 2023-02-07 14:58:45 +01:00
David Angel 3604cb3ec7
Solve for resize() 2023-02-07 14:58:45 +01:00
David Angel aa01a92278
Persist menuActive state in Local Storage 2023-02-07 14:58:44 +01:00
Dominik Pschenitschni 7b96397e3b feat: use klona instead of lodash.clonedeep (#3073)
Resolves: vikunja/frontend#3032
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#3073
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2023-02-07 13:04:03 +00:00
renovate b45a4e1aaf chore(deps): update dependency @types/node to v18.13.0 2023-02-07 09:04:26 +00:00
Frederick [Bot] d3365d6add [skip ci] Updated translations via Crowdin 2023-02-07 00:10:26 +00:00
renovate 49cb2b9e6f chore(deps): update dependency @cypress/vue to v5.0.4 2023-02-06 22:04:02 +00:00
renovate d4ce10e79a chore(deps): update dependency esbuild to v0.17.6 2023-02-06 20:04:01 +00:00
renovate 345c5e3588 chore(deps): update typescript-eslint monorepo to v5.51.0 2023-02-06 18:04:17 +00:00
renovate 7ff84bcd29 chore(deps): update dependency happy-dom to v8.2.6 2023-02-06 10:56:28 +00:00
renovate d1633ef622 chore(deps): update dependency @histoire/plugin-vue to v0.13.0 2023-02-06 10:04:27 +00:00
renovate 7e92bc63ac chore(deps): update caniuse-and-related 2023-02-06 09:41:54 +00:00
renovate be076b65cf chore(deps): update dependency histoire to v0.13.0 2023-02-06 01:05:40 +00:00
Frederick [Bot] 65b90cbee0 [skip ci] Updated translations via Crowdin 2023-02-06 00:09:40 +00:00
renovate 74aac1b245 chore(deps): update dependency @histoire/plugin-screenshot to v0.13.0 2023-02-05 23:38:03 +00:00
renovate ade791ed43 chore(deps): update dependency @types/node to v18.11.19 2023-02-05 12:04:08 +00:00
renovate 55b008c67c chore(deps): update dependency rollup to v3.14.0 2023-02-05 06:03:59 +00:00
Frederick [Bot] 1f088cca18 [skip ci] Updated translations via Crowdin 2023-02-05 00:10:26 +00:00
renovate 6fad1e4969 fix(deps): update dependency axios to v1.3.2 2023-02-03 19:04:04 +00:00
Dominik Pschenitschni eaeddda4e4
feat: improve naming and styles 2023-02-03 17:25:38 +01:00
kolaente 7cbf0acac5
fix: always show update popup on top 2023-02-03 17:04:51 +01:00
Dominik Pschenitschni 3db5ea45d7
feat: move update from navigation to app 2023-02-03 17:04:51 +01:00
RoboMagus dcd5c3fd6a
Disable listening on IPv6 ports when IPv6 is not supported (#102) 2023-02-03 15:55:36 +01:00
renovate 61fff44764 chore(deps): update dependency rollup to v3.13.0 2023-02-03 13:03:57 +00:00
renovate ecdae4e03e chore(deps): update dependency vitest to v0.28.4 2023-02-03 11:04:06 +00:00
kolaente b26ea45fe0
chore: update funding links 2023-02-03 11:47:46 +01:00
kolaente 7cb0cd293d
chore: update funding links 2023-02-03 11:46:37 +01:00
konrad 6572f75e5d fix: Use Build Time Base Path (#2964)
Reviewed-on: vikunja/frontend#2964
Reviewed-by: konrad <k@knt.li>
2023-02-03 08:57:27 +00:00
Jef Oliver af55992057
feat(config): Support Setting Base Path in .env
* This uses loadEnv to load an environment file at configuration
  time.
  * Documentation:
    * https://vitejs.dev/config/#environment-variables
  * More on environment files:
    * https://vitejs.dev/guide/env-and-mode.html
  * `VIKUNJA_FRONTEND_BASE` is the variable in the environment
     file that will be used to set Vite’s base option.
* This adds a commented example to .env.local.example

Signed-off-by: Jef Oliver <jef@eljef.me>
2023-02-03 09:21:08 +01:00
Jef Oliver e92559dc00
fix(base): Use Build Time Base Path
* If a base path is provided at build time, use it.
  * Base path can be set with `VIKUNJA_FRONTEND_BASE` at
    build time
    * `VIKUNJA_FRONTEND_BASE` sets `import.meta.env.BASE_URL` after Vite resolves it.
    * Usages of `import.meta.env.BASE_URL` are statically replaced
      at build time.
    * If base path is not provided, `import.meta.env.BASE_URL`
      defaults to '/'.
    * Documentation:
      https://vitejs.dev/guide/env-and-mode.html

* Fixes:
  * Manifest not loading because of incorrect path.
  * Service Worker not loading because path is incorrect in
    manifest.
  * Service Worker crashing because import of workbox is from
    wrong path.
  * Service Worker not loading a task because path is incorrect
    in event listener.
  * Incorrect URLs being set on window because base path is
    incorrect.
    * ex: `/login` vs `/base/login`

Signed-off-by: Jef Oliver <jef@eljef.me>
2023-02-03 09:21:06 +01:00
renovate 3dbf02fd7a chore(deps): update dependency @vue/test-utils to v2.2.10 2023-02-03 00:05:11 +00:00
Dominik Pschenitschni 81a4f2d977 chore: typo 2023-02-02 19:11:08 +00:00
renovate 2972d0d400 chore(deps): update dependency cypress to v12.5.1 2023-02-02 18:03:56 +00:00
renovate c11ebc44c4 chore(deps): update dependency vite to v4.1.1 2023-02-02 15:04:03 +00:00
renovate 144f90c5f7 fix(deps): update sentry-javascript monorepo to v7.36.0 2023-02-02 14:14:47 +00:00
renovate 913879604a chore(deps): update dependency @vitejs/plugin-legacy to v4.0.1 2023-02-02 14:03:59 +00:00
renovate 1589ed5739 chore(deps): update dependency @vitejs/plugin-legacy to v4 2023-02-02 12:04:02 +00:00
renovate a991c537ac chore(deps): update dependency postcss-preset-env to v8 (#3000)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#3000
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-02 11:49:30 +00:00
renovate 69b57aa23a chore(deps): update dependency vite to v4.1.0 2023-02-02 11:04:00 +00:00
renovate 1a1939963a fix(deps): update dependency vue to v3.2.47 2023-02-02 07:03:57 +00:00
renovate 3d62c9789c fix(deps): update dependency axios to v1.3.1 2023-02-02 06:56:51 +00:00
renovate c18df8687c chore(deps): update dependency @vue/test-utils to v2.2.9 2023-02-02 00:05:20 +00:00
renovate d83ba0c158 fix(deps): update dependency pinia to v2.0.30 (#3042)
Reviewed-on: vikunja/frontend#3042
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-01 14:26:35 +00:00
kolaente cea31d1da7
fix(docker): cross compilation with buildx 2023-02-01 15:08:12 +01:00
renovate 12509a7e0f fix(deps): update sentry-javascript monorepo to v7.35.0 (#3041)
Reviewed-on: vikunja/frontend#3041
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-02-01 13:12:20 +00:00
renovate dd43057a08 chore(deps): update dependency rollup to v3.12.1 2023-02-01 10:03:47 +00:00
renovate 19d3cf01cd chore(deps): update pnpm to v7.26.3 2023-02-01 09:36:45 +00:00
renovate 80012bf035 chore(deps): update dependency cypress to v12.5.0 2023-02-01 09:11:17 +00:00
renovate 899d9e1cb7 chore(deps): update dependency sass to v1.58.0 2023-02-01 02:04:06 +00:00
renovate 56830ddadc fix(deps): update dependency axios to v1.3.0 (#3036)
Reviewed-on: vikunja/frontend#3036
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-31 17:16:19 +00:00
kolaente 1749d6ba0a
fix(list): make sure favorite lists are not duplicated in the menu when renaming them
Resolves vikunja/frontend#3031
2023-01-31 17:12:11 +01:00
renovate b29008d304 chore(deps): update typescript-eslint monorepo to v5.50.0 2023-01-31 10:03:47 +00:00
renovate 8ae3054b1a chore(deps): update dependency typescript to v4.9.5 2023-01-30 22:03:50 +00:00
renovate f9dad79b23 chore(deps): update dependency caniuse-lite to v1.0.30001449 2023-01-30 07:21:12 +00:00
renovate 30f5cb0656 chore(deps): update dependency happy-dom to v8.2.0 2023-01-30 07:20:45 +00:00
renovate 3f58c983da chore(deps): update dependency netlify-cli to v12.10.0 2023-01-30 01:04:11 +00:00
kolaente 8fa8b03aa6
fix(tests): only look in src for tests 2023-01-29 20:24:44 +01:00
Yurii Vlasov e4499f44b7 Docker refactoring (#3018)
Co-authored-by: Yurii Vlasov <yv@itsvit.org>
Reviewed-on: vikunja/frontend#3018
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Yurii Vlasov <yuriy@vlasov.pro>
Co-committed-by: Yurii Vlasov <yuriy@vlasov.pro>
2023-01-29 14:47:22 +00:00
kolaente b799233bca
fix(quick add magic): correctly parse "next {weekday}" on the beginning of the text
Resolves vikunja/frontend#3022
2023-01-29 15:32:01 +01:00
renovate be0ae4bc29 chore(deps): update dependency eslint to v8.33.0 2023-01-29 13:49:19 +00:00
renovate 60d99f3bba chore(deps): update pnpm to v7.26.2 2023-01-29 13:48:52 +00:00
renovate fa666d2817 fix(deps): update dependency @vueuse/core to v9.12.0 2023-01-29 04:04:10 +00:00
renovate 9312aa14fa fix(deps): update dependency axios to v1.2.6 2023-01-28 17:03:57 +00:00
renovate 68e4f776b9 chore(deps): update dependency esbuild to v0.17.5 2023-01-28 07:32:56 +00:00
renovate 2d137d564e chore(deps): update dependency rollup to v3.12.0 2023-01-28 06:03:58 +00:00
Frederick [Bot] fc8824d942 [skip ci] Updated translations via Crowdin 2023-01-28 00:27:10 +00:00
renovate 6d4ca57601 chore(deps): update dependency cypress to v12.4.1 2023-01-27 16:04:04 +00:00
renovate d2bf4e38b1 chore(deps): update dependency vitest to v0.28.3 (#3019)
Reviewed-on: vikunja/frontend#3019
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-27 13:13:52 +00:00
renovate a5f6857a40 chore(deps): update dependency @vue/test-utils to v2.2.8 2023-01-27 07:48:21 +00:00
renovate ed3d79fa4c chore(deps): update pnpm to v7.26.1 2023-01-27 05:03:37 +00:00
Frederick [Bot] 81c5c54aed [skip ci] Updated translations via Crowdin 2023-01-27 00:28:14 +00:00
renovate 793e06c6ac fix(deps): update sentry-javascript monorepo to v7.34.0 2023-01-26 22:04:00 +00:00
Nikola Sivkov v2 7eb07e92f8
Add Ipv6 support to nginx (#100)
(cherry picked from commit 0e68bcfd5a518b5cbd0bafce1fc48d31b25e1fa1)
2023-01-26 22:00:49 +01:00
renovate 2a15878b81 fix(deps): update dependency axios to v1.2.5 2023-01-26 16:03:45 +00:00
renovate ebd2b1e8c0 chore(deps): update dependency @vitejs/plugin-legacy to v3.0.2 (#3012)
Reviewed-on: vikunja/frontend#3012
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-26 15:20:58 +00:00
renovate d11fcfa072 chore(deps): update dependency rollup to v3.11.0 (#3013)
Reviewed-on: vikunja/frontend#3013
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-26 14:19:03 +00:00
Frederick [Bot] 8e6e976867 [skip ci] Updated translations via Crowdin 2023-01-26 00:28:07 +00:00
kolaente 9adf1aba89
chore: simplify getting the error text from an exception 2023-01-25 18:44:02 +01:00
kolaente e67088fdb7
chore: simplify error handling for login and OpenId Auth 2023-01-25 18:41:30 +01:00
kolaente da241d21f3
fix(quick actions): hide edges of last entry on hover 2023-01-25 16:26:05 +01:00
kolaente 97133010af
fix(quick actions): don't throw an error message when selecting the last items with the arrow keys 2023-01-25 16:23:46 +01:00
kolaente 4576da0dd3
fix: make sure global error handler handles unrejected promises correctly
Resolves vikunja/frontend#2992
2023-01-25 15:05:54 +01:00
renovate fd4a68daf0 chore(deps): update dependency vitest to v0.28.2 (#3008)
Reviewed-on: vikunja/frontend#3008
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-25 12:30:19 +00:00
renovate 6f02d43801 fix(deps): update dependency @infectoone/vue-ganttastic to v2.1.4 (#3009)
Reviewed-on: vikunja/frontend#3009
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-25 12:29:41 +00:00
konrad 2be784766f feat: small content auth improvements (#2998)
Reviewed-on: vikunja/frontend#2998
2023-01-24 22:09:25 +00:00
Dominik Pschenitschni 13a39be3de feat: unindent settings page (#2996)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2996
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2023-01-24 21:54:48 +00:00
renovate d2e07efc7d chore(deps): update dependency cypress to v12.4.0 (#3006)
Reviewed-on: vikunja/frontend#3006
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-24 21:16:24 +00:00
renovate a44299e786 chore(deps): update pnpm to v7.26.0 (#3002)
Reviewed-on: vikunja/frontend#3002
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-24 20:50:13 +00:00
renovate 221f73c347 fix(deps): update dependency axios to v1.2.4 (#3005)
Reviewed-on: vikunja/frontend#3005
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-24 18:21:26 +00:00
renovate 9b170d0d81 fix(deps): update sentry-javascript monorepo to v7.33.0 (#3004)
Reviewed-on: vikunja/frontend#3004
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-01-24 17:49:43 +00:00
Dominik Pschenitschni c6ed925424
chore: move class name to top 2023-01-23 22:26:26 +01:00
Dominik Pschenitschni 7ed1a37de5
feat: use v-show for navigation buttons 2023-01-23 22:26:14 +01:00
86 changed files with 3356 additions and 2952 deletions

View File

@ -1,8 +1,13 @@
# Duplicate this file and remove the '.example' suffix.
# Adjust the values as needed.
# (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
VITE_IS_ONLINE=true
VITE_WORKBOX_DEBUG=false
SENTRY_AUTH_TOKEN=YOUR_TOKEN
SENTRY_ORG=vikunja
SENTRY_PROJECT=frontend-oss
# (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

3
.github/FUNDING.yml vendored
View File

@ -1,2 +1,3 @@
github: kolaente
custom: https://www.buymeacoffee.com/kolaente
open_collective: vikunja
custom: ["https://vikunja.cloud", "https://www.buymeacoffee.com/kolaente"]

2
.nvmrc
View File

@ -1 +1 @@
v18
18.14.2

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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')

View File

@ -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')

View File

@ -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)

View File

@ -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()
}

View File

@ -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')
})
})

15
docker/injector.sh Normal file
View File

@ -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'

19
docker/ipv6-disable.sh Normal file
View File

@ -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

112
docker/nginx.conf Normal file
View File

@ -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;
}

View File

@ -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 { }
}

View File

@ -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 {
}
}
}

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@7.25.1",
"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/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.32.1",
"@sentry/vue": "7.32.1",
"@infectoone/vue-ganttastic": "2.1.4",
"@intlify/unplugin-vue-i18n": "0.8.2",
"@kyvg/vue3-notification": "2.9.0",
"@sentry/tracing": "7.40.0",
"@sentry/vue": "7.40.0",
"@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.15.0",
"@vueuse/core": "9.11.1",
"axios": "1.2.3",
"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.1",
"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,11 +93,11 @@
},
"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.7",
"@types/dompurify": "2.4.0",
@ -106,41 +105,41 @@
"@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.6",
"@types/postcss-preset-env": "7.7.0",
"@typescript-eslint/eslint-plugin": "5.49.0",
"@typescript-eslint/parser": "5.49.0",
"@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.30001447",
"browserslist": "4.21.5",
"caniuse-lite": "1.0.30001458",
"csstype": "3.1.1",
"cypress": "12.3.0",
"esbuild": "0.17.4",
"eslint": "8.32.0",
"cypress": "12.7.0",
"esbuild": "0.17.11",
"eslint": "8.35.0",
"eslint-plugin-vue": "9.9.0",
"happy-dom": "8.1.5",
"histoire": "0.12.4",
"netlify-cli": "12.9.1",
"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.1",
"postcss-preset-env": "8.0.1",
"rollup": "3.18.0",
"rollup-plugin-visualizer": "5.9.0",
"sass": "1.57.1",
"start-server-and-test": "1.15.3",
"typescript": "4.9.4",
"vite": "4.0.4",
"vite-plugin-inject-preload": "1.2.0",
"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.28.1",
"vue-tsc": "1.0.24",
"vitest": "0.29.2",
"vue-tsc": "1.2.0",
"wait-on": "7.0.1",
"workbox-cli": "6.5.4"
}

File diff suppressed because it is too large Load Diff

View File

@ -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,

View File

@ -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;"

View File

@ -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()

View File

@ -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>

View File

@ -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>

View File

@ -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) => {
@ -224,9 +221,4 @@ labelStore.loadAllLabels()
position: relative;
z-index: 1;
}
.is-touch .content-auth,
.content-auth.z-unset {
z-index: unset;
}
</style>

View File

@ -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]
}
@ -230,7 +221,7 @@ function updateActiveLists(namespace: INamespace, activeLists: IList[]) {
// This is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// because now all archived lists are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier.
const lists = [
...activeLists,
@ -255,8 +246,8 @@ async function saveListPosition(e: SortableEvent) {
// If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive
// array instead of using the position. Because the index is wrong in that case, dragging the list will fail.
// To work around that we're explicitly checking that case here and decrease the index.
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
const list = listsActive[newIndex]
const listBefore = listsActive[newIndex - 1] ?? null
const listAfter = listsActive[newIndex + 1] ?? null
@ -351,13 +342,20 @@ $vikunja-nav-selected-width: 0.4rem;
}
.menu-list-dropdown {
opacity: 0;
opacity: 1;
transition: $transition;
}
&:hover .menu-list-dropdown {
opacity: 1;
@media(hover: hover) and (pointer: fine) {
.menu-list-dropdown {
opacity: 0;
}
&:hover .menu-list-dropdown {
opacity: 1;
}
}
}
.menu-item-icon {
@ -421,18 +419,21 @@ $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;
}
}
}
.top-menu {
margin-top: math.div($navbar-padding, 2);
.menu-list {
li {
font-weight: 600;
@ -487,17 +488,24 @@ $vikunja-nav-selected-width: 0.4rem;
.favorite {
margin-left: .25rem;
transition: opacity $transition, color $transition;
opacity: 0;
opacity: 1;
&:hover,
&.is-favorite {
color: var(--warning);
opacity: 1;
}
}
.favorite.is-favorite,
.list-menu:hover .favorite {
opacity: 1;
@media(hover: hover) and (pointer: fine) {
.list-menu .favorite {
opacity: 0;
}
.list-menu:hover .favorite,
.favorite.is-favorite {
opacity: 1;
}
}
.list-menu-title {

View File

@ -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 {

View File

@ -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>

View File

@ -147,7 +147,7 @@ const listStore = useListStore()
top: var(--list-card-padding);
right: var(--list-card-padding);
transition: opacity $transition, color $transition;
opacity: 0;
opacity: 1;
&:hover {
color: var(--warning);
@ -160,8 +160,14 @@ const listStore = useListStore()
}
}
.list-card:hover .favorite {
opacity: 1;
@media(hover: hover) and (pointer: fine) {
.list-card .favorite {
opacity: 0;
}
.list-card:hover .favorite {
opacity: 1;
}
}
.background-fade-in {
@ -173,4 +179,4 @@ const listStore = useListStore()
opacity: 1;
}
}
</style>
</style>

View File

@ -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 {

View File

@ -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;

View File

@ -428,7 +428,7 @@ function searchTeams() {
teamService.getAll({}, { s: t }),
)
const teamsResult = await Promise.all(teamSearchPromises)
foundTeams.value = teamsResult.flatMap((team) => {
foundTeams.value = teamsResult.flat().map((team) => {
team.title = team.name
return team
})
@ -458,6 +458,13 @@ async function doAction(type: ACTION_TYPE, item: DoAction) {
params: { id: (item as DoAction<ITask>).id },
})
break
case ACTION_TYPE.TEAM:
closeQuickActions()
await router.push({
name: 'teams.edit',
params: { id: (item as DoAction<ITeam>).id },
})
break
case ACTION_TYPE.CMD:
query.value = ''
selectedCmd.value = item as DoAction<Command>
@ -546,7 +553,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 +583,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;

View File

@ -1,19 +1,22 @@
<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"
v-model="task.done"
/>
<ColorBubble
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">
@ -285,7 +288,11 @@ function hideDeferDueDatePopup(e) {
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,17 +338,8 @@ function hideDeferDueDatePopup(e) {
}
a {
color: var(--text);
transition: color ease $transition-duration;
&:hover {
color: var(--grey-900);
}
}
.favorite {
opacity: 0;
opacity: 1;
text-align: center;
width: 27px;
transition: opacity $transition, color $transition;
@ -356,21 +354,26 @@ function hideDeferDueDatePopup(e) {
}
}
&:hover .favorite {
opacity: 1;
}
.handle {
opacity: 0;
opacity: 1;
transition: opacity $transition;
margin-right: .25rem;
cursor: grab;
}
&:hover .handle {
opacity: 1;
@media(hover: hover) and (pointer: fine) {
& .favorite,
& .handle {
opacity: 0;
}
&:hover .favorite,
&:hover .handle {
opacity: 1;
}
}
:deep(.fancycheckbox) {
height: 18px;
padding-top: 0;
@ -422,4 +425,4 @@ function hideDeferDueDatePopup(e) {
margin-bottom: 0;
}
}
</style>
</style>

View File

@ -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,
}
}

View File

@ -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

View File

@ -5,6 +5,22 @@ import TaskCollectionService from '@/services/taskCollection'
import type {ITask} from '@/modelTypes/ITask'
import {error} from '@/message'
export type Order = 'asc' | 'desc' | 'none'
export interface SortBy {
id?: Order
index?: Order
done?: Order
title?: Order
priority?: Order
due_date?: Order
start_date?: Order
end_date?: Order
percent_done?: Order
created?: Order
updated?: Order
}
// FIXME: merge with DEFAULT_PARAMS in filters.vue
export const getDefaultParams = () => ({
sort_by: ['position', 'id'],
@ -15,7 +31,7 @@ export const getDefaultParams = () => ({
filter_concat: 'and',
})
const SORT_BY_DEFAULT = {
const SORT_BY_DEFAULT: SortBy = {
id: 'desc',
}
@ -44,7 +60,7 @@ const SORT_BY_DEFAULT = {
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
export function useTaskList(listId, sortByDefault: SortBy = SORT_BY_DEFAULT) {
const params = ref({...getDefaultParams()})
const search = ref('')

View File

@ -113,8 +113,8 @@ export const checkAndSetApiUrl = (url: string): Promise<string> => {
window.API_URL = oldUrl
throw e
})
.then(r => {
if (typeof r !== 'undefined') {
.then(success => {
if (success) {
localStorage.setItem('API_URL', window.API_URL)
return window.API_URL
}

View File

@ -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 + '/'
}

View File

@ -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

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "There is an update for Vikunja available!",
"available": "There is an update available!",
"do": "Update Now"
},
"menu": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "K dispozici je aktualizace pro Vikunja!",
"available": "There is an update available!",
"do": "Aktualizovat nyní"
},
"menu": {

View File

@ -404,7 +404,8 @@
"create": {
"title": "Nyt Gemt Filter",
"description": "Et gemt filter er en virtuel liste, som beregnes ud fra et sæt filtre, hver gang det er tilgået. Når den er oprettet, vises den i et særligt navneområde.",
"action": "Opret nyt gemt filter"
"action": "Opret nyt gemt filter",
"titleRequired": "Please provide a title for the filter."
},
"delete": {
"header": "Slet dette gemte filter",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "Der er en opdatering til Vikunja tilgængelig!",
"available": "There is an update available!",
"do": "Opdater nu"
},
"menu": {

View File

@ -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",
@ -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": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "Es het es Update für Vikiunja!",
"available": "Es ist ein Update verfügbar!",
"do": "Jetzt aktualisierä"
},
"menu": {

View File

@ -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": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "¡Hay una actualización de Vikunja disponible!",
"available": "There is an update available!",
"do": "Actualizar Ahora"
},
"menu": {

View File

@ -404,7 +404,8 @@
"create": {
"title": "Nouveau filtre enregistré",
"description": "Un filtre enregistré est une liste virtuelle qui est calculée à partir dun ensemble de filtres à chaque fois quon 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é",
@ -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": {

View File

@ -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": "È necessario un titolo per il filtro."
},
"delete": {
"header": "Elimina questo filtro salvato",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "È disponibile un aggiornamento per Vikunja!",
"available": "È disponibile un aggiornamento!",
"do": "Aggiorna Adesso"
},
"menu": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "Er is een update voor Vikunja beschikbaar!",
"available": "There is an update available!",
"do": "Nu bijwerken"
},
"menu": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "Det er en oppdatering for Vikunja tilgjengelig!",
"available": "Det er en oppdatering tilgjengelig!",
"do": "Oppdater Nå"
},
"menu": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "Dostępna jest aktualizacja Vikunji!",
"available": "There is an update available!",
"do": "Aktualizuj teraz"
},
"menu": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "There is an update for Vikunja available!",
"available": "There is an update available!",
"do": "Atualizar agora"
},
"menu": {

View File

@ -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",
@ -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": {

View File

@ -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",
@ -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

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "There is an update for Vikunja available!",
"available": "There is an update available!",
"do": "Update Now"
},
"menu": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "There is an update for Vikunja available!",
"available": "There is an update available!",
"do": "Update Now"
},
"menu": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "There is an update for Vikunja available!",
"available": "There is an update available!",
"do": "Update Now"
},
"menu": {

View File

@ -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",
@ -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": {

View File

@ -404,7 +404,8 @@
"create": {
"title": "新保存的过滤器",
"description": "保存的过滤器是一个虚拟列表,在每次访问时从一组过滤器中计算出来。 创建后,它将出现在一个特殊的命名空间里。",
"action": "创建新保存的过滤器"
"action": "创建新保存的过滤器",
"titleRequired": "Please provide a title for the filter."
},
"delete": {
"header": "删除此保存的过滤器",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "Vikunja 有可用的更新!",
"available": "There is an update available!",
"do": "立即更新"
},
"menu": {

View File

@ -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",
@ -911,7 +912,7 @@
}
},
"update": {
"available": "There is an update for Vikunja available!",
"available": "There is an update available!",
"do": "Update Now"
},
"menu": {

View File

@ -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,
},

View File

@ -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())

View File

@ -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.')
},

View File

@ -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) {

View File

@ -126,6 +126,12 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
/**
* Returns an object with all route parameters and their values.
* @example
* getRouteReplacements(
* '/tasks/{taskId}/assignees/{userId}',
* { taskId: 7, userId: 2 },
* )
* // { "{taskId}": 7, "{userId}": 2 }
*/
getRouteReplacements(route : string, parameters : Record<string, unknown> = {}) {
const replace$$1: Record<string, unknown> = {}
@ -148,6 +154,8 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
/**
* Returns a fully-ready-ready-to-make-a-request-to route with replaced parameters.
* @example
* getReplacedRoute('/lists/{listId}/tasks', { listId: 3 }) === '/lists/1/tasks'
*/
getReplacedRoute(path : string, pathparams : Record<string, unknown>) : string {
const replacements = this.getRouteReplacements(path, pathparams)
@ -303,7 +311,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
* @param params Optional query parameters
* @param page The page to get
*/
async getAll(model : Model = new AbstractModel({}), params = {}, page = 1) {
async getAll(model : Model = new AbstractModel({}), params = {}, page = 1): Promise<Model[]> {
if (this.paths.getAll === '') {
throw new Error('This model is not able to get data.')
}
@ -323,10 +331,7 @@ export default abstract class AbstractService<Model extends IAbstract = IAbstrac
return []
}
if (Array.isArray(response.data)) {
return response.data.map(entry => this.modelGetAllFactory(entry))
}
return this.modelGetAllFactory(response.data)
return response.data.map(entry => this.modelGetAllFactory(entry))
} finally {
cancel()
}

View File

@ -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,
}
}

View File

@ -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(),
}
})

View File

@ -6,6 +6,7 @@ import {HTTPFactory} from '@/helpers/fetcher'
import {objectToCamelCase} from '@/helpers/case'
import type {IProvider} from '@/types/IProvider'
import type {MIGRATORS} from '@/views/migrate/migrators'
export interface ConfigState {
version: string,
@ -14,10 +15,10 @@ export interface ConfigState {
linkSharingEnabled: boolean,
maxFileSize: string,
registrationEnabled: boolean,
availableMigrators: [],
availableMigrators: Array<keyof typeof MIGRATORS>,
taskAttachmentsEnabled: boolean,
totpEnabled: boolean,
enabledBackgroundProviders: [],
enabledBackgroundProviders: Array<'unsplash' | 'upload'>,
legal: {
imprintUrl: string,
privacyPolicyUrl: string,
@ -78,11 +79,12 @@ export const useConfigStore = defineStore('config', () => {
function setConfig(config: ConfigState) {
Object.assign(state, config)
}
async function update() {
async function update(): Promise<boolean> {
const HTTP = HTTPFactory()
const {data: config} = await HTTP.get('info')
setConfig(objectToCamelCase(config))
return config
const success = !!config
return success
}
return {

View File

@ -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,

View File

@ -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()

View File

@ -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;

View File

@ -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));

View File

@ -2,8 +2,4 @@
@media print {
display: none !important;
}
}
.has-no-text-wrap {
white-space: nowrap !important;
}
}

View File

@ -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
}
})

View File

@ -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>

View File

@ -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>

View File

@ -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,

View File

@ -196,7 +196,7 @@ import FilterPopup from '@/components/list/partials/filter-popup.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup.vue'
import {useTaskList} from '@/composables/useTaskList'
import {useTaskList, SortBy} from '@/composables/useTaskList'
import type {ITask} from '@/modelTypes/ITask'
const ACTIVE_COLUMNS_DEFAULT = {
@ -222,21 +222,6 @@ const props = defineProps({
},
})
type Order = 'asc' | 'desc' | 'none'
interface SortBy {
index: Order
done?: Order
title?: Order
priority?: Order
due_date?: Order
start_date?: Order
end_date?: Order
percent_done?: Order
created?: Order
updated?: Order
}
const SORT_BY_DEFAULT: SortBy = {
index: 'desc',
}
@ -244,7 +229,7 @@ const SORT_BY_DEFAULT: SortBy = {
const activeColumns = useStorage('tableViewColumns', {...ACTIVE_COLUMNS_DEFAULT})
const sortBy = useStorage<SortBy>('tableViewSortBy', {...SORT_BY_DEFAULT})
const taskList = useTaskList(toRef(props, 'listId'))
const taskList = useTaskList(toRef(props, 'listId'), sortBy.value)
const {
loading,

View File

@ -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

View File

@ -16,7 +16,7 @@ interface IMigratorRecord {
[key: Migrator['id']]: Migrator
}
export const MIGRATORS: IMigratorRecord = {
export const MIGRATORS = {
wunderlist: {
id: 'wunderlist',
name: 'Wunderlist',
@ -49,4 +49,4 @@ export const MIGRATORS: IMigratorRecord = {
icon: tickTickIcon as string,
isFileMigrator: true,
},
} as const
} as const satisfies IMigratorRecord

View File

@ -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,
}

View File

@ -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>

View File

@ -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')
}

View File

@ -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>

View File

@ -5,7 +5,7 @@
"compilerOptions": {
"composite": true,
"baseUrl": ".",
"lib": ["ESNext"],
"lib": ["ESNext", "DOM", "WebWorker"],
"importHelpers": true,
"sourceMap": true,

View File

@ -1,14 +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 {VitePWA} from 'vite-plugin-pwa'
import VitePluginInjectPreload from 'vite-plugin-inject-preload'
import {visualizer} from 'rollup-plugin-visualizer'
import {visualizer} from 'rollup-plugin-visualizer'
import svgLoader from 'vite-svg-loader'
import postcssPresetEnv from 'postcss-preset-env'
import postcssEasings from 'postcss-easings'
@ -41,7 +41,7 @@ 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 weiht axis.
// 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
@ -49,146 +49,153 @@ function createFontMatcher(fontNames: string[]) {
}
// 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/**'),
}),
// 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',
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,
],
},
},
}
})