Compare commits

...

428 Commits

Author SHA1 Message Date
renovate 7d5cde53e3 chore(deps): update dependency vite to v3.2.5 (#2785)
Reviewed-on: vikunja/frontend#2785
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-05 09:27:20 +00:00
renovate 8361640559 chore(deps): update dependency happy-dom to v7.7.2 (#2781)
Reviewed-on: vikunja/frontend#2781
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-05 08:13:36 +00:00
renovate 517a6cea1e chore(deps): update dependency netlify-cli to v12.2.8 (#2782)
Reviewed-on: vikunja/frontend#2782
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-05 07:40:17 +00:00
renovate ed4dd93bba chore(deps): update dependency esbuild to v0.15.18 (#2783)
Reviewed-on: vikunja/frontend#2783
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-05 07:39:38 +00:00
renovate d50de97490 chore(deps): update dependency @vue/test-utils to v2.2.6 (#2784)
Reviewed-on: vikunja/frontend#2784
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-05 07:38:49 +00:00
renovate aa719d3a68 chore(deps): update dependency caniuse-lite to v1.0.30001436 (#2780)
Reviewed-on: vikunja/frontend#2780
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-05 07:38:08 +00:00
kolaente 463d22b07c
fix(quick add magic): don't create a new label multiple times if it is used in multiple tasks
Resolves https://github.com/go-vikunja/frontend/issues/94
2022-12-04 20:19:43 +01:00
renovate 33494cab6b chore(deps): update dependency esbuild to v0.15.17 (#2779)
Reviewed-on: vikunja/frontend#2779
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-04 12:51:12 +00:00
renovate 8fa922a0ca chore(deps): update pnpm to v7.18.0 (#2778)
Reviewed-on: vikunja/frontend#2778
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-03 23:19:41 +00:00
renovate e5815e21cb chore(deps): update dependency @cypress/vite-dev-server to v5 (#2776)
Reviewed-on: vikunja/frontend#2776
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-03 10:12:17 +00:00
renovate 529b47e488 chore(deps): update dependency vue-tsc to v1.0.11 (#2777)
Reviewed-on: vikunja/frontend#2777
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-03 09:33:19 +00:00
renovate 63c3e4ea58 chore(deps): update dependency @cypress/vue to v5.0.3 (#2775)
Reviewed-on: vikunja/frontend#2775
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-03 09:32:32 +00:00
renovate d52e917357 chore(deps): update dependency eslint to v8.29.0 (#2774)
Reviewed-on: vikunja/frontend#2774
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-03 09:31:53 +00:00
kolaente b2da4fd126
fix(task): pass a list specified via quick add magic down to all subtasks created via indention
Resolves vikunja/frontend#2771
2022-12-02 18:39:52 +01:00
kolaente 83fb8c3ded
fix(tasks): missing space when showing parent tasks and list title
See vikunja/frontend#2771
2022-12-02 18:05:48 +01:00
Dominik Pschenitschni b44d11cfc0 feat: add @intlify/unplugin-vue-i18n (#2772)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2772
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-12-02 15:16:15 +00:00
renovate d4133b9e78 chore(deps): update dependency @vue/test-utils to v2.2.5 (#2773)
Reviewed-on: vikunja/frontend#2773
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-02 14:27:23 +00:00
renovate c478926038 fix(deps): update sentry-javascript monorepo to v7.23.0 2022-12-01 17:03:38 +00:00
renovate 00e40a0f53 chore(deps): update dependency rollup to v3.5.1 (#2769)
Reviewed-on: vikunja/frontend#2769
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-12-01 07:23:21 +00:00
renovate 0567ba2a47 chore(deps): update dependency @types/node to v18.11.10 (#2768)
Reviewed-on: vikunja/frontend#2768
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-30 21:39:34 +00:00
Dominik Pschenitschni 3b95824f58 feat: use Intl.DateTimeFormat for gantt weekdays (#2766)
Fixes vikunja/frontend#2728

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2766
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-30 15:58:04 +00:00
renovate 963f3bfb07 fix(deps): update sentry-javascript monorepo to v7.22.0 (#2765)
Reviewed-on: vikunja/frontend#2765
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-29 14:16:53 +00:00
renovate d1c05eb3fb chore(deps): update dependency vue-tsc to v1.0.10 (#2764)
Reviewed-on: vikunja/frontend#2764
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-29 14:15:48 +00:00
renovate 2326e50d5d fix(deps): update dependency ufo to v1.0.1 (#2763)
Reviewed-on: vikunja/frontend#2763
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-29 13:13:46 +00:00
drone b7fa1a3ca1 [skip ci] Updated translations via Crowdin 2022-11-29 00:29:59 +00:00
renovate a3e1e43ec7 chore(deps): update typescript-eslint monorepo to v5.45.0 (#2762)
Reviewed-on: vikunja/frontend#2762
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-28 18:15:03 +00:00
renovate 39f163df4a fix(deps): update dependency @kyvg/vue3-notification to v2.7.0 (#2761)
Reviewed-on: vikunja/frontend#2761
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-28 11:13:49 +00:00
renovate f0e8ff93ff chore(deps): update dependency netlify-cli to v12.2.7 (#2760)
Reviewed-on: vikunja/frontend#2760
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-28 10:13:47 +00:00
Dominik Pschenitschni 3ee0bc345d feat: remove useRouteQuery (#2751)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2751
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-28 09:06:07 +00:00
renovate b4ffee8929 chore(deps): update dependency caniuse-lite to v1.0.30001434 (#2759)
Reviewed-on: vikunja/frontend#2759
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-28 09:04:04 +00:00
renovate e3c3d3ee53 fix(deps): update dependency pinia to v2.0.27 (#2757)
Reviewed-on: vikunja/frontend#2757
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-28 09:03:03 +00:00
renovate 67e7b94f5d chore(deps): update dependency esbuild to v0.15.16 2022-11-27 17:03:30 +00:00
renovate 6bbddeae8c chore(deps): update pnpm to v7.17.1 (#2755)
Reviewed-on: vikunja/frontend#2755
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-27 09:49:01 +00:00
renovate 94a0e1e25f chore(deps): update dependency rollup to v3.5.0 (#2756)
Reviewed-on: vikunja/frontend#2756
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-27 09:48:26 +00:00
renovate 4df9bc33df fix(deps): update dependency @infectoone/vue-ganttastic to v2.1.3 2022-11-26 18:03:30 +00:00
drone 5c64e8a2d7 [skip ci] Updated translations via Crowdin 2022-11-25 00:12:02 +00:00
renovate e10791f28c chore(deps): update dependency eslint-plugin-vue to v9.8.0 (#2753)
Reviewed-on: vikunja/frontend#2753
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-24 07:44:19 +00:00
drone 44b58ff34b [skip ci] Updated translations via Crowdin 2022-11-24 00:11:48 +00:00
renovate da17f78d30 fix(deps): update dependency highlight.js to v11.7.0 (#2752)
Reviewed-on: vikunja/frontend#2752
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-23 23:15:37 +00:00
renovate 61cdb7a91f chore(deps): update dependency @cypress/vue to v5.0.2 2022-11-23 19:03:58 +00:00
renovate 1b8ed9417a fix(deps): update dependency pinia to v2.0.26 2022-11-23 14:04:04 +00:00
renovate 4657da8c90 fix(deps): update sentry-javascript monorepo to v7.21.1 (#2747)
Reviewed-on: vikunja/frontend#2747
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-23 12:41:33 +00:00
renovate 6cd2908040 chore(deps): update dependency @4tw/cypress-drag-drop to v2.2.2 2022-11-23 09:03:55 +00:00
renovate 4dd99ae6fc fix(deps): update sentry-javascript monorepo to v7.21.0 2022-11-22 20:22:49 +00:00
renovate 0d5fa1326d chore(deps): update dependency cypress to v11.2.0 2022-11-22 20:03:57 +00:00
renovate dd692de7c4 chore(deps): update dependency vitest to v0.25.3 (#2743)
Reviewed-on: vikunja/frontend#2743
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-22 13:26:18 +00:00
Dominik Pschenitschni 93d95b0821 feat: use fetch instead of axios for deploy preview (#2719)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2719
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-22 13:11:26 +00:00
Dominik Pschenitschni 422e731fe0 fix: add all json files in src (#2737)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2737
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-22 13:08:32 +00:00
Dominik Pschenitschni 7db79ff04e fix: only load buckets if listId set (#2741)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2741
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-22 13:00:36 +00:00
renovate 59cc241226 fix(deps): update vueuse to v9.6.0 (#2742)
Reviewed-on: vikunja/frontend#2742
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-22 12:58:21 +00:00
renovate 2ac2e95cf2 chore(deps): update typescript-eslint monorepo to v5.44.0 2022-11-22 09:32:54 +00:00
renovate f8ce3d6ed6 chore(deps): update dependency rollup to v3.4.0 2022-11-22 06:04:25 +00:00
drone 93f33d9647 [skip ci] Updated translations via Crowdin 2022-11-22 00:12:21 +00:00
renovate 6d32b22da3 fix(deps): update dependency pinia to v2.0.25 2022-11-21 22:04:33 +00:00
renovate b333898595 fix(deps): update sentry-javascript monorepo to v7.20.1 2022-11-21 13:04:18 +00:00
renovate ccc633f3d9 fix(deps): update dependency codemirror to v5.65.10 2022-11-21 10:39:30 +00:00
renovate d39b0675d3 fix(deps): update dependency marked to v4.2.3 2022-11-21 10:38:45 +00:00
renovate 274092bfc4 chore(deps): update pnpm to v7.17.0 2022-11-21 10:38:05 +00:00
renovate cb2c032e60 chore(deps): update dependency @vue/test-utils to v2.2.4 2022-11-21 10:37:06 +00:00
renovate fdf294bcb3 chore(deps): update dependency netlify-cli to v12.2.4 2022-11-21 10:36:15 +00:00
renovate 58baa5960c chore(deps): update dependency esbuild to v0.15.15 2022-11-21 05:04:34 +00:00
renovate e948678e42 chore(deps): update dependency eslint to v8.28.0 2022-11-19 01:04:33 +00:00
Dominik Pschenitschni 5ccedc6f67 [skip ci] Updated translations via Crowdin 2022-11-19 00:12:18 +00:00
Dominik Pschenitschni 74ad98de68 fix: icon offset and color 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni 3282f55c34 chore: add TODO comment 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni d9984b28f7 feat: move link color location together 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni 4fc7b9c67e feat: group navigation styles further 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni ff9efe7889 feat: outdent navigation logo styles 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni 66be0e6ac4 feat: undent and order navigation css 2022-11-18 15:49:38 +00:00
Dominik Pschenitschni da8df8b667 feat: move avatar class to where it is used (#2725)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2725
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-18 13:30:41 +00:00
Dominik Pschenitschni 42e9f306e8
feat: grid for list cards 2022-11-18 14:04:20 +01:00
Angelo Delicato 4b47478440
feat: change list-content style (#91)
Co-authored-by: thelicato <thelicato@users.noreply.github.com>
Reviewed-on: https://github.com/go-vikunja/frontend/pull/91
2022-11-17 17:35:06 +01:00
Dominik Pschenitschni b42e4cca59 feat: more horizontal space on mobile (#2722)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2722
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 16:17:18 +00:00
Dominik Pschenitschni 33d4efecc4 feat: move useAutoHeightTextarea to composable (#2723)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2723
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 15:39:34 +00:00
Dominik Pschenitschni 45ec1623d5 feat: remove edit-task from list view (#2721)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2721
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 15:35:18 +00:00
Dominik Pschenitschni 8ef309243d feat: improve loadTask logic (#2715)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2715
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 15:31:21 +00:00
Dominik Pschenitschni 3aaacf4533 fix: remove vuex leftover from setModuleLoading (#2716)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2716
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-17 15:02:26 +00:00
renovate 0350e37fbb fix(deps): update sentry-javascript monorepo to v7.20.0 (#2720)
Reviewed-on: vikunja/frontend#2720
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-17 12:13:40 +00:00
renovate 244c436202 fix(deps): update dependency pinia to v2.0.24 2022-11-17 08:04:15 +00:00
drone 18d0c8ba2c [skip ci] Updated translations via Crowdin 2022-11-17 00:12:14 +00:00
kolaente 3891d5b876
feat: only automatically redirect to provider if the url contains ?redirectToProvider=true and it's the only one
Resolves https://github.com/go-vikunja/frontend/issues/90
2022-11-16 16:37:00 +01:00
Dominik Pschenitschni 98b38af43c feat: disable fullscreen for EasyMDE side-by-side mode (#2710)
Fixes https://github.com/go-vikunja/frontend/issues/92
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2710
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-16 14:37:03 +00:00
konrad 77ff0aa256 feat: move transition in component (#2694)
Reviewed-on: vikunja/frontend#2694
Reviewed-by: konrad <k@knt.li>
2022-11-16 14:36:17 +00:00
renovate 2ab26ee7c5 chore(deps): update pnpm to v7.16.1 (#2717)
Reviewed-on: vikunja/frontend#2717
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-16 14:16:20 +00:00
renovate 58f38bcfc3 fix(deps): update font awesome to v6.2.1 (#2712)
Reviewed-on: vikunja/frontend#2712
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-16 09:04:05 +00:00
renovate bcb5190365 chore(deps): update dependency cypress to v11.1.0 2022-11-15 21:06:37 +00:00
renovate c99d09c83e chore(deps): update dependency typescript to v4.9.3 2022-11-15 19:04:55 +00:00
renovate f49ea9752d chore(deps): update dependency vite to v3.2.4 2022-11-15 14:04:41 +00:00
renovate a56683cdc2 chore(deps): update dependency @vue/test-utils to v2.2.3 (#2707)
Reviewed-on: vikunja/frontend#2707
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-15 09:17:14 +00:00
renovate 7f4af63003 chore(deps): update dependency esbuild to v0.15.14 (#2706)
Reviewed-on: vikunja/frontend#2706
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-15 08:36:58 +00:00
Dominik Pschenitschni 8c44ed83e6
feat: use transition component everywhere 2022-11-14 22:08:54 +01:00
renovate b388677eaf fix(deps): update dependency ufo to v1 2022-11-14 18:42:12 +00:00
renovate bd7430b405 chore(deps): update typescript-eslint monorepo to v5.43.0 2022-11-14 18:05:00 +00:00
renovate 4baed8fe79 chore(deps): update pnpm to v7.16.0 (#2703)
Reviewed-on: vikunja/frontend#2703
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 16:01:36 +00:00
renovate fdbe4e8314 chore(deps): update dependency vitest to v0.25.2 (#2702)
Reviewed-on: vikunja/frontend#2702
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 16:00:57 +00:00
renovate 4a7f839449 chore(deps): update dependency postcss-preset-env to v7.8.3 (#2701)
Reviewed-on: vikunja/frontend#2701
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 10:15:12 +00:00
renovate c359f4d4dd chore(deps): update dependency netlify-cli to v12.1.1 (#2699)
Reviewed-on: vikunja/frontend#2699
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-14 09:16:36 +00:00
renovate 79d6212e48 chore(deps): update dependency happy-dom to v7.7.0 2022-11-14 08:54:27 +00:00
renovate e541213872 chore(deps): update dependency caniuse-lite to v1.0.30001431 2022-11-14 00:05:39 +00:00
Dominik Pschenitschni fd1d01164f feature/load-views-async (#2672)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2672
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-13 21:52:28 +00:00
renovate 34edf0dc5f chore(deps): update dependency @vue/test-utils to v2.2.2 (#2696)
Reviewed-on: vikunja/frontend#2696
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-13 10:17:32 +00:00
Dominik Pschenitschni 631a19fa92
feat: move transition in own component 2022-11-12 19:32:39 +01:00
Dominik Pschenitschni fba402fcd0
feat: reduce TaskDetailView selector specificity 2022-11-12 19:29:20 +01:00
Dominik Pschenitschni 4c4adfdf4e fix: reactive const assignment (#2692)
Resolves #2691

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2692
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 16:14:32 +00:00
Dominik Pschenitschni 06775cf4c7 fix: use scss for datemathHelp (#2690)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2690
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 14:38:31 +00:00
kolaente c07954f2b8
feat(ci): use docker buildx for multiarch builds 2022-11-12 14:43:29 +01:00
kolaente 995cc12880
fix(tasks): remove a task from its bucket when it is in the first kanban bucket
Resolves https://github.com/go-vikunja/frontend/issues/89
2022-11-12 12:13:00 +01:00
Dominik Pschenitschni 293402b6fd fix: move heading styles to component (#2686)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2686
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:52:16 +00:00
Dominik Pschenitschni 708ef2d72e feat: improve user component (#2687)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2687
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:51:35 +00:00
Dominik Pschenitschni 4c458a1ad0 fix: move createdUpdated styles to component (#2685)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2685
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:50:48 +00:00
Dominik Pschenitschni 02de481297 feat: use img for logo so that it's not part of the main bundle (#2684)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2684
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:48:52 +00:00
Dominik Pschenitschni 9d604f7a3b feat: reduce ready selector specificity (#2683)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2683
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:48:15 +00:00
Dominik Pschenitschni 0f1f131f7a feat: reduce attachments selector specificity (#2682)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2682
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:47:46 +00:00
Dominik Pschenitschni eb4c2a4b9d feat: reduce dropdown-item selector specificity (#2680)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2680
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:46:39 +00:00
Dominik Pschenitschni 599c1ba4b5 feat: reduce ListWrapper selector specificity (#2679)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2679
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:46:00 +00:00
Dominik Pschenitschni 12a8f7ebe9 feat: reduce contentAuth selector specifity (#2677)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2677
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:45:24 +00:00
Dominik Pschenitschni 9f0f0b39f8 feat: reduce multiselect selector specificity (#2678)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2678
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-12 10:44:49 +00:00
konrad 4a550da6a6 feat: filters script setup (#2671)
Reviewed-on: vikunja/frontend#2671
2022-11-12 10:43:24 +00:00
renovate 52ba168d41 chore(deps): update dependency @types/dompurify to v2.4.0 (#2688)
Reviewed-on: vikunja/frontend#2688
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-12 10:42:19 +00:00
renovate 9c7680aa55 chore(deps): update dependency rollup to v3.3.0 (#2689)
Reviewed-on: vikunja/frontend#2689
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-12 10:41:30 +00:00
Dominik Pschenitschni 83bb030c6e [skip ci] Updated translations via Crowdin 2022-11-12 00:12:21 +00:00
Dominik Pschenitschni 163d9366d3 feat: add vite build target esnext (#2674)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2674
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-11 14:43:23 +00:00
kolaente 5cff9988a3
chore: 0.20.1 release preperations 2022-11-11 12:02:16 +01:00
renovate a15ace0dbc fix(deps): update dependency vue to v3.2.45 2022-11-11 10:04:28 +00:00
renovate 65bb514093 chore(deps): update dependency postcss to v8.4.19 (#2673)
Reviewed-on: vikunja/frontend#2673
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-11 07:59:45 +00:00
renovate 403f1ee400 fix(deps): update sentry-javascript monorepo to v7.19.0 (#2670)
Reviewed-on: vikunja/frontend#2670
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-10 16:17:18 +00:00
Dominik Pschenitschni bb58dba8e0
feat: move select filters to dedicated components 2022-11-10 17:11:56 +01:00
Dominik Pschenitschni 4bad685f39
feat: filters script setup 2022-11-10 17:11:56 +01:00
kolaente e5f631af8d
fix(tasks): show any errors happening during task load 2022-11-10 16:44:16 +01:00
renovate 925f2aa837 fix(deps): update dependency dompurify to v2.4.1 (#2669)
Reviewed-on: vikunja/frontend#2669
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-10 14:14:01 +00:00
renovate 1de22386da chore(deps): update dependency cypress to v11 (#2659)
Reviewed-on: vikunja/frontend#2659
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-10 13:52:24 +00:00
renovate 602af9ec96 chore(deps): update pnpm to v7.15.0 (#2667)
Reviewed-on: vikunja/frontend#2667
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-10 07:29:51 +00:00
renovate 57feb65e00 fix(deps): update dependency vue to v3.2.44 (#2666)
Reviewed-on: vikunja/frontend#2666
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-10 07:29:10 +00:00
konrad 94508173dc fix: gantt route sync (#2664)
Reviewed-on: vikunja/frontend#2664
2022-11-09 19:48:17 +00:00
kolaente bd0c4d0355
feat(tests): add tests for gantt chart task detail open 2022-11-09 20:16:30 +01:00
kolaente 2952a0155f
feat(tests): add tests for gantt chart time range 2022-11-09 20:10:18 +01:00
kolaente 6055fecc5d
fix(gantt): don't try to load list NaN when opening a task from the gantt chart 2022-11-09 19:54:53 +01:00
Dominik Pschenitschni 7ec2b6c0d2
fix: gantt route sync 2022-11-09 18:39:29 +01:00
renovate 13bd434cb9 fix(deps): update dependency vue to v3.2.43 (#2663)
Reviewed-on: vikunja/frontend#2663
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-09 14:18:57 +00:00
kolaente b98d9fb7ec
fix(table): sort tasks by index instead of id 2022-11-09 14:46:58 +01:00
kolaente c2dd18edaa
fix: lint & formatting 2022-11-09 14:27:26 +01:00
kolaente d47791b957
fix: too much recursion error when opening a task from the gantt chart
Resolves F-905
Resolves https://community.vikunja.io/t/gantt-view-showing-too-much-recursion-error/935
2022-11-09 14:05:13 +01:00
renovate e6eaac1b46
fix(deps): update dependency @fortawesome/vue-fontawesome to v3.0.2 2022-11-09 12:30:50 +01:00
renovate cf2103734b fix(deps): update dependency vue to v3.2.42 2022-11-09 11:05:14 +00:00
renovate bb9c5046b3 chore(deps): update dependency sass to v1.56.1 (#2661)
Reviewed-on: vikunja/frontend#2661
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-09 10:20:32 +00:00
renovate a7a6a4c2d6 fix(deps): update vueuse to v9.5.0 (#2660)
Reviewed-on: vikunja/frontend#2660
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-09 10:19:58 +00:00
renovate 28257fcf5f chore(deps): update dependency @cypress/vite-dev-server to v4.0.1 (#2658)
Reviewed-on: vikunja/frontend#2658
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-09 10:18:57 +00:00
renovate c314d56f73 chore(deps): update dependency vitest to v0.25.1 (#2657)
Reviewed-on: vikunja/frontend#2657
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-08 16:15:14 +00:00
kolaente 612e592da7
fix: sort task alphabetically
Resolves F-906
2022-11-08 16:16:22 +01:00
konrad 1a329464ab feat(ci): improve drone config (#2637)
Reviewed-on: vikunja/frontend#2637
2022-11-08 14:56:45 +00:00
kolaente d5efe9f656
chore(ci): sign drone config 2022-11-08 15:42:41 +01:00
Dominik Pschenitschni 4a5f1a783a
fix(ci): cache folder name 2022-11-08 15:42:37 +01:00
Dominik Pschenitschni 906b3a5cdf
feat(ci): update cypress image 2022-11-08 15:42:33 +01:00
Dominik Pschenitschni 678dc8ef51
feat(ci): add kind everywhere 2022-11-08 15:42:28 +01:00
Dominik Pschenitschni da1d5eaba1
feat(ci): use 'always' for pull 2022-11-08 15:42:13 +01:00
kolaente 02448700b3
fix(quick add magic): don't parse labels, assignees or lists as date expressions if they are called that
Resolves https://community.vikunja.io/t/setting-today-label-using-quick-add-magic/969
2022-11-08 15:35:13 +01:00
renovate d9ca798aad fix(deps): update sentry-javascript monorepo to v7.18.0 2022-11-08 10:05:54 +00:00
renovate 23668e55d7 chore(deps): update dependency @cypress/vue to v5.0.1 (#2655)
Reviewed-on: vikunja/frontend#2655
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-08 07:25:00 +00:00
drone 3be9de76c5 [skip ci] Updated translations via Crowdin 2022-11-08 00:12:23 +00:00
renovate a9f41e3f37 chore(deps): update typescript-eslint monorepo to v5.42.1 (#2653)
Reviewed-on: vikunja/frontend#2653
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 18:29:19 +00:00
Dominik Pschenitschni f0492d49ef
feat: kanban store with composition api 2022-11-07 18:25:52 +01:00
Dominik Pschenitschni d85abbd77a feat: ListKanban script setup (#2643)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2643
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-07 17:23:11 +00:00
renovate 5186aeb086 chore(deps): update dependency @cypress/vue to v5 (#2652)
Reviewed-on: vikunja/frontend#2652
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 17:16:40 +00:00
renovate 7ba421e810 chore(deps): update dependency vitest to v0.25.0 (#2650)
Reviewed-on: vikunja/frontend#2650
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 16:17:25 +00:00
renovate e0fd4f216f chore(deps): update dependency @cypress/vite-dev-server to v4 (#2651)
Reviewed-on: vikunja/frontend#2651
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 16:17:20 +00:00
Dominik Pschenitschni 5057b69382 chore: move run.sh in scripts folder (#2649)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2649
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-07 14:33:37 +00:00
renovate 07297196f9 chore(deps): update dependency vite-plugin-pwa to v0.13.3 (#2648)
Reviewed-on: vikunja/frontend#2648
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 14:13:09 +00:00
Dominik Pschenitschni 7fbb6e8f70 fix: Flatpickr types (#2647)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2647
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-07 14:05:29 +00:00
Dominik Pschenitschni 38cef79f68 fix: remove duplicate store assignment (#2644)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2644
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-07 11:43:19 +00:00
Dominik Pschenitschni 6a93701649 feat: remove comments from prioritySelect (#2645)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2645
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-07 11:42:32 +00:00
Dominik Pschenitschni d9a8382049 feat: simpliy editAssignees (#2646)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2646
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-07 11:41:49 +00:00
renovate 66e60a4e6a chore(deps): update dependency @vitejs/plugin-legacy to v2.3.1 (#2641)
Reviewed-on: vikunja/frontend#2641
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 10:47:56 +00:00
renovate 0f67a78ec8 chore(deps): update dependency vite to v3.2.3 2022-11-07 09:04:32 +00:00
renovate 722802fb2e chore(deps): update dependency netlify-cli to v12.1.0 (#2640)
Reviewed-on: vikunja/frontend#2640
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 07:14:15 +00:00
renovate 752ead3a75 chore(deps): update dependency caniuse-lite to v1.0.30001430 (#2639)
Reviewed-on: vikunja/frontend#2639
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 06:35:25 +00:00
renovate b0b261d647 chore(deps): update dependency eslint to v8.27.0 2022-11-06 07:04:02 +00:00
renovate 442a14242c fix(deps): update dependency marked to v4.2.2 (#2636)
Reviewed-on: vikunja/frontend#2636
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-05 09:43:08 +00:00
Dominik Pschenitschni 66c0c322a2 [skip ci] Updated translations via Crowdin 2022-11-05 00:29:13 +00:00
Dominik Pschenitschni f4bc2b94f0 feat: sticky action buttons (#2622)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2622
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-04 13:49:28 +00:00
Dominik Pschenitschni f7728e5384 fix: remove wrong loadTask params (#2635)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2635
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-04 13:39:14 +00:00
kolaente 78b765ddc4 fix(kanban): don't allow dragging a bucket if a task input is focused
Resolves vikunja/frontend#2452
2022-11-04 12:16:25 +00:00
kolaente f967bcb205
fix(auth): always redirect to external openid provider if only one is enabled 2022-11-04 13:08:12 +01:00
Dominik Pschenitschni e49f960aea chore: inline simple helper (#2631)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2631
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-04 08:22:58 +00:00
renovate 98cb878250 chore(deps): update dependency sass to v1.56.0 (#2633)
Reviewed-on: vikunja/frontend#2633
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-04 08:17:28 +00:00
renovate 03f2b253b8 chore(deps): update dependency vite-plugin-pwa to v0.13.2 (#2632)
Reviewed-on: vikunja/frontend#2632
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-04 07:54:53 +00:00
renovate 69c0726b9d chore(deps): pin dependency @types/codemirror to 5.60.5 2022-11-03 15:03:57 +00:00
Dominik Pschenitschni eb59ca5836 fix: resolve issues with vue-easymde (#2629)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2629
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-03 14:37:24 +00:00
Dominik Pschenitschni 8b7b4d61a3 feat: MigrateService script setup (#2432)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2432
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-03 14:19:42 +00:00
renovate 0ed7114260 fix(deps): update sentry-javascript monorepo to v7.17.4 (#2628)
Reviewed-on: vikunja/frontend#2628
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-03 11:14:18 +00:00
renovate dda162c16f chore(deps): update dependency esbuild to v0.15.13 (#2627)
Reviewed-on: vikunja/frontend#2627
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-03 07:44:19 +00:00
Dominik Pschenitschni eeb562314e [skip ci] Updated translations via Crowdin 2022-11-03 00:29:00 +00:00
Dominik Pschenitschni 7f00c7dabd chore: remove unused processModel in services (#2624)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2624
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-02 16:19:26 +00:00
konrad 0ff0d8c5b8 feat: improved types (#2547)
Reviewed-on: vikunja/frontend#2547
Reviewed-by: konrad <k@knt.li>
2022-11-02 16:06:55 +00:00
renovate 9c9a5d08ff chore(deps): update pnpm to v7.14.2 2022-11-02 10:04:26 +00:00
renovate 9a2b88d295 fix(deps): update dependency marked to v4.2.1 (#2625)
Reviewed-on: vikunja/frontend#2625
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-02 09:45:05 +00:00
kolaente 09b76b7bd4
fix: don't show user deletion menu entry in user settings if the server disabled it 2022-11-02 10:44:14 +01:00
renovate f72c847e99 chore(deps): update dependency @cypress/vue to v4.2.2 2022-11-01 21:04:04 +00:00
renovate 8ea899fa26 chore(deps): update dependency vitest to v0.24.5 (#2621)
Reviewed-on: vikunja/frontend#2621
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 18:15:29 +00:00
Dominik Pschenitschni e01df4d369
fix: coverImageAttachmentId 2022-11-01 14:27:35 +01:00
Dominik Pschenitschni 096daad80a feat: rename http-common to fetcher (#2620)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2620
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-01 13:06:27 +00:00
Dominik Pschenitschni 3c5bfcc6f3
fix: potential issue with refs in Avatar 2022-11-01 13:12:13 +01:00
Dominik Pschenitschni 0182695cda
feat: add type info 2022-11-01 13:12:12 +01:00
Dominik Pschenitschni 44e6981759 feat: singleTaskInList script setup (#2463)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2463
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-01 10:43:01 +00:00
renovate 15b64c7e8a chore(deps): update dependency @types/node to v18.11.9 (#2619)
Reviewed-on: vikunja/frontend#2619
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 10:13:19 +00:00
renovate 9c357cb83e fix(deps): update dependency marked to v4.2.0 (#2616)
Reviewed-on: vikunja/frontend#2616
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 07:27:30 +00:00
renovate 0cf9b7595a chore(deps): update dependency @cypress/vite-dev-server to v3.4.0 (#2617)
Reviewed-on: vikuja/frontend#2617
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 07:26:42 +00:00
renovate 21dce0d8a8 chore(deps): update dependency rollup to v3.2.5 (#2618)
Reviewed-on: vikunja/frontend#2618
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 07:25:55 +00:00
renovate 218b96b230 fix(deps): update dependency @kyvg/vue3-notification to v2.6.1 (#2615)
Reviewed-on: vikunja/frontend#2615
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 07:25:14 +00:00
Dominik Pschenitschni d19c48a4f5 [skip ci] Updated translations via Crowdin 2022-11-01 00:28:44 +00:00
Dominik Pschenitschni 480aa8813e
fix: Multiselect modelValue prop type 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni caa29c152d
chore: improve multiselect hover types
when hovering over props you can only see comments if written with JSDoc
2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 1101fcb3ff
chore: remove comment 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 5d601ca4b3
fix: missing href 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 53c9a9bc9c
jsx templates 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni d6cb965ea7
fix: disable props destructure error 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 964aba4824
fix: better kanban updateBucket types 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 35f4bb1385
fix: setModuleLoading LoadingState type 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni 0b58973d87
feat: rework popup 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni 02deb0bedd
feat: rework dropdown-item 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni 4cd0e90fea
feat: rework XButton 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni e8c6afce72
feat: rework BaseButton 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni a2c1702eef
feat: type global components and especially icon prop 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni 599e28e5e5
feat: type improvements 2022-10-31 22:42:28 +01:00
Dominik Pschenitschni 1002579173 feat: label store with composition api (#2605)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2605
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:45:36 +00:00
Dominik Pschenitschni 5ae8bace82 feat: lists store with composition api (#2606)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2606
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:41:23 +00:00
Dominik Pschenitschni 0832184222 feat: namespaces store with composition api (#2607)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2607
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:40:55 +00:00
Dominik Pschenitschni a50eca852f feat: attachments store with composition api (#2603)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2603
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:40:26 +00:00
Dominik Pschenitschni b4f4fd45a4 feat: base store with composition api (#2601)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2601
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:31:58 +00:00
Dominik Pschenitschni 15ef86d597 feat: config store with composition api (#2604)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2604
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:29:56 +00:00
Dominik Pschenitschni 825ba100f0 feat: auth store with composition api (#2602)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2602
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:25:35 +00:00
Dominik Pschenitschni 839d331bf5 feat: task store with composition api (#2610)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2610
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:22:00 +00:00
renovate 1798388e31 chore(deps): update dependency rollup to v3.2.4 (#2614)
Reviewed-on: vikunja/frontend#2614
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-31 20:17:07 +00:00
renovate c3f8dcefb6 chore(deps): update typescript-eslint monorepo to v5.42.0 2022-10-31 18:04:06 +00:00
renovate 816292e86a fix(deps): update dependency @kyvg/vue3-notification to v2.6.0 (#2612)
Reviewed-on: vikunja/frontend#2612
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-31 17:51:22 +00:00
renovate ea1c7f1a7e fix(deps): update dependency @kyvg/vue3-notification to v2.5.1 2022-10-31 15:04:09 +00:00
renovate 6cb17c1267 fix(deps): update dependency @kyvg/vue3-notification to v2.5.0 2022-10-31 14:29:35 +00:00
renovate cbb2cf2951 chore(deps): update dependency vite to v3.2.2 2022-10-31 14:04:16 +00:00
renovate 85e1b36b00 chore(deps): update dependency @types/node to v18.11.8 2022-10-31 13:15:55 +00:00
renovate c9b9367c0b chore(deps): update dependency vitest to v0.24.4 2022-10-31 13:14:46 +00:00
renovate a14644c156 fix(deps): update dependency blurhash to v2.0.4 2022-10-31 13:09:02 +00:00
renovate 189b5ee8aa chore(deps): update dependency caniuse-lite to v1.0.30001427 2022-10-31 13:08:20 +00:00
renovate 61ed47fab4 chore(deps): update dependency eslint-plugin-vue to v9.7.0 2022-10-31 13:07:35 +00:00
renovate f18c03fa4d fix(deps): update sentry-javascript monorepo to v7.17.3 2022-10-31 12:04:02 +00:00
renovate 0e219b48a3 fix(deps): update dependency vue-flatpickr-component to v11 2022-10-30 12:48:43 +00:00
renovate 9ee05d5583 chore(deps): update pnpm to v7.14.1 (#2593)
Reviewed-on: vikunja/frontend#2593
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-30 10:25:37 +00:00
renovate 6d20e762ee chore(deps): update dependency @vue/test-utils to v2.2.1 (#2591)
Reviewed-on: vikunja/frontend#2591
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-30 10:24:54 +00:00
drone 5143e09d2b [skip ci] Updated translations via Crowdin 2022-10-30 00:29:14 +00:00
drone b428523c89 [skip ci] Updated translations via Crowdin 2022-10-29 00:29:10 +00:00
renovate 6ef0a0ded9 chore(deps): update dependency vite to v3.2.1 2022-10-28 11:03:47 +00:00
kolaente bd7fc44722
chore: release preparations 2022-10-28 12:21:18 +02:00
kolaente 549e7b4310
chore: add git-cliff to flake 2022-10-28 12:21:18 +02:00
renovate 89a125599e fix(deps): update sentry-javascript monorepo to v7.17.2 (#2587)
Reviewed-on: vikunja/frontend#2587
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-28 09:25:47 +00:00
kolaente da2a7a224e
fix: label multiselect styling on focus 2022-10-28 11:23:55 +02:00
drone da478a49d1 [skip ci] Updated translations via Crowdin 2022-10-28 00:21:53 +00:00
kolaente 98943377b8
fix: lint 2022-10-27 23:18:59 +02:00
renovate d28bbb7dc0 chore(deps): update dependency autoprefixer to v10.4.13 (#2586)
Reviewed-on: vikunja/frontend#2586
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 21:14:28 +00:00
Dominik Pschenitschni 386fd79b49 feat: quick-actions script setup (#2478)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2478
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-27 21:10:27 +00:00
Dominik Pschenitschni 9807858436 feat: unify savedFilter logic in service (#2491)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2491
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-27 19:56:14 +00:00
Dominik Pschenitschni 9ded3d0cd6 fix: improve notifications (#2583)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2583
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-27 19:49:17 +00:00
konrad d5258b7315 feat: improve useTaskList (#2582)
Reviewed-on: vikunja/frontend#2582
2022-10-27 19:48:26 +00:00
renovate eccaeae9e9 fix(deps): update sentry-javascript monorepo to v7.17.1 (#2585)
Reviewed-on: vikunja/frontend#2585
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 19:28:28 +00:00
konrad fd3e7e655d feat: replace our home-grown gantt implementation with ganttastic (#2180)
Reviewed-on: vikunja/frontend#2180
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-27 16:03:25 +00:00
kolaente 5271166120
chore(gantt): pnpm install after merge 2022-10-27 17:54:56 +02:00
Dominik Pschenitschni 61a89117d2
chore(gantt): upgrade packages 2022-10-27 17:51:44 +02:00
Dominik Pschenitschni 066553838a
fix: improve return type 2022-10-27 17:51:09 +02:00
Dominik Pschenitschni 443e1a063d
chore: refactor parseTimeLabel to own function 2022-10-27 17:51:09 +02:00
Dominik Pschenitschni 9a84fb6d7f
feat(gantt): disable useDayjsLanguageSync 2022-10-27 17:51:04 +02:00
Dominik Pschenitschni d8d3e4c8a6
fix(gantt): useDayjsLanguageSync and move to separate file 2022-10-27 17:50:47 +02:00
Dominik Pschenitschni b4f88bd4a6
fix: remove IE fallback 2022-10-27 17:50:31 +02:00
Dominik Pschenitschni abc26496cf
fix: do not change language to the current one 2022-10-27 17:50:31 +02:00
Dominik Pschenitschni b8cc828bc0
chore(gantt): upgrade packages 2022-10-27 17:50:31 +02:00
Dominik Pschenitschni 874dc1e5fc
feat: align with vue-flatpickr-component 10 2022-10-27 17:49:50 +02:00
Dominik Pschenitschni e74e6fcc99
feat: move config preparation in separate function 2022-10-27 17:49:50 +02:00
Dominik Pschenitschni 52d4d0bdb9
feat(gantt): reset gantt filter 2022-10-27 17:49:45 +02:00
Dominik Pschenitschni 6bf6357cbd
feat: use plural for filters consequently 2022-10-27 17:49:35 +02:00
Dominik Pschenitschni cf0eaf9ba1
chore: don't pass other params to ListGantt than route 2022-10-27 17:49:35 +02:00
Dominik Pschenitschni 8dea4082bb
fix: parseBooleanProp 2022-10-27 17:49:35 +02:00
Dominik Pschenitschni 51dc123d89
feat: use ref for filters 2022-10-27 17:49:35 +02:00
Dominik Pschenitschni acb3ddc73f
feat: use PascalCase for component name 2022-10-27 17:49:34 +02:00
Dominik Pschenitschni 407f5f2ef8
fix: initial transformation of ganttBars 2022-10-27 17:49:34 +02:00
Dominik Pschenitschni 73eab6c5b5
fix: scope ListGantt styles 2022-10-27 17:49:34 +02:00
Dominik Pschenitschni aefda38bdd
feat: remove gantt-chart wrapper 2022-10-27 17:49:34 +02:00
Dominik Pschenitschni a70a2e3ba6
feat(gantt): use time constants 2022-10-27 17:49:29 +02:00
Dominik Pschenitschni db611ab2d3
fix(gantt): only unmount chart if there aren't any loaded tasks yet 2022-10-27 17:48:30 +02:00
kolaente e1f49f2ff1
fix: disable dayjsLanguageSync function 2022-10-27 17:47:53 +02:00
kolaente b8e7b87f96
fix: don't try to dynamically load dayjs locales 2022-10-27 17:47:53 +02:00
kolaente 6c619072b4
chore: use vue-ganttastic release 2022-10-27 17:47:53 +02:00
kolaente 26e522cf8c
chore: pnpm install after merge 2022-10-27 17:47:53 +02:00
Dominik Pschenitschni 7f4114b703
feat: move useGanttTaskList in separate file 2022-10-27 17:47:53 +02:00
Dominik Pschenitschni c7dd20ef57
feat: simplify ListGantt styles 2022-10-27 17:47:53 +02:00
Dominik Pschenitschni c1da04eda1
feat(gantt): add task collection to useGanttFilter 2022-10-27 17:47:47 +02:00
Dominik Pschenitschni 2c732eb0d5
feat: abstract to useGanttFilter / and useRouteFilter 2022-10-27 17:47:15 +02:00
Dominik Pschenitschni 2acb70c562
chore: clean up 2022-10-27 17:47:15 +02:00
Dominik Pschenitschni eaf777864a
feat: working gantt-chart 2022-10-27 17:47:15 +02:00
Dominik Pschenitschni 0b194bb0cf
fix: update eslint env to 2022 2022-10-27 17:47:15 +02:00
Dominik Pschenitschni e968c88cfd
feat(gantt): trying to load gantt-chart 2022-10-27 17:47:09 +02:00
Dominik Pschenitschni df02dd5291
chore: better naming for input 2022-10-27 17:46:36 +02:00
Dominik Pschenitschni acdbf2f8f5
feat: working route sync 2022-10-27 17:46:36 +02:00
Dominik Pschenitschni 9f146c8c7f
chore(gantt): wip daterange 2022-10-27 17:46:28 +02:00
Dominik Pschenitschni 3b244dfdbe
feat: improve types 2022-10-27 17:45:29 +02:00
Dominik Pschenitschni 2f820e517f
feat: update ganttastic version 2022-10-27 17:45:29 +02:00
kolaente 56b88218b3
fix(tests): adjust gantt rows identifier 2022-10-27 17:45:29 +02:00
kolaente 957d8f05a5
chore: update lockfile 2022-10-27 17:45:26 +02:00
kolaente 31f2065d20
fix: correctly import all components 2022-10-27 17:45:09 +02:00
kolaente f5fd14124f
fix: use base store 2022-10-27 17:45:09 +02:00
Dominik Pschenitschni d91bc5090a
fix imports 2022-10-27 17:45:09 +02:00
Dominik Pschenitschni f21a4e1e9f
feat: review changes
move TaskForm in separate component, improve types
2022-10-27 17:45:09 +02:00
kolaente 970a04d973
fix: remove precision setting 2022-10-27 17:45:09 +02:00
kolaente fd9d0ad155
chore: don't use ref when not nessecary 2022-10-27 17:45:09 +02:00
kolaente 4be0977014
chore: add types for template ref 2022-10-27 17:45:09 +02:00
kolaente 6975a2b286
chore: don't use for..in 2022-10-27 17:45:09 +02:00
kolaente 64fdae81ec
feat: only use one watcher 2022-10-27 17:45:09 +02:00
kolaente 56a25734d7
chore: define types 2022-10-27 17:45:09 +02:00
kolaente ed5d3be7cb
chore: don't set required if there's a default value 2022-10-27 17:45:08 +02:00
kolaente 98d0398ca8
chore: uppercase const 2022-10-27 17:45:08 +02:00
kolaente d3925b8d80
chore: use @/models 2022-10-27 17:45:08 +02:00
kolaente b7b4530a11
fix: use inherit for font family 2022-10-27 17:45:08 +02:00
kolaente 766b4c669f
chore: use Loading component 2022-10-27 17:45:08 +02:00
kolaente 5f7159ebc4
feat: increase the default date range 2022-10-27 17:44:53 +02:00
kolaente 0a9588e097
feat: create task when pressing the button 2022-10-27 17:44:53 +02:00
kolaente 091beecc19
fix: make tests work again with new selectors 2022-10-27 17:44:53 +02:00
kolaente 6cb331ee0f
chore: remove old component and dependencies 2022-10-27 17:44:50 +02:00
kolaente 8c62a9e198
feat: loading animation 2022-10-27 17:44:30 +02:00
kolaente 29dcc02217
feat: handle changing props 2022-10-27 17:44:30 +02:00
kolaente 3eacc0754f
feat: show done tasks strikethrough 2022-10-27 17:44:30 +02:00
kolaente ebd824bddf
feat: update task in gantt bar after dragging to make sure it changes its color 2022-10-27 17:44:30 +02:00
kolaente 2c012e1a08
fix: make sure the date format is actually valid 2022-10-27 17:44:30 +02:00
kolaente 10c6db3849
fix: handle bar styling so they can actually be used 2022-10-27 17:44:30 +02:00
kolaente 80c151ca6c
feat: styling 2022-10-27 17:44:30 +02:00
kolaente 7a7a1c985e
chore: use width property 2022-10-27 17:44:30 +02:00
kolaente c8eac914d1
feat: scroll 2022-10-27 17:44:29 +02:00
kolaente d2c40926de
feat: add open task detail when double clicking 2022-10-27 17:44:29 +02:00
kolaente c3cae78213
fix: new task input styling 2022-10-27 17:44:29 +02:00
kolaente c289a6ae18
chore: use flatpickr range instead of two datepickers 2022-10-27 17:44:29 +02:00
kolaente ef4689335b
feat: create new tasks 2022-10-27 17:44:29 +02:00
kolaente 3b48adad67
feat: dynamically set default date 2022-10-27 17:44:29 +02:00
kolaente 736e5a8bf5
feat: dynamically set default date 2022-10-27 17:44:29 +02:00
kolaente ed241d21be
feat: only load tasks which start in the currently selected range 2022-10-27 17:44:29 +02:00
kolaente 49a24977f9
feat: allow passing props down to the gantt component 2022-10-27 17:44:29 +02:00
kolaente 2b0df8c237
feat: add basic implementation of ganttastic 2022-10-27 17:44:26 +02:00
renovate ef3f19d046 fix(deps): update sentry-javascript monorepo to v7.17.0 2022-10-27 14:04:07 +00:00
Dominik Pschenitschni 7ce880239e
feat: rename useTaskList 2022-10-27 15:47:48 +02:00
Dominik Pschenitschni aa2278a564
chore: move helper function outside of composable 2022-10-27 15:47:03 +02:00
renovate 96e44bf225 chore(deps): update dependency @vitejs/plugin-vue to v3.2.0 (#2579)
Reviewed-on: vikunja/frontend#2579
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 08:11:58 +00:00
renovate 4ad99bdad1 chore(deps): update dependency @vitejs/plugin-legacy to v2.3.0 (#2578)
Reviewed-on: vikunja/frontend#2578
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 07:14:45 +00:00
renovate 5e7fe3280c chore(deps): update dependency @types/node to v18.11.7 (#2581)
Reviewed-on: vikunja/frontend#2581
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 06:30:24 +00:00
renovate 7ec31363c3 chore(deps): update dependency vite to v3.2.0 (#2580)
Reviewed-on: vikunja/frontend#2580
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 06:29:25 +00:00
drone c40c1fb10a [skip ci] Updated translations via Crowdin 2022-10-27 00:21:10 +00:00
renovate 59be904d4a chore(deps): update dependency @types/node to v18.11.6 2022-10-26 13:03:57 +00:00
drone 1d9d093b31 [skip ci] Updated translations via Crowdin 2022-10-26 00:21:30 +00:00
renovate ef6bc3cbab chore(deps): update dependency cypress to v10.11.0 (#2576)
Reviewed-on: vikunja/frontend#2576
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-25 21:02:21 +00:00
kolaente e13e477682
fix: lint 2022-10-25 21:16:55 +02:00
kolaente 8a5b1ab3e3
fix(sharing): correctly check if the user has admin rights when sharing 2022-10-25 18:48:51 +02:00
renovate 70e81ee682 fix(deps): update vueuse to v9.4.0 (#2575)
Reviewed-on: vikunja/frontend#2575
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-25 13:12:51 +00:00
kolaente a0795db040
fix: building version into releases 2022-10-25 09:16:37 +02:00
renovate 35649d0e87 chore(deps): update dependency @types/node to v18 (#2574)
Reviewed-on: vikunja/frontend#2574
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-25 05:08:10 +00:00
drone 67145fe00b [skip ci] Updated translations via Crowdin 2022-10-25 00:14:16 +00:00
renovate 22d93a1a3c chore(deps): update typescript-eslint monorepo to v5.41.0 (#2573)
Reviewed-on: vikunja/frontend#2573
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 18:14:23 +00:00
kolaente 51471b9551
fix(i18n): rename "right" to permission so that it's clearer what it is used for 2022-10-24 19:07:08 +02:00
kolaente 22a18f8437
fix(subscription): make sure list subscription state is propagated everywhere for the current list 2022-10-24 19:01:01 +02:00
kolaente f17bbeddec
fix(subscription): don't remove every namespace but the one subscribing to 2022-10-24 18:56:50 +02:00
kolaente eae555475d
fix(teams): show an error message when no user is selected to add to a team 2022-10-24 18:52:31 +02:00
kolaente 12faafbe7c
fix(i18n): spelling typo 2022-10-24 18:41:12 +02:00
kolaente 5ddce387fe
fix: show frontend version in about dialog 2022-10-24 15:41:12 +02:00
renovate 05d000fc50 fix(deps): update dependency vue-router to v4.1.6 (#2572)
Reviewed-on: vikunja/frontend#2572
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 09:14:24 +00:00
renovate 333df9b247 chore(deps): update dependency netlify-cli to v12.0.11 (#2569)
Reviewed-on: vikunja/frontend#2569
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 08:31:17 +00:00
renovate 8d368c552d chore(deps): update dependency caniuse-lite to v1.0.30001423 (#2568)
Reviewed-on: vikunja/frontend#2568
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 07:53:01 +00:00
renovate 57cc7b8f37 chore(deps): update dependency @vue/test-utils to v2.2.0 (#2570)
Reviewed-on: vikunja/frontend#2570
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 07:52:25 +00:00
renovate 527873dad4 chore(deps): update dependency happy-dom to v7.6.0 (#2571)
Reviewed-on: vikunja/frontend#2571
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 07:51:38 +00:00
renovate d67dca4a81 chore(deps): update dependency @types/node to v16.18.0 (#2567)
Reviewed-on: vikunja/frontend#2567
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-23 21:44:54 +00:00
kolaente 6083301d1f
fix: wait until everything is loaded before replacing the current view with the last or login view 2022-10-23 16:12:48 +02:00
renovate 3f04571e43 chore(deps): update dependency vue-tsc to v1.0.9 (#2566)
Reviewed-on: vikunja/frontend#2566
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-23 13:41:01 +00:00
kolaente d7ac2ad697
fix(task): scroll the task field into view after activating it 2022-10-23 15:39:27 +02:00
kolaente 820823b5c3
fix(task): focusing on assignee search field when activating it 2022-10-23 15:27:29 +02:00
kolaente d7fb1a1e14
fix(task): marking checklist items as done 2022-10-23 14:39:28 +02:00
kolaente 7e218e03b2
fix(task): only show create list or import cta when there are no tasks 2022-10-23 14:01:20 +02:00
kolaente d7048d589e
fix(task): stop loading when no list was specified while creating a task 2022-10-23 13:58:40 +02:00
kolaente 80230069c6
fix: make sure the filter button is always shown on the kanban board 2022-10-23 13:48:45 +02:00
kolaente a695719128
fix: task detail view top spacing on mobile 2022-10-23 13:14:07 +02:00
kolaente f61723dac2
fix: redirect with query parameters 2022-10-23 13:12:04 +02:00
kolaente ae27502022
fix: make sure share modals don't have a create button
Resolves F-869
2022-10-23 13:03:09 +02:00
kolaente 8fdd3e785d
fix: make sure services without a modelFactory override still return data
Resolves F-850 and F-879
2022-10-23 12:56:44 +02:00
drone 5d038dc79f [skip ci] Updated translations via Crowdin 2022-10-23 00:13:07 +00:00
renovate b2ef66e5df chore(deps): update pnpm to v7.14.0 (#2565)
Reviewed-on: vikunja/frontend#2565
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-22 14:13:03 +00:00
renovate af819f9539 chore(deps): update dependency eslint to v8.26.0 (#2564)
Reviewed-on: vikunja/frontend#2564
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-22 06:38:41 +00:00
drone 9cf0a9a89e [skip ci] Updated translations via Crowdin 2022-10-22 00:13:04 +00:00
renovate 91b70c2de4 fix(deps): update dependency vue-flatpickr-component to v10 (#2563)
Reviewed-on: vikunja/frontend#2563
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-21 09:01:29 +00:00
kolaente 643a5b6d7d
fix: lint 2022-10-20 16:23:01 +02:00
kolaente e6f7ddc9ce
fix: email confirmation 2022-10-20 16:19:19 +02:00
kolaente 73575302de
fix: password reset 2022-10-20 16:15:58 +02:00
kolaente 4ed665fbd9
feat: refactor password reset to use a single password field 2022-10-20 16:07:36 +02:00
renovate f30e948abd chore(deps): update pnpm to v7.13.6 (#2562)
Reviewed-on: vikunja/frontend#2562
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-20 13:01:11 +00:00
drone ea758f0c58 [skip ci] Updated translations via Crowdin 2022-10-20 00:13:05 +00:00
renovate 617a48157d chore(deps): update dependency esbuild to v0.15.12 (#2561)
Reviewed-on: vikunja/frontend#2561
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-19 19:13:21 +00:00
renovate 419c0b2d96 fix(deps): update sentry-javascript monorepo to v7.16.0 (#2560)
Reviewed-on: vikunja/frontend#2560
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-19 07:39:55 +00:00
renovate 449b11c1ff chore(deps): update dependency @types/node to v16.11.68 (#2558)
Reviewed-on: vikunja/frontend#2558
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-19 05:07:29 +00:00
drone f818c207c2 [skip ci] Updated translations via Crowdin 2022-10-19 00:13:00 +00:00
renovate 4ee8f600a3 chore(deps): update typescript-eslint monorepo to v5.40.1 (#2557)
Reviewed-on: vikunja/frontend#2557
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-18 09:22:00 +00:00
konrad 29f68747bb feat: make salutation i18n static (#2546)
Reviewed-on: vikunja/frontend#2546
Reviewed-by: konrad <k@knt.li>
2022-10-18 08:35:52 +00:00
renovate ce201e0880 chore(deps): update dependency rollup to v3.2.3 (#2556)
Reviewed-on: vikunja/frontend#2556
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-18 08:19:53 +00:00
renovate bd26b81318 chore(deps): pin dependency @types/postcss-preset-env to 7.7.0 (#2555)
Reviewed-on: vikunja/frontend#2555
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-18 08:07:53 +00:00
Dominik Pschenitschni b80f82c411 fix: postcss-preset-env configuration (#2554)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2554
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-17 11:52:27 +00:00
Dominik Pschenitschni 5afafb7c82
fix: move hourToDaytime to separate file in order to pass tests 2022-10-17 12:35:47 +02:00
Dominik Pschenitschni 9de20b4c54
feat: use getter and helper in other components as well 2022-10-17 12:35:47 +02:00
renovate c9b18232c9 chore(deps): update dependency happy-dom to v7.5.12 (#2553)
Reviewed-on: vikunja/frontend#2553
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-17 09:02:37 +00:00
renovate 8e460f9856 chore(deps): update dependency caniuse-lite to v1.0.30001420 (#2550)
Reviewed-on: vikunja/frontend#2550
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-17 07:39:54 +00:00
renovate 2db263f2d2 fix(deps): update vueuse to v9.3.1 (#2552)
Reviewed-on: vikunja/frontend#2552
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-17 07:39:00 +00:00
renovate 79872c96de chore(deps): update dependency netlify-cli to v12.0.9 (#2551)
Reviewed-on: vikunja/frontend#2551
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-17 07:37:52 +00:00
renovate 2c881b3126 chore(deps): update dependency rollup to v3.2.2 (#2549)
Reviewed-on: vikunja/frontend#2549
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-17 07:37:24 +00:00
renovate 8093ce9441 chore(deps): update pnpm to v7.13.5 2022-10-16 19:03:19 +00:00
Dominik Pschenitschni c4d7f6fdfa
feat: get username from store getter 2022-10-16 19:36:04 +02:00
Dominik Pschenitschni c20de51a3c
feat: make salutation i18n static 2022-10-16 15:28:58 +02:00
renovate ed56176f2d chore(deps): update dependency rollup-plugin-visualizer to v5.8.3 (#2543)
Reviewed-on: vikunja/frontend#2543
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-16 10:21:04 +00:00
renovate ab7d889650 fix(deps): update dependency ufo to v0.8.6 (#2542)
Reviewed-on: vikunja/frontend#2542
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-16 09:19:27 +00:00
renovate b334712dfe chore(deps): update dependency @types/node to v16.11.66 (#2544)
Reviewed-on: vikunja/frontend#2544
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-16 09:18:50 +00:00
renovate 454b680117 chore(deps): update dependency rollup to v3.2.1 (#2545)
Reviewed-on: vikunja/frontend#2545
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-16 09:18:07 +00:00
renovate 830ecc2c03 chore(deps): update dependency vue-tsc to v1.0.8 (#2540)
Reviewed-on: vikunja/frontend#2540
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-15 07:56:39 +00:00
renovate 0e448a123e chore(deps): update dependency rollup to v3.2.0 (#2541)
Reviewed-on: vikunja/frontend#2541
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-15 07:13:52 +00:00
renovate 1c01fcbb84 chore(deps): update dependency esbuild to v0.15.11 (#2539)
Reviewed-on: vikunja/frontend#2539
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-14 15:12:14 +00:00
renovate e8fb4ce1fa chore(deps): update dependency @cypress/vue to v4.2.1 (#2535)
Reviewed-on: vikunja/frontend#2535
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-14 14:16:41 +00:00
renovate 37cbbdbec8 chore(deps): update dependency vitest to v0.24.3 (#2536)
Reviewed-on: vikunja/frontend#2536
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-14 14:16:00 +00:00
renovate e5aabfc753 fix(deps): update dependency vue to v3.2.41 (#2538)
Reviewed-on: vikunja/frontend#2538
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-14 13:19:55 +00:00
renovate 66f193871a chore(deps): update dependency vite to v3.1.8 (#2534)
Reviewed-on: vikunja/frontend#2534
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-13 09:37:28 +00:00
renovate d7907d8075 chore(deps): update dependency vue-tsc to v1.0.7 (#2533)
Reviewed-on: vikunja/frontend#2533
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-13 06:14:38 +00:00
renovate 38aa32c42d chore(deps): update dependency postcss to v8.4.18 (#2532)
Reviewed-on: vikunja/frontend#2532
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-12 21:33:22 +00:00
renovate 39d868278e chore(deps): update dependency vue-tsc to v1.0.6 (#2529)
Reviewed-on: vikunja/frontend#2529
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-12 13:13:59 +00:00
renovate 1c8919ee2a chore(deps): update dependency @faker-js/faker to v7.6.0 (#2530)
Reviewed-on: vikunja/frontend#2530
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-12 13:13:20 +00:00
renovate e26932aa95 chore(deps): update dependency rollup to v3.1.0 (#2528)
Reviewed-on: vikunja/frontend#2528
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-12 13:12:49 +00:00
252 changed files with 13921 additions and 9929 deletions

View File

@ -1,5 +1,6 @@
---
kind: pipeline
type: docker
name: build
trigger:
@ -22,7 +23,7 @@ steps:
# Disabled until we figure out why it is so slow
# - name: restore-cache
# image: meltwater/drone-cache:dev
# pull: true
# pull: always
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
@ -41,7 +42,7 @@ steps:
- name: dependencies
image: node:18-alpine
pull: true
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
CYPRESS_CACHE_FOLDER: .cache/cypress
@ -53,7 +54,7 @@ steps:
- name: lint
image: node:18-alpine
pull: true
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
@ -64,7 +65,7 @@ steps:
- name: build-prod
image: node:18-alpine
pull: true
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
@ -75,7 +76,7 @@ steps:
- name: test-unit
image: node:18-alpine
pull: true
pull: always
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run test:unit
@ -85,7 +86,7 @@ steps:
- name: typecheck
failure: ignore
image: node:18-alpine
pull: true
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
@ -95,8 +96,8 @@ steps:
- dependencies
- name: test-frontend
image: cypress/browsers:node16.14.0-chrome99-ff97
pull: true
image: cypress/browsers:node18.12.0-chrome106-ff106
pull: always
environment:
CYPRESS_API_URL: http://api:3456/api/v1
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
@ -116,7 +117,7 @@ steps:
# - name: rebuild-cache
# image: meltwater/drone-cache:dev
# pull: true
# pull: always
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
@ -136,7 +137,7 @@ steps:
- name: deploy-preview
image: node:18-alpine
pull: true
pull: always
environment:
NETLIFY_AUTH_TOKEN:
from_secret: netlify_auth_token
@ -160,6 +161,7 @@ steps:
---
kind: pipeline
type: docker
name: release-latest
depends_on:
@ -179,7 +181,7 @@ steps:
# - name: restore-cache
# image: meltwater/drone-cache:dev
# pull: true
# pull: always
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
@ -197,13 +199,13 @@ steps:
- name: build
image: node:18-alpine
pull: true
group: build-static
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
- corepack enable && pnpm config set store-dir .cache/.pnp
- pnpm install --fetch-timeout 100000
- apk add git
- corepack enable && pnpm config set store-dir .cache/.pnpm
- pnpm install --fetch-timeout 100000 --frozen-lockfile
- pnpm run lint
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- pnpm run build
@ -213,7 +215,7 @@ steps:
- name: static
image: kolaente/zip
pull: true
pull: always
commands:
- cd dist
- zip -r ../vikunja-frontend-unstable.zip *
@ -222,7 +224,7 @@ steps:
- name: release
image: plugins/s3
pull: true
pull: always
settings:
bucket: vikunja-releases
access_key:
@ -238,6 +240,7 @@ steps:
---
kind: pipeline
type: docker
name: release-version
depends_on:
@ -255,7 +258,7 @@ steps:
# - name: restore-cache
# image: meltwater/drone-cache:dev
# pull: true
# pull: always
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
@ -273,13 +276,13 @@ steps:
- name: build
image: node:18-alpine
pull: true
group: build-static
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
- apk add git
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000
- pnpm install --fetch-timeout 100000 --frozen-lockfile
- pnpm run lint
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- pnpm run build
@ -289,7 +292,7 @@ steps:
- name: static
image: kolaente/zip
pull: true
pull: always
commands:
- cd dist
- zip -r ../vikunja-frontend-${DRONE_TAG##v}.zip *
@ -298,7 +301,7 @@ steps:
- name: release
image: plugins/s3
pull: true
pull: always
settings:
bucket: vikunja-releases
access_key:
@ -314,6 +317,7 @@ steps:
---
kind: pipeline
type: docker
name: trigger-desktop-update
trigger:
@ -338,111 +342,7 @@ steps:
---
kind: pipeline
type: docker
name: docker-arm-release
depends_on:
- release-latest
- release-version
platform:
os: linux
arch: arm64
trigger:
ref:
- refs/heads/main
- "refs/tags/**"
event:
exclude:
- cron
steps:
- name: docker-unstable
image: plugins/docker:linux-arm
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/frontend
tags: unstable-linux-arm
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=unstable
when:
ref:
- refs/heads/main
depends_on:
- clone
- name: docker-version
image: plugins/docker:linux-arm
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/frontend
auto_tag: true
auto_tag_suffix: linux-arm
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=${DRONE_TAG##v}
when:
ref:
- "refs/tags/**"
depends_on:
- clone
- name: docker-unstable-arm64
image: plugins/docker:linux-arm64
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/frontend
tags: unstable-linux-arm64
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=unstable
when:
ref:
- refs/heads/main
depends_on:
- clone
- name: docker-version-arm64
image: plugins/docker:linux-arm64
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/frontend
auto_tag: true
auto_tag_suffix: linux-arm64
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=${DRONE_TAG##v}
when:
ref:
- "refs/tags/**"
depends_on:
- clone
---
kind: pipeline
type: docker
name: docker-amd64-release
platform:
os: linux
arch: amd64
name: docker-release
depends_on:
- release-latest
@ -457,8 +357,14 @@ trigger:
- cron
steps:
- name: fetch-tags
image: docker:git
commands:
- git fetch --tags
- name: docker-unstable
image: plugins/docker:linux-amd64
image: thegeeklab/drone-docker-buildx
privileged: true
pull: true
settings:
username:
@ -466,92 +372,42 @@ steps:
password:
from_secret: docker_password
repo: vikunja/frontend
tags: unstable-linux-amd64
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=unstable
when:
ref:
- refs/heads/main
- name: docker-version
image: plugins/docker:linux-amd64
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/frontend
auto_tag: true
auto_tag_suffix: linux-amd64
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=${DRONE_TAG##v}
when:
ref:
- "refs/tags/**"
---
kind: pipeline
type: docker
name: docker-manifest
trigger:
ref:
- refs/heads/main
- "refs/tags/**"
event:
exclude:
- cron
depends_on:
- docker-amd64-release
- docker-arm-release
steps:
- name: manifest-unstable
pull: always
image: plugins/manifest
settings:
tags: unstable
spec: docker-manifest-unstable.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=unstable
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64/v8
depends_on: [ fetch-tags ]
when:
ref:
- refs/heads/main
- name: manifest-release
pull: always
image: plugins/manifest
- name: docker-release
image: thegeeklab/drone-docker-buildx
privileged: true
pull: true
settings:
username:
from_secret: docker_username
password:
from_secret: docker_password
repo: vikunja/frontend
auto_tag: true
ignore_missing: true
spec: docker-manifest.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
when:
ref:
- "refs/tags/**"
- name: manifest-release-latest
pull: always
image: plugins/manifest
depends_on:
- clone
settings:
tags: latest
ignore_missing: true
spec: docker-manifest.tmpl
password:
from_secret: docker_password
username:
from_secret: docker_username
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=${DRONE_TAG##v}
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
- linux/arm64/v8
depends_on: [ fetch-tags ]
when:
ref:
- "refs/tags/**"
@ -574,9 +430,7 @@ depends_on:
- release-version
- release-latest
- trigger-desktop-update
- docker-arm-release
- docker-amd64-release
- docker-manifest
- docker-release
steps:
- name: notify
@ -659,6 +513,6 @@ steps:
from_secret: crowdin_key
---
kind: signature
hmac: c885a0e50db729842402494aa645dd3ac662828b691108550f6bf302158295ba
hmac: 9f26b5af73e3464e9ee1b5fbcb96854ca8a7e5f8d6ee2d85fd8376aad951b446
...

View File

@ -5,7 +5,7 @@ module.exports = {
'root': true,
'env': {
'browser': true,
'es2021': true,
'es2022': true,
'node': true,
'vue/setup-compiler-macros': true,
},
@ -37,6 +37,10 @@ module.exports = {
'@typescript-eslint/no-unused-vars': ['error', { vars: 'all', args: 'after-used', ignoreRestSiblings: true }],
'vue/multi-word-component-names': 0,
// disabled until we have support for reactivityTransform
// See https://github.com/vuejs/eslint-plugin-vue/issues/1948
// see also setting in `vite.config`
'vue/no-setup-props-destructure': 0,
},
'parser': 'vue-eslint-parser',
'parserOptions': {

View File

@ -9,6 +9,692 @@ All releases can be found on https://code.vikunja.io/frontend/releases.
The releases aim at the api versions which is why there are missing versions.
## [0.20.1] - 2022-11-11
### Bug Fixes
* *(auth)* Always redirect to external openid provider if only one is enabled
* *(ci)* Cache folder name
* *(gantt)* Don't try to load list NaN when opening a task from the gantt chart
* *(kanban)* Don't allow dragging a bucket if a task input is focused
* *(quick add magic)* Don't parse labels, assignees or lists as date expressions if they are called that
* *(table)* Sort tasks by index instead of id
* *(tasks)* Show any errors happening during task load* SetModuleLoading LoadingState type ([35f4bb1](35f4bb138554d300757420261d70d1a6bf6b9cc0))
* Better kanban updateBucket types ([964aba4](964aba4824418e431955881be284e35f412e873b))
* Disable props destructure error ([d6cb965](d6cb965ea7330f80f1e3c213442a049f63cba57e))
* Missing href ([5d601ca](5d601ca4b34cd7368ff6061659617fff2836cdbc))
* Multiselect modelValue prop type ([480aa88](480aa8813ec28e1228e02ba78dd3ee3037f4928a))
* Potential issue with refs in Avatar ([3c5bfcc](3c5bfcc6f3cece0f3bd6e4f862a187c17a2c4d6c))
* CoverImageAttachmentId ([e01df4d](e01df4d36996aa281ef73ee74f3ac5316a0b8a98))
* Don't show user deletion menu entry in user settings if the server disabled it ([09b76b7](09b76b7bd476b9de653e53de579f1c533d101d4d))
* Resolve issues with vue-easymde (#2629) ([eb59ca5](eb59ca5836ae8454885827bcf28a8476600bd122))
* Remove wrong loadTask params (#2635) ([f7728e5](f7728e538408d15fcbfcd9ce02cd235447dfa6f0))
* Remove duplicate store assignment (#2644) ([38cef79](38cef79f680ddf3612376a90c69198e01283a5a0))
* Flatpickr types (#2647) ([7fbb6e8](7fbb6e8f700157238f8924ce95424d79a34b7543))
* Sort task alphabetically ([612e592](612e592da799ee6a76d32c8ebc567aeadde3ee11))
* Too much recursion error when opening a task from the gantt chart ([d47791b](d47791b95793aabf1524544494621b237479c15d))
* Lint & formatting ([c2dd18e](c2dd18edaa8ac29446845a5028d1a04c1f39fc76))
* Gantt route sync ([7ec2b6c](7ec2b6c0d28a1ae1799b1ed7a781efbf4c4542d7))
* Gantt route sync (#2664) ([9450817](94508173dcfc75d606d490a536f80e10397fb69c))
### Dependencies
* *(deps)* Update dependency vite to v3.2.1
* *(deps)* Update dependency @vue/test-utils to v2.2.1 (#2591)
* *(deps)* Update pnpm to v7.14.1 (#2593)
* *(deps)* Update dependency vue-flatpickr-component to v11
* *(deps)* Update sentry-javascript monorepo to v7.17.3
* *(deps)* Update dependency eslint-plugin-vue to v9.7.0
* *(deps)* Update dependency caniuse-lite to v1.0.30001427
* *(deps)* Update dependency blurhash to v2.0.4
* *(deps)* Update dependency vitest to v0.24.4
* *(deps)* Update dependency @types/node to v18.11.8
* *(deps)* Update dependency vite to v3.2.2
* *(deps)* Update dependency @kyvg/vue3-notification to v2.5.0
* *(deps)* Update dependency @kyvg/vue3-notification to v2.5.1
* *(deps)* Update dependency @kyvg/vue3-notification to v2.6.0 (#2612)
* *(deps)* Update typescript-eslint monorepo to v5.42.0
* *(deps)* Update dependency rollup to v3.2.4 (#2614)
* *(deps)* Update dependency @kyvg/vue3-notification to v2.6.1 (#2615)
* *(deps)* Update dependency rollup to v3.2.5 (#2618)
* *(deps)* Update dependency @cypress/vite-dev-server to v3.4.0 (#2617)
* *(deps)* Update dependency marked to v4.2.0 (#2616)
* *(deps)* Update dependency @types/node to v18.11.9 (#2619)
* *(deps)* Update dependency vitest to v0.24.5 (#2621)
* *(deps)* Update dependency @cypress/vue to v4.2.2
* *(deps)* Update dependency marked to v4.2.1 (#2625)
* *(deps)* Update pnpm to v7.14.2
* *(deps)* Update dependency esbuild to v0.15.13 (#2627)
* *(deps)* Update sentry-javascript monorepo to v7.17.4 (#2628)
* *(deps)* Pin dependency @types/codemirror to 5.60.5
* *(deps)* Update dependency vite-plugin-pwa to v0.13.2 (#2632)
* *(deps)* Update dependency sass to v1.56.0 (#2633)
* *(deps)* Update dependency marked to v4.2.2 (#2636)
* *(deps)* Update dependency eslint to v8.27.0
* *(deps)* Update dependency caniuse-lite to v1.0.30001430 (#2639)
* *(deps)* Update dependency netlify-cli to v12.1.0 (#2640)
* *(deps)* Update dependency vite to v3.2.3
* *(deps)* Update dependency @vitejs/plugin-legacy to v2.3.1 (#2641)
* *(deps)* Update dependency vite-plugin-pwa to v0.13.3 (#2648)
* *(deps)* Update dependency @cypress/vite-dev-server to v4 (#2651)
* *(deps)* Update dependency vitest to v0.25.0 (#2650)
* *(deps)* Update dependency @cypress/vue to v5 (#2652)
* *(deps)* Update typescript-eslint monorepo to v5.42.1 (#2653)
* *(deps)* Update dependency @cypress/vue to v5.0.1 (#2655)
* *(deps)* Update sentry-javascript monorepo to v7.18.0
* *(deps)* Update dependency vitest to v0.25.1 (#2657)
* *(deps)* Update dependency @cypress/vite-dev-server to v4.0.1 (#2658)
* *(deps)* Update vueuse to v9.5.0 (#2660)
* *(deps)* Update dependency sass to v1.56.1 (#2661)
* *(deps)* Update dependency vue to v3.2.42
* *(deps)* Update dependency @fortawesome/vue-fontawesome to v3.0.2
* *(deps)* Update dependency vue to v3.2.43 (#2663)
* *(deps)* Update dependency vue to v3.2.44 (#2666)
* *(deps)* Update pnpm to v7.15.0 (#2667)
* *(deps)* Update dependency cypress to v11 (#2659)
* *(deps)* Update dependency dompurify to v2.4.1 (#2669)
### Features
* *(ci)* Use 'always' for pull
* *(ci)* Add kind everywhere
* *(ci)* Update cypress image
* *(ci)* Improve drone config (#2637)
* *(tests)* Add tests for gantt chart time range
* *(tests)* Add tests for gantt chart task detail open* Task store with composition api (#2610) ([839d331](839d331bf51f9a0e9742b9972dbd6a88fa38f1c3))
* Auth store with composition api (#2602) ([825ba10](825ba100f0c05e1ab98d401157c30aad8658afa6))
* Config store with composition api (#2604) ([15ef86d](15ef86d597ceb8731febf789f1b812a339273e40))
* Base store with composition api (#2601) ([b4f4fd4](b4f4fd45a4c98629de182033e808cf7b22a1fe4a))
* Attachments store with composition api (#2603) ([a50eca8](a50eca852fcb841166baa07a6cc405eeb70c6e9d))
* Namespaces store with composition api (#2607) ([0832184](08321842220798b478ffaef7e9e11c527cb5b3bd))
* Lists store with composition api (#2606) ([5ae8bac](5ae8bace820b05d3ad05f40ab51164ec2c35c068))
* Label store with composition api (#2605) ([1002579](1002579173bd4b89e157c78ac607abd7969d85bc))
* Type improvements ([599e28e](599e28e5e5d56e4ced338ec1c79fea7d4576b85a))
* Type global components and especially icon prop ([a2c1702](a2c1702eef64dd779c86940898bd49fc2c96233f))
* Rework BaseButton ([e8c6afc](e8c6afce7298267f2f77ece0a746218c2eb3f7b7))
* Rework XButton ([4cd0e90](4cd0e90feaab05a2275e92affda23dde7453013f))
* Rework dropdown-item ([02deb0b](02deb0beddbc9221bdcafd0d09cee383571dae55))
* Rework popup ([0b58973](0b58973d872d8d54c9a829a06c8535a7a7115613))
* SingleTaskInList script setup (#2463) ([44e6981](44e6981759261cdada6388384cbad96e5401b8a9))
* Add type info ([0182695](0182695cda1252a65df3f48fdc316e82cd7fadbd))
* Rename http-common to fetcher (#2620) ([096daad](096daad80a9c089e732116ce3b8aa4310a611368))
* Improved types (#2547) ([0ff0d8c](0ff0d8c5b89bd6a8b628ddbe6074f61797b6b9c1))
* MigrateService script setup (#2432) ([8b7b4d6](8b7b4d61a3b9dd01ab58b7e7dd30bf649b62fcf6))
* Sticky action buttons (#2622) ([f4bc2b9](f4bc2b94f0466a357361a69cfb3562e84d1ea439))
* Simpliy editAssignees (#2646) ([d9a8382](d9a83820495f34ddbd776f70cabdc24bbb1c3f32))
* Remove comments from prioritySelect (#2645) ([6a93701](6a93701649d35622d13dda969aae4aedf145d4d0))
* ListKanban script setup (#2643) ([d85abbd](d85abbd77a8197e977fdbfec0ee309736cce05fa))
* Kanban store with composition api ([f0492d4](f0492d49ef5cd99d95085deec066cec85f4688b3))
### Miscellaneous Tasks
* *(ci)* Sign drone config* Remove comment ([1101fcb](1101fcb3fff1fce102a7418b1e2734a71cdf84e2))
* Improve multiselect hover types ([caa29c1](caa29c152d35b28658773b838de0a8909d0e509f))
* Remove unused processModel in services (#2624) ([7f00c7d](7f00c7dabd1e55ec0e9a86ca495f702a38ddb18d))
* Inline simple helper (#2631) ([e49f960](e49f960aea2ead5baca6965649821db6584cbac2))
* Move run.sh in scripts folder (#2649) ([5057b69](5057b69382ca65659b624206b381d8f1500bae82))
### Other
* *(other)* [skip ci] Updated translations via Crowdin
## [0.20.0] - 2022-10-28
### Bug Fixes
* *(filters)* Changing filter checkbox values not being emitted to parent components
* *(filters)* Make sure all checkboxes are aligned properly
* *(filters)* Page freezing when entering a date as a result of an endless loop
* *(gantt)* Only unmount chart if there aren't any loaded tasks yet
* *(gantt)* UseDayjsLanguageSync and move to separate file
* *(i18n)* Spelling typo
* *(i18n)* Rename "right" to permission so that it's clearer what it is used for
* *(labels)* Unset loading state after loading all labels
* *(lint)* Unnecessary catch clause
* *(list)* Automatically close task edit pane when switching between lists
* *(quick add magic)* Time parsing for certain conditions (#2367)
* *(sharing)* Correctly check if the user has admin rights when sharing
* *(subscription)* Don't remove every namespace but the one subscribing to
* *(subscription)* Make sure list subscription state is propagated everywhere for the current list
* *(task)* Make sure users can be assigned via quick add magic via their real name as well
* *(task)* Cancel loading state when creating a new task does not work
* *(task)* Cancel loading state when creating a new task does not work
* *(task)* New tasks with quick add magic not showing up in task list
* *(task)* Setting a priority was not properly saved
* *(task)* Setting progress was not properly saved
* *(task)* Setting a label would not show up on the kanban board after setting it
* *(task)* Stop loading when no list was specified while creating a task
* *(task)* Only show create list or import cta when there are no tasks
* *(task)* Marking checklist items as done
* *(task)* Focusing on assignee search field when activating it
* *(task)* Scroll the task field into view after activating it
* *(tasks)* Don't allow adding the same assignee multiple times
* *(teams)* Show an error message when no user is selected to add to a team
* *(tests)* Fake current time in gantt tests to make them more reliable
* *(tests)* Adjust gantt rows identifier* Authenticate per request (#2258) ([6e4a3ff](6e4a3ff1996f55d99896a0e8267c1915de09dd39))
* Add lodash.clonedeep types ([80eaf38](80eaf38090413b74524ddc5a7dfcc9a845a6ba26))
* Use correct model for generics ([3ba423e](3ba423ed238a5f8f445246793829c7645dfe42aa))
* Merge duplicate types ([106abfc](106abfc842ca0c916ef7574b0fe5c89940869ac2))
* CreateNewTask typing ([f9b5130](f9b51306c396ceb0d8fa0c4af3fea24d2b28b64b))
* Improve some types ([4a50e6a](4a50e6aae28d22c3d441f1fead4edce7d0e30ff1))
* Use definite assignment assertion operator ([96f5f00](96f5f00c073f71c71d85c351f86ad16a67db6992))
* Mark abstractModel and abstractService abstract ([d36577c](d36577c04e1eea00fb21a5fb774e7f6b1f667d54))
* Use IAbstract to extend model interface ([8be1f81](8be1f81848303d590adb890743dd688fbf5cdf1c))
* Use new assignData method for default data ([8416b1f](8416b1f44811ff477d81db20370ff68e899c7252))
* Don't push a select event when nothing was selected ([9616bad](9616badc33173483e0b5cc0c99655e0c9a4907f9))
* Don't try to set the bucket of a task when it was moved to a new list ([c06b781](c06b781837c66174be41f40c967fbfcbcc35495e))
* Mutation error in TaskDetailView ([b4cba6f](b4cba6f7d96334b46e5e2d6be5ac87432b01f0c0))
* DefaultListId ([878b5bf](878b5bf236f7d1ddc9825d8dca8415313b08fd94))
* Use typed useStore ([54de368](54de368642519fc900ce89e4ee38989555054a05))
* Don't encode attachment upload file blob as json ([d819b9b](d819b9b0ba08db24a77751061ae285fc11205c2c))
* Dragging a list on mobile Safari ([6bf5f6e](6bf5f6efd46c47293fb54b9e9a25d91d8c6bec0d))
* Introduce a ListView type to properly type all available list views ([23598dd](23598dd2ee649449f2176ae86acbc16ecbf01e6f))
* Use proper computed for available views list ([e67fc7f](e67fc7fb7e1678b1b691fee77d3237b222ad50c6))
* Only warn once if triggeredNotifications are not supported (#2344) ([f083f18](f083f181e2c8aa0af3ac1381303f61792d5975f5))
* Bucket title edit success message appearing twice ([4921788](49217889b50da73d0f4851c4ee21f0dec11c7958))
* Don't parse dates in urls ([92f24e5](92f24e59a794a25098f5fb50f2101d516829cd36))
* Vue-i18n global scope (#2366) ([602ab83](602ab8379e3fb11eb8b547d036921311f193fb12))
* Redirect to login when the jwt token expires ([91976e2](91976e23f989f39fb25d3341aa3f4b632ea66f35))
* Only try to save user settings when a user is authenticated ([2df2bd3](2df2bd38e2b9f86be7e7c5aab744f27cbf2644c3))
* Remove margin from the color bubble component itself ([4fce71f](4fce71f729878d47c3ec79d0c10fae8fbaabbd91))
* Test pnpm cache ([e5d04c9](e5d04c98dabc6b597ecc32dd01ab31c4dd9882d1))
* Remove console.log ([43e2d03](43e2d036d77731fcce18cbea1d82196b10016609))
* Explicitly install cypress ([62e227c](62e227c767a43578f4487e3dc244f4756e073f5d))
* Only pass date to flatpickr if it's a valid date ([ede5cdd](ede5cdd8cf5575bba96d3e7b6824a7ad7b414ea7))
* Loading state when creating a new task from list view ([aa64e98](aa64e9835c6b9ef2bb10ab8d2a1b4a695cb4321b))
* Make add task button 100% height ([3c9c5ef](3c9c5eff1258b6e04e3d0e9299110fa9b5c9757d))
* Lint ([2bf9771](2bf9771e2894acb7ad3e563b7b31442d91c49e1a))
* Color list titles so that they are visible on cards with a background ([62ed7c5](62ed7c5964f1252f09fe432c42aaf327da5a8c4f))
* Missed porting these getters and commits ([95ad245](95ad245b59b0c6398b0bca217572ca36f6ea5a54))
* Use https for api url (#2425) ([9f39365](9f3936544d5906f0031412139b53c286023c2405))
* Don't use corepack prepare at all ([a199fc7](a199fc7a8e7f621ee96b2079e9558987f1350493))
* Add types for node ([6a82807](6a828078a398ab920f0e29d0801b918ae092ef30))
* VueI18n global scope fallback warnings (#2437) ([e9cf562](e9cf562969e42cc3ce3ffba3ed093db7a2089395))
* Fix missed conversion to ref (#2445) ([94d6f38](94d6f38e89174f879be4e5b1897b52603b40a745))
* Don't emit a possible null task ([5f5ed41](5f5ed410df1a2fe73e821d7dee7ebd4c0b918069))
* Docker build ([5b60693](5b606936c3f7b0dc1232ad269f3666f8170c6e11))
* Update top header list title when saving a filter ([fd3c15d](fd3c15d0642a8d91260ba24eaae52e0ba62c2871))
* Type of config stores maxFileSize (#2455) ([78a6d38](78a6d38641c5e4e68f117e37ee36a4ca3b40a24b))
* Don't add class method to interface ([367ad1e](367ad1e5a5972ac6ff353275b31f309ebcf5cb4c))
* Attachment deletion (#2472) ([f1852f1](f1852f1f33401576ae5033f54613c96cd80e0f95))
* Add lodash.debounce types (#2487) ([00e0a23](00e0a23d48c19c440aea7857c8b162a0dfa34361))
* Initial modal scroll lock (#2489) ([eae7cc5](eae7cc5a6b506cbbbe694b831cba7c5d1febaf05))
* Unset cover image when the task does not have one ([054d70c](054d70cbe5344e39d0e5f277a7db2f26573e1efa))
* Lint ([43258ab](43258ab74e0733e91be3ade1f0b13dcf9342cc18))
* Lint ([84a1abf](84a1abf3477abbbee136979bd0bde08ae6c54ceb))
* Don't try to render auth routes when the user is not authenticated ([3af20b6](3af20b6220d8fcded9c8c2f0bdef21dc26d748f6))
* Lint ([f405b21](f405b2105bf4d1cfd4f6acf03210b37ac91eff5e))
* Make sure subscriptions are properly inherited between lists and namespaces ([a895bde](a895bde6612e7a2b22a84b6ca7c583bafc9ebc9e))
* Make sure subscription strings work consistently across languages ([172d353](172d353df7a86baa9c2759907c7f855679138cc0))
* Make sure subscriptions are properly inherited between namespaces and lists ([0a29197](0a29197715f22602faf353fb8fe850150aa710d1))
* Lint ([c6d6da3](c6d6da31712906f094a88dbfdb5e9b6db66c29e3))
* Move hourToDaytime to separate file in order to pass tests ([5afafb7](5afafb7c82837a3af58c7bdc18174a785691b885))
* Postcss-preset-env configuration (#2554) ([b80f82c](b80f82c4118bb372263130df80d15a2a79d2191e))
* Password reset ([7357530](73575302debbe095ce031e4871fb3797a801db18))
* Email confirmation ([e6f7ddc](e6f7ddc9ce90ddcb3b58b2c001320b6b2c3ac169))
* Lint ([643a5b6](643a5b6d7d00bfab4b338582c85217dffa7d9b22))
* Make sure services without a modelFactory override still return data ([8fdd3e7](8fdd3e785d3c55281b557827860d0532b94ac758))
* Make sure share modals don't have a create button ([ae27502](ae27502022469882656459b0a9e7e8a4b6972c58))
* Redirect with query parameters ([f61723d](f61723dac251c9d85102beae73c6a03df10bd4bf))
* Task detail view top spacing on mobile ([a695719](a6957191284a8da38e56b4ed3fe0a57b69d6e2b9))
* Make sure the filter button is always shown on the kanban board ([8023006](80230069c6f09ced484cd356b816df6b1dd296d6))
* Wait until everything is loaded before replacing the current view with the last or login view ([6083301](6083301d1f410ede5fe62127e484169d74ff6dc0))
* Show frontend version in about dialog ([5ddce38](5ddce387fe589c574adf0cce438732faf4ad9fd1))
* Building version into releases ([a0795db](a0795db0408b5fece13d8a74e9e243375883ca6f))
* Lint ([e13e477](e13e477682ef9fd647925f459d8d4527d3c55b9b))
* New task input styling ([c3cae78](c3cae78213b791c9e6fd8143ee59e3ca256c374a))
* Handle bar styling so they can actually be used ([10c6db3](10c6db3849e734d0508c8d435164a0f771175740))
* Make sure the date format is actually valid ([2c012e1](2c012e1a080bd9519384d65ee0653483aa52d1c3))
* Make tests work again with new selectors ([091beec](091beecc19cf5ff49fc252c4eeb98aa8a65ddb67))
* Use inherit for font family ([b7b4530](b7b4530a111d93e81fc6398dc3f7267cc6e255fb))
* Remove precision setting ([970a04d](970a04d9733f4cbdc35e5b772ce4a34fa71e6c4c))
* Fix imports ([d91bc50](d91bc5090a6cec38e655c944df7cf57ac16e4133))
* Use base store ([f5fd141](f5fd14124fa139f3e76f7a4915b2efc85de6c789))
* Correctly import all components ([31f2065](31f2065d2005b27ff8a0abbc4efaa7138cfe27c1))
* Update eslint env to 2022 ([0b194bb](0b194bb0cf326104c249c953194997a1f9a80dbf))
* Don't try to dynamically load dayjs locales ([b8e7b87](b8e7b87f96bdccf19066ce31d40cf40379014bbe))
* Disable dayjsLanguageSync function ([e1f49f2](e1f49f2ff15286ee8903c29dbe708cda90e5d70d))
* Scope ListGantt styles ([73eab6c](73eab6c5b5bfe0d72393ab378cce77ad5cbb59b6))
* Initial transformation of ganttBars ([407f5f2](407f5f2ef8c4759ea46f5fb74717bafb16f606c5))
* ParseBooleanProp ([8dea408](8dea4082bb0766297f74acef0352f8a6a6168d3c))
* Do not change language to the current one ([abc2649](abc26496cf0e20d0124af327d47e086b39e2bd23))
* Remove IE fallback ([b4f88bd](b4f88bd4a6ba50be1f972794c3e87b7a09f7c2ca))
* Improve return type ([0665538](066553838ad289d6c6c0a8b1c6ed0b84139ace54))
* Improve notifications (#2583) ([9ded3d0](9ded3d0cd69dd974ffea2531e3ca92438e420f29))
* Lint ([9894337](98943377b8344f1f5a8e38c23eff79d7678f51bc))
* Label multiselect styling on focus ([da2a7a2](da2a7a224e3c8015939e189692813bc215dbd72c))
### Dependencies
* *(deps)* Update sentry-javascript monorepo to v7.11.0 (#2274)
* *(deps)* Update sentry-javascript monorepo to v7.11.1 (#2275)
* *(deps)* Update dependency vitest to v0.22.1 (#2276)
* *(deps)* Update dependency sass to v1.54.8 (#2281)
* *(deps)* Update dependency caniuse-lite to v1.0.30001387 (#2285)
* *(deps)* Update dependency rollup to v2.79.0 (#2278)
* *(deps)* Update dependency marked to v4.1.0 (#2284)
* *(deps)* Update dependency netlify-cli to v11 (#2287)
* *(deps)* Update dependency vite to v3.0.9 (#2279)
* *(deps)* Update dependency date-fns to v2.29.2 (#2277)
* *(deps)* Update dependency esbuild to v0.15.6 (#2290)
* *(deps)* Update dependency vite-plugin-pwa to v0.12.4 (#2291)
* *(deps)* Update dependency rollup-plugin-visualizer to v5.8.0 (#2282)
* *(deps)* Update dependency easymde to v2.17.0 (#2283)
* *(deps)* Update dependency vue-tsc to v0.40.5 (#2292)
* *(deps)* Update dependency vue to v3.2.38 (#2293)
* *(deps)* Update dependency vue-router to v4.1.5 (#2294)
* *(deps)* Update vueuse to v9.1.1 (#2295)
* *(deps)* Update dependency @cypress/vue to v4.2.0 (#2296)
* *(deps)* Update dependency @faker-js/faker to v7.5.0 (#2297)
* *(deps)* Update dependency eslint to v8.23.0 (#2299)
* *(deps)* Update dependency cypress to v10.7.0 (#2298)
* *(deps)* Update dependency eslint-plugin-vue to v9.4.0 (#2300)
* *(deps)* Update sentry-javascript monorepo to v7.12.0 (#2307)
* *(deps)* Update dependency dompurify to v2.4.0 (#2306)
* *(deps)* Update typescript-eslint monorepo to v5.36.1 (#2304)
* *(deps)* Update dependency vite-svg-loader to v3.5.1 (#2302)
* *(deps)* Update dependency typescript to v4.8.2 (#2301)
* *(deps)* Update font awesome to v6.2.0 (#2303)
* *(deps)* Update dependency @kyvg/vue3-notification to v2.4.1 (#2305)
* *(deps)* Update sentry-javascript monorepo to v7.12.1 (#2308)
* *(deps)* Update dependency vite-plugin-pwa to v0.12.6 (#2309)
* *(deps)* Update dependency vue-tsc to v0.40.6 (#2310)
* *(deps)* Update dependency rollup-plugin-visualizer to v5.8.1 (#2311)
* *(deps)* Update dependency vitest to v0.23.0 (#2312)
* *(deps)* Update dependency esbuild to v0.15.7 (#2313)
* *(deps)* Update dependency caniuse-lite to v1.0.30001390 (#2314)
* *(deps)* Update dependency vue-tsc to v0.40.7 (#2315)
* *(deps)* Update dependency vitest to v0.23.1 (#2316)
* *(deps)* Update dependency @vitejs/plugin-legacy to v2.1.0 (#2317)
* *(deps)* Update dependency @vitejs/plugin-vue to v3.1.0 (#2318)
* *(deps)* Update dependency vite to v3.1.0 (#2319)
* *(deps)* Update vueuse to v9.2.0 (#2320)
* *(deps)* Update typescript-eslint monorepo to v5.36.2 (#2321)
* *(deps)* Update dependency vue-tsc to v0.40.9 (#2322)
* *(deps)* Pin dependency @types/lodash.clonedeep to 4.5.7 (#2323)
* *(deps)* Update dependency @vue/eslint-config-typescript to v11.0.1 (#2324)
* *(deps)* Update dependency vite-plugin-pwa to v0.12.7 (#2325)
* *(deps)* Update dependency vue-tsc to v0.40.10 (#2326)
* *(deps)* Update dependency postcss-preset-env to v7.8.1 (#2328)
* *(deps)* Update dependency vite-svg-loader to v3.6.0 (#2327)
* *(deps)* Update dependency vue-tsc to v0.40.11 (#2333)
* *(deps)* Update dependency sass to v1.54.9 (#2336)
* *(deps)* Update dependency vue-tsc to v0.40.13
* *(deps)* Update dependency vue to v3.2.39
* *(deps)* Update dependency typescript to v4.8.3 (#2341)
* *(deps)* Update dependency vitest to v0.23.2
* *(deps)* Update dependency autoprefixer to v10.4.9
* *(deps)* Update dependency caniuse-lite to v1.0.30001397
* *(deps)* Update dependency netlify-cli to v11.7.1
* *(deps)* Update dependency eslint to v8.23.1
* *(deps)* Update typescript-eslint monorepo to v5.37.0
* *(deps)* Update dependency blurhash to v2 (#2351)
* *(deps)* Update dependency date-fns to v2.29.3 (#2354)
* *(deps)* Update dependency autoprefixer to v10.4.10 (#2355)
* *(deps)* Update dependency cypress to v10.8.0 (#2359)
* *(deps)* Update dependency autoprefixer to v10.4.11 (#2363)
* *(deps)* Update dependency postcss-preset-env to v7.8.2
* *(deps)* Update dependency vite to v3.1.1 (#2365)
* *(deps)* Pin dependency @types/dompurify to 2.3.4
* *(deps)* Update sentry-javascript monorepo to v7.13.0
* *(deps)* Update dependency eslint-plugin-vue to v9.5.0 (#2371)
* *(deps)* Update dependency eslint-plugin-vue to v9.5.1 (#2373)
* *(deps)* Update dependency vite to v3.1.2
* *(deps)* Update dependency @types/sortablejs to v1.15.0
* *(deps)* Update dependency vitest to v0.23.4
* *(deps)* Update dependency esbuild to v0.15.8
* *(deps)* Update dependency vite-plugin-pwa to v0.12.8 (#2375)
* *(deps)* Update caniuse-and-related to v4.21.4 (#2379)
* *(deps)* Update dependency netlify-cli to v11.8.0 (#2380)
* *(deps)* Update dependency @vitejs/plugin-legacy to v2.2.0 (#2381)
* *(deps)* Update dependency vite to v3.1.3 (#2382)
* *(deps)* Update typescript-eslint monorepo to v5.38.0 (#2383)
* *(deps)* Update dependency vite-plugin-pwa to v0.13.0 (#2385)
* *(deps)* Update dependency easymde to v2.18.0 (#2386)
* *(deps)* Update dependency autoprefixer to v10.4.12
* *(deps)* Update dependency pinia to v2.0.22 (#2400)
* *(deps)* Update dependency @vue/eslint-config-typescript to v11.0.2
* *(deps)* Update dependency vite-plugin-pwa to v0.13.1
* *(deps)* Update dependency rollup to v2.79.1
* *(deps)* Update dependency codemirror to v5.65.9
* *(deps)* Update pnpm to v7.12.1
* *(deps)* Update dependency sass to v1.55.0
* *(deps)* Update dependency esbuild to v0.15.9
* *(deps)* Update pnpm to v7.12.2 (#2408)
* *(deps)* Update dependency caniuse-lite to v1.0.30001412 (#2421)
* *(deps)* Update dependency netlify-cli to v11.8.3 (#2422)
* *(deps)* Update dependency eslint to v8.24.0 (#2410)
* *(deps)* Update vueuse to v9.3.0 (#2423)
* *(deps)* Update dependency rollup-plugin-visualizer to v5.8.2 (#2420)
* *(deps)* Update typescript-eslint monorepo to v5.38.1 (#2426)
* *(deps)* Update dependency blurhash to v2.0.1
* *(deps)* Update dependency cypress to v10.9.0 (#2429)
* *(deps)* Update dependency @types/node to v16.11.62 (#2430)
* *(deps)* Update dependency typescript to v4.8.4
* *(deps)* Update dependency vue to v3.2.40
* *(deps)* Update dependency blurhash to v2.0.2
* *(deps)* Update sentry-javascript monorepo to v7.14.0 (#2440)
* *(deps)* Update dependency vite to v3.1.4 (#2439)
* *(deps)* Update dependency @vue/test-utils to v2.1.0
* *(deps)* Update dependency esbuild to v0.15.10
* *(deps)* Update dependency @cypress/vite-dev-server to v3.2.0 (#2448)
* *(deps)* Update dependency postcss to v8.4.17 (#2449)
* *(deps)* Update dependency marked to v4.1.1
* *(deps)* Update dependency @vitejs/plugin-vue to v3.1.2 (#2461)
* *(deps)* Update dependency @types/node to v16.11.63 (#2464)
* *(deps)* Update dependency caniuse-lite to v1.0.30001414 (#2465)
* *(deps)* Update pnpm to v7.13.0 (#2467)
* *(deps)* Update dependency netlify-cli to v12 (#2466)
* *(deps)* Update dependency vue-advanced-cropper to v2.8.5 (#2469)
* *(deps)* Update dependency blurhash to v2.0.3 (#2468)
* *(deps)* Update sentry-javascript monorepo to v7.14.1 (#2471)
* *(deps)* Update typescript-eslint monorepo to v5.39.0
* *(deps)* Update dependency @types/node to v16.11.64 (#2479)
* *(deps)* Update dependency eslint-plugin-vue to v9.6.0 (#2480)
* *(deps)* Update pnpm to v7.13.1
* *(deps)* Update dependency vue-advanced-cropper to v2.8.6 (#2483)
* *(deps)* Pin dependency @rushstack/eslint-patch to 1.2.0 (#2486)
* *(deps)* Pin dependency @types/lodash.debounce to 4.0.7 (#2488)
* *(deps)* Update dependency happy-dom to v7 (#2492)
* *(deps)* Update dependency vite to v3.1.5
* *(deps)* Update dependency happy-dom to v7.0.2
* *(deps)* Update sentry-javascript monorepo to v7.14.2
* *(deps)* Update pnpm to v7.13.2
* *(deps)* Update dependency vue-flatpickr-component to v9.0.8 (#2494)
* *(deps)* Update dependency vite to v3.1.6
* *(deps)* Update dependency happy-dom to v7.0.4 (#2499)
* *(deps)* Update dependency @cypress/vite-dev-server to v3.3.0 (#2501)
* *(deps)* Update dependency happy-dom to v7.0.6 (#2500)
* *(deps)* Update dependency happy-dom to v7.3.0 (#2502)
* *(deps)* Update dependency vitest to v0.24.0 (#2503)
* *(deps)* Update dependency vue-tsc to v1 (#2504)
* *(deps)* Update dependency happy-dom to v7.4.0 (#2505)
* *(deps)* Update dependency eslint to v8.25.0
* *(deps)* Update dependency vue-tsc to v1.0.1 (#2507)
* *(deps)* Update dependency pinia to v2.0.23 (#2509)
* *(deps)* Update dependency express to v4.18.2
* *(deps)* Update pnpm to v7.13.3 (#2511)
* *(deps)* Update dependency vue-tsc to v1.0.2 (#2510)
* *(deps)* Update dependency vue-tsc to v1.0.3 (#2512)
* *(deps)* Update dependency netlify-cli to v12.0.7 (#2514)
* *(deps)* Update dependency caniuse-lite to v1.0.30001418 (#2513)
* *(deps)* Update dependency vite to v3.1.7 (#2515)
* *(deps)* Update sentry-javascript monorepo to v7.15.0 (#2516)
* *(deps)* Update dependency vitest to v0.24.1 (#2517)
* *(deps)* Update pnpm to v7.13.4 (#2518)
* *(deps)* Update typescript-eslint monorepo to v5.40.0 (#2519)
* *(deps)* Update dependency @types/node to v16.11.65 (#2520)
* *(deps)* Update dependency minimist to v1.2.7 (#2521)
* *(deps)* Update dependency rollup to v3 (#2524)
* *(deps)* Update dependency @cypress/vite-dev-server to v3.3.1 (#2523)
* *(deps)* Update dependency cypress to v10.10.0 (#2525)
* *(deps)* Update dependency vue-tsc to v1.0.4 (#2526)
* *(deps)* Update dependency vue-tsc to v1.0.5 (#2527)
* *(deps)* Update dependency rollup to v3.1.0 (#2528)
* *(deps)* Update dependency @faker-js/faker to v7.6.0 (#2530)
* *(deps)* Update dependency vue-tsc to v1.0.6 (#2529)
* *(deps)* Update dependency postcss to v8.4.18 (#2532)
* *(deps)* Update dependency vue-tsc to v1.0.7 (#2533)
* *(deps)* Update dependency vite to v3.1.8 (#2534)
* *(deps)* Update dependency vue to v3.2.41 (#2538)
* *(deps)* Update dependency vitest to v0.24.3 (#2536)
* *(deps)* Update dependency @cypress/vue to v4.2.1 (#2535)
* *(deps)* Update dependency esbuild to v0.15.11 (#2539)
* *(deps)* Update dependency rollup to v3.2.0 (#2541)
* *(deps)* Update dependency vue-tsc to v1.0.8 (#2540)
* *(deps)* Update dependency rollup to v3.2.1 (#2545)
* *(deps)* Update dependency @types/node to v16.11.66 (#2544)
* *(deps)* Update dependency ufo to v0.8.6 (#2542)
* *(deps)* Update dependency rollup-plugin-visualizer to v5.8.3 (#2543)
* *(deps)* Update pnpm to v7.13.5
* *(deps)* Update dependency rollup to v3.2.2 (#2549)
* *(deps)* Update dependency netlify-cli to v12.0.9 (#2551)
* *(deps)* Update vueuse to v9.3.1 (#2552)
* *(deps)* Update dependency caniuse-lite to v1.0.30001420 (#2550)
* *(deps)* Update dependency happy-dom to v7.5.12 (#2553)
* *(deps)* Pin dependency @types/postcss-preset-env to 7.7.0 (#2555)
* *(deps)* Update dependency rollup to v3.2.3 (#2556)
* *(deps)* Update typescript-eslint monorepo to v5.40.1 (#2557)
* *(deps)* Update dependency @types/node to v16.11.68 (#2558)
* *(deps)* Update sentry-javascript monorepo to v7.16.0 (#2560)
* *(deps)* Update dependency esbuild to v0.15.12 (#2561)
* *(deps)* Update pnpm to v7.13.6 (#2562)
* *(deps)* Update dependency vue-flatpickr-component to v10 (#2563)
* *(deps)* Update dependency eslint to v8.26.0 (#2564)
* *(deps)* Update pnpm to v7.14.0 (#2565)
* *(deps)* Update dependency vue-tsc to v1.0.9 (#2566)
* *(deps)* Update dependency @types/node to v16.18.0 (#2567)
* *(deps)* Update dependency happy-dom to v7.6.0 (#2571)
* *(deps)* Update dependency @vue/test-utils to v2.2.0 (#2570)
* *(deps)* Update dependency caniuse-lite to v1.0.30001423 (#2568)
* *(deps)* Update dependency netlify-cli to v12.0.11 (#2569)
* *(deps)* Update dependency vue-router to v4.1.6 (#2572)
* *(deps)* Update typescript-eslint monorepo to v5.41.0 (#2573)
* *(deps)* Update dependency @types/node to v18 (#2574)
* *(deps)* Update vueuse to v9.4.0 (#2575)
* *(deps)* Update dependency cypress to v10.11.0 (#2576)
* *(deps)* Update dependency @types/node to v18.11.6
* *(deps)* Update dependency vite to v3.2.0 (#2580)
* *(deps)* Update dependency @types/node to v18.11.7 (#2581)
* *(deps)* Update dependency @vitejs/plugin-legacy to v2.3.0 (#2578)
* *(deps)* Update dependency @vitejs/plugin-vue to v3.2.0 (#2579)
* *(deps)* Update sentry-javascript monorepo to v7.17.0
* *(deps)* Update sentry-javascript monorepo to v7.17.1 (#2585)
* *(deps)* Update dependency autoprefixer to v10.4.13 (#2586)
### Features
* *(gantt)* Trying to load gantt-chart
* *(gantt)* Add task collection to useGanttFilter
* *(gantt)* Use time constants
* *(gantt)* Reset gantt filter
* *(gantt)* Disable useDayjsLanguageSync
* *(link shares)* Hide the logo if a query parameter was passed
* *(link shares)* Allows switching the initial view by passing a query parameter
* *(link shares)* Cleanup link share table
* *(link shares)* Allows switching the initial view by passing a query parameter (#2335)
* *(list)* Add info dialoge to show list description (#2368)
* *(openid)* Show error message from query after being redirected from third party
* *(task)* Cover image for tasks (#2460)
* *(tests)* Add tests for task attachments* Settings background script setup (#2104) ([ff65580](ff655808b3cb562bd1c843ff70bf3641718ae61d))
* List settings edit script setup (#1988) ([f6437c8](f6437c81da73b7e3406c28b9bd7b201e376f15c3))
* Convert abstractService to ts ([74ad6e6](74ad6e65e88d6aa5702686dd0b6f55e2dc6b7b77))
* Add properties to models ([797de0c](797de0c5432face3887f4d77bcb7dd7ee2e7e0c1))
* Constants ([8fb0065](8fb00653e47c6f41a0e461c944b401d58b4a2351))
* Function attribute typing ([332acf0](332acf012c423d3201ec1811093226447cd065e8))
* Improve types ([c9e85cb](c9e85cb52b562cf9dcfac3ed54d8289e2b499992))
* Improve store and model typing ([3766b5e](3766b5e51ba9c40a6affa91ce5cc11519e2da5c3))
* Use lib ESNext setting for typescript ([79e7e4a](79e7e4a8aefe9f4d00bcbad76c4206c409384b61))
* Extend mode interface from class instead from interface ([a6b96f8](a6b96f857d949874ba75f657b887a7c997aa7c57))
* Improve store typing ([2444784](244478400ad8b8243ae2b29d741c03fa2b83601b))
* Add modelTypes ([7d4ba62](7d4ba6249e300b6711369476f5d6a84728668b0f))
* Convert services and models to ts (#1798) ([dbea1f7](dbea1f7a51f3cf5173b5f381944c4ef19ef97ec8))
* Add sponsor logo to readme (relm) ([e959043](e95904351fbd30776306225f3be55978d70ae42e))
* Show user display name when searching for assignees on a list ([65fd2f1](65fd2f14a067ea9d79b352af00f3c316be883fdf))
* Add keyboard shortcut to toggle task description edit (#2332) ([7f6f896](7f6f8963e7db236f3beb9e6a36fab4ba479b969b))
* Programmatically generate list of available views ([26d02d5](26d02d5593283c3ad2fb961348ba2f412cc9eaa8))
* Add fallback for useCopyToClipboard (#2343) ([7b398f7](7b398f73f604d6564a41c3ce5031883c677f02c7))
* Improve models ([1a11b43](1a11b43ca8d51bf998019fbc741e845b07d70157))
* Use v-model more consequent (#2356) ([db8b881](db8b8812af731fb6acbdd1aec173e37b84066eea))
* Make share link name italic ([224cea3](224cea33ced403f45c7d833ab576be44c89d199a))
* Move the url link to the bottom of the items ([6576b61](6576b6148ce1b02dbe6a335778592c4b72e275de))
* Color the task color button when the task has a color set ([51c806c](51c806c12b90aa124384497856590f5010b9ff49))
* Color the color button icon instead of the button itself ([bdf992c](bdf992c9bfe9de176a22f7b5a6fdae1bc5e5010f))
* Move the update available dialoge always to the bottom ([a18c6ab](a18c6ab8d860a496905f58278315222992bacd07))
* Show the task color bubble everywhere ([2683fec](2683fec0a67f6afd16579bb44a6ceadc0edd565f))
* Color the task color button when the task has a color set (#2331) ([f70b1d2](f70b1d2902f91a88eaf33f1a9799489c20a6a143))
* Namespace settings archive script setup ([ad6b335](ad6b335d41e07e8ce2e74e4282d572ba4c04ea30))
* ListNamespaces script setup (#2389) ([ff5d1fc](ff5d1fc8c1961134ef3baec09be52b02c0b6898e))
* NewTeam script setup (#2388) ([e91b5fd](e91b5fde0216e15f739da22efbcaae3829e31ba1))
* Port label store to pinia | pinia 1/9 (#2391) ([d67e5e3](d67e5e386d7d1901694fe0004f580807754bcae1))
* Use pnpm ([d76b526](d76b526916d4aca279670d2690f7bb8e63e432a7))
* Move list store to pina (#2392) ([a38075f](a38075f376aa5cc2d8a06943cf8932366a0d4011))
* Task relatedTasks script setup ([943d5f7](943d5f79757b73f447c51641812e7766edeffe9e))
* Allow marking a related task done directly from the list ([ce0f58c](ce0f58c7833bbb37974709112cdedad88ae07cc8))
* DeleteNamespace script setup (#2387) ([0814890](0814890cac92b813b5b93bb42c7a40e2dc13cb94))
* Task relatedTasks script setup (#1939) ([d57e27b](d57e27b4a62aaa0f0a739f030515fff72a56f7fc))
* Use pnpm (#1789) ([f7ca064](f7ca064127863de4a4c1e3ae29d84d6bd5311cb9))
* Add hot reloading support ([1c58fcc](1c58fccd926586b2303ce41939a535b2044a78a9))
* Move namespaces store to stores ([9474240](9474240cb9159a0e1b42f82cb492cc267782ce4f))
* Port namespace store to pinia ([093ab76](093ab766d45247b3b1d12740dc6b24c6b48f21c4))
* Feat-attachments-script-setup (#2358) ([4dfcd8e](4dfcd8e70f54d2ed977d4b8de5fb8bf9469819aa))
* Convert namespaces store to pina (#2393) ([937fd36](937fd36f724f2b383fe51ae25a55ba90f58c8975))
* Move attachments store to stores ([c2ba1b2](c2ba1b2828439d3bd1e846a4bb9a4c456562c460))
* Port attachments store to pinia ([20e9420](20e94206388ab694248942996fdb67b7be87e76f))
* Move config to stores ([9e8c429](9e8c429864923215be5b110fdcb7c4a586c60f3d))
* Port config store to pinia ([a737fc5](a737fc5bc2affc87b209746ecf04c66e1f6077db))
* Filter-popup script setup (#2418) ([ba2605a](ba2605af1bb6f9ba7d3bd1b99ed862d510c6bb31))
* ListLabels script setup (#2416) ([89e428b](89e428b4d285f3465a40773fbda564c432fb371e))
* Possible fix for pnpm ci errors ([e8f0b56](e8f0b5665161e77bcc961ec0dc57c5b127b93a1f))
* NewLabel script setup (#2414) ([7f581cb](7f581cbe2780633fdfa03609824182fe93fe77e3))
* Possible fix for pnpm ci errors (#2413) ([bc83309](bc833091f2b919177ce75815b562818c93ea2884))
* Feat NewNamespace script setup (#2415) ([63f2e6b](63f2e6ba6f22502becf61aa89c729fa9d01cdc7b))
* ListList script setup (#2441) ([bbf4ef4](bbf4ef4697fc6338ad603e2491fe4aed61057cd8))
* Move auth to stores ([f30c964](f30c964c06987f87b615c3eec25197241175db96))
* Port auth store to pinia ([7b53e68](7b53e684aa405a7874f189dcb404c031dfed1388))
* Auth store type improvements ([176ad56](176ad565cc64e2212eedb1601c844e458d7e4bb6))
* Improve api-config (#2444) ([8f25f5d](8f25f5d353064f383e97bbc524ce6e00ba559d0f))
* Convert model methods to named functions ([8e3f54a](8e3f54ae42c21fdae62225892ad340877651df27))
* Migrate auth store to pina (#2398) ([9856fab](9856fab38f62f82a42d5cb3b69b232eb319b8050))
* Move tasks to stores ([1fdda07](1fdda07f650702b7e3943e0afc7532367ee20100))
* Port tasks store to pinia ([34ffd1d](34ffd1d5729341bdede217387a4a4c490d7d60d8))
* Move kanban to stores ([9f26ae1](9f26ae1ee6241b2ef529f01d3511380c9d7a4576))
* Port kanban store to pinia ([c35810f](c35810f28fc5aacefabad7526b0ac4e982d53cc7))
* Port tasks store to pina (#2409) ([8c394d8](8c394d8024a825b961e825543453d188c28fa370))
* Automatically create subtask relations based on indention ([cc378b8](cc378b83fee2b326610cdda1997cc5236f947fbf))
* Automatically create subtask relations based on indention (#2443) ([ec227a6](ec227a6872ababb612cb0b7e68ca0c20676117c1))
* Migrate kanban store to pina (#2411) ([d1d7cd5](d1d7cd535ed992fc0a8be8afaf13250ac9b61132))
* Move base store to stores ([df74f9d](df74f9d80cdd44315a29189ecb2f236482cb70f5))
* Port base store to pinia ([7f281fc](7f281fc5e98c5eb83f926100c7f79ee374c5a784))
* Rework loading state of stores ([1d7f857](1d7f857070651f676bbb5bd7e6d79c7fed56be5f))
* TaskDetail as script setup (#1792) ([2dc36c0](2dc36c032bad93654fbd64a68682685870972feb))
* Add github issue template ([9400637](940063784b3ec129e99fe18c4eb2b205ffb15163))
* Login script setup (#2417) ([63fb8a1](63fb8a1962f9ecd8c9a079e2770b4658c5559d84))
* Datepicker script setup (#2456) ([ff1968a](ff1968aa36254d788d0d80ba2d156ce66f4a9df8))
* Multiselect script setup (#2458) ([0620b8f](0620b8f0b308e358526bed0d82322ffb9c0627cf))
* ColorPicker script setup (#2457) ([b08dd58](b08dd58552edb763f007f355f5c0d36d6dccbd05))
* Migrate kanban card to script setup ([a5925ba](a5925baff03ac2809b7c601b45b93363b6188083))
* Migrate kanban card to script setup (#2459) ([3e21a8e](3e21a8ed6ee74d85628feedd8855c817af8de538))
* Add nix flake for dev shell ([12215c0](12215c043d45d2f2294e65671587a923997e6f6f))
* Fancycheckbox script setup (#2462) ([06c1a54](06c1a548867e37a74a8493bd44fef728e10c658b))
* Editor script setup ([db627ed](db627ed28af8432e6971ad08864d11e56d3512c6))
* Use floating-ui (#2482) ([f360ebf](f360ebfe9854aeae9cb426c67b1bb48aa74a9c08))
* Update eslint config ([4655e1c](4655e1ce34223337c953ebbe52f94ef811034e6b))
* Feature/update-eslint-config (#2484) ([6f2dedc](6f2dedcb488ec6a38182e85e702ec880263ecbd3))
* Move composables in separate files (#2485) ([c206fc6](c206fc6f3462be2e0ebc0bd16d96b3c0099fdda1))
* Add display of kanban card attachment image ([3d88fda](3d88fdaaddca15b98efa938f0b2813420d56ad84))
* Promote an attachment to task cover image ([877e425](877e4250554b31db2d57f44a7443c5d04c783e59))
* Add indicator if an attachment is task cover ([f01107f](f01107fd737e2205bf60498b3d2954a251c3d9d4))
* Show done tasks as strikethrough when searching for new tasks to relate ([74a9b9a](74a9b9ab1b31740fe84a7dddd91a04995c1eb58d))
* Allow users to leave a team they're in ([feeaca2](feeaca2c02fb233c35a81f786acd5cbdf5c5d21d))
* Add TickTick migrator support ([1af4f78](1af4f7811a63826c4aa4740a55f606757e22c7ae))
* Make salutation i18n static ([c20de51](c20de51a3c98792580c0a2f2751648582ac5ac0c))
* Get username from store getter ([c4d7f6f](c4d7f6fdfa18c221597b28198d5fa432b1e934dc))
* Use getter and helper in other components as well ([9de20b4](9de20b4c54d192a20f9135388de9fa13121ed322))
* Make salutation i18n static (#2546) ([29f6874](29f68747bbd7da50d37ae3238b6b19782ec8022b))
* Refactor password reset to use a single password field ([4ed665f](4ed665fbd9dc4db1ecb6afc1a75d1818c3518186))
* Rename useTaskList ([7ce8802](7ce880239ec3ce16313d93bfefa657c499bbfb29))
* Add basic implementation of ganttastic ([2b0df8c](2b0df8c2375ec5f9afe43207807e999bcc693d21))
* Allow passing props down to the gantt component ([49a2497](49a24977f96cff1e90e706321505ae43bf7efadf))
* Only load tasks which start in the currently selected range ([ed241d2](ed241d21bea91795a10cdc1af92561d435c9eedc))
* Dynamically set default date ([736e5a8](736e5a8bf55ccf7cbed23fd3af48122c459bcdc6))
* Dynamically set default date ([3b48ada](3b48adad675b0b20dc91a08f8ebbfe1dd1c3806b))
* Create new tasks ([ef46893](ef4689335b3e738b7e1338657e9dcd69c82fbcb9))
* Add open task detail when double clicking ([d2c4092](d2c40926ded479db92d0f3b77d2ece5842bcacbb))
* Scroll ([c8eac91](c8eac914d10a09453afb70d35c6d16faac9cd00c))
* Styling ([80c151c](80c151ca6c4a76a5f912505672eee471f77a3bba))
* Update task in gantt bar after dragging to make sure it changes its color ([ebd824b](ebd824bddf8d37a66d2dbf7f330b39c8849db9b2))
* Show done tasks strikethrough ([3eacc07](3eacc0754ff50fed2d5a50198480c5c8d697f6ce))
* Handle changing props ([29dcc02](29dcc02217dfe9d52b3cdd6166ca82cc8be1022e))
* Loading animation ([8c62a9e](8c62a9e198fb5b8221a13747e9510f5036ed3095))
* Create task when pressing the button ([0a9588e](0a9588e09730e83ddc61630012e62c0530a9997d))
* Increase the default date range ([5f7159e](5f7159ebc49e73bc4757c7cefa9a10ed14d65b46))
* Only use one watcher ([64fdae8](64fdae81ec8a1b807a1b1788a6954c8d7850dc36))
* Review changes ([f21a4e1](f21a4e1e9f558e999e1f6638847aeab4d73b9636))
* Update ganttastic version ([2f820e5](2f820e517f6dea384440a9574da4f82c02c86143))
* Improve types ([3b244df](3b244dfdbecf2f1feaa766b5c9e52c7e66dfe52a))
* Working route sync ([acdbf2f](acdbf2f8f5b8e28e923d7598696dadec373c7a67))
* Working gantt-chart ([eaf7778](eaf777864ac857275bc657bf39f1886460d307d2))
* Abstract to useGanttFilter / and useRouteFilter ([2c732eb](2c732eb0d55c9161b8d47cbc850421136994bff4))
* Simplify ListGantt styles ([c7dd20e](c7dd20ef57f037db0ac8bbdc583463ae98ffe9ac))
* Move useGanttTaskList in separate file ([7f4114b](7f4114b7032c24d9305c7c731ad1fef2f9390dcd))
* Remove gantt-chart wrapper ([aefda38](aefda38bdd8fa5f5b4f4d2c7486566f669dd6929))
* Use PascalCase for component name ([acb3ddc](acb3ddc73fd7a8240d42774c80c68b5a725c3734))
* Use ref for filters ([51dc123](51dc123d893517a30c2dbb26a68e877b493ec95e))
* Use plural for filters consequently ([6bf6357](6bf6357cbd281fa5b99b7aae9845fee90c758ae7))
* Move config preparation in separate function ([e74e6fc](e74e6fcc996cced93f040782ac278db6baea975e))
* Align with vue-flatpickr-component 10 ([874dc1e](874dc1e5fc9f76ad3d45f555b9d04585cd9a2704))
* Replace our home-grown gantt implementation with ganttastic (#2180) ([fd3e7e6](fd3e7e655dbbd59f9a94db0f18a3ef4876cec059))
* Improve useTaskList (#2582) ([d5258b7](d5258b73153a477a82c750482a6fd504c5823b7a))
* Unify savedFilter logic in service (#2491) ([9807858](9807858436e4b7d6de8dcb71b2a03a55ed8a7d52))
* Quick-actions script setup (#2478) ([386fd79](386fd79b4983b9d472d46219fc60c1a1a2cc1012))
### Miscellaneous Tasks
* *(ci)* Sign drone config
* *(ci)* Sign drone config
* *(gantt)* Wip daterange
* *(gantt)* Upgrade packages
* *(gantt)* Upgrade packages
* *(gantt)* Pnpm install after merge
* *(i18n)* Use global scope
* *(task)* Move cover image setter to store* Improve type imports ([af630d3](af630d3b8c1536c1a9a320172aaf19e000bb2517))
* Remove date mixins ([b0ee316](b0ee316a262ca71b9cfecbaaeccab7f9465ec09d))
* Remove global mixing ([4a247b2](4a247b2a7d6741bfec9fbdb387c9313d7b6381d1))
* Remove unnecessary defineComponent ([6f93d63](6f93d6343c1c518fec3591b83b999efcbccf9607))
* Better variable typing ([42e72d1](42e72d14a4a804aa38908cc2a9d6b4cb120c988a))
* Align docker cypress image version with drone ([2445f0e](2445f0eec8b130d8d71e5fc399a399c0d1cf6836))
* Minor fixes ([49f3b92](49f3b928cbc16031cf65fa3ed1cc908968e1083b))
* Automerge renovate dev dependency updates ([d822709](d822709991ee4dc52ee8aa56a03248c6e4a3a709))
* Rearrange non-dev dependencies ([b8d77a6](b8d77a617b0b205fbf8553d3ce060547c96f0f22))
* Remove &nbsp; ([d91d1fe](d91d1fecf1b34734ef8af21c3c34bdaaa6d53e09))
* Remove unused id ([5f678e2](5f678e2449529758cd6ade233c52a0c091889fd9))
* Set more expressive variable names for available views dropdowns ([7e7fa80](7e7fa807fd1c6a34c5236cca4fb20141ca9d0454))
* Improve types ([6d9c4a7](6d9c4a7aa083425e252b96729b57c16ab13fd295))
* Don't cache node_modules ([b542221](b542221dac6a14cd84aab446ceab0888bc98bb38))
* Don't use node alpine image ([6624db1](6624db1d49545524083d124698fa5b6e02bbfb0c))
* Use node alpine image ([dfb3561](dfb3561310bec49043a630136a2d51cc80184cc1))
* Optimise loading order (#2435) ([ca899d3](ca899d3b5172be6f39a60bdaffab58330225ecd9))
* Make const out of export download file name (#2436) ([878c6ea](878c6ea9e17527b3f199f4acf10588e910b5727c))
* Spread title ([3970d0f](3970d0fd315488427df0c4a37447eb52dca322b4))
* Use better variable names ([8ce242b](8ce242bb6595ef12442a6ba0fb37eb66c65dd71b))
* Break earlier if index === 0 ([d58f8b4](d58f8b4ba1d873abb0fc8dc4c2cec64a33b55ab8))
* Use jsDoc to explain param ([5bd7c77](5bd7c77b68f08ab4771f3d80d5191def9d634204))
* Small review adjustments ([af7f840](af7f8400e901c2f4d9c5c4cca7614af62892a75e))
* Remove unneeded this from PasswordReset.vue (#2473) ([c232170](c2321703a767395b77523d4551ea508396b7cae8))
* Remove IE edge fallback (#2477) ([3248dcd](3248dcd6636627548f2df869900a7943c0dde0ba))
* Add line-wrap ([eb80bfa](eb80bfa00de891ee12643d664e8610d1f3bc851f))
* Better wording for cover set button ([a773137](a7731370a0bcdd8a393036a617dd1953cd39f5df))
* Update happy-dom less frequently ([458df80](458df8044306642e5da813ff8341bed07f67f26a))
* Move helper function outside of composable ([aa2278a](aa2278a56411dc8045fa468b090755cf5d899d09))
* Use flatpickr range instead of two datepickers ([c289a6a](c289a6ae18fd5936b789270cc72408374a790edc))
* Use width property ([7a7a1c9](7a7a1c985e0feb8de62ddbdd54f36f2a09a9d765))
* Remove old component and dependencies ([6cb331e](6cb331ee0f26dffcbf700426da17acb6159aea3e))
* Use Loading component ([766b4c6](766b4c669ff52f6d6c888727e62142eaa90de54d))
* Use @/models ([d3925b8](d3925b8d80e16e25e9b82d057fb47ed9f41f61a0))
* Uppercase const ([98d0398](98d0398ca840d8d8077f850c8ca4e65784373b61))
* Don't set required if there's a default value ([ed5d3be](ed5d3be7cba7992eb18a3ed1844c085cf88b3bdd))
* Define types ([56a2573](56a25734d7557663e2ba43ba41f4922f0b10ed8b))
* Don't use for..in ([6975a2b](6975a2b286628294b8909bce3d43334cc383d987))
* Add types for template ref ([4be0977](4be097701449b74bbeb7218b539db65961539591))
* Don't use ref when not nessecary ([fd9d0ad](fd9d0ad1553756414696315508bc2d8928f63d9d))
* Update lockfile ([957d8f0](957d8f05a5e9548138f8dce192513928deb02669))
* Better naming for input ([df02dd5](df02dd529181e9701ce586dba9025c83eeaf48d8))
* Clean up ([2acb70c](2acb70c56257202fe7d136b36ceaaa2fe122491e))
* Pnpm install after merge ([26e522c](26e522cf8c302f5d63b26134e5fa37bed5c808ef))
* Use vue-ganttastic release ([6c61907](6c619072b4863328c24588bb08a9543806942be1))
* Don't pass other params to ListGantt than route ([cf0eaf9](cf0eaf9ba1816b610ba1cbc9b4a6c661f00f61a5))
* Refactor parseTimeLabel to own function ([443e1a0](443e1a063dfff3cbb82a9f625e05bf7e2b606cbe))
* Add git-cliff to flake ([b817720](b817720907b0c4bb848e9624e3fdf71437ba0bde))
### Other
* *(other)* [skip ci] Updated translations via Crowdin
## [0.19.1] - 2022-08-17
### Bug Fixes

View File

@ -1,5 +1,5 @@
# Stage 1: Build application
FROM node:18-alpine AS compile-image
FROM --platform=$BUILDPLATFORM node:18-alpine AS compile-image
WORKDIR /build
@ -29,7 +29,7 @@ RUN \
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
COPY run.sh /run.sh
COPY scripts/run.sh /run.sh
# copy compiled files from stage 1
COPY --from=compile-image /build/dist /usr/share/nginx/html

View File

@ -4,7 +4,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.19.1-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.20.1-brightgreen.svg)](https://dl.vikunja.io)
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
This is the web frontend for Vikunja, written in Vue.js.

View File

@ -45,7 +45,7 @@ describe('List History', () => {
cy.get('body')
.should('contain', 'Last viewed')
cy.get('.list-cards-wrapper-2-rows')
cy.get('[data-cy="listCardGrid"]')
.should('not.contain', lists[0].title)
.should('contain', lists[1].title)
.should('contain', lists[2].title)

View File

@ -11,7 +11,7 @@ describe('List View Gantt', () => {
const tasks = TaskFactory.create(1)
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart .tasks')
cy.get('.g-gantt-rows-container')
.should('not.contain', tasks[0].title)
})
@ -25,7 +25,7 @@ describe('List View Gantt', () => {
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart .months')
cy.get('.g-timeunits-container')
.should('contain', format(now, 'MMMM'))
.should('contain', format(nextMonth, 'MMMM'))
})
@ -38,14 +38,13 @@ describe('List View Gantt', () => {
})
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart .tasks')
cy.get('.g-gantt-rows-container')
.should('not.be.empty')
cy.get('.gantt-chart .tasks')
.should('contain', tasks[0].title)
})
it('Shows tasks with no dates after enabling them', () => {
TaskFactory.create(1, {
const tasks = TaskFactory.create(1, {
start_date: null,
end_date: null,
})
@ -55,13 +54,15 @@ describe('List View Gantt', () => {
.contains('Show tasks which don\'t have dates set')
.click()
cy.get('.gantt-chart .tasks')
cy.get('.g-gantt-rows-container')
.should('not.be.empty')
cy.get('.gantt-chart .tasks .task.nodate')
.should('exist')
.should('contain', tasks[0].title)
})
it('Drags a task around', () => {
cy.intercept('**/api/v1/tasks/*')
.as('taskUpdate')
const now = new Date()
TaskFactory.create(1, {
start_date: formatISO(now),
@ -69,10 +70,56 @@ describe('List View Gantt', () => {
})
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart .tasks .task')
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
.first()
.trigger('mousedown', {which: 1})
.trigger('mousemove', {clientX: 500, clientY: 0})
.trigger('mouseup', {force: true})
cy.wait('@taskUpdate')
})
it('Should change the query parameters when selecting a date range', () => {
const now = Date.UTC(2022, 10, 9)
cy.clock(now, ['Date'])
cy.visit('/lists/1/gantt')
cy.get('.list-gantt .gantt-options .field .control input.input.form-control')
.click()
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
.first()
.click()
cy.get('.flatpickr-calendar .flatpickr-innerContainer .dayContainer .flatpickr-day')
.last()
.click()
cy.url().should('contain', 'dateFrom=2022-09-25')
cy.url().should('contain', 'dateTo=2022-11-05')
})
it('Should change the date range based on date query parameters', () => {
cy.visit('/lists/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
cy.get('.g-timeunits-container')
.should('contain', 'September 2022')
.should('contain', 'October 2022')
.should('contain', 'November 2022')
cy.get('.list-gantt .gantt-options .field .control input.input.form-control')
.should('have.value', '25 Sep 2022 to 5 Nov 2022')
})
it('Should open a task when double clicked on it', () => {
const now = new Date()
const tasks = TaskFactory.create(1, {
start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)),
})
cy.visit('/lists/1/gantt')
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
.dblclick()
cy.url()
.should('contain', `/tasks/${tasks[0].id}`)
})
})

View File

@ -193,4 +193,48 @@ describe('List View Kanban', () => {
cy.get('.kanban .bucket')
.should('not.contain', task.title)
})
it('Shows a button to filter the kanban board', () => {
const data = TaskFactory.create(10, {
list_id: 1,
bucket_id: 1,
})
cy.visit('/lists/1/kanban')
cy.get('.list-kanban .filter-container .base-button')
.should('exist')
})
it('Should remove a task from the board when deleting it', () => {
const lists = ListFactory.create(1)
const buckets = BucketFactory.create(2, {
list_id: lists[0].id,
})
const tasks = TaskFactory.create(5, {
list_id: 1,
bucket_id: buckets[0].id,
})
const task = tasks[0]
cy.visit('/lists/1/kanban')
cy.getSettled('.kanban .bucket .tasks .task')
.contains(task.title)
.should('be.visible')
.click()
cy.get('.task-view .action-buttons .button')
.should('be.visible')
.contains('Delete')
.click()
cy.get('.modal-mask .modal-container .modal-content .header')
.should('contain', 'Delete this task')
cy.get('.modal-mask .modal-container .modal-content .actions .button')
.contains('Do it!')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.getSettled('.kanban .bucket .tasks')
.should('not.contain', task.title)
})
})

View File

@ -78,7 +78,7 @@ describe('List View List', () => {
cy.get('.menu-list li .list-menu-link .color-bubble')
.should('have.css', 'background-color', 'rgb(0, 219, 96)')
cy.get('.tasks-container .tasks .color-bubble')
cy.get('.tasks .color-bubble')
.should('not.exist')
})
@ -90,9 +90,9 @@ describe('List View List', () => {
})
cy.visit('/lists/1/list')
cy.get('.tasks-container .tasks')
cy.get('.tasks')
.should('contain', tasks[1].title)
cy.get('.tasks-container .tasks')
cy.get('.tasks')
.should('not.contain', tasks[99].title)
cy.get('.card-content .pagination .pagination-link')
@ -101,9 +101,9 @@ describe('List View List', () => {
cy.url()
.should('contain', '?page=2')
cy.get('.tasks-container .tasks')
cy.get('.tasks')
.should('contain', tasks[99].title)
cy.get('.tasks-container .tasks')
cy.get('.tasks')
.should('not.contain', tasks[1].title)
})
})

View File

@ -52,7 +52,7 @@ describe('Lists', () => {
cy.get('.list-title h1')
.should('contain', 'First List')
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
.click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
.contains('Edit')
@ -80,7 +80,7 @@ describe('Lists', () => {
it('Should remove a list', () => {
cy.visit(`/lists/${lists[0].id}`)
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-trigger')
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
.click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content')
.contains('Delete')

View File

@ -128,4 +128,24 @@ describe('Home Page Task Overview', () => {
.last()
.should('contain.text', newTaskTitle)
})
it('Should show the cta buttons for new list when there are no tasks', () => {
TaskFactory.truncate()
cy.visit('/')
cy.get('.home.app-content .content')
.should('contain.text', 'You can create a new list for your new tasks:')
.should('contain.text', 'Or import your lists and tasks from other services into Vikunja:')
})
it('Should not show the cta buttons for new list when there are tasks', () => {
seedTasks()
cy.visit('/')
cy.get('.home.app-content .content')
.should('not.contain.text', 'You can create a new list for your new tasks:')
.should('not.contain.text', 'Or import your lists and tasks from other services into Vikunja:')
})
})

View File

@ -546,5 +546,35 @@ describe('Task', () => {
cy.get('.bucket .task .footer .icon svg.fa-paperclip')
.should('exist')
})
it('Can check items off a checklist', () => {
const tasks = TaskFactory.create(1, {
id: 1,
description: `
This is a checklist:
* [ ] one item
* [ ] another item
* [ ] third item
* [ ] fourth item
* [x] and this one is already done
`,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .checklist-summary')
.should('contain.text', '1 of 5 tasks')
cy.get('.editor .content ul > li input[type=checkbox]')
.eq(2)
.click()
cy.get('.editor .content ul > li input[type=checkbox]')
.eq(2)
.should('be.checked')
cy.get('.editor .content input[type=checkbox]')
.should('have.length', 5)
cy.get('.task-view .checklist-summary')
.should('contain.text', '2 of 5 tasks')
})
})
})

View File

@ -55,4 +55,9 @@ context('Login', () => {
testAndAssertFailed(fixture)
})
it('Should redirect to /login when no user is logged in', () => {
cy.visit('/')
cy.url().should('include', '/login')
})
})

View File

@ -1,17 +0,0 @@
image: vikunja/frontend:unstable
manifests:
-
image: vikunja/frontend:unstable-linux-amd64
platform:
architecture: amd64
os: linux
-
image: vikunja/frontend:unstable-linux-arm64
platform:
architecture: arm64
os: linux
-
image: vikunja/frontend:unstable-linux-arm
platform:
architecture: arm
os: linux

View File

@ -1,23 +0,0 @@
image: vikunja/frontend:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
{{#if build.tags}}
tags:
{{#each build.tags}}
- {{this}}
{{/each}}
{{/if}}
manifests:
-
image: vikunja/frontend:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
platform:
architecture: amd64
os: linux
-
image: vikunja/frontend:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
platform:
architecture: arm64
os: linux
-
image: vikunja/frontend:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
platform:
architecture: arm
os: linux

View File

@ -5,6 +5,6 @@
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
defaultPackage.x86_64-linux =
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress ]; };
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress pkgs.git-cliff ]; };
};
}

View File

@ -18,97 +18,97 @@
"browserslist:update": "npx browserslist@latest --update-db"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.2.0",
"@fortawesome/free-regular-svg-icons": "6.2.0",
"@fortawesome/free-solid-svg-icons": "6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@fortawesome/fontawesome-svg-core": "6.2.1",
"@fortawesome/free-regular-svg-icons": "6.2.1",
"@fortawesome/free-solid-svg-icons": "6.2.1",
"@fortawesome/vue-fontawesome": "3.0.2",
"@github/hotkey": "2.0.1",
"@kyvg/vue3-notification": "2.4.1",
"@sentry/tracing": "7.15.0",
"@sentry/vue": "7.15.0",
"@infectoone/vue-ganttastic": "2.1.3",
"@intlify/unplugin-vue-i18n": "0.8.0",
"@kyvg/vue3-notification": "2.7.0",
"@sentry/tracing": "7.23.0",
"@sentry/vue": "7.23.0",
"@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.15.0",
"@vueuse/core": "9.3.0",
"@vueuse/router": "9.3.0",
"@vueuse/core": "9.6.0",
"axios": "0.27.2",
"blurhash": "2.0.3",
"blurhash": "2.0.4",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.9",
"codemirror": "5.65.10",
"date-fns": "2.29.3",
"dompurify": "2.4.0",
"dayjs": "1.11.6",
"dompurify": "2.4.1",
"easymde": "2.18.0",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.21",
"floating-vue": "2.0.0-beta.20",
"highlight.js": "11.6.0",
"highlight.js": "11.7.0",
"is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.1.1",
"marked": "4.2.3",
"minimist": "1.2.7",
"pinia": "2.0.23",
"pinia": "2.0.27",
"register-service-worker": "1.7.2",
"snake-case": "3.0.4",
"sortablejs": "1.15.0",
"ufo": "0.8.5",
"vue": "3.2.40",
"ufo": "1.0.1",
"vue": "3.2.45",
"vue-advanced-cropper": "2.8.6",
"vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.8",
"vue-flatpickr-component": "11.0.1",
"vue-i18n": "9.2.2",
"vue-router": "4.1.5",
"vue-router": "4.1.6",
"workbox-precaching": "6.5.4",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.1",
"@cypress/vite-dev-server": "3.3.1",
"@cypress/vue": "4.2.0",
"@faker-js/faker": "7.5.0",
"@4tw/cypress-drag-drop": "2.2.2",
"@cypress/vite-dev-server": "5.0.0",
"@cypress/vue": "5.0.3",
"@faker-js/faker": "7.6.0",
"@rushstack/eslint-patch": "1.2.0",
"@types/dompurify": "2.3.4",
"@types/codemirror": "5.60.5",
"@types/dompurify": "2.4.0",
"@types/flexsearch": "0.7.3",
"@types/lodash.debounce": "4.0.7",
"@types/marked": "4.0.7",
"@types/node": "16.11.65",
"@typescript-eslint/eslint-plugin": "5.40.0",
"@typescript-eslint/parser": "5.40.0",
"@vitejs/plugin-legacy": "2.2.0",
"@vitejs/plugin-vue": "3.1.2",
"@types/node": "18.11.10",
"@types/postcss-preset-env": "7.7.0",
"@typescript-eslint/eslint-plugin": "5.45.0",
"@typescript-eslint/parser": "5.45.0",
"@vitejs/plugin-legacy": "2.3.1",
"@vitejs/plugin-vue": "3.2.0",
"@vue/eslint-config-typescript": "11.0.2",
"@vue/test-utils": "2.1.0",
"@vue/test-utils": "2.2.6",
"@vue/tsconfig": "0.1.3",
"autoprefixer": "10.4.12",
"autoprefixer": "10.4.13",
"browserslist": "4.21.4",
"caniuse-lite": "1.0.30001418",
"cypress": "10.10.0",
"esbuild": "0.15.10",
"eslint": "8.25.0",
"eslint-plugin-vue": "9.6.0",
"caniuse-lite": "1.0.30001436",
"csstype": "3.1.1",
"cypress": "11.2.0",
"esbuild": "0.15.18",
"eslint": "8.29.0",
"eslint-plugin-vue": "9.8.0",
"express": "4.18.2",
"happy-dom": "7.4.0",
"netlify-cli": "12.0.7",
"postcss": "8.4.17",
"postcss-preset-env": "7.8.2",
"rollup": "3.0.0",
"rollup-plugin-visualizer": "5.8.2",
"sass": "1.55.0",
"typescript": "4.8.4",
"vite": "3.1.7",
"vite-plugin-pwa": "0.13.1",
"happy-dom": "7.7.2",
"netlify-cli": "12.2.8",
"postcss": "8.4.19",
"postcss-preset-env": "7.8.3",
"rollup": "3.5.1",
"rollup-plugin-visualizer": "5.8.3",
"sass": "1.56.1",
"typescript": "4.9.3",
"vite": "3.2.5",
"vite-plugin-pwa": "0.13.3",
"vite-svg-loader": "3.6.0",
"vitest": "0.24.1",
"vue-tsc": "1.0.5",
"vitest": "0.25.3",
"vue-tsc": "1.0.11",
"wait-on": "6.0.1",
"workbox-cli": "6.5.4"
},
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"license": "AGPL-3.0-or-later",
"packageManager": "pnpm@7.13.4"
"packageManager": "pnpm@7.18.0"
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,4 @@
const {exec} = require('child_process')
const axios = require('axios')
const { exec } = require('child_process')
const BOT_USER_ID = 513
const giteaToken = process.env.GITEA_TOKEN
@ -35,7 +34,7 @@ const promiseExec = cmd => {
stdout = await promiseExec(`./node_modules/.bin/netlify deploy --alias ${alias}`)
console.log(stdout)
const {data} = await axios.get(prIssueCommentsUrl)
const data = await fetch(prIssueCommentsUrl).then(response => response.json())
const hasComment = data.some(c => c.user.id === BOT_USER_ID)
if (hasComment) {
@ -43,8 +42,7 @@ const promiseExec = cmd => {
return
}
await axios.post(prIssueCommentsUrl, {
body: `
const message = `
Hi ${process.env.DRONE_COMMIT_AUTHOR}!
Thank you for creating a PR!
@ -57,14 +55,25 @@ You will need to manually connect this to an api running somehwere. The easiest
Have a nice day!
> Beep boop, I'm a bot.
`,
}, {
headers: {
'Content-Type': 'application/json',
'accept': 'application/json',
'Authorization': `token ${giteaToken}`,
},
})
`
console.log(`Preview comment sent successfully to PR #${prNumber}!`)
try {
const response = await fetch(prIssueCommentsUrl, {
method: 'POST',
body: JSON.stringify({
body: message,
}),
headers: {
'Content-Type': 'application/json',
'accept': 'application/json',
'Authorization': `token ${giteaToken}`,
},
})
if (!response.ok) {
throw new Error(`HTTP error, status = ${response.status}`)
}
console.log(`Preview comment sent successfully to PR #${prNumber}!`)
} catch (e) {
console.log(`Could not send preview comment to PR #${prNumber}! ${e.message}`)
}
})()

View File

@ -1 +1 @@
bb46342a0a08105b340ba7976cff9d80ef89901120ec0639669caa70bb7d2dbc43e78b1f635a7654ab2456e8358c98a4 ./scripts/deploy-preview-netlify.js
05c69e5323a4d4bac041ade830735becd52c230277396d1f72be8fde83683a75dc095f6678804083b2ca66f27cc7995f ./scripts/deploy-preview-netlify.js

View File

@ -15,9 +15,8 @@
</template>
<script lang="ts" setup>
import {computed, watch, type Ref} from 'vue'
import {useRouter} from 'vue-router'
import {useRouteQuery} from '@vueuse/router'
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'
@ -41,6 +40,7 @@ import {useAuthStore} from './stores/auth'
const baseStore = useBaseStore()
const authStore = useAuthStore()
const router = useRouter()
const route = useRoute()
useBodyClass('is-touch', isTouchDevice())
const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
@ -51,9 +51,9 @@ const authLinkShare = computed(() => authStore.authLinkShare)
const {t} = useI18n({useScope: 'global'})
// setup account deletion verification
const accountDeletionConfirm = useRouteQuery('accountDeletionConfirm') as Ref<null | string>
const accountDeletionConfirm = computed(() => route.query?.accountDeletionConfirm as (string | undefined))
watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
if (accountDeletionConfirm === null) {
if (accountDeletionConfirm === undefined) {
return
}
@ -64,9 +64,9 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
}, { immediate: true })
// setup password reset redirect
const userPasswordReset = useRouteQuery('userPasswordReset') as Ref<null | string>
const userPasswordReset = computed(() => route.query?.userPasswordReset as (string | undefined))
watch(userPasswordReset, (userPasswordReset) => {
if (userPasswordReset === null) {
if (userPasswordReset === undefined) {
return
}
@ -75,9 +75,9 @@ watch(userPasswordReset, (userPasswordReset) => {
}, { immediate: true })
// setup email verification redirect
const userEmailConfirm = useRouteQuery('userEmailConfirm') as Ref<null | string>
const userEmailConfirm = computed(() => route.query?.userEmailConfirm as (string | undefined))
watch(userEmailConfirm, (userEmailConfirm) => {
if (userEmailConfirm === null) {
if (userEmailConfirm === undefined) {
return
}

View File

@ -1,18 +1,53 @@
<!-- a disabled link of any kind is not a link -->
<!-- we have a router link -->
<!-- just a normal link -->
<!-- a button it shall be -->
<!-- note that we only pass the click listener here -->
<template>
<component
:is="componentNodeName"
<div
v-if="disabled === true && (to !== undefined || href !== undefined)"
class="base-button"
:class="{ 'base-button--type-button': isButton }"
v-bind="elementBindings"
:disabled="disabled || undefined"
:aria-disabled="disabled || undefined"
ref="button"
>
<slot/>
</component>
</div>
<router-link
v-else-if="to !== undefined"
:to="to"
class="base-button"
ref="button"
>
<slot/>
</router-link>
<a v-else-if="href !== undefined"
class="base-button"
:href="href"
rel="noreferrer noopener nofollow"
target="_blank"
ref="button"
>
<slot/>
</a>
<button
v-else
:type="type"
class="base-button base-button--type-button"
:disabled="disabled || undefined"
ref="button"
@click="(event: MouseEvent) => emit('click', event)"
>
<slot/>
</button>
</template>
<script lang="ts">
export default { inheritAttrs: false }
const BASE_BUTTON_TYPES_MAP = {
BUTTON: 'button',
SUBMIT: 'submit',
} as const
export type BaseButtonTypes = typeof BASE_BUTTON_TYPES_MAP[keyof typeof BASE_BUTTON_TYPES_MAP] | undefined
</script>
<script lang="ts" setup>
@ -20,77 +55,36 @@ export default { inheritAttrs: false }
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
// the component tries to heuristically determine what it should be checking the props (see the
// componentNodeName and elementBindings ref for this).
// the component tries to heuristically determine what it should be checking the props
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
import { ref, watchEffect, computed, useAttrs, type PropType } from 'vue'
import {unrefElement} from '@vueuse/core'
import {ref, type HTMLAttributes} from 'vue'
import type {RouteLocationNamedRaw} from 'vue-router'
const BASE_BUTTON_TYPES_MAP = Object.freeze({
button: 'button',
submit: 'submit',
})
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
const props = defineProps({
type: {
type: String as PropType<BaseButtonTypes>,
default: 'button',
},
disabled: {
type: Boolean,
default: false,
},
})
const componentNodeName = ref<Node['nodeName']>('button')
interface ElementBindings {
type?: string;
rel?: string;
target?: string;
export interface BaseButtonProps extends HTMLAttributes {
type?: BaseButtonTypes
disabled?: boolean
to?: RouteLocationNamedRaw
href?: string
}
const elementBindings = ref({})
export interface BaseButtonEmits {
(e: 'click', payload: MouseEvent): void
}
const attrs = useAttrs()
watchEffect(() => {
// by default this component is a button element with the attribute of the type "button" (default prop value)
let nodeName = 'button'
let bindings: ElementBindings = {type: props.type}
const {
type = BASE_BUTTON_TYPES_MAP.BUTTON,
disabled = false,
} = defineProps<BaseButtonProps>()
// if we find a "to" prop we set it as router-link
if ('to' in attrs) {
nodeName = 'router-link'
bindings = {}
}
const emit = defineEmits<BaseButtonEmits>()
// if there is a href we assume the user wants an external link via a link element
// we also set a predefined value for the attribute rel, but make it possible to overwrite this by the user.
if ('href' in attrs) {
nodeName = 'a'
bindings = {
rel: 'noreferrer noopener nofollow',
target: '_blank',
}
}
componentNodeName.value = nodeName
elementBindings.value = {
...bindings,
...attrs,
}
})
const isButton = computed(() => componentNodeName.value === 'button')
const button = ref()
const button = ref<HTMLElement | null>(null)
function focus() {
button.value.focus()
unrefElement(button)?.focus()
}
defineExpose({

View File

@ -110,13 +110,13 @@
</template>
<script lang="ts" setup>
import { formatDate } from '@/helpers/time/formatDate'
import {formatDate} from '@/helpers/time/formatDate'
import BaseButton from '@/components/base/BaseButton.vue'
const exampleDate = formatDate(new Date(), 'yyyy-MM-dd')
</script>
<style scoped>
<style scoped lang="scss">
.how-it-works-modal {
font-size: 1rem;
}

View File

@ -54,8 +54,8 @@
</p>
<modal
@close="() => showHowItWorks = false"
:enabled="showHowItWorks"
@close="() => showHowItWorks = false"
transition-name="fade"
:overflow="true"
variant="hint-modal"

View File

@ -1,16 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useNow } from '@vueuse/core'
import {computed} from 'vue'
import {useNow} from '@vueuse/core'
import LogoFull from '@/assets/logo-full.svg?component'
import LogoFullPride from '@/assets/logo-full-pride.svg?component'
import LogoFull from '@/assets/logo-full.svg?url'
import LogoFullPride from '@/assets/logo-full-pride.svg?url'
const now = useNow()
const Logo = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
const logoUrl = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
</script>
<template>
<Logo alt="Vikunja" class="logo" />
<img alt="Vikunja" :src="logoUrl" class="logo" />
</template>
<style lang="scss" scoped>

View File

@ -44,8 +44,8 @@
variant="secondary"
:shadow="false"
>
<img :src="userAvatar" alt="" class="avatar" width="40" height="40"/>
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
<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>
@ -80,7 +80,7 @@
{{ $t('about.title') }}
</dropdown-item>
<dropdown-item
@click="logout()"
@click="authStore.logout()"
>
{{ $t('user.auth.logout') }}
</dropdown-item>
@ -117,8 +117,6 @@ const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Righ
const menuActive = computed(() => baseStore.menuActive)
const authStore = useAuthStore()
const userInfo = computed(() => authStore.info)
const userAvatar = computed(() => authStore.avatarUrl)
const configStore = useConfigStore()
const imprintUrl = computed(() => configStore.legal.imprintUrl)
@ -136,10 +134,6 @@ onMounted(async () => {
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
})
function logout() {
authStore.logout()
}
function openQuickActions() {
baseStore.setQuickActionsActive(true)
}

View File

@ -39,7 +39,7 @@
</router-view>
<modal
v-if="currentModal"
:enabled="Boolean(currentModal)"
@close="closeModal()"
variant="scrolling"
class="task-detail-view-modal"
@ -154,41 +154,36 @@ labelStore.loadAllLabels()
@media screen and (max-width: $tablet) {
padding-top: $navbar-height;
}
}
.app-content {
z-index: 10;
position: relative;
padding-top: 1rem;
.app-content {
z-index: 10;
position: relative;
padding: 1.5rem 0.5rem 1rem;
@media screen {
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
@media screen and (max-width: $tablet) {
margin-left: 0;
min-height: calc(100vh - 4rem);
}
@media screen and (min-width: $tablet) {
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
}
&.is-menu-enabled {
@media screen and (min-width: $tablet) {
margin-left: $navbar-width;
}
}
// Used to make sure the spinner is always in the middle while loading
> .loader-container {
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem});
}
// Used to make sure the spinner is always in the middle while loading
> .loader-container {
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem});
}
@media screen and (max-width: $tablet) {
margin-left: 0;
padding-top: 1.5rem;
min-height: calc(100vh - 4rem);
}
@media screen {
&.is-menu-enabled {
margin-left: $navbar-width;
@media screen and (max-width: $tablet) {
min-width: 100%;
margin-left: 0;
}
}
}
.card {
background: var(--white);
}
// FIXME: This should be somehow defined inside Card.vue
.card {
background: var(--white);
}
}
@ -235,6 +230,4 @@ labelStore.loadAllLabels()
.content-auth.z-unset {
z-index: unset;
}
@include modal-transition();
</style>

View File

@ -7,7 +7,7 @@
<ul class="menu-list">
<li>
<router-link :to="{ name: 'home'}" v-shortcut="'g o'">
<span class="icon">
<span class="menu-item-icon icon">
<icon icon="calendar"/>
</span>
{{ $t('navigation.overview') }}
@ -15,7 +15,7 @@
</li>
<li>
<router-link :to="{ name: 'tasks.range'}" v-shortcut="'g u'">
<span class="icon">
<span class="menu-item-icon icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
{{ $t('navigation.upcoming') }}
@ -23,7 +23,7 @@
</li>
<li>
<router-link :to="{ name: 'namespaces.index'}" v-shortcut="'g n'">
<span class="icon">
<span class="menu-item-icon icon">
<icon icon="layer-group"/>
</span>
{{ $t('namespace.title') }}
@ -31,7 +31,7 @@
</li>
<li>
<router-link :to="{ name: 'labels.index'}" v-shortcut="'g a'">
<span class="icon">
<span class="menu-item-icon icon">
<icon icon="tags"/>
</span>
{{ $t('label.title') }}
@ -39,7 +39,7 @@
</li>
<li>
<router-link :to="{ name: 'teams.index'}" v-shortcut="'g m'">
<span class="icon">
<span class="menu-item-icon icon">
<icon icon="users"/>
</span>
{{ $t('team.title') }}
@ -63,7 +63,7 @@
/>
<span class="name">{{ namespaceTitles[nk] }}</span>
<div
class="icon is-small toggle-lists-icon pl-2"
class="icon menu-item-icon is-small toggle-lists-icon pl-2"
:class="{'active': typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true}"
>
<icon icon="chevron-down"/>
@ -72,7 +72,7 @@
({{ namespaceListsCount[nk] }})
</span>
</BaseButton>
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
</div>
<!--
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
@ -111,11 +111,11 @@
class="list-menu-link"
:class="{'router-link-exact-active': currentList.id === l.id}"
>
<span class="icon handle">
<span class="icon menu-item-icon handle">
<icon icon="grip-lines"/>
</span>
<ColorBubble
v-if="l.hexColor !== ''"
v-if="l.hexColor !== ''"
:color="l.hexColor"
class="mr-1"
/>
@ -128,7 +128,13 @@
>
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton>
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
<list-settings-dropdown class="menu-list-dropdown" :list="l" v-if="l.id > 0">
<template #trigger="{toggleOpen}">
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</template>
</list-settings-dropdown>
<span class="list-setting-spacer" v-else></span>
</li>
</template>
@ -280,6 +286,18 @@ $vikunja-nav-background: var(--site-background);
$vikunja-nav-color: var(--grey-700);
$vikunja-nav-selected-width: 0.4rem;
.logo {
display: block;
padding-left: 1rem;
margin-right: 1rem;
margin-bottom: 1rem;
@media screen and (min-width: $tablet) {
display: none;
}
}
.namespace-container {
background: $vikunja-nav-background;
color: $vikunja-nav-color;
@ -303,248 +321,226 @@ $vikunja-nav-selected-width: 0.4rem;
transform: translateX(0);
transition: transform $transition-duration ease-out;
}
}
.menu {
.menu-label {
font-size: 1rem;
font-weight: 700;
font-weight: bold;
font-family: $vikunja-font;
color: $vikunja-nav-color;
font-weight: 500;
min-height: 2.5rem;
padding-top: 0;
padding-left: $navbar-padding;
// these are general menu styles
// should be in own components
.menu {
.menu-label,
.menu-list .list-menu-link,
.menu-list a {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
overflow: hidden;
}
.menu-label,
.menu-list .list-menu-link,
.menu-list a {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.list-menu-title {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.color-bubble {
height: 12px;
flex: 0 0 12px;
}
}
.favorite {
margin-left: .25rem;
transition: opacity $transition, color $transition;
opacity: 0;
&:hover,
&.is-favorite {
color: var(--warning);
}
}
.favorite.is-favorite,
.list-menu:hover .favorite {
opacity: 1;
}
.menu-label {
.color-bubble {
width: 14px;
height: 14px;
flex-basis: auto;
}
.is-archived {
min-width: 85px;
}
}
.namespace-title {
display: flex;
align-items: center;
justify-content: space-between;
color: $vikunja-nav-color;
padding: 0 .25rem;
.menu-label {
margin-bottom: 0;
flex: 1 1 auto;
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: auto;
}
.count {
color: var(--grey-500);
margin-right: .5rem;
}
}
:deep(.dropdown-trigger) {
padding: .5rem;
cursor: pointer;
}
.toggle-lists-icon {
svg {
transition: all $transition;
transform: rotate(90deg);
opacity: 1;
}
&.active svg {
transform: rotate(0deg);
opacity: 0;
}
}
&:hover .toggle-lists-icon svg {
opacity: 1;
}
&:not(.has-menu) .toggle-lists-icon {
padding-right: 1rem;
}
}
.menu-label,
.nsettings,
.menu-list .list-menu-link,
.menu-list a {
color: $vikunja-nav-color;
}
.menu-list {
li {
height: 44px;
display: flex;
align-items: center;
&:hover {
background: var(--white);
}
:deep(.dropdown-trigger) {
opacity: 0;
padding: .5rem;
cursor: pointer;
transition: $transition;
}
&:hover :deep(.dropdown-trigger) {
opacity: 1;
}
}
.flip-list-move {
transition: transform $transition-duration;
}
.ghost {
background: var(--grey-200);
* {
opacity: 0;
}
}
a:hover {
background: transparent;
}
.list-menu-link, li > a {
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
transition: all 0.2s ease;
border-radius: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
border-left: $vikunja-nav-selected-width solid transparent;
.icon {
height: 1rem;
vertical-align: middle;
padding-right: 0.5rem;
&.handle {
opacity: 0;
transition: opacity $transition;
margin-right: .25rem;
cursor: grab;
}
}
&:hover .icon.handle {
opacity: 1;
}
&.router-link-exact-active {
color: var(--primary);
border-left: $vikunja-nav-selected-width solid var(--primary);
.icon {
color: var(--primary);
}
}
&:hover {
border-left: $vikunja-nav-selected-width solid var(--primary);
}
}
}
.logo {
display: block;
padding-left: 1rem;
margin-right: 1rem;
margin-bottom: 1rem;
@media screen and (min-width: $tablet) {
display: none;
}
}
&.namespaces-lists {
padding-top: math.div($navbar-padding, 2);
}
.icon {
color: var(--grey-400) !important;
.color-bubble {
height: 12px;
flex: 0 0 12px;
}
}
.top-menu {
margin-top: math.div($navbar-padding, 2);
.menu-list {
li {
height: 44px;
display: flex;
align-items: center;
.menu-list {
li {
font-weight: 500;
font-family: $vikunja-font;
&:hover {
background: var(--white);
}
.list-menu-link, li > a {
padding-left: 2rem;
display: inline-block;
.menu-list-dropdown {
opacity: 0;
transition: $transition;
}
.icon {
padding-bottom: .25rem;
}
&:hover .menu-list-dropdown {
opacity: 1;
}
}
.menu-item-icon {
color: var(--grey-400);
}
.menu-list-dropdown-trigger {
display: flex;
padding: 0.5rem;
}
.flip-list-move {
transition: transform $transition-duration;
}
.ghost {
background: var(--grey-200);
* {
opacity: 0;
}
}
a:hover {
background: transparent;
}
.list-menu-link,
li > a {
color: $vikunja-nav-color;
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
transition: all 0.2s ease;
border-radius: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
border-left: $vikunja-nav-selected-width solid transparent;
&:hover {
border-left: $vikunja-nav-selected-width solid var(--primary);
}
&.router-link-exact-active {
color: var(--primary);
border-left: $vikunja-nav-selected-width solid var(--primary);
}
.icon {
height: 1rem;
vertical-align: middle;
padding-right: 0.5rem;
}
&.router-link-exact-active .icon:not(.handle) {
color: var(--primary);
}
.handle {
opacity: 0;
transition: opacity $transition;
margin-right: .25rem;
cursor: grab;
}
&:hover .handle {
opacity: 1;
}
}
}
}
.top-menu {
margin-top: math.div($navbar-padding, 2);
.menu-list {
li {
font-weight: 500;
font-family: $vikunja-font;
}
.list-menu-link,
li > a {
padding-left: 2rem;
display: inline-block;
.icon {
padding-bottom: .25rem;
}
}
}
}
.namespaces-lists {
padding-top: math.div($navbar-padding, 2);
.menu-label {
font-size: 1rem;
font-weight: 700;
font-weight: bold;
font-family: $vikunja-font;
color: $vikunja-nav-color;
font-weight: 500;
min-height: 2.5rem;
padding-top: 0;
padding-left: $navbar-padding;
overflow: hidden;
margin-bottom: 0;
flex: 1 1 auto;
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: auto;
}
.count {
color: var(--grey-500);
margin-right: .5rem;
}
}
.favorite {
margin-left: .25rem;
transition: opacity $transition, color $transition;
opacity: 0;
&:hover,
&.is-favorite {
color: var(--warning);
}
}
.favorite.is-favorite,
.list-menu:hover .favorite {
opacity: 1;
}
.list-menu-title {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.color-bubble {
width: 14px;
height: 14px;
flex-basis: auto;
}
.is-archived {
min-width: 85px;
}
}
.namespace-title {
display: flex;
align-items: center;
justify-content: space-between;
color: $vikunja-nav-color;
padding: 0 .25rem;
.toggle-lists-icon {
svg {
transition: all $transition;
transform: rotate(90deg);
opacity: 1;
}
&.active svg {
transform: rotate(0deg);
opacity: 0;
}
}
&:hover .toggle-lists-icon svg {
opacity: 1;
}
&:not(.has-menu) .toggle-lists-icon {
padding-right: 1rem;
}
}

View File

@ -26,7 +26,7 @@ if (navigator && navigator.serviceWorker) {
)
}
function showRefreshUI(e) {
function showRefreshUI(e: Event) {
console.log('recieved refresh event', e)
registration.value = e.detail
updateAvailable.value = true

View File

@ -1,12 +1,3 @@
import { defineAsyncComponent } from 'vue'
import ErrorComponent from '@/components/misc/error.vue'
import LoadingComponent from '@/components/misc/loading.vue'
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
const Editor = () => import('@/components/input/editor.vue')
export default defineAsyncComponent({
loader: Editor,
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
timeout: 60000,
})
export default createAsyncComponent(() => import('@/components/input/editor.vue'))

View File

@ -0,0 +1,63 @@
<template>
<multiselect
v-model="selectedLists"
:search-results="foundLists"
:loading="listService.loading"
:multiple="true"
:placeholder="$t('list.search')"
label="title"
@search="findLists"
/>
</template>
<script setup lang="ts">
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
import Multiselect from '@/components/input/multiselect.vue'
import type {IList} from '@/modelTypes/IList'
import ListService from '@/services/list'
import {includesById} from '@/helpers/utils'
const props = defineProps({
modelValue: {
type: Array as PropType<IList[]>,
default: () => [],
},
})
const emit = defineEmits<{
(e: 'update:modelValue', value: IList[]): void
}>()
const lists = ref<IList[]>([])
watchEffect(() => {
lists.value = props.modelValue
})
const selectedLists = computed({
get() {
return lists.value
},
set: (value) => {
lists.value = value
emit('update:modelValue', value)
},
})
const listService = shallowReactive(new ListService())
const foundLists = ref<IList[]>([])
async function findLists(query: string) {
if (query === '') {
foundLists.value = []
return
}
const response = await listService.getAll({}, {s: query}) as IList[]
// Filter selected items from the results
foundLists.value = response.filter(({id}) => !includesById(lists.value, id))
}
</script>

View File

@ -0,0 +1,63 @@
<template>
<multiselect
v-model="selectedNamespaces"
:search-results="foundNamespaces"
:loading="namespaceService.loading"
:multiple="true"
:placeholder="$t('namespace.search')"
label="namespace"
@search="findNamespaces"
/>
</template>
<script setup lang="ts">
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
import Multiselect from '@/components/input/multiselect.vue'
import type {INamespace} from '@/modelTypes/INamespace'
import NamespaceService from '@/services/namespace'
import {includesById} from '@/helpers/utils'
const props = defineProps({
modelValue: {
type: Array as PropType<INamespace[]>,
default: () => [],
},
})
const emit = defineEmits<{
(e: 'update:modelValue', value: INamespace[]): void
}>()
const namespaces = ref<INamespace[]>([])
watchEffect(() => {
namespaces.value = props.modelValue
})
const selectedNamespaces = computed({
get() {
return namespaces.value
},
set: (value) => {
namespaces.value = value
emit('update:modelValue', value)
},
})
const namespaceService = shallowReactive(new NamespaceService())
const foundNamespaces = ref<INamespace[]>([])
async function findNamespaces(query: string) {
if (query === '') {
foundNamespaces.value = []
return
}
const response = await namespaceService.getAll({}, {s: query}) as INamespace[]
// Filter selected items from the results
foundNamespaces.value = response.filter(({id}) => !includesById(namespaces.value, id))
}
</script>

View File

@ -0,0 +1,63 @@
<template>
<multiselect
v-model="selectedUsers"
:search-results="foundUsers"
:loading="userService.loading"
:multiple="true"
:placeholder="$t('team.edit.search')"
label="username"
@search="findUsers"
/>
</template>
<script setup lang="ts">
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
import Multiselect from '@/components/input/multiselect.vue'
import type {IUser} from '@/modelTypes/IUser'
import UserService from '@/services/user'
import {includesById} from '@/helpers/utils'
const props = defineProps({
modelValue: {
type: Array as PropType<IUser[]>,
default: () => [],
},
})
const emit = defineEmits<{
(e: 'update:modelValue', value: IUser[]): void
}>()
const users = ref<IUser[]>([])
watchEffect(() => {
users.value = props.modelValue
})
const selectedUsers = computed({
get() {
return users.value
},
set: (value) => {
users.value = value
emit('update:modelValue', value)
},
})
const userService = shallowReactive(new UserService())
const foundUsers = ref<IUser[]>([])
async function findUsers(query: string) {
if (query === '') {
foundUsers.value = []
return
}
const response = await userService.getAll({}, {s: query}) as IUser[]
// Filter selected items from the results
foundUsers.value = response.filter(({id}) => !includesById(users.value, id))
}
</script>

View File

@ -9,64 +9,61 @@
}
]"
>
<icon
v-if="showIconOnly"
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : false}"
/>
<span class="icon is-small" v-else-if="icon !== ''">
<template v-if="icon">
<icon
v-if="showIconOnly"
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : false}"
/>
</span>
<span class="icon is-small" v-else>
<icon
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : false}"
/>
</span>
</template>
<slot />
</BaseButton>
</template>
<script lang="ts">
const BUTTON_TYPES_MAP = {
primary: 'is-primary',
secondary: 'is-outlined',
tertiary: 'is-text is-inverted underline-none',
} as const
export type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
export default { name: 'x-button' }
</script>
<script setup lang="ts">
import {computed, useSlots, type PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {computed, useSlots} from 'vue'
import BaseButton, {type BaseButtonProps} from '@/components/base/BaseButton.vue'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const BUTTON_TYPES_MAP = Object.freeze({
primary: 'is-primary',
secondary: 'is-outlined',
tertiary: 'is-text is-inverted underline-none',
})
// extending the props of the BaseButton
export interface ButtonProps extends BaseButtonProps {
variant?: ButtonTypes
icon?: IconProp
iconColor?: string
loading?: boolean
shadow?: boolean
}
type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
const {
variant = 'primary',
icon = '',
iconColor = '',
loading = false,
shadow = true,
} = defineProps<ButtonProps>()
const props = defineProps({
variant: {
type: String as PropType<ButtonTypes>,
default: 'primary',
},
icon: {
type: [String, Array],
default: '',
},
iconColor: {
type: String,
default: '',
},
loading: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: true,
},
})
const variantClass = computed(() => BUTTON_TYPES_MAP[props.variant])
const variantClass = computed(() => BUTTON_TYPES_MAP[variant])
const slots = useSlots()
const showIconOnly = computed(() => props.icon !== '' && typeof slots.default === 'undefined')
const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'undefined')
</script>
<style lang="scss" scoped>

View File

@ -4,7 +4,7 @@
{{ date === null ? chooseDateLabel : formatDateShort(date) }}
</BaseButton>
<transition name="fade">
<CustomTransition name="fade">
<div v-if="show" class="datepicker-popup" ref="datepickerPopup">
<BaseButton
@ -84,7 +84,7 @@
{{ $t('misc.confirm') }}
</x-button>
</div>
</transition>
</CustomTransition>
</div>
</template>
@ -94,6 +94,7 @@ import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
@ -193,7 +194,7 @@ function toggleDatePopup() {
}
const datepickerPopup = ref<HTMLElement | null>(null)
function hideDatePopup(e) {
function hideDatePopup(e: MouseEvent) {
if (show.value) {
closeWhenClickedOutside(e, datepickerPopup.value, close)
}

View File

@ -115,6 +115,7 @@ const props = defineProps({
default: true,
},
bottomActions: {
type: Array,
default: () => [],
},
emptyText: {
@ -285,9 +286,9 @@ function handleCheckboxClick(e: Event) {
console.debug('no index found')
return
}
console.debug(index, text.value.slice(index, 9))
const listPrefix = text.value.slice(index, 1)
const listPrefix = text.value.substring(index, index + 1)
console.debug({index, listPrefix, checked, text: text.value})
text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `)
bubble()

View File

@ -9,6 +9,7 @@ export function createEasyMDEConfig({ placeholder, uploadImage, imageUploadFunct
uploadImage,
imageUploadFunction,
minHeight: '150px',
sideBySideFullscreen: false,
toolbar: [
{
name: 'heading-1',

View File

@ -9,7 +9,8 @@
<div class="control" :class="{'is-loading': loading || localLoading}">
<div
class="input-wrapper input"
:class="{'has-multiple': hasMultiple}">
:class="{'has-multiple': hasMultiple}"
>
<template v-if="Array.isArray(internalValue)">
<template v-for="(item, key) in internalValue">
<slot name="tag" :item="item">
@ -35,10 +36,10 @@
</div>
</div>
<transition name="fade">
<CustomTransition name="fade">
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
<BaseButton
class="is-fullwidth"
class="search-result-button is-fullwidth"
v-for="(data, index) in filteredSearchResults"
:key="index"
:ref="(el) => setResult(el, index)"
@ -58,7 +59,7 @@
<BaseButton
v-if="creatableAvailable"
class="is-fullwidth"
class="search-result-button is-fullwidth"
:ref="(el) => setResult(el, filteredSearchResults.length)"
@keydown.up.prevent="() => preSelect(filteredSearchResults.length - 1)"
@keydown.down.prevent="() => preSelect(filteredSearchResults.length + 1)"
@ -77,8 +78,7 @@
</span>
</BaseButton>
</div>
</transition>
</CustomTransition>
</div>
</template>
@ -89,6 +89,7 @@ import {useI18n} from 'vue-i18n'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
function elementInResults(elem: string | any, label: string, query: string): boolean {
// Don't make create available if we have an exact match in our search results.
@ -100,37 +101,52 @@ function elementInResults(elem: string | any, label: string, query: string): boo
}
const props = defineProps({
// When true, shows a loading spinner
/**
* When true, shows a loading spinner
*/
loading: {
type: Boolean,
default: false,
},
// The placeholder of the search input
/**
* The placeholder of the search input
*/
placeholder: {
type: String,
default: '',
},
// The search results where the @search listener needs to put the results into
/**
* The search results where the @search listener needs to put the results into
*/
searchResults: {
type: Array as PropType<{[id: string]: any}>,
default: () => [],
},
// The name of the property of the searched object to show the user.
// If empty the component will show all raw data of an entry.
/**
* The name of the property of the searched object to show the user.
* If empty the component will show all raw data of an entry.
*/
label: {
type: String,
default: '',
},
// The object with the value, updated every time an entry is selected.
/**
* The object with the value, updated every time an entry is selected.
*/
modelValue: {
type: [Object] as PropType<{[key: string]: any}>,
default: null,
},
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
/**
* If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
*/
creatable: {
type: Boolean,
default: false,
},
// The text shown next to the new value option.
/**
* The text shown next to the new value option.
*/
createPlaceholder: {
type: String,
default() {
@ -138,7 +154,9 @@ const props = defineProps({
return t('input.multiselect.createPlaceholder')
},
},
// The text shown next to an option.
/**
* The text shown next to an option.
*/
selectPlaceholder: {
type: String,
default() {
@ -146,22 +164,30 @@ const props = defineProps({
return t('input.multiselect.selectPlaceholder')
},
},
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
/**
* If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
*/
multiple: {
type: Boolean,
default: false,
},
// If true, displays the search results inline instead of using a dropdown.
/**
* If true, displays the search results inline instead of using a dropdown.
*/
inline: {
type: Boolean,
default: false,
},
// If true, shows search results when no query is specified.
/**
* If true, shows search results when no query is specified.
*/
showEmpty: {
type: Boolean,
default: true,
},
// The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke.
/**
* The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke.
*/
searchDelay: {
type: Number,
default: 200,
@ -174,17 +200,25 @@ const props = defineProps({
const emit = defineEmits<{
(e: 'update:modelValue', value: null): void
// @search: Triggered every time the search query input changes
/**
* Triggered every time the search query input changes
*/
(e: 'search', query: string): void
// @select: Triggered every time an option from the search results is selected. Also triggers a change in v-model.
(e: 'select', value: null): void
// @create: If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
/**
* Triggered every time an option from the search results is selected. Also triggers a change in v-model.
*/
(e: 'select', value: {[key: string]: any}): void
/**
* If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
*/
(e: 'create', query: string): void
// @remove: If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
/**
* If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
*/
(e: 'remove', value: null): void
}>()
const query = ref('')
const query = ref<string | {[key: string]: any}>('')
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
const localLoading = ref(false)
const showSearchResults = ref(false)
@ -401,122 +435,125 @@ function focus() {
.control.is-loading::after {
top: .75rem;
}
}
&.has-search-results .input-wrapper {
border-radius: $radius $radius 0 0;
border-color: var(--primary) !important;
background: var(--white) !important;
.input-wrapper {
padding: 0;
background: var(--white);
border-color: var(--grey-200);
flex-wrap: wrap;
height: auto;
&, &:focus-within {
border-bottom-color: var(--grey-200) !important;
}
&:hover {
border-color: var(--grey-300) !important;
}
.input-wrapper {
padding: 0;
background: var(--white) !important;
border-color: var(--grey-200) !important;
flex-wrap: wrap;
.input {
display: flex;
max-width: 100%;
width: 100%;
align-items: center;
border: none !important;
background: transparent;
height: auto;
&:hover {
border-color: var(--grey-300) !important;
}
.input {
display: flex;
max-width: 100%;
width: 100%;
align-items: center;
border: none !important;
background: transparent;
height: auto;
&::placeholder {
font-style: normal !important;
}
}
&.has-multiple .input {
max-width: 250px;
input {
padding-left: 0;
}
}
&:focus-within {
border-color: var(--primary) !important;
background: var(--white) !important;
}
.loader {
margin: 0 .5rem;
&::placeholder {
font-style: normal !important;
}
}
.search-results {
background: var(--white);
border-radius: 0 0 $radius $radius;
border: 1px solid var(--primary);
border-top: none;
&.has-multiple .input {
max-width: 250px;
max-height: 50vh;
overflow-x: auto;
position: absolute;
z-index: 100;
max-width: 100%;
min-width: 100%;
&-inline {
position: static;
input {
padding-left: 0;
}
}
button {
background: transparent;
text-align: left;
box-shadow: none;
border-radius: 0;
text-transform: none;
font-family: $family-sans-serif;
font-weight: normal;
padding: .5rem;
border: none;
cursor: pointer;
color: var(--grey-800);
&:focus-within {
border-color: var(--primary) !important;
background: var(--white) !important;
}
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
.search-result {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding: .5rem .75rem;
}
.hint-text {
font-size: .75rem;
color: transparent;
transition: color $transition;
padding-left: .5rem;
}
&:focus, &:hover {
background: var(--grey-100);
box-shadow: none !important;
.hint-text {
color: var(--text);
}
}
&:active {
background: var(--grey-200);
}
}
// doesn't seem to be used. maybe inside the slot?
.loader {
margin: 0 .5rem;
}
}
.has-search-results .input-wrapper {
border-radius: $radius $radius 0 0;
border-color: var(--primary) !important;
background: var(--white) !important;
&, &:focus-within {
border-bottom-color: var(--grey-200) !important;
}
}
.search-results {
background: var(--white);
border-radius: 0 0 $radius $radius;
border: 1px solid var(--primary);
border-top: none;
max-height: 50vh;
overflow-x: auto;
position: absolute;
z-index: 100;
max-width: 100%;
min-width: 100%;
}
.search-results-inline {
position: static;
}
.search-result-button {
background: transparent;
text-align: left;
box-shadow: none;
border-radius: 0;
text-transform: none;
font-family: $family-sans-serif;
font-weight: normal;
padding: .5rem;
border: none;
cursor: pointer;
color: var(--grey-800);
display: flex;
justify-content: space-between;
align-items: center;
overflow: hidden;
&:focus,
&:hover {
background: var(--grey-100);
box-shadow: none !important;
.hint-text {
color: var(--text);
}
}
&:active {
background: var(--grey-200);
}
}
.search-result {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
padding: .5rem .75rem;
}
.hint-text {
font-size: .75rem;
color: transparent;
transition: color $transition;
padding-left: .5rem;
}
</style>

View File

@ -4,22 +4,28 @@
class="vue-simplemde-textarea"
:name="name"
:value="modelValue"
@input="handleInput($event.target.value)"
@input="handleInput(($event.target as HTMLTextAreaElement).value)"
/>
</div>
</template>
<script setup lang="ts">
import {ref, watch, onMounted, onDeactivated, onBeforeUnmount, nextTick, shallowReactive} from 'vue'
import type { ShallowReactive } from 'vue'
import EasyMDE from 'easymde'
import {ref, watch, onMounted, onDeactivated, onBeforeUnmount, nextTick, shallowReactive, type ShallowReactive, type PropType} from 'vue'
import EasyMDE, {toggleFullScreen} from 'easymde'
import {marked} from 'marked'
import type CodeMirror from 'codemirror'
const props = defineProps({
modelValue: String,
name: String,
previewClass: String,
modelValue: {
type: String,
default: '',
},
name: {
type: String,
},
previewClass: {
type: String,
},
autoinit: {
type: Boolean,
default: true,
@ -37,7 +43,7 @@ const props = defineProps({
default: () => ({}),
},
previewRender: {
type: Function,
type: Function as PropType<EasyMDE.Options['previewRender']>,
},
})
@ -51,9 +57,9 @@ onMounted(() => {
})
onDeactivated(() => {
if (!easymde) return
const isFullScreen = easymde.codemirror.getOption('fullScreen')
if (isFullScreen) easymde.toggleFullScreen()
if (easymde === undefined) return
if (easymde.isFullscreenActive()) toggleFullScreen(easymde)
easymde.toTextArea
})
onBeforeUnmount(() => {
@ -67,8 +73,8 @@ onBeforeUnmount(() => {
const easymdeRef = ref<HTMLElement | null>(null)
function initialize() {
const configs = Object.assign({
element: easymdeRef.value?.firstElementChild,
const configs: EasyMDE.Options = Object.assign({
element: easymdeRef.value?.firstElementChild as HTMLElement,
initialValue: props.modelValue,
previewRender: props.previewRender,
renderingConfig: {},
@ -81,7 +87,7 @@ function initialize() {
// Determine whether to enable code highlighting
if (props.highlight) {
configs.renderingConfig.codeSyntaxHighlighting = true
configs.renderingConfig!.codeSyntaxHighlighting = true
}
// Set whether to render the input html
@ -92,15 +98,16 @@ function initialize() {
// Add a custom previewClass
const className = props.previewClass || ''
addPreviewClass(className)
addPreviewClass(easymde, className)
// Binding event
bindingEvents()
easymde.codemirror.on('change', handleCodemirrorInput)
easymde.codemirror.on('blur', handleCodemirrorBlur)
nextTick(() => emit('initialized', easymde))
}
function addPreviewClass(className: string) {
function addPreviewClass(easymde: EasyMDE, className: string) {
const wrapper = easymde.codemirror.getWrapperElement()
const preview = document.createElement('div')
wrapper.nextSibling.className += ` ${className}`
@ -108,28 +115,24 @@ function addPreviewClass(className: string) {
wrapper.appendChild(preview)
}
function bindingEvents() {
easymde.codemirror.on('change', handleCodemirrorInput)
easymde.codemirror.on('blur', handleCodemirrorBlur)
function handleInput(val: string) {
isValueUpdateFromInner.value = true
emit('update:modelValue', val)
}
function handleCodemirrorInput(instance, changeObj) {
if (changeObj.origin === 'setValue') {
function handleCodemirrorInput(instance: CodeMirror.Editor, changeObj: CodeMirror.EditorChange) {
if (changeObj.origin === 'setValue' || easymde === undefined) {
return
}
const val = easymde.value()
handleInput(val)
handleInput(easymde.value())
}
function handleCodemirrorBlur() {
const val = easymde.value()
if (easymde === undefined) {
return
}
isValueUpdateFromInner.value = true
emit('blur', val)
}
function handleInput(val) {
isValueUpdateFromInner.value = true
emit('update:modelValue', val)
emit('blur', easymde.value())
}
watch(
@ -138,7 +141,7 @@ watch(
if (isValueUpdateFromInner.value) {
isValueUpdateFromInner.value = false
} else {
easymde.value(val)
easymde?.value(val)
}
},
)

View File

@ -5,42 +5,50 @@
>
<div class="switch-view-container">
<div class="switch-view">
<router-link
<BaseButton
v-shortcut="'g l'"
:title="$t('keyboardShortcuts.list.switchToListView')"
class="switch-view-button"
:class="{'is-active': viewName === 'list'}"
:to="{ name: 'list.list', params: { listId } }">
:to="{ name: 'list.list', params: { listId } }"
>
{{ $t('list.list.title') }}
</router-link>
<router-link
</BaseButton>
<BaseButton
v-shortcut="'g g'"
:title="$t('keyboardShortcuts.list.switchToGanttView')"
class="switch-view-button"
:class="{'is-active': viewName === 'gantt'}"
:to="{ name: 'list.gantt', params: { listId } }">
:to="{ name: 'list.gantt', params: { listId } }"
>
{{ $t('list.gantt.title') }}
</router-link>
<router-link
</BaseButton>
<BaseButton
v-shortcut="'g t'"
:title="$t('keyboardShortcuts.list.switchToTableView')"
class="switch-view-button"
:class="{'is-active': viewName === 'table'}"
:to="{ name: 'list.table', params: { listId } }">
:to="{ name: 'list.table', params: { listId } }"
>
{{ $t('list.table.title') }}
</router-link>
<router-link
</BaseButton>
<BaseButton
v-shortcut="'g k'"
:title="$t('keyboardShortcuts.list.switchToKanbanView')"
class="switch-view-button"
:class="{'is-active': viewName === 'kanban'}"
:to="{ name: 'list.kanban', params: { listId } }">
:to="{ name: 'list.kanban', params: { listId } }"
>
{{ $t('list.kanban.title') }}
</router-link>
</BaseButton>
</div>
<slot name="header" />
</div>
<transition name="fade">
<CustomTransition name="fade">
<Message variant="warning" v-if="currentList.isArchived" class="mb-4">
{{ $t('list.archived') }}
</Message>
</transition>
</CustomTransition>
<slot v-if="loadedListId"/>
</div>
@ -50,7 +58,9 @@
import {ref, computed, watch} from 'vue'
import {useRoute} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue'
import Message from '@/components/misc/message.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import ListModel from '@/models/list'
import ListService from '@/services/list'
@ -61,7 +71,6 @@ import {useTitle} from '@/composables/useTitle'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists'
import {useKanbanStore} from '@/stores/kanban'
const props = defineProps({
listId: {
@ -77,7 +86,6 @@ const props = defineProps({
const route = useRoute()
const baseStore = useBaseStore()
const kanbanStore = useKanbanStore()
const listStore = useListStore()
const listService = ref(new ListService())
const loadedListId = ref(0)
@ -90,6 +98,7 @@ const currentList = computed(() => {
maxRight: null,
} : baseStore.currentList
})
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
// This resulted in loading and setting the list multiple times, even when navigating away from it.
@ -98,62 +107,47 @@ const currentList = computed(() => {
// of it, most likely due to the rights not being properly populated.
watch(
() => props.listId,
listId => loadList(listId),
// loadList
async (listIdToLoad: number) => {
const listData = {id: listIdToLoad}
saveListToHistory(listData)
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and
// the currently loaded list has the right set.
if (
(
listIdToLoad === loadedListId.value ||
typeof listIdToLoad === 'undefined' ||
listIdToLoad === currentList.value.id
)
&& typeof currentList.value !== 'undefined' && currentList.value.maxRight !== null
) {
loadedListId.value = props.listId
return
}
console.debug(`Loading list, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedListId = ${loadedListId.value}, currentList = `, currentList.value)
// Set the current list to the one we're about to load so that the title is already shown at the top
loadedListId.value = 0
const listFromStore = listStore.getListById(listData.id)
if (listFromStore !== null) {
baseStore.setBackground(null)
baseStore.setBlurHash(null)
baseStore.handleSetCurrentList({list: listFromStore})
}
// We create an extra list object instead of creating it in list.value because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData)
try {
const loadedList = await listService.value.get(list)
baseStore.handleSetCurrentList({list: loadedList})
} finally {
loadedListId.value = props.listId
}
},
{immediate: true},
)
useTitle(() => currentList.value.id ? getListTitle(currentList.value) : '')
async function loadList(listIdToLoad: number) {
const listData = {id: listIdToLoad}
saveListToHistory(listData)
// This invalidates the loaded list at the kanban board which lets it reload its content when
// switched to it. This ensures updates done to tasks in the gantt or list views are consistently
// shown in all views while preventing reloads when closing a task popup.
// We don't do this for the table view because that does not change tasks.
// FIXME: remove this
if (
props.viewName === 'list.list' ||
props.viewName === 'list.gantt'
) {
kanbanStore.setListId(0)
}
// Don't load the list if we either already loaded it or aren't dealing with a list at all currently and
// the currently loaded list has the right set.
if (
(
listIdToLoad === loadedListId.value ||
typeof listIdToLoad === 'undefined' ||
listIdToLoad === currentList.value.id
)
&& typeof currentList.value !== 'undefined' && currentList.value.maxRight !== null
) {
loadedListId.value = props.listId
return
}
console.debug(`Loading list, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedListId = ${loadedListId.value}, currentList = `, currentList.value)
// Set the current list to the one we're about to load so that the title is already shown at the top
loadedListId.value = 0
const listFromStore = listStore.getListById(listData.id)
if (listFromStore !== null) {
baseStore.setBackground(null)
baseStore.setBlurHash(null)
baseStore.handleSetCurrentList({list: listFromStore})
}
// We create an extra list object instead of creating it in list.value because that would trigger a ui update which would result in bad ux.
const list = new ListModel(listData)
try {
const loadedList = await listService.value.get(list)
await baseStore.handleSetCurrentList({list: loadedList})
} finally {
loadedListId.value = props.listId
}
}
</script>
<style lang="scss" scoped>
@ -174,35 +168,32 @@ async function loadList(listIdToLoad: number) {
height: $switch-view-height;
margin: 0 auto 1rem;
padding: .5rem;
a {
padding: .25rem .5rem;
display: block;
border-radius: $radius;
transition: all 100ms;
&:not(:last-child) {
margin-right: .5rem;
}
&.is-active,
&:hover {
color: var(--switch-view-color);
}
&.is-active {
background: var(--primary);
font-weight: bold;
box-shadow: var(--shadow-xs);
}
&:hover {
background: var(--primary);
}
}
}
.switch-view-button {
padding: .25rem .5rem;
display: block;
border-radius: $radius;
transition: all 100ms;
&:not(:last-child) {
margin-right: .5rem;
}
&:hover {
color: var(--switch-view-color);
background: var(--primary);
}
&.is-active {
color: var(--switch-view-color);
background: var(--primary);
font-weight: bold;
box-shadow: var(--shadow-xs);
}
}
// FIXME: this should be in notification and set via a prop
.is-archived .notification.is-warning {
margin-bottom: 1rem;
}

View File

@ -1,5 +1,13 @@
<template>
<dropdown>
<template #trigger="triggerProps">
<slot name="trigger" v-bind="triggerProps">
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</slot>
</template>
<template v-if="isSavedFilter(list)">
<dropdown-item
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
@ -78,12 +86,14 @@
<script setup lang="ts">
import {ref, computed, watchEffect, type PropType} from 'vue'
import {isSavedFilter} from '@/helpers/savedFilter'
import BaseButton from '@/components/base/BaseButton.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import Subscription from '@/components/misc/subscription.vue'
import type {IList} from '@/modelTypes/IList'
import type {ISubscription} from '@/modelTypes/ISubscription'
import {isSavedFilter} from '@/services/savedFilter'
import {useConfigStore} from '@/stores/config'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
@ -114,4 +124,4 @@ function setSubscriptionInStore(sub: ISubscription) {
listStore.setList(updatedList)
namespaceStore.setListInNamespaceById(updatedList)
}
</script>
</script>

View File

@ -0,0 +1,176 @@
<template>
<div
class="list-card"
:class="{
'has-light-text': background !== null,
'has-background': blurHashUrl !== '' || background !== null
}"
:style="{
'border-left': list.hexColor ? `0.25rem solid ${list.hexColor}` : undefined,
'background-image': blurHashUrl !== '' ? `url(${blurHashUrl})` : undefined,
}"
>
<div
class="list-background background-fade-in"
:class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
/>
<span v-if="list.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span>
<div class="list-title" aria-hidden="true">{{ list.title }}</div>
<BaseButton
class="list-button"
:aria-label="list.title"
:title="list.description"
:to="{
name: 'list.index',
params: { listId: list.id}
}"
/>
<BaseButton
v-if="!list.isArchived"
class="favorite"
:class="{'is-favorite': list.isFavorite}"
@click.prevent.stop="listStore.toggleListFavorite(list)"
>
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']" />
</BaseButton>
</div>
</template>
<script lang="ts" setup>
import {toRef, type PropType} from 'vue'
import type {IList} from '@/modelTypes/IList'
import BaseButton from '@/components/base/BaseButton.vue'
import {useListBackground} from './useListBackground'
import {useListStore} from '@/stores/lists'
const props = defineProps({
list: {
type: Object as PropType<IList>,
required: true,
},
})
const {background, blurHashUrl} = useListBackground(toRef(props, 'list'))
const listStore = useListStore()
</script>
<style lang="scss" scoped>
.list-card {
--list-card-padding: 1rem;
background: var(--white);
padding: var(--list-card-padding);
border-radius: $radius;
box-shadow: var(--shadow-sm);
transition: box-shadow $transition;
position: relative;
overflow: hidden; // hide background
display: flex;
justify-content: space-between;
flex-wrap: wrap;
&:hover {
box-shadow: var(--shadow-md);
}
&:active,
&:focus {
box-shadow: var(--shadow-xs) !important;
}
> * {
// so the elements are on top of the background
position: relative;
}
}
.has-background,
.list-background {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
.list-background,
.list-button {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
.is-archived {
font-size: .75rem;
float: left;
}
.list-title {
align-self: flex-end;
font-family: $vikunja-font;
font-weight: 400;
font-size: 1.5rem;
line-height: var(--title-line-height);
color: var(--text);
width: 100%;
margin-bottom: 0;
max-height: calc(100% - (var(--list-card-padding) + 1rem)); // padding & height of the "is archived" badge
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
.has-light-text .list-title {
color: var(--grey-100);
}
.has-background .list-title {
text-shadow:
0 0 10px var(--black),
1px 1px 5px var(--grey-700),
-1px -1px 5px var(--grey-700);
color: var(--white);
}
.favorite {
position: absolute;
top: var(--list-card-padding);
right: var(--list-card-padding);
transition: opacity $transition, color $transition;
opacity: 0;
&:hover {
color: var(--warning);
}
&.is-favorite {
display: inline-block;
opacity: 1;
color: var(--warning);
}
}
.list-card:hover .favorite {
opacity: 1;
}
.background-fade-in {
opacity: 0;
transition: opacity $transition;
transition-delay: $transition-duration * 2; // To fake an appearing background
&.is-visible {
opacity: 1;
}
}
</style>

View File

@ -0,0 +1,77 @@
<template>
<ul class="list-grid">
<li
v-for="(item, index) in filteredLists"
:key="`list_${item.id}_${index}`"
class="list-grid-item"
>
<ListCard :list="item" />
</li>
</ul>
</template>
<script lang="ts" setup>
import {computed, type PropType} from 'vue'
import type {IList} from '@/modelTypes/IList'
import ListCard from './ListCard.vue'
const props = defineProps({
lists: {
type: Array as PropType<IList[]>,
default: () => [],
},
showArchived: {
default: false,
type: Boolean,
},
itemLimit: {
type: Boolean,
default: false,
},
})
const filteredLists = computed(() => {
return props.showArchived
? props.lists
: props.lists.filter(l => !l.isArchived)
})
</script>
<style lang="scss" scoped>
$list-height: 150px;
$list-spacing: 1rem;
.list-grid {
margin: 0; // reset li
list-style-type: none;
display: grid;
grid-template-columns: repeat(var(--list-columns), 1fr);
grid-auto-rows: $list-height;
gap: $list-spacing;
@media screen and (min-width: $mobile) {
--list-rows: 4;
--list-columns: 1;
}
@media screen and (min-width: $mobile) and (max-width: $tablet) {
--list-columns: 2;
}
@media screen and (min-width: $tablet) and (max-width: $widescreen) {
--list-columns: 3;
--list-rows: 3;
}
@media screen and (min-width: $widescreen) {
--list-columns: 5;
--list-rows: 2;
}
}
.list-grid-item {
display: grid;
margin-top: 0; // remove padding coming form .content li + li
}
</style>

View File

@ -14,11 +14,11 @@
{{ $t('filters.title') }}
</x-button>
<modal
@close="() => modalOpen = false"
:enabled="modalOpen"
transition-name="fade"
:overflow="true"
variant="hint-modal"
@close="() => modalOpen = false"
>
<filters
:has-title="true"
@ -34,7 +34,7 @@ import {computed, ref, watch} from 'vue'
import Filters from '@/components/list/partials/filters.vue'
import {getDefaultParams} from '@/composables/taskList'
import {getDefaultParams} from '@/composables/useTaskList'
const props = defineProps({
modelValue: {

View File

@ -13,11 +13,14 @@
>
{{ $t('filters.attributes.requireAll') }}
</fancycheckbox>
<fancycheckbox v-model="filters.done" @update:model-value="setDoneFilter">
<fancycheckbox
v-model="filters.done"
@update:model-value="setDoneFilter"
>
{{ $t('filters.attributes.showDoneTasks') }}
</fancycheckbox>
<fancycheckbox
v-if="!$route.name.includes('list.kanban') || !$route.name.includes('list.table')"
v-if="!['list.kanban', 'list.table'].includes($route.name as string)"
v-model="sortAlphabetically"
@update:model-value="change()"
>
@ -40,9 +43,9 @@
<label class="label">{{ $t('task.attributes.priority') }}</label>
<div class="control single-value-control">
<priority-select
:disabled="!filters.usePriority || undefined"
v-model.number="filters.priority"
@update:model-value="setPriority"
:disabled="!filters.usePriority || undefined"
/>
<fancycheckbox
v-model="filters.usePriority"
@ -132,16 +135,10 @@
<div class="field">
<label class="label">{{ $t('task.attributes.assignees') }}</label>
<div class="control">
<multiselect
:loading="usersService.loading"
:placeholder="$t('team.edit.search')"
@search="query => find('users', query)"
:search-results="foundusers"
@select="() => add('users', 'assignees')"
label="username"
:multiple="true"
@remove="() => remove('users', 'assignees')"
v-model="users"
<SelectUser
v-model="entities.users"
@select="changeMultiselectFilter('users', 'assignees')"
@remove="changeMultiselectFilter('users', 'assignees')"
/>
</div>
</div>
@ -149,41 +146,32 @@
<div class="field">
<label class="label">{{ $t('task.attributes.labels') }}</label>
<div class="control labels-list">
<edit-labels v-model="labels" @update:model-value="changeLabelFilter"/>
<edit-labels
v-model="entities.labels"
@update:model-value="changeLabelFilter"
/>
</div>
</div>
<template
v-if="$route.name === 'filters.create' || $route.name === 'list.edit' || $route.name === 'filter.settings.edit'">
v-if="['filters.create', 'list.edit', 'filter.settings.edit'].includes($route.name as string)">
<div class="field">
<label class="label">{{ $t('list.lists') }}</label>
<div class="control">
<multiselect
:loading="listsService.loading"
:placeholder="$t('list.search')"
@search="query => find('lists', query)"
:search-results="foundlists"
@select="() => add('lists', 'list_id')"
label="title"
@remove="() => remove('lists', 'list_id')"
:multiple="true"
v-model="lists"
<SelectList
v-model="entities.lists"
@select="changeMultiselectFilter('lists', 'list_id')"
@remove="changeMultiselectFilter('lists', 'list_id')"
/>
</div>
</div>
<div class="field">
<label class="label">{{ $t('namespace.namespaces') }}</label>
<div class="control">
<multiselect
:loading="namespaceService.loading"
:placeholder="$t('namespace.search')"
@search="query => find('namespace', query)"
:search-results="foundnamespace"
@select="() => add('namespace', 'namespace')"
label="title"
@remove="() => remove('namespace', 'namespace')"
:multiple="true"
v-model="namespace"
<SelectNamespace
v-model="entities.namespace"
@select="changeMultiselectFilter('namespace', 'namespace')"
@remove="changeMultiselectFilter('namespace', 'namespace')"
/>
</div>
</div>
@ -192,28 +180,39 @@
</template>
<script lang="ts">
import {defineComponent} from 'vue'
export const ALPHABETICAL_SORT = 'title'
</script>
<script setup lang="ts">
import {computed, nextTick, onMounted, reactive, ref, shallowReactive, toRefs, watch} from 'vue'
import {camelCase} from 'camel-case'
import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser'
import type {INamespace} from '@/modelTypes/INamespace'
import type {IList} from '@/modelTypes/IList'
import {useLabelStore} from '@/stores/labels'
import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {includesById} from '@/helpers/utils'
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue'
import Multiselect from '@/components/input/multiselect.vue'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import SelectUser from '@/components/input/SelectUser.vue'
import SelectList from '@/components/input/SelectList.vue'
import SelectNamespace from '@/components/input/SelectNamespace.vue'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
import {objectToSnakeCase} from '@/helpers/case'
import UserService from '@/services/user'
import ListService from '@/services/list'
import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
import {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/composables/taskList'
import {camelCase} from 'camel-case'
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
import {getDefaultParams} from '@/composables/useTaskList'
// FIXME: merge with DEFAULT_PARAMS in taskList.js
const DEFAULT_PARAMS = {
@ -225,7 +224,7 @@ const DEFAULT_PARAMS = {
filter_include_nulls: true,
filter_concat: 'or',
s: '',
}
} as const
const DEFAULT_FILTERS = {
done: false,
@ -242,395 +241,350 @@ const DEFAULT_FILTERS = {
labels: '',
list_id: '',
namespace: '',
}
} as const
export const ALPHABETICAL_SORT = 'title'
export default defineComponent({
name: 'filters',
components: {
DatepickerWithRange,
EditLabels,
PrioritySelect,
Fancycheckbox,
PercentDoneSelect,
Multiselect,
const props = defineProps({
modelValue: {
required: true,
},
data() {
return {
params: DEFAULT_PARAMS,
filters: DEFAULT_FILTERS,
usersService: new UserService(),
foundusers: [],
users: [],
labelQuery: '',
labels: [],
listsService: new ListService(),
foundlists: [],
lists: [],
namespaceService: new NamespaceService(),
foundnamespace: [],
namespace: [],
}
},
mounted() {
this.filters.requireAllFilters = this.params.filter_concat === 'and'
},
props: {
modelValue: {
required: true,
},
hasTitle: {
type: Boolean,
default: false,
},
},
emits: ['update:modelValue'],
watch: {
modelValue: {
handler(value) {
// FIXME: filters should only be converted to snake case in
// the last moment
this.params = objectToSnakeCase(value)
this.prepareFilters()
},
immediate: true,
},
},
computed: {
sortAlphabetically: {
get() {
return this.params?.sort_by?.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
},
set(sortAlphabetically) {
this.params.sort_by = sortAlphabetically
? [ALPHABETICAL_SORT]
: getDefaultParams().sort_by
this.change()
},
},
foundLabels() {
const labelStore = useLabelStore()
return labelStore.filterLabelsByQuery(this.labels, this.labelQuery)
},
},
methods: {
change() {
const params = {...this.params}
params.filter_value = params.filter_value.map(v => v instanceof Date ? v.toISOString() : v)
this.$emit('update:modelValue', params)
},
prepareFilters() {
this.prepareDone()
this.prepareDate('due_date', 'dueDate')
this.prepareDate('start_date', 'startDate')
this.prepareDate('end_date', 'endDate')
this.prepareSingleValue('priority', 'priority', 'usePriority', true)
this.prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
this.prepareDate('reminders')
this.prepareRelatedObjectFilter('users', 'assignees')
this.prepareRelatedObjectFilter('lists', 'list_id')
this.prepareRelatedObjectFilter('namespace')
this.prepareSingleValue('labels')
const labels = typeof this.filters.labels === 'string'
? this.filters.labels
: ''
const labelIds = labels.split(',').map(i => parseInt(i))
const labelStore = useLabelStore()
this.labels = labelStore.getLabelsByIds(labelIds)
},
removePropertyFromFilter(propertyName) {
// Because of the way arrays work, we can only ever remove one element at once.
// To remove multiple filter elements of the same name this function has to be called multiple times.
for (const i in this.params.filter_by) {
if (this.params.filter_by[i] === propertyName) {
this.params.filter_by.splice(i, 1)
this.params.filter_comparator.splice(i, 1)
this.params.filter_value.splice(i, 1)
break
}
}
},
setDateFilter(filterName, {dateFrom, dateTo}) {
dateFrom = parseDateOrString(dateFrom, null)
dateTo = parseDateOrString(dateTo, null)
// Only filter if we have a date
if (dateFrom !== null && dateTo !== null) {
// Check if we already have values in params and only update them if we do
let foundStart = false
let foundEnd = false
this.params.filter_by.forEach((f, i) => {
if (f === filterName && this.params.filter_comparator[i] === 'greater_equals') {
foundStart = true
this.params.filter_value[i] = dateFrom
}
if (f === filterName && this.params.filter_comparator[i] === 'less_equals') {
foundEnd = true
this.params.filter_value[i] = dateTo
}
})
if (!foundStart) {
this.params.filter_by.push(filterName)
this.params.filter_comparator.push('greater_equals')
this.params.filter_value.push(dateFrom)
}
if (!foundEnd) {
this.params.filter_by.push(filterName)
this.params.filter_comparator.push('less_equals')
this.params.filter_value.push(dateTo)
}
this.filters[camelCase(filterName)] = {
// Passing the dates as string values avoids an endless loop between values changing
// in the datepicker (bubbling up to here) and changing here and bubbling down to the
// datepicker (because there's a new date instance every time this function gets called).
// See https://kolaente.dev/vikunja/frontend/issues/2384
dateFrom: dateIsValid(dateFrom) ? formatISO(dateFrom) : dateFrom,
dateTo: dateIsValid(dateTo) ? formatISO(dateTo) : dateTo,
}
this.change()
return
}
this.removePropertyFromFilter(filterName)
this.removePropertyFromFilter(filterName)
this.change()
},
prepareDate(filterName, variableName) {
if (typeof this.params.filter_by === 'undefined') {
return
}
let foundDateStart = false
let foundDateEnd = false
for (const i in this.params.filter_by) {
if (this.params.filter_by[i] === filterName && this.params.filter_comparator[i] === 'greater_equals') {
foundDateStart = i
}
if (this.params.filter_by[i] === filterName && this.params.filter_comparator[i] === 'less_equals') {
foundDateEnd = i
}
if (foundDateStart !== false && foundDateEnd !== false) {
break
}
}
if (foundDateStart !== false && foundDateEnd !== false) {
const startDate = new Date(this.params.filter_value[foundDateStart])
const endDate = new Date(this.params.filter_value[foundDateEnd])
this.filters[variableName] = {
dateFrom: !isNaN(startDate)
? `${startDate.getFullYear()}-${startDate.getMonth() + 1}-${startDate.getDate()}`
: this.params.filter_value[foundDateStart],
dateTo: !isNaN(endDate)
? `${endDate.getFullYear()}-${endDate.getMonth() + 1}-${endDate.getDate()}`
: this.params.filter_value[foundDateEnd],
}
}
},
setSingleValueFilter(filterName, variableName, useVariableName = '', comparator = 'equals') {
if (useVariableName !== '' && !this.filters[useVariableName]) {
this.removePropertyFromFilter(filterName)
return
}
let found = false
this.params.filter_by.forEach((f, i) => {
if (f === filterName) {
found = true
this.params.filter_value[i] = this.filters[variableName]
}
})
if (!found) {
this.params.filter_by.push(filterName)
this.params.filter_comparator.push(comparator)
this.params.filter_value.push(this.filters[variableName])
}
this.change()
},
/**
*
* @param filterName The filter name in the api.
* @param variableName The name of the variable in this.filters.
* @param useVariableName The name of the variable of the "Use this filter" variable. Will only be set if the parameter is not null.
* @param isNumber Toggles if the value should be parsed as a number.
*/
prepareSingleValue(filterName, variableName = null, useVariableName = null, isNumber = false) {
if (variableName === null) {
variableName = filterName
}
let found = false
for (const i in this.params.filter_by) {
if (this.params.filter_by[i] === filterName) {
found = i
break
}
}
if (found === false && useVariableName !== null) {
this.filters[useVariableName] = false
return
}
if (isNumber) {
this.filters[variableName] = Number(this.params.filter_value[found])
} else {
this.filters[variableName] = this.params.filter_value[found]
}
if (useVariableName !== null) {
this.filters[useVariableName] = true
}
},
prepareDone() {
// Set filters.done based on params
if (typeof this.params.filter_by === 'undefined') {
return
}
this.filters.done = this.params.filter_by.some((f) => f === 'done') === false
},
async prepareRelatedObjectFilter(kind, filterName = null, servicePrefix = null) {
if (filterName === null) {
filterName = kind
}
if (servicePrefix === null) {
servicePrefix = kind
}
this.prepareSingleValue(filterName)
if (typeof this.filters[filterName] === 'undefined' || this.filters[filterName] === '') {
return
}
// Don't load things if we already have something loaded.
// This is not the most ideal solution because it prevents a re-population when filters are changed
// from the outside. It is still fine because we're not changing them from the outside, other than
// loading them initially.
if (this[kind].length > 0) {
return
}
this[kind] = await this[`${servicePrefix}Service`].getAll({}, {s: this.filters[filterName]})
},
setDoneFilter() {
if (this.filters.done) {
this.removePropertyFromFilter('done')
} else {
this.params.filter_by.push('done')
this.params.filter_comparator.push('equals')
this.params.filter_value.push('false')
}
this.change()
},
setFilterConcat() {
if (this.filters.requireAllFilters) {
this.params.filter_concat = 'and'
} else {
this.params.filter_concat = 'or'
}
this.change()
},
setPriority() {
this.setSingleValueFilter('priority', 'priority', 'usePriority')
},
setPercentDoneFilter() {
this.setSingleValueFilter('percent_done', 'percentDone', 'usePercentDone')
},
clear(kind) {
this[`found${kind}`] = []
},
async find(kind, query) {
if (query === '') {
this.clear(kind)
return
}
const response = await this[`${kind}Service`].getAll({}, {s: query})
// Filter users from the results who are already assigned
this[`found${kind}`] = response.filter(({id}) => !includesById(this[kind], id))
},
add(kind, filterName) {
this.$nextTick(() => {
this.changeMultiselectFilter(kind, filterName)
})
},
remove(kind, filterName) {
this.$nextTick(() => {
this.changeMultiselectFilter(kind, filterName)
})
},
changeMultiselectFilter(kind, filterName) {
if (this[kind].length === 0) {
this.removePropertyFromFilter(filterName)
this.change()
return
}
const ids = []
this[kind].forEach(u => {
ids.push(kind === 'users' ? u.username : u.id)
})
this.filters[filterName] = ids.join(',')
this.setSingleValueFilter(filterName, filterName, '', 'in')
},
findLabels(query) {
this.labelQuery = query
},
addLabel() {
this.$nextTick(() => {
this.changeLabelFilter()
})
},
removeLabel(label) {
this.$nextTick(() => {
for (const l in this.labels) {
if (this.labels[l].id === label.id) {
this.labels.splice(l, 1)
}
break
}
this.changeLabelFilter()
})
},
changeLabelFilter() {
if (this.labels.length === 0) {
this.removePropertyFromFilter('labels')
this.change()
return
}
const labelIDs = []
this.labels.forEach(u => {
labelIDs.push(u.id)
})
this.filters.labels = labelIDs.join(',')
this.setSingleValueFilter('labels', 'labels', '', 'in')
},
hasTitle: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const {modelValue} = toRefs(props)
const labelStore = useLabelStore()
const params = ref({...DEFAULT_PARAMS})
const filters = ref({...DEFAULT_FILTERS})
const services = {
users: shallowReactive(new UserService()),
lists: shallowReactive(new ListService()),
namespace: shallowReactive(new NamespaceService()),
}
interface Entities {
users: IUser[]
labels: ILabel[]
lists: IList[]
namespace: INamespace[]
}
type EntityType = 'users' | 'labels' | 'lists' | 'namespace'
const entities: Entities = reactive({
users: [],
labels: [],
lists: [],
namespace: [],
})
onMounted(() => {
filters.value.requireAllFilters = params.value.filter_concat === 'and'
})
watch(
modelValue,
(value) => {
// FIXME: filters should only be converted to snake case in
// the last moment
params.value = objectToSnakeCase(value)
prepareFilters()
},
{immediate: true},
)
const sortAlphabetically = computed({
get() {
return params.value?.sort_by?.find(sortBy => sortBy === ALPHABETICAL_SORT) !== undefined
},
set(sortAlphabetically) {
params.value.sort_by = sortAlphabetically
? [ALPHABETICAL_SORT]
: getDefaultParams().sort_by
change()
},
})
function change() {
const newParams = {...params.value}
newParams.filter_value = newParams.filter_value.map(v => v instanceof Date ? v.toISOString() : v)
emit('update:modelValue', newParams)
}
function prepareFilters() {
prepareDone()
prepareDate('due_date', 'dueDate')
prepareDate('start_date', 'startDate')
prepareDate('end_date', 'endDate')
prepareSingleValue('priority', 'priority', 'usePriority', true)
prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
prepareDate('reminders')
prepareRelatedObjectFilter('users', 'assignees')
prepareRelatedObjectFilter('lists', 'list_id')
prepareRelatedObjectFilter('namespace')
prepareSingleValue('labels')
const newLabels = typeof filters.value.labels === 'string'
? filters.value.labels
: ''
const labelIds = newLabels.split(',').map(i => parseInt(i))
entities.labels = labelStore.getLabelsByIds(labelIds)
}
function removePropertyFromFilter(filterName) {
// Because of the way arrays work, we can only ever remove one element at once.
// To remove multiple filter elements of the same name this function has to be called multiple times.
for (const i in params.value.filter_by) {
if (params.value.filter_by[i] === filterName) {
params.value.filter_by.splice(i, 1)
params.value.filter_comparator.splice(i, 1)
params.value.filter_value.splice(i, 1)
break
}
}
}
function setDateFilter(filterName, {dateFrom, dateTo}) {
dateFrom = parseDateOrString(dateFrom, null)
dateTo = parseDateOrString(dateTo, null)
// Only filter if we have a date
if (dateFrom !== null && dateTo !== null) {
// Check if we already have values in params and only update them if we do
let foundStart = false
let foundEnd = false
params.value.filter_by.forEach((f, i) => {
if (f === filterName && params.value.filter_comparator[i] === 'greater_equals') {
foundStart = true
params.value.filter_value[i] = dateFrom
}
if (f === filterName && params.value.filter_comparator[i] === 'less_equals') {
foundEnd = true
params.value.filter_value[i] = dateTo
}
})
if (!foundStart) {
params.value.filter_by.push(filterName)
params.value.filter_comparator.push('greater_equals')
params.value.filter_value.push(dateFrom)
}
if (!foundEnd) {
params.value.filter_by.push(filterName)
params.value.filter_comparator.push('less_equals')
params.value.filter_value.push(dateTo)
}
filters.value[camelCase(filterName)] = {
// Passing the dates as string values avoids an endless loop between values changing
// in the datepicker (bubbling up to here) and changing here and bubbling down to the
// datepicker (because there's a new date instance every time this function gets called).
// See https://kolaente.dev/vikunja/frontend/issues/2384
dateFrom: dateIsValid(dateFrom) ? formatISO(dateFrom) : dateFrom,
dateTo: dateIsValid(dateTo) ? formatISO(dateTo) : dateTo,
}
change()
return
}
removePropertyFromFilter(filterName)
removePropertyFromFilter(filterName)
change()
}
function prepareDate(filterName, variableName) {
if (typeof params.value.filter_by === 'undefined') {
return
}
let foundDateStart = false
let foundDateEnd = false
for (const i in params.value.filter_by) {
if (params.value.filter_by[i] === filterName && params.value.filter_comparator[i] === 'greater_equals') {
foundDateStart = i
}
if (params.value.filter_by[i] === filterName && params.value.filter_comparator[i] === 'less_equals') {
foundDateEnd = i
}
if (foundDateStart !== false && foundDateEnd !== false) {
break
}
}
if (foundDateStart !== false && foundDateEnd !== false) {
const startDate = new Date(params.value.filter_value[foundDateStart])
const endDate = new Date(params.value.filter_value[foundDateEnd])
filters.value[variableName] = {
dateFrom: !isNaN(startDate)
? `${startDate.getFullYear()}-${startDate.getMonth() + 1}-${startDate.getDate()}`
: params.value.filter_value[foundDateStart],
dateTo: !isNaN(endDate)
? `${endDate.getFullYear()}-${endDate.getMonth() + 1}-${endDate.getDate()}`
: params.value.filter_value[foundDateEnd],
}
}
}
function setSingleValueFilter(filterName, variableName, useVariableName = '', comparator = 'equals') {
if (useVariableName !== '' && !filters.value[useVariableName]) {
removePropertyFromFilter(filterName)
return
}
let found = false
params.value.filter_by.forEach((f, i) => {
if (f === filterName) {
found = true
params.value.filter_value[i] = filters.value[variableName]
}
})
if (!found) {
params.value.filter_by.push(filterName)
params.value.filter_comparator.push(comparator)
params.value.filter_value.push(filters.value[variableName])
}
change()
}
function prepareSingleValue(
/** The filter name in the api. */
filterName,
/** The name of the variable in filters ref. */
variableName = null,
/** The name of the variable of the "Use this filter" variable. Will only be set if the parameter is not null. */
useVariableName = null,
/** Toggles if the value should be parsed as a number. */
isNumber = false,
) {
if (variableName === null) {
variableName = filterName
}
let found = false
for (const i in params.value.filter_by) {
if (params.value.filter_by[i] === filterName) {
found = i
break
}
}
if (found === false && useVariableName !== null) {
filters.value[useVariableName] = false
return
}
if (isNumber) {
filters.value[variableName] = Number(params.value.filter_value[found])
} else {
filters.value[variableName] = params.value.filter_value[found]
}
if (useVariableName !== null) {
filters.value[useVariableName] = true
}
}
function prepareDone() {
// Set filters.done based on params
if (typeof params.value.filter_by === 'undefined') {
return
}
filters.value.done = params.value.filter_by.some((f) => f === 'done') === false
}
async function prepareRelatedObjectFilter(kind: EntityType, filterName = null, servicePrefix: Omit<EntityType, 'labels'> | null = null) {
if (filterName === null) {
filterName = kind
}
if (servicePrefix === null) {
servicePrefix = kind
}
prepareSingleValue(filterName)
if (typeof filters.value[filterName] === 'undefined' || filters.value[filterName] === '') {
return
}
// Don't load things if we already have something loaded.
// This is not the most ideal solution because it prevents a re-population when filters are changed
// from the outside. It is still fine because we're not changing them from the outside, other than
// loading them initially.
if (entities[kind].length > 0) {
return
}
entities[kind] = await services[servicePrefix].getAll({}, {s: filters.value[filterName]})
}
function setDoneFilter() {
if (filters.value.done) {
removePropertyFromFilter('done')
} else {
params.value.filter_by.push('done')
params.value.filter_comparator.push('equals')
params.value.filter_value.push('false')
}
change()
}
function setFilterConcat() {
if (filters.value.requireAllFilters) {
params.value.filter_concat = 'and'
} else {
params.value.filter_concat = 'or'
}
change()
}
function setPriority() {
setSingleValueFilter('priority', 'priority', 'usePriority')
}
function setPercentDoneFilter() {
setSingleValueFilter('percent_done', 'percentDone', 'usePercentDone')
}
async function changeMultiselectFilter(kind: EntityType, filterName) {
await nextTick()
if (entities[kind].length === 0) {
removePropertyFromFilter(filterName)
change()
return
}
const ids = entities[kind].map(u => kind === 'users' ? u.username : u.id)
filters.value[filterName] = ids.join(',')
setSingleValueFilter(filterName, filterName, '', 'in')
}
function changeLabelFilter() {
if (entities.labels.length === 0) {
removePropertyFromFilter('labels')
change()
return
}
const labelIDs = entities.labels.map(u => u.id)
filters.value.labels = labelIDs.join(',')
setSingleValueFilter('labels', 'labels', '', 'in')
}
</script>
<style lang="scss" scoped>

View File

@ -1,222 +0,0 @@
<template>
<router-link
:class="{
'has-light-text': !colorIsDark(list.hexColor) || background !== null,
'has-background': blurHashUrl !== '' || background !== null,
}"
:style="{
'background-color': list.hexColor,
'background-image': blurHashUrl !== null ? `url(${blurHashUrl})` : false,
}"
:to="{ name: 'list.index', params: { listId: list.id} }"
class="list-card"
v-if="list !== null && (showArchived ? true : !list.isArchived)"
>
<div
class="list-background background-fade-in"
:class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : undefined}"
/>
<div class="list-content">
<span class="is-archived" v-if="list.isArchived">
{{ $t('namespace.archived') }}
</span>
<BaseButton
v-else
:class="{'is-favorite': list.isFavorite}"
@click.stop="listStore.toggleListFavorite(list)"
class="favorite"
>
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton>
<div class="title">{{ list.title }}</div>
</div>
</router-link>
</template>
<script lang="ts" setup>
import {type PropType, ref, watch} from 'vue'
import ListService from '@/services/list'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import BaseButton from '@/components/base/BaseButton.vue'
import type {IList} from '@/modelTypes/IList'
import {useListStore} from '@/stores/lists'
const background = ref<string | null>(null)
const backgroundLoading = ref(false)
const blurHashUrl = ref('')
const props = defineProps({
list: {
type: Object as PropType<IList>,
required: true,
},
showArchived: {
default: false,
type: Boolean,
},
})
watch(props.list, loadBackground, {immediate: true})
async function loadBackground() {
if (props.list === null || !props.list.backgroundInformation || backgroundLoading.value) {
return
}
const blurHash = await getBlobFromBlurHash(props.list.backgroundBlurHash)
if (blurHash) {
blurHashUrl.value = window.URL.createObjectURL(blurHash)
}
backgroundLoading.value = true
const listService = new ListService()
try {
background.value = await listService.background(props.list)
} finally {
backgroundLoading.value = false
}
}
const listStore = useListStore()
</script>
<style lang="scss" scoped>
.list-card {
cursor: pointer;
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
height: $list-height;
background: var(--white);
margin: 0 $list-spacing $list-spacing 0;
border-radius: $radius;
box-shadow: var(--shadow-sm);
transition: box-shadow $transition;
position: relative;
overflow: hidden;
&.has-light-text .title {
color: var(--grey-100) !important;
}
&.has-background,
.list-background {
background-size: cover;
background-repeat: no-repeat;
background-position: center;
}
&.has-background .title {
text-shadow: 0 0 10px var(--black), 1px 1px 5px var(--grey-700), -1px -1px 5px var(--grey-700);
color: var(--white);
}
.list-background {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
}
&:hover {
box-shadow: var(--shadow-md);
}
&:active,
&:focus,
&:focus:not(:active) {
box-shadow: var(--shadow-xs) !important;
}
@media screen and (min-width: $widescreen) {
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
@media screen and (max-width: $widescreen) and (min-width: $tablet) {
$lists-per-row: 3;
& {
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
}
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
@media screen and (max-width: $tablet) {
$lists-per-row: 2;
& {
width: calc((100% - #{($lists-per-row - 1) * 1rem}) / #{$lists-per-row});
}
&:nth-child(#{$lists-per-row}n) {
margin-right: 0;
}
}
@media screen and (max-width: $mobile) {
$lists-per-row: 1;
& {
width: 100%;
margin-right: 0;
}
}
.list-content {
display: flex;
align-content: space-between;
flex-wrap: wrap;
padding: 1rem;
position: absolute;
height: 100%;
width: 100%;
.is-archived {
font-size: .75rem;
}
.favorite {
margin-left: auto;
transition: opacity $transition, color $transition;
opacity: 0;
display: block;
&:hover,
&.is-favorite {
color: var(--warning);
}
}
.favorite.is-favorite,
&:hover .favorite {
opacity: 1;
}
.title {
align-self: flex-end;
font-family: $vikunja-font;
font-weight: 400;
font-size: 1.5rem;
color: var(--text);
width: 100%;
margin-bottom: 0;
max-height: calc(100% - 2rem); // 1rem padding, 1rem height of the "is archived" badge
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
}
}
}
</style>

View File

@ -0,0 +1,55 @@
import {ref, watch, type Ref} from 'vue'
import ListService from '@/services/list'
import type {IList} from '@/modelTypes/IList'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
export function useListBackground(list: Ref<IList>) {
const background = ref<string | null>(null)
const backgroundLoading = ref(false)
const blurHashUrl = ref('')
watch(
() => [list.value.id, list.value.backgroundBlurHash] as [IList['id'], IList['backgroundBlurHash']],
async ([listId, blurHash], oldValue) => {
if (
list.value === null ||
!list.value.backgroundInformation ||
backgroundLoading.value
) {
return
}
const [oldListId, oldBlurHash] = oldValue || []
if (
oldValue !== undefined &&
listId === oldListId && blurHash === oldBlurHash
) {
// list hasn't changed
return
}
backgroundLoading.value = true
try {
const blurHashPromise = getBlobFromBlurHash(blurHash).then((blurHash) => {
blurHashUrl.value = blurHash ? window.URL.createObjectURL(blurHash) : ''
})
const listService = new ListService()
const backgroundPromise = listService.background(list.value).then((result) => {
background.value = result
})
await Promise.all([blurHashPromise, backgroundPromise])
} finally {
backgroundLoading.value = false
}
},
{ immediate: true },
)
return {
background,
blurHashUrl,
backgroundLoading,
}
}

View File

@ -0,0 +1,67 @@
<template>
<transition :name="name">
<slot />
</transition>
</template>
<script setup lang="ts">
defineProps<{
name: 'flash-background' | 'fade' | 'width' | 'modal'
}>()
</script>
<style scoped lang="scss">
$flash-background-duration: 750ms;
.flash-background-enter-from,
.flash-background-enter-active {
animation: flash-background $flash-background-duration ease 1;
}
@keyframes flash-background {
0% {
background: var(--primary-light);
}
100% {
background: transparent;
}
}
@media (prefers-reduced-motion: reduce) {
@keyframes flash-background {
0% {
background: transparent;
}
}
}
.fade-enter-active,
.fade-leave-active {
transition: opacity $transition-duration;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
.width-enter-active,
.width-leave-active {
transition: width $transition-duration;
}
.width-enter-from,
.width-leave-to {
width: 0;
}
.modal-enter,
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
transform: scale(0.9);
}
</style>

View File

@ -70,6 +70,8 @@ import {
} from '@fortawesome/free-regular-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from '@/types/vue-fontawesome'
library.add(faAlignLeft)
library.add(faAngleRight)
library.add(faArchive)
@ -136,4 +138,5 @@ library.add(faTrashAlt)
library.add(faUser)
library.add(faUsers)
export default FontAwesomeIcon
// overwriting the wrong types
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes

View File

@ -35,6 +35,9 @@
</template>
<script setup lang="ts">
import type {PropType} from 'vue'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
import BaseButton from '@/components/base/BaseButton.vue'
defineProps({
@ -51,7 +54,7 @@ defineProps({
default: false,
},
closeIcon: {
type: String,
type: String as PropType<IconProp>,
default: 'times',
},
shadow: {

View File

@ -6,10 +6,10 @@
</template>
<script lang="ts" setup>
import type { Color } from 'csstype'
import type { DataType } from 'csstype'
defineProps< {
color: Color,
color: DataType.Color,
}>()
</script>

View File

@ -10,7 +10,7 @@
:loading="loading"
>
<div class="p-4">
<slot />
<slot/>
</div>
<template #footer>
@ -30,10 +30,12 @@
{{ $t('misc.cancel') }}
</x-button>
<x-button
v-if="hasPrimaryAction"
variant="primary"
@click.prevent.stop="primary()"
:icon="primaryIcon"
:disabled="primaryDisabled || loading"
class="ml-2"
>
{{ primaryLabel || $t('misc.create') }}
</x-button>
@ -44,6 +46,9 @@
</template>
<script setup lang="ts">
import type {PropType} from 'vue'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
defineProps({
title: {
type: String,
@ -53,13 +58,17 @@ defineProps({
type: String,
},
primaryIcon: {
type: String,
type: String as PropType<IconProp>,
default: 'plus',
},
primaryDisabled: {
type: Boolean,
default: false,
},
hasPrimaryAction: {
type: Boolean,
default: true,
},
tertiary: {
type: String,
default: '',

View File

@ -1,46 +1,24 @@
<template>
<component
:is="componentNodeName"
v-bind="elementBindings"
:to="to"
class="dropdown-item">
<BaseButton class="dropdown-item">
<span class="icon" v-if="icon">
<icon :icon="icon"/>
<Icon :icon="icon"/>
</span>
<span>
<slot></slot>
<slot />
</span>
</component>
</BaseButton>
</template>
<script lang="ts" setup>
import {ref, useAttrs, watchEffect} from 'vue'
import BaseButton, { type BaseButtonProps } from '@/components/base//BaseButton.vue'
import Icon from '@/components/misc/Icon'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const props = defineProps<{
to?: object,
icon?: string | string[],
}>()
export interface DropDownItemProps extends BaseButtonProps {
icon?: IconProp,
}
const componentNodeName = ref<Node['nodeName']>('a')
const elementBindings = ref({})
const attrs = useAttrs()
watchEffect(() => {
let nodeName = 'a'
if (props.to) {
nodeName = 'router-link'
}
if ('href' in attrs) {
nodeName = 'BaseButton'
}
componentNodeName.value = nodeName
elementBindings.value = {
...attrs,
}
})
defineProps<DropDownItemProps>()
</script>
<style scoped lang="scss">
@ -51,10 +29,6 @@ watchEffect(() => {
line-height: 1.5;
padding: $item-padding;
position: relative;
}
a.dropdown-item,
button.dropdown-item {
text-align: inherit;
white-space: nowrap;
width: 100%;
@ -62,34 +36,29 @@ button.dropdown-item {
align-items: center;
justify-content: left !important;
&:hover {
background-color: var(--grey-100) !important;
}
&.is-active {
background-color: var(--link);
color: var(--link-invert);
}
.icon {
padding-right: .5rem;
}
.icon:not(.has-text-success) {
color: var(--grey-300) !important;
}
&.has-text-danger .icon {
color: var(--danger) !important;
&:hover:not(.is-disabled) {
background-color: var(--grey-100);
}
&.is-disabled {
cursor: not-allowed;
&:hover {
background-color: transparent;
}
}
}
.icon {
padding-right: .5rem;
&:not(.has-text-success) {
color: var(--grey-300) !important;
}
}
.has-text-danger .icon {
color: var(--danger) !important;
}
</style>

View File

@ -6,25 +6,27 @@
</BaseButton>
</slot>
<transition name="fade">
<CustomTransition name="fade">
<div class="dropdown-menu" v-if="open">
<div class="dropdown-content">
<slot :close="close"></slot>
</div>
</div>
</transition>
</CustomTransition>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import {ref, type PropType} from 'vue'
import {onClickOutside} from '@vueuse/core'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import BaseButton from '@/components/base/BaseButton.vue'
defineProps({
triggerIcon: {
type: String,
type: String as PropType<IconProp>,
default: 'ellipsis-h',
},
})

View File

@ -7,6 +7,12 @@
</message>
</template>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
import Message from '@/components/misc/message.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'

View File

@ -0,0 +1,225 @@
<template>
<input
type="text"
data-input
:disabled="disabled"
v-bind="attrs"
ref="root"
/>
</template>
<script lang="ts">
import flatpickr from 'flatpickr'
import 'flatpickr/dist/flatpickr.css'
// FIXME: Not sure how to alias these correctly
// import Options = Flatpickr.Options doesn't work
type Hook = flatpickr.Options.Hook
type HookKey = flatpickr.Options.HookKey
type Options = flatpickr.Options.Options
type DateOption = flatpickr.Options.DateOption
function camelToKebab(string: string) {
return string.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase()
}
function arrayify<T = unknown>(obj: T) {
return obj instanceof Array
? obj
: [obj]
}
function nullify<T = unknown>(value: T) {
return (value && (value as unknown[]).length)
? value
: null
}
// Events to emit, copied from flatpickr source
const includedEvents = [
'onChange',
'onClose',
'onDestroy',
'onMonthChange',
'onOpen',
'onYearChange',
] as HookKey[]
// Let's not emit these events by default
const excludedEvents = [
'onValueUpdate',
'onDayCreate',
'onParseConfig',
'onReady',
'onPreCalendarPosition',
'onKeyDown',
] as HookKey[]
// Keep a copy of all events for later use
const allEvents = includedEvents.concat(excludedEvents)
export default {inheritAttrs: false}
</script>
<script setup lang="ts">
import {computed, onBeforeUnmount, onMounted, ref, toRefs, useAttrs, watch, watchEffect, type PropType} from 'vue'
const props = defineProps({
modelValue: {
type: [String, Number, Date, Array] as PropType<DateOption | DateOption[] | null>,
default: null,
},
// https://flatpickr.js.org/options/
config: {
type: Object as PropType<Options>,
default: () => ({
defaultDate: null,
wrap: false,
}),
},
events: {
type: Array as PropType<HookKey[]>,
default: () => includedEvents,
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits([
'blur',
'update:modelValue',
...allEvents.map(camelToKebab),
])
const {modelValue, config, disabled} = toRefs(props)
// bind listener like onBlur
const attrs = useAttrs()
const root = ref<HTMLInputElement | null>(null)
const fp = ref<flatpickr.Instance | null>(null)
const safeConfig = ref<Options>({ ...props.config })
function prepareConfig() {
// Don't mutate original object on parent component
const newConfig: Options = { ...props.config }
props.events.forEach((hook) => {
// Respect global callbacks registered via setDefault() method
const globalCallbacks = flatpickr.defaultConfig[hook] || []
// Inject our own method along with user callback
const localCallback: Hook = (...args) => emit(camelToKebab(hook), ...args)
// Overwrite with merged array
newConfig[hook] = arrayify(newConfig[hook] || []).concat(
globalCallbacks,
localCallback,
)
})
// Watch for value changed by date-picker itself and notify parent component
const onChange: Hook = (dates) => emit('update:modelValue', dates)
newConfig['onChange'] = arrayify(newConfig['onChange'] || []).concat(onChange)
// Flatpickr does not emit input event in some cases
// const onClose: Hook = (_selectedDates, dateStr) => emit('update:modelValue', dateStr)
// newConfig['onClose'] = arrayify(newConfig['onClose'] || []).concat(onClose)
// Set initial date without emitting any event
newConfig.defaultDate = props.modelValue || newConfig.defaultDate
safeConfig.value = newConfig
return safeConfig.value
}
onMounted(() => {
if (
fp.value || // Return early if flatpickr is already loaded
!root.value // our input needs to be mounted
) {
return
}
prepareConfig()
/**
* Get the HTML node where flatpickr to be attached
* Bind on parent element if wrap is true
*/
const element = props.config.wrap
? root.value.parentNode
: root.value
// Init flatpickr
fp.value = flatpickr(element, safeConfig.value)
})
onBeforeUnmount(() => fp.value?.destroy())
watch(config, () => {
if (!fp.value) return
// Workaround: Don't pass hooks to configs again otherwise
// previously registered hooks will stop working
// Notice: we are looping through all events
// This also means that new callbacks can not be passed once component has been initialized
allEvents.forEach((hook) => {
delete safeConfig.value?.[hook]
})
fp.value.set(safeConfig.value)
// Passing these properties in `set()` method will cause flatpickr to trigger some callbacks
const configCallbacks = ['locale', 'showMonths'] as (keyof Options)[]
// Workaround: Allow to change locale dynamically
configCallbacks.forEach(name => {
if (typeof safeConfig.value?.[name] !== 'undefined' && fp.value) {
fp.value.set(name, safeConfig.value[name])
}
})
}, {deep:true})
const fpInput = computed(() => {
if (!fp.value) return
return fp.value.altInput || fp.value.input
})
/**
* init blur event
* (is required by many validation libraries)
*/
function onBlur(event: Event) {
emit('blur', nullify((event.target as HTMLInputElement).value))
}
watchEffect(() => fpInput.value?.addEventListener('blur', onBlur))
onBeforeUnmount(() => fpInput.value?.removeEventListener('blur', onBlur))
/**
* Watch for the disabled property and sets the value to the real input.
*/
watchEffect(() => {
if (disabled.value) {
fpInput.value?.setAttribute('disabled', '')
} else {
fpInput.value?.removeAttribute('disabled')
}
})
/**
* Watch for changes from parent component and update DOM
*/
watch(
modelValue,
newValue => {
// Prevent updates if v-model value is same as input's current value
if (!root.value || newValue === nullify(root.value.value)) return
// Make sure we have a flatpickr instance and
// notify flatpickr instance that there is a change in value
fp.value?.setDate(newValue, true)
},
{deep: true},
)
</script>

View File

@ -2,6 +2,12 @@
<div class="loader-container is-loading"></div>
</template>
<script lang="ts">
export default {
inheritAttrs: false,
}
</script>
<style scoped lang="scss">
.loader-container {
height: 100%;

View File

@ -1,7 +1,7 @@
<template>
<Teleport to="body">
<!-- FIXME: transition should not be included in the modal -->
<transition :name="transitionName">
<CustomTransition :name="transitionName" appear>
<section
v-if="enabled"
class="modal-mask"
@ -59,7 +59,7 @@
</div>
</div>
</section>
</transition>
</CustomTransition>
</Teleport>
</template>
@ -70,6 +70,7 @@ export default {
</script>
<script lang="ts" setup>
import CustomTransition from '@/components/misc/CustomTransition.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {ref, useAttrs, watchEffect} from 'vue'
import {useScrollLock} from '@vueuse/core'
@ -99,6 +100,9 @@ watchEffect(() => {
</script>
<style lang="scss" scoped>
$modal-margin: 4rem;
$modal-width: 1024px;
.modal-mask {
position: fixed;
z-index: 4000;
@ -147,16 +151,16 @@ watchEffect(() => {
// scrolling-content
// used e.g. for <TaskDetailViewModal>
.scrolling .modal-content {
max-width: 1024px;
max-width: $modal-width;
width: 100%;
margin: 4rem auto;
margin: $modal-margin auto;
max-height: none; // reset bulma
overflow: visible; // reset bulma
@media screen and (min-width: $tablet) {
max-height: none; // reset bulma
margin: 4rem auto; // reset bulma
margin: $modal-margin auto; // reset bulma
width: 100%;
}
@ -189,14 +193,23 @@ watchEffect(() => {
}
.close {
$close-button-min-space: 84px;
$close-button-padding: 26px;
position: fixed;
top: 5px;
right: 26px;
color: var(--white);
right: $close-button-padding;
color: var(--grey-900);
font-size: 2rem;
@media screen and (max-width: $desktop) {
color: var(--grey-900);
@media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
top: calc(5px + $modal-margin);
right: 50%;
// we align the close button to the modal until there is enough space outside for it
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
}
// we can only use light color when there is enough space for the close button next to the modal
@media screen and (min-width: calc(#{$desktop } + #{$close-button-min-space})) {
color: var(--white);
}
}
</style>

View File

@ -16,11 +16,7 @@
</div>
<div
class="buttons is-right"
v-if="
item.data &&
item.data.actions &&
item.data.actions.length > 0
"
v-if="item.data?.actions?.length > 0"
>
<x-button
:key="'action_' + i"

View File

@ -1,20 +1,20 @@
<template>
<slot name="trigger" :isOpen="open" :toggle="toggle"></slot>
<div class="popup" :class="{'is-open': open, 'has-overflow': props.hasOverflow && open}" ref="popup">
<div
class="popup"
:class="{
'is-open': open,
'has-overflow': props.hasOverflow && open
}"
ref="popup"
>
<slot name="content" :isOpen="open"/>
</div>
</template>
<script setup lang="ts">
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {onBeforeUnmount, onMounted, ref} from 'vue'
const open = ref(false)
const popup = ref(null)
const toggle = () => {
open.value = !open.value
}
import {ref} from 'vue'
import {onClickOutside} from '@vueuse/core'
const props = defineProps({
hasOverflow: {
@ -23,24 +23,22 @@ const props = defineProps({
},
})
function hidePopup(e) {
const open = ref(false)
const popup = ref<HTMLElement | null>(null)
function close() {
open.value = false
}
function toggle() {
open.value = !open.value
}
onClickOutside(popup, () => {
if (!open.value) {
return
}
// we actually want to use popup.$el, not its value.
// eslint-disable-next-line vue/no-ref-as-operand
closeWhenClickedOutside(e, popup.value, () => {
open.value = false
})
}
onMounted(() => {
document.addEventListener('click', hidePopup)
})
onBeforeUnmount(() => {
document.removeEventListener('click', hidePopup)
close()
})
</script>

View File

@ -3,7 +3,7 @@
<div class="offline" style="height: 0;width: 0;"></div>
<div class="app offline" v-if="!online">
<div class="offline-message">
<h1>{{ $t('offline.title') }}</h1>
<h1 class="title">{{ $t('offline.title') }}</h1>
<p>{{ $t('offline.text') }}</p>
</div>
</div>
@ -29,7 +29,7 @@
</card>
</no-auth-wrapper>
</section>
<transition name="fade">
<CustomTransition name="fade">
<section class="vikunja-loading" v-if="showLoading">
<Logo class="logo"/>
<p>
@ -37,7 +37,7 @@
{{ $t('ready.loading') }}
</p>
</section>
</transition>
</CustomTransition>
</template>
<script lang="ts" setup>
@ -47,6 +47,7 @@ import {useRouter, useRoute} from 'vue-router'
import Logo from '@/assets/logo.svg?component'
import ApiConfig from '@/components/misc/api-config.vue'
import Message from '@/components/misc/message.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
@ -61,7 +62,7 @@ const route = useRoute()
const baseStore = useBaseStore()
const ready = ref(false)
const ready = computed(() => baseStore.ready)
const online = useOnline()
const error = ref('')
@ -70,11 +71,11 @@ const showLoading = computed(() => !ready.value && error.value === '')
async function load() {
try {
await baseStore.loadApp()
const redirectTo = getAuthForRoute(route)
baseStore.setReady(true)
const redirectTo = await getAuthForRoute(route)
if (typeof redirectTo !== 'undefined') {
await router.push(redirectTo)
}
ready.value = true
} catch (e: unknown) {
error.value = String(e)
}
@ -128,14 +129,14 @@ load()
bottom: 5vh;
color: $white;
padding: 0 1rem;
}
h1 {
font-weight: bold;
font-size: 1.5rem;
text-align: center;
color: $white;
font-weight: 700 !important;
font-size: 1.5rem;
}
.title {
font-weight: bold;
font-size: 1.5rem;
text-align: center;
color: $white;
font-weight: 700 !important;
font-size: 1.5rem;
}
</style>

View File

@ -13,7 +13,7 @@
v-else-if="type === 'dropdown'"
v-tooltip="tooltipText"
@click="changeSubscription"
:class="{'is-disabled': disabled}"
:disabled="disabled"
:icon="iconName"
>
{{ buttonText }}
@ -44,6 +44,7 @@ import SubscriptionModel from '@/models/subscription'
import type {ISubscription} from '@/modelTypes/ISubscription'
import {success} from '@/message'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const props = defineProps({
entity: String,
@ -104,7 +105,7 @@ const tooltipText = computed(() => {
})
const buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const iconName = computed(() => props.modelValue ? ['far', 'bell-slash'] : 'bell')
const iconName = computed<IconProp>(() => props.modelValue ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => props.modelValue && subscriptionEntity.value !== props.entity)
function changeSubscription() {

View File

@ -1,23 +1,27 @@
<template>
<div :class="{'is-inline': isInline}" class="user">
<div
class="user"
:class="{'is-inline': isInline}"
>
<img
:height="avatarSize"
:src="getAvatarUrl(user, avatarSize)"
:width="avatarSize"
alt=""
:alt="'Avatar of ' + displayName"
class="avatar"
v-tooltip="getDisplayName(user)"/>
<span class="username" v-if="showUsername">{{ getDisplayName(user) }}</span>
v-tooltip="displayName"
/>
<span class="username" v-if="showUsername">{{ displayName }}</span>
</div>
</template>
<script lang="ts" setup>
import type {PropType} from 'vue'
import {computed, type PropType} from 'vue'
import {getAvatarUrl, getDisplayName} from '@/models/user'
import type {IUser} from '@/modelTypes/IUser'
defineProps({
const props = defineProps({
user: {
type: Object as PropType<IUser>,
required: true,
@ -38,6 +42,8 @@ defineProps({
default: false,
},
})
const displayName = computed(() => getDisplayName(props.user))
</script>
<style lang="scss" scoped>
@ -47,12 +53,11 @@ defineProps({
&.is-inline {
display: inline;
}
}
img {
border-radius: 100%;
vertical-align: middle;
margin-right: .5rem;
}
.avatar {
border-radius: 100%;
vertical-align: middle;
margin-right: .5rem;
}
</style>

View File

@ -1,5 +1,13 @@
<template>
<dropdown>
<template #trigger="triggerProps">
<slot name="trigger" v-bind="triggerProps">
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</slot>
</template>
<template v-if="namespace.isArchived">
<dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
@ -56,6 +64,7 @@
<script setup lang="ts">
import {ref, onMounted, type PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import Subscription from '@/components/misc/subscription.vue'
@ -79,11 +88,15 @@ onMounted(() => {
function setSubscriptionInStore(sub: ISubscription) {
subscription.value = sub
namespaceStore.setNamespaces([
{
...props.namespace,
subscription: sub,
},
])
namespaceStore.setNamespaceById({
...props.namespace,
subscription: sub,
})
}
</script>
<style scoped lang="scss">
.dropdown-trigger {
padding: 0.5rem;
}
</style>

View File

@ -7,7 +7,7 @@
</BaseButton>
</div>
<transition name="fade">
<CustomTransition name="fade">
<div class="notifications-list" v-if="showNotifications" ref="popup">
<span class="head">{{ $t('notification.title') }}</span>
<div
@ -42,7 +42,7 @@
</span>
</p>
</div>
</transition>
</CustomTransition>
</div>
</template>
@ -52,6 +52,7 @@ import {useRouter} from 'vue-router'
import NotificationService from '@/services/notification'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import User from '@/components/misc/user.vue'
import { NOTIFICATION_NAMES as names, type INotification} from '@/modelTypes/INotification'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
@ -76,7 +77,7 @@ const notifications = computed(() => {
})
const userInfo = computed(() => authStore.info)
let interval: number
let interval: ReturnType<typeof setInterval>
onMounted(() => {
loadNotifications()

File diff suppressed because it is too large Load Diff

View File

@ -169,21 +169,19 @@
</table>
</div>
<transition name="modal">
<modal
@close="showDeleteModal = false"
@submit="remove(listId)"
v-if="showDeleteModal"
>
<template #header>
<span>{{ $t('list.share.links.remove') }}</span>
</template>
<modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@submit="remove(listId)"
>
<template #header>
<span>{{ $t('list.share.links.remove') }}</span>
</template>
<template #text>
<p>{{ $t('list.share.links.removeText') }}</p>
</template>
</modal>
</transition>
<template #text>
<p>{{ $t('list.share.links.removeText') }}</p>
</template>
</modal>
</div>
</template>
@ -297,6 +295,4 @@ function getShareLink(hash: string, view: ListView = LIST_VIEWS.LIST) {
.sharables-list:not(.card-content) {
overflow-y: auto
}
@include modal-transition();
</style>

View File

@ -113,22 +113,20 @@
{{ $t('list.share.userTeam.notShared', {type: shareTypeNames}) }}
</nothing>
<transition name="modal">
<modal
@close="showDeleteModal = false"
@submit="deleteSharable()"
v-if="showDeleteModal"
>
<template #header>
<span>{{
$t('list.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
}}</span>
</template>
<template #text>
<p>{{ $t('list.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
</template>
</modal>
</transition>
<modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@submit="deleteSharable()"
>
<template #header>
<span>{{
$t('list.share.userTeam.removeHeader', {type: shareTypeName, sharable: sharableName})
}}</span>
</template>
<template #text>
<p>{{ $t('list.share.userTeam.removeText', {type: shareTypeName, sharable: sharableName}) }}</p>
</template>
</modal>
</div>
</template>
@ -381,8 +379,4 @@ async function find(query: string) {
return typeof sharables.value.find(s => s.id === m.id) === 'undefined'
})
}
</script>
<style lang="scss" scoped>
@include modal-transition();
</style>
</script>

View File

@ -0,0 +1,246 @@
<template>
<Loading
v-if="props.isLoading && !ganttBars.length || dayjsLanguageLoading"
class="gantt-container"
/>
<div class="gantt-container" v-else>
<GGanttChart
:date-format="DAYJS_ISO_DATE_FORMAT"
:chart-start="isoToKebabDate(filters.dateFrom)"
:chart-end="isoToKebabDate(filters.dateTo)"
precision="day"
bar-start="startDate"
bar-end="endDate"
:grid="true"
@dragend-bar="updateGanttTask"
@dblclick-bar="openTask"
:width="ganttChartWidth + 'px'"
>
<template #timeunit="{value, date}">
<div
class="timeunit-wrapper"
:class="{'today': dateIsToday(date)}"
>
<span>{{ value }}</span>
<span class="weekday">
{{ weekDayFromDate(date) }}
</span>
</div>
</template>
<GGanttRow
v-for="(bar, k) in ganttBars"
:key="k"
label=""
:bars="bar"
/>
</GGanttChart>
</div>
</template>
<script setup lang="ts">
import {computed, ref, watch, toRefs} from 'vue'
import {useRouter} from 'vue-router'
import {useNow} from '@vueuse/core'
import {getHexColor} from '@/models/task'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import {isoToKebabDate} from '@/helpers/time/isoToKebabDate'
import {parseKebabDate} from '@/helpers/time/parseKebabDate'
import type {ITask, ITaskPartialWithId} from '@/modelTypes/ITask'
import type {DateISO} from '@/types/DateISO'
import type {GanttFilters} from '@/views/list/helpers/useGanttFilters'
import {
extendDayjs,
GGanttChart,
GGanttRow,
type GanttBarObject,
} from '@infectoone/vue-ganttastic'
import Loading from '@/components/misc/loading.vue'
import {MILLISECONDS_A_DAY} from '@/constants/date'
import {useWeekDayFromDate} from '@/helpers/time/formatDate'
export interface GanttChartProps {
isLoading: boolean,
filters: GanttFilters,
tasks: Map<ITask['id'], ITask>,
defaultTaskStartDate: DateISO
defaultTaskEndDate: DateISO
}
const DAYJS_ISO_DATE_FORMAT = 'YYYY-MM-DD'
const props = defineProps<GanttChartProps>()
const emit = defineEmits<{
(e: 'update:task', task: ITaskPartialWithId): void
}>()
const {tasks, filters} = toRefs(props)
// setup dayjs for vue-ganttastic
const dayjsLanguageLoading = ref(false)
// const dayjsLanguageLoading = useDayjsLanguageSync(dayjs)
extendDayjs()
const router = useRouter()
const dateFromDate = computed(() => new Date(new Date(filters.value.dateFrom).setHours(0,0,0,0)))
const dateToDate = computed(() => new Date(new Date(filters.value.dateTo).setHours(23,59,0,0)))
const DAY_WIDTH_PIXELS = 30
const ganttChartWidth = computed(() => {
const dateDiff = Math.floor((dateToDate.value.valueOf() - dateFromDate.value.valueOf()) / MILLISECONDS_A_DAY)
return dateDiff * DAY_WIDTH_PIXELS
})
const ganttBars = ref<GanttBarObject[][]>([])
/**
* Update ganttBars when tasks change
*/
watch(
tasks,
() => {
ganttBars.value = []
tasks.value.forEach(t => ganttBars.value.push(transformTaskToGanttBar(t)))
},
{deep: true, immediate: true},
)
function transformTaskToGanttBar(t: ITask) {
const black = 'var(--grey-800)'
return [{
startDate: isoToKebabDate(t.startDate ? t.startDate.toISOString() : props.defaultTaskStartDate),
endDate: isoToKebabDate(t.endDate ? t.endDate.toISOString() : props.defaultTaskEndDate),
ganttBarConfig: {
id: String(t.id),
label: t.title,
hasHandles: true,
style: {
color: t.startDate ? (colorIsDark(getHexColor(t.hexColor)) ? black : 'white') : black,
backgroundColor: t.startDate ? getHexColor(t.hexColor) : 'var(--grey-100)',
border: t.startDate ? '' : '2px dashed var(--grey-300)',
'text-decoration': t.done ? 'line-through' : null,
},
},
} as GanttBarObject]
}
async function updateGanttTask(e: {
bar: GanttBarObject;
e: MouseEvent;
datetime?: string | undefined;
}) {
emit('update:task', {
id: Number(e.bar.ganttBarConfig.id),
startDate: new Date(parseKebabDate(e.bar.startDate).setHours(0,0,0,0)),
endDate: new Date(parseKebabDate(e.bar.endDate).setHours(23,59,0,0)),
})
}
function openTask(e: {
bar: GanttBarObject;
e: MouseEvent;
datetime?: string | undefined;
}) {
router.push({
name: 'task.detail',
params: {id: e.bar.ganttBarConfig.id},
state: {backdropView: router.currentRoute.value.fullPath},
})
}
const weekDayFromDate = useWeekDayFromDate()
const today = useNow()
const dateIsToday = computed(() => (date: Date) => {
return (
date.getDate() === today.value.getDate() &&
date.getMonth() === today.value.getMonth() &&
date.getFullYear() === today.value.getFullYear()
)
})
</script>
<style scoped lang="scss">
.gantt-container {
overflow-x: auto;
}
</style>
<style lang="scss">
// Not scoped because we need to style the elements inside the gantt chart component
.g-gantt-chart {
width: max-content;
}
.g-gantt-row-label {
display: none !important;
}
.g-upper-timeunit, .g-timeunit {
background: var(--white) !important;
font-family: $vikunja-font;
}
.g-upper-timeunit {
font-weight: bold;
border-right: 1px solid var(--grey-200);
padding: .5rem 0;
}
.g-timeunit .timeunit-wrapper {
padding: 0.5rem 0;
font-size: 1rem !important;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
&.today {
background: var(--primary);
color: var(--white);
border-radius: 5px 5px 0 0;
font-weight: bold;
}
.weekday {
font-size: 0.8rem;
}
}
.g-timeaxis {
height: auto !important;
box-shadow: none !important;
}
.g-gantt-row > .g-gantt-row-bars-container {
border-bottom: none !important;
border-top: none !important;
}
.g-gantt-row:nth-child(odd) {
background: hsla(var(--grey-100-hsl), .5);
}
.g-gantt-bar {
border-radius: $radius * 1.5;
overflow: visible;
font-size: .85rem;
&-handle-left,
&-handle-right {
width: 6px !important;
height: 75% !important;
opacity: .75 !important;
border-radius: $radius !important;
margin-top: 4px;
}
}
</style>

View File

@ -0,0 +1,80 @@
<template>
<form
@submit.prevent="createTask"
class="add-new-task"
>
<CustomTransition name="width">
<input
v-if="newTaskFieldActive"
v-model="newTaskTitle"
@blur="hideCreateNewTask"
@keyup.esc="newTaskFieldActive = false"
class="input"
ref="newTaskTitleField"
type="text"
/>
</CustomTransition>
<x-button @click="showCreateTaskOrCreate" :shadow="false" icon="plus">
{{ $t('task.new') }}
</x-button>
</form>
</template>
<script setup lang="ts">
import {nextTick, ref} from 'vue'
import type {ITask} from '@/modelTypes/ITask'
import CustomTransition from '@/components/misc/CustomTransition.vue'
const emit = defineEmits<{
(e: 'create-task', title: string): Promise<ITask>
}>()
const newTaskFieldActive = ref(false)
const newTaskTitleField = ref()
const newTaskTitle = ref('')
function showCreateTaskOrCreate() {
if (!newTaskFieldActive.value) {
// Timeout to not send the form if the field isn't even shown
setTimeout(() => {
newTaskFieldActive.value = true
nextTick(() => newTaskTitleField.value.focus())
}, 100)
} else {
createTask()
}
}
function hideCreateNewTask() {
if (newTaskTitle.value === '') {
nextTick(() => (newTaskFieldActive.value = false))
}
}
async function createTask() {
if (!newTaskFieldActive.value) {
return
}
await emit('create-task', newTaskTitle.value)
newTaskTitle.value = ''
hideCreateNewTask()
}
</script>
<style scoped lang="scss">
.add-new-task {
padding: 1rem .7rem .4rem .7rem;
display: flex;
max-width: 450px;
.input {
margin-right: .7rem;
font-size: .8rem;
}
.button {
font-size: .68rem;
}
}
</style>

View File

@ -41,9 +41,8 @@
</template>
<script setup lang="ts">
import {computed, ref, unref, watch} from 'vue'
import {computed, ref} from 'vue'
import {useI18n} from 'vue-i18n'
import {debouncedWatch, type MaybeRef, tryOnMounted, useWindowSize} from '@vueuse/core'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import type {ITask} from '@/modelTypes/ITask'
@ -53,74 +52,8 @@ import TaskRelationModel from '@/models/taskRelation'
import {RELATION_KIND} from '@/types/IRelationKind'
import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
function useAutoHeightTextarea(value: MaybeRef<string>) {
const textarea = ref<HTMLInputElement>()
const minHeight = ref(0)
// adapted from https://github.com/LeaVerou/stretchy/blob/47f5f065c733029acccb755cae793009645809e2/src/stretchy.js#L34
function resize(textareaEl: HTMLInputElement | undefined) {
if (!textareaEl) return
let empty
// the value here is the attribute value
if (!textareaEl.value && textareaEl.placeholder) {
empty = true
textareaEl.value = textareaEl.placeholder
}
const cs = getComputedStyle(textareaEl)
textareaEl.style.minHeight = ''
textareaEl.style.height = '0'
const offset = textareaEl.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)
const height = textareaEl.scrollHeight + offset + 'px'
textareaEl.style.height = height
// calculate min-height for the first time
if (!minHeight.value) {
minHeight.value = parseFloat(height)
}
textareaEl.style.minHeight = minHeight.value.toString()
if (empty) {
textareaEl.value = ''
}
}
tryOnMounted(() => {
if (textarea.value) {
// we don't want scrollbars
textarea.value.style.overflowY = 'hidden'
}
})
const {width: windowWidth} = useWindowSize()
debouncedWatch(
windowWidth,
() => resize(textarea.value),
{debounce: 200},
)
// It is not possible to get notified of a change of the value attribute of a textarea without workarounds (setTimeout)
// So instead we watch the value that we bound to it.
watch(
() => [textarea.value, unref(value)],
() => resize(textarea.value),
{
immediate: true, // calculate initial size
flush: 'post', // resize after value change is rendered to DOM
},
)
return textarea
}
import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea'
import {getLabelsFromPrefix} from '@/modules/parseTaskText'
const props = defineProps({
defaultPosition: {
@ -150,6 +83,7 @@ function resetEmptyTitleError(e) {
}
const loading = computed(() => taskStore.isLoading)
async function addTask() {
if (newTaskTitle.value === '') {
errorMessage.value = t('list.create.addTitleRequired')
@ -166,14 +100,27 @@ async function addTask() {
// by quick add magic.
const createdTasks: { [key: ITask['title']]: ITask } = {}
const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value)
const newTasks = tasksToCreate.map(async ({title}) => {
// We ensure all labels exist prior to passing them down to the create task method
// In the store it will only ever see one task at a time so there's no way to reliably
// check if a new label was created before (because everything happens async).
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title) ?? [])
await taskStore.ensureLabelsExist(allLabels.flat())
const newTasks = tasksToCreate.map(async ({title, list}) => {
if (title === '') {
return
}
// If the task has a list specified, make sure to use it
let listId = null
if (list !== null) {
listId = await taskStore.findListId({list, listId: 0})
}
const task = await taskStore.createNewTask({
title,
listId: authStore.settings.defaultListId,
listId: listId || authStore.settings.defaultListId,
position: props.defaultPosition,
})
createdTasks[title] = task
@ -214,7 +161,7 @@ async function addTask() {
return rel
})
await Promise.all(relations)
} catch (e: { message?: string }) {
} catch (e: any) {
newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_LIST') {
errorMessage.value = t('list.create.addListRequired')

View File

@ -1,187 +0,0 @@
<template>
<card
class="taskedit"
:title="$t('list.list.editTask')"
@close="$emit('close')"
:has-close="true"
>
<form @submit.prevent="editTaskSubmit()">
<div class="field">
<label class="label" for="tasktext">{{ $t('task.attributes.title') }}</label>
<div class="control">
<input
:class="{ disabled: taskService.loading }"
:disabled="taskService.loading || undefined"
@change="editTaskSubmit()"
class="input"
id="tasktext"
type="text"
v-focus
v-model="taskEditTask.title"
/>
</div>
</div>
<div class="field">
<label class="label" for="taskdescription">{{ $t('task.attributes.description') }}</label>
<div class="control">
<editor
:preview-is-default="false"
id="taskdescription"
:placeholder="$t('task.description.placeholder')"
v-if="editorActive"
v-model="taskEditTask.description"
/>
</div>
</div>
<strong>{{ $t('task.attributes.reminders') }}</strong>
<reminders
v-model="taskEditTask.reminderDates"
@update:model-value="editTaskSubmit()"
/>
<div class="field">
<label class="label">{{ $t('task.attributes.labels') }}</label>
<div class="control">
<edit-labels
:task-id="taskEditTask.id"
v-model="taskEditTask.labels"
/>
</div>
</div>
<div class="field">
<label class="label">{{ $t('task.attributes.color') }}</label>
<div class="control">
<color-picker v-model="taskEditTask.hexColor" />
</div>
</div>
<x-button
:loading="taskService.loading"
class="is-fullwidth"
@click="editTaskSubmit()"
>
{{ $t('misc.save') }}
</x-button>
<router-link
class="mt-2 has-text-centered is-block"
:to="taskDetailRoute"
>
{{ $t('task.openDetail') }}
</router-link>
</form>
</card>
</template>
<script setup lang="ts">
import {ref, reactive, computed, shallowReactive, watch, nextTick, type PropType} from 'vue'
import {useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor'
import TaskService from '@/services/task'
import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import EditLabels from './partials/editLabels.vue'
import Reminders from './partials/reminders.vue'
import ColorPicker from '../input/colorPicker.vue'
import {success} from '@/message'
const {t} = useI18n({useScope: 'global'})
const router = useRouter()
const props = defineProps({
task: {
type: Object as PropType<ITask | null>,
},
})
const taskService = shallowReactive(new TaskService())
const editorActive = ref(false)
let taskEditTask: ITask | undefined
// FIXME: this initialization should not be necessary here
function initTaskFields() {
taskEditTask.dueDate =
+new Date(props.task.dueDate) === 0 ? null : props.task.dueDate
taskEditTask.startDate =
+new Date(props.task.startDate) === 0
? null
: props.task.startDate
taskEditTask.endDate =
+new Date(props.task.endDate) === 0 ? null : props.task.endDate
// This makes the editor trigger its mounted function again which makes it forget every input
// it currently has in its textarea. This is a counter-hack to a hack inside of vue-easymde
// which made it impossible to detect change from the outside. Therefore the component would
// not update if new content from the outside was made available.
// See https://github.com/NikulinIlya/vue-easymde/issues/3
editorActive.value = false
nextTick(() => (editorActive.value = true))
}
watch(
() => props.task,
() => {
if (!taskEditTask) {
taskEditTask = reactive(props.task)
} else {
Object.assign(taskEditTask, new TaskModel(props.task))
}
initTaskFields()
},
{immediate: true },
)
const taskDetailRoute = computed(() => {
return {
name: 'task.detail',
params: { id: taskEditTask.id },
state: { backdropView: router.currentRoute.value.fullPath },
}
})
async function editTaskSubmit() {
const newTask = await taskService.update(taskEditTask)
Object.assign(taskEditTask, newTask)
initTaskFields()
success({message: t('task.detail.updateSuccess')})
}
</script>
<style lang="scss" scoped>
.priority-select {
.select,
select {
width: 100%;
}
}
ul.assingees {
list-style: none;
margin: 0;
li {
padding: 0.5rem 0.5rem 0;
a {
float: right;
color: var(--danger);
transition: all $transition;
}
}
}
.tag {
margin-right: 0.5rem;
margin-bottom: 0.5rem;
&:last-child {
margin-right: 0;
}
}
</style>

View File

@ -1,642 +0,0 @@
<template>
<div class="gantt-chart">
<div class="filter-container">
<div class="items">
<filter-popup
v-model="params"
@update:modelValue="loadTasks()"
/>
</div>
</div>
<div class="dates">
<template v-for="(y, yk) in days" :key="yk + 'year'">
<div class="months">
<div
:key="mk + 'month'"
class="month"
v-for="(m, mk) in days[yk]"
>
{{ formatMonthAndYear(yk, parseInt(mk) + 1) }}
<div class="days">
<div
:class="{ today: d.toDateString() === now.toDateString() }"
:key="dk + 'day'"
:style="{ width: dayWidth + 'px' }"
class="day"
v-for="(d, dk) in days[yk][mk]"
>
<span class="theday" v-if="dayWidth > 25">
{{ d.getDate() }}
</span>
<span class="weekday" v-if="dayWidth > 25">
{{
d.toLocaleString('en-us', {
weekday: 'short',
})
}}
</span>
</div>
</div>
</div>
</div>
</template>
</div>
<div :style="{ width: fullWidth + 'px' }" class="tasks">
<div
v-for="(t, k) in theTasks"
:key="t ? t.id : 0"
:style="{
background:
'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' +
(k % 2 === 0
? '#fafafa 1px, #fafafa '
: '#fff 1px, #fff ') +
dayWidth +
'px)',
}"
class="row"
>
<VueDragResize
:class="{
done: t ? t.done : false,
'is-current-edit': taskToEdit !== null && taskToEdit.id === t.id,
'has-light-text': !colorIsDark(t.getHexColor()),
'has-dark-text': colorIsDark(t.getHexColor()),
}"
:gridX="dayWidth"
:h="31"
:isActive="canWrite"
:minw="dayWidth"
:parentLimitation="true"
:parentW="fullWidth"
:snapToGrid="true"
:sticks="['mr', 'ml']"
:style="{
'border-color': t.getHexColor(),
'background-color': t.getHexColor(),
}"
:w="t.durationDays * dayWidth"
:x="t.offsetDays * dayWidth - 6"
:y="0"
@dragstop="(e) => resizeTask(t, e)"
@resizestop="(e) => resizeTask(t, e)"
axis="x"
class="task"
>
<span
:class="{
'has-high-priority': t.priority >= priorities.HIGH,
'has-not-so-high-priority':
t.priority === priorities.HIGH,
'has-super-high-priority':
t.priority === priorities.DO_NOW,
}"
>
{{ t.title }}
</span>
<priority-label :priority="t.priority" :done="t.done"/>
<!-- using the key here forces vue to use the updated version model and not the response returned by the api -->
<!-- FIXME: add label -->
<BaseButton @click="editTask(theTasks[k])" class="edit-toggle">
<icon icon="pen"/>
</BaseButton>
</VueDragResize>
</div>
<template v-if="showTaskswithoutDates">
<div
:key="t.id"
:style="{
background:
'repeating-linear-gradient(90deg, #ededed, #ededed 1px, ' +
(k % 2 === 0
? '#fafafa 1px, #fafafa '
: '#fff 1px, #fff ') +
dayWidth +
'px)',
}"
class="row"
v-for="(t, k) in tasksWithoutDates"
>
<VueDragResize
:gridX="dayWidth"
:h="31"
:isActive="canWrite"
:minw="dayWidth"
:parentLimitation="true"
:parentW="fullWidth"
:snapToGrid="true"
:sticks="['mr', 'ml']"
:x="dayOffsetUntilToday * dayWidth - 6"
:y="0"
@dragstop="(e) => resizeTask(t, e)"
@resizestop="(e) => resizeTask(t, e)"
axis="x"
class="task nodate"
v-tooltip="$t('list.gantt.noDates')"
>
<span>{{ t.title }}</span>
</VueDragResize>
</div>
</template>
</div>
<form
@submit.prevent="addNewTask()"
class="add-new-task"
v-if="canWrite"
>
<transition name="width">
<input
@blur="hideCrateNewTask"
@keyup.esc="newTaskFieldActive = false"
class="input"
ref="newTaskTitleField"
type="text"
v-if="newTaskFieldActive"
v-model="newTaskTitle"
/>
</transition>
<x-button @click="showCreateNewTask" :shadow="false" icon="plus">
{{ $t('list.list.newTaskCta') }}
</x-button>
</form>
<transition name="fade">
<edit-task
v-if="isTaskEdit"
class="taskedit"
:title="$t('list.list.editTask')"
@close="() => {isTaskEdit = false;taskToEdit = null}"
:task="taskToEdit"
/>
</transition>
</div>
</template>
<script lang="ts">
import {defineComponent} from 'vue'
import {mapState} from 'pinia'
import VueDragResize from 'vue-drag-resize'
import EditTask from './edit-task.vue'
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import {PRIORITIES as priorities} from '@/constants/priorities'
import PriorityLabel from './partials/priorityLabel.vue'
import TaskCollectionService from '../../services/taskCollection'
import {RIGHTS as Rights} from '@/constants/rights'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import {formatDate} from '@/helpers/time/formatDate'
import {useBaseStore} from '@/stores/base'
export default defineComponent({
name: 'GanttChart',
components: {
BaseButton,
FilterPopup,
PriorityLabel,
EditTask,
VueDragResize,
},
props: {
listId: {
type: Number,
required: true,
},
showTaskswithoutDates: {
type: Boolean,
default: false,
},
dateFrom: {
default: () => new Date(new Date().setDate(new Date().getDate() - 15)),
},
dateTo: {
default: () => new Date(new Date().setDate(new Date().getDate() + 30)),
},
// The width of a day in pixels, used to calculate all sorts of things.
dayWidth: {
type: Number,
default: 35,
},
},
data() {
return {
days: [],
startDate: null,
endDate: null,
theTasks: [], // Pretty much a copy of the prop, since we cant mutate the prop directly
tasksWithoutDates: [],
taskService: new TaskService(),
fullWidth: 0,
now: new Date(),
dayOffsetUntilToday: 0,
isTaskEdit: false,
taskToEdit: null,
newTaskTitle: '',
newTaskFieldActive: false,
priorities: priorities,
taskCollectionService: new TaskCollectionService(),
params: {
sort_by: ['done', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
},
}
},
watch: {
dateFrom: 'buildTheGanttChart',
dateTo: 'buildTheGanttChart',
listId: 'parseTasks',
},
mounted() {
this.buildTheGanttChart()
},
computed: mapState(useBaseStore, {
canWrite: (state) => state.currentList.maxRight > Rights.READ,
}),
methods: {
colorIsDark,
buildTheGanttChart() {
this.setDates()
this.prepareGanttDays()
this.parseTasks()
},
setDates() {
this.startDate = new Date(this.dateFrom)
this.endDate = new Date(this.dateTo)
console.debug('setDates; start date: ', this.startDate, 'end date:', this.endDate, 'date from:', this.dateFrom, 'date to:', this.dateTo)
this.dayOffsetUntilToday = Math.floor((this.now - this.startDate) / 1000 / 60 / 60 / 24) + 1
},
prepareGanttDays() {
console.debug('prepareGanttDays; start date: ', this.startDate, 'end date:', this.endDate)
// Layout: years => [months => [days]]
const years = {}
for (
let d = this.startDate;
d <= this.endDate;
d.setDate(d.getDate() + 1)
) {
const date = new Date(d)
if (years[date.getFullYear() + ''] === undefined) {
years[date.getFullYear() + ''] = {}
}
if (years[date.getFullYear() + ''][date.getMonth() + ''] === undefined) {
years[date.getFullYear() + ''][date.getMonth() + ''] = []
}
years[date.getFullYear() + ''][date.getMonth() + ''].push(date)
this.fullWidth += this.dayWidth
}
console.debug('prepareGanttDays; years:', years)
this.days = years
},
parseTasks() {
this.setDates()
this.loadTasks()
},
async loadTasks() {
this.theTasks = []
this.tasksWithoutDates = []
const getAllTasks = async (page = 1) => {
const tasks = await this.taskCollectionService.getAll({listId: this.listId}, this.params, page)
if (page < this.taskCollectionService.totalPages) {
const nextTasks = await getAllTasks(page + 1)
return tasks.concat(nextTasks)
}
return tasks
}
const tasks = await getAllTasks()
this.theTasks = tasks
.filter((t) => {
if (t.startDate === null && !t.done) {
this.tasksWithoutDates.push(t)
}
return (
t.startDate >= this.startDate &&
t.endDate <= this.endDate
)
})
.map((t) => this.addGantAttributes(t))
.sort(function (a, b) {
if (a.startDate < b.startDate) return -1
if (a.startDate > b.startDate) return 1
return 0
})
},
addGantAttributes(t) {
if (typeof t.durationDays !== 'undefined' && typeof t.offsetDays !== 'undefined') {
return t
}
t.endDate === null ? this.endDate : t.endDate
t.durationDays = Math.floor((t.endDate - t.startDate) / 1000 / 60 / 60 / 24)
t.offsetDays = Math.floor((t.startDate - this.startDate) / 1000 / 60 / 60 / 24)
return t
},
async resizeTask(taskDragged, newRect) {
if (this.isTaskEdit) {
return
}
let newTask = {...taskDragged}
const didntHaveDates = newTask.startDate === null ? true : false
const startDate = new Date(this.startDate)
startDate.setDate(
startDate.getDate() + newRect.left / this.dayWidth,
)
startDate.setUTCHours(0)
startDate.setUTCMinutes(0)
startDate.setUTCSeconds(0)
startDate.setUTCMilliseconds(0)
newTask.startDate = startDate
const endDate = new Date(startDate)
endDate.setDate(
startDate.getDate() + newRect.width / this.dayWidth,
)
newTask.startDate = startDate
newTask.endDate = endDate
// We take the task from the overall tasks array because the one in it has bad data after it was updated once.
// FIXME: This is a workaround. We should use a better mechanism to get the task or, even better,
// prevent it from containing outdated Data in the first place.
for (const tt in this.theTasks) {
if (this.theTasks[tt].id === newTask.id) {
newTask = this.theTasks[tt]
break
}
}
const ganttData = {
endDate: newTask.endDate,
durationDays: newTask.durationDays,
offsetDays: newTask.offsetDays,
}
const r = await this.taskService.update(newTask)
r.endDate = ganttData.endDate
r.durationDays = ganttData.durationDays
r.offsetDays = ganttData.offsetDays
// If the task didn't have dates before, we'll update the list
if (didntHaveDates) {
for (const t in this.tasksWithoutDates) {
if (this.tasksWithoutDates[t].id === r.id) {
this.tasksWithoutDates.splice(t, 1)
break
}
}
this.theTasks.push(this.addGantAttributes(r))
} else {
for (const tt in this.theTasks) {
if (this.theTasks[tt].id === r.id) {
this.theTasks[tt] = this.addGantAttributes(r)
break
}
}
}
},
editTask(task) {
this.taskToEdit = task
this.isTaskEdit = true
},
showCreateNewTask() {
if (!this.newTaskFieldActive) {
// Timeout to not send the form if the field isn't even shown
setTimeout(() => {
this.newTaskFieldActive = true
this.$nextTick(() => this.$refs.newTaskTitleField.focus())
}, 100)
}
},
hideCrateNewTask() {
if (this.newTaskTitle === '') {
this.$nextTick(() => (this.newTaskFieldActive = false))
}
},
async addNewTask() {
if (!this.newTaskFieldActive) {
return
}
const task = new TaskModel({
title: this.newTaskTitle,
listId: this.listId,
})
const r = await this.taskService.create(task)
this.tasksWithoutDates.push(this.addGantAttributes(r))
this.newTaskTitle = ''
this.hideCrateNewTask()
},
formatMonthAndYear(year, month) {
month = month < 10 ? '0' + month : month
const date = new Date(`${year}-${month}-01`)
return formatDate(date, 'MMMM, yyyy')
},
},
})
</script>
<style lang="scss" scoped>
$gantt-border: 1px solid var(--grey-200);
$gantt-vertical-border-color: var(--grey-100);
.gantt-chart {
overflow-x: auto;
border-top: 1px solid var(--grey-200);
.dates {
display: flex;
text-align: center;
.months {
display: flex;
.month {
padding: 0.5rem 0 0;
border-right: $gantt-border;
font-family: $vikunja-font;
font-weight: bold;
&:last-child {
border-right: none;
}
.days {
display: flex;
.day {
padding: 0.5rem 0;
font-weight: normal;
&.today {
background: var(--primary);
color: var(--white);
border-radius: 5px 5px 0 0;
font-weight: bold;
}
.theday {
padding: 0 .5rem;
width: 100%;
display: block;
}
.weekday {
font-size: 0.8rem;
}
}
}
}
}
}
.tasks {
max-width: unset !important;
border-top: $gantt-border;
.row {
height: 45px;
.task {
display: inline-block;
border: 2px solid var(--primary);
font-size: 0.85rem;
margin: 0.5rem;
border-radius: 6px;
padding: 0.25rem 0.5rem;
cursor: grab;
position: relative;
height: 31px !important;
-webkit-touch-callout: none; // iOS Safari
user-select: none; // Non-prefixed version
&.is-current-edit {
border-color: var(--warning) !important;
}
&.has-light-text {
color: var(--grey-100);
&.done span:after {
border-top: 1px solid var(--grey-100);
}
.edit-toggle {
color: var(--grey-100);
}
}
&.has-dark-text {
color: var(--text);
&.done span:after {
border-top: 1px solid var(--dark);
}
.edit-toggle {
color: var(--text);
}
}
&.done span {
position: relative;
&::after {
content: '';
position: absolute;
right: 0;
left: 0;
top: 57%;
}
}
span:not(.high-priority) {
max-width: calc(100% - 20px);
display: inline-block;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&.has-high-priority {
max-width: calc(100% - 90px);
}
&.has-not-so-high-priority {
max-width: calc(100% - 70px);
}
&.has-super-high-priority {
max-width: calc(100% - 111px);
}
&.icon {
width: 10px;
text-align: center;
}
}
.high-priority {
margin: 0 0 0 .5rem;
vertical-align: bottom;
}
.edit-toggle {
float: right;
cursor: pointer;
margin-right: 4px;
}
&.nodate {
border: 2px dashed var(--grey-300);
background: var(--grey-100);
}
&:active {
cursor: grabbing;
}
}
}
}
.taskedit {
position: fixed;
top: 10vh;
right: 10vw;
z-index: 5;
// FIXME: should be an option of the card, e.g. overflow
:deep(.card-content) {
max-height: 60vh;
overflow-y: auto;
}
}
.add-new-task {
padding: 1rem .7rem .4rem .7rem;
display: flex;
max-width: 450px;
.input {
margin-right: .7rem;
font-size: .8rem;
}
.button {
font-size: .68rem;
}
}
}
</style>

View File

@ -130,7 +130,7 @@
<!-- Delete modal -->
<modal
v-if="attachmentToDelete !== null"
:enabled="attachmentToDelete !== null"
@close="setAttachmentToDelete(null)"
@submit="deleteAttachment()"
>
@ -148,7 +148,7 @@
<!-- Attachment image modal -->
<modal
v-if="attachmentImageBlobUrl !== null"
:enabled="attachmentImageBlobUrl !== null"
@close="attachmentImageBlobUrl = null"
>
<img :src="attachmentImageBlobUrl" alt=""/>
@ -165,7 +165,6 @@ import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import type AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask'
@ -227,9 +226,9 @@ function uploadFilesToTask(files: File[] | FileList) {
uploadFiles(attachmentService, props.task.id, files)
}
const attachmentToDelete = ref<AttachmentModel | null>(null)
const attachmentToDelete = ref<IAttachment | null>(null)
function setAttachmentToDelete(attachment: AttachmentModel | null) {
function setAttachmentToDelete(attachment: IAttachment | null) {
attachmentToDelete.value = attachment
}
@ -250,7 +249,7 @@ async function deleteAttachment() {
const attachmentImageBlobUrl = ref<string | null>(null)
async function viewOrDownload(attachment: AttachmentModel) {
async function viewOrDownload(attachment: IAttachment) {
if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} else {
@ -335,35 +334,35 @@ async function setCoverImage(attachment: IAttachment | null) {
&.hidden {
display: none;
}
}
.drop-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
.drop-hint {
position: absolute;
bottom: 0;
left: 0;
right: 0;
.icon {
width: 100%;
font-size: 5rem;
height: auto;
text-shadow: var(--shadow-md);
animation: bounce 2s infinite;
.icon {
width: 100%;
font-size: 5rem;
height: auto;
text-shadow: var(--shadow-md);
animation: bounce 2s infinite;
@media (prefers-reduced-motion: reduce) {
animation: none;
}
@media (prefers-reduced-motion: reduce) {
animation: none;
}
}
.hint {
margin: .5rem auto 2rem;
border-radius: 2px;
box-shadow: var(--shadow-md);
background: var(--primary);
padding: 1rem;
color: var(--white);
width: 100%;
max-width: 300px;
}
.hint {
margin: .5rem auto 2rem;
border-radius: 2px;
box-shadow: var(--shadow-md);
background: var(--primary);
padding: 1rem;
color: var(--white);
width: 100%;
max-width: 300px;
}
}
@ -433,6 +432,4 @@ async function setCoverImage(attachment: IAttachment | null) {
border-radius: 4px;
font-size: .75rem;
}
@include modal-transition();
</style>

View File

@ -49,14 +49,12 @@ const label = computed(() => {
align-items: center;
padding-left: .5rem;
font-size: .9rem;
}
svg {
transform: rotate(-90deg);
transition: stroke-dashoffset 0.35s;
margin-right: .25rem;
}
circle {

View File

@ -43,7 +43,7 @@
>
· {{ $t('task.comment.edited', {date: formatDateSince(c.updated)}) }}
</span>
<transition name="fade">
<CustomTransition name="fade">
<span
class="is-inline-flex"
v-if="
@ -63,7 +63,7 @@
>
{{ $t('misc.saved') }}
</span>
</transition>
</CustomTransition>
</div>
<editor
:hasPreview="true"
@ -94,15 +94,15 @@
</figure>
<div class="media-content">
<div class="form">
<transition name="fade">
<CustomTransition name="fade">
<span
class="is-inline-flex"
v-if="taskCommentService.loading && creating"
class="is-inline-flex"
>
<span class="loader is-inline-block mr-2"></span>
{{ $t('task.comment.creating') }}
</span>
</transition>
</CustomTransition>
<div class="field">
<editor
:class="{
@ -132,22 +132,20 @@
</div>
</div>
<transition name="modal">
<modal
v-if="showDeleteModal"
@close="showDeleteModal = false"
@submit="() => deleteComment(commentToDelete)"
>
<template #header><span>{{ $t('task.comment.delete') }}</span></template>
<modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@submit="() => deleteComment(commentToDelete)"
>
<template #header><span>{{ $t('task.comment.delete') }}</span></template>
<template #text>
<p>
{{ $t('task.comment.deleteText1') }}<br/>
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
</p>
</template>
</modal>
</transition>
<template #text>
<p>
{{ $t('task.comment.deleteText1') }}<br/>
<strong class="has-text-white">{{ $t('misc.cannotBeUndone') }}</strong>
</p>
</template>
</modal>
</div>
</template>
@ -155,6 +153,7 @@
import {ref, reactive, computed, shallowReactive, watch, nextTick} from 'vue'
import {useI18n} from 'vue-i18n'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Editor from '@/components/input/AsyncEditor'
import TaskCommentService from '@/services/taskComment'
@ -348,9 +347,11 @@ async function deleteComment(commentToDelete: ITaskComment) {
}
}
.image.is-avatar {
border-radius: 100%;
}
.media-content {
width: calc(100% - 48px - 2rem);
}
@include modal-transition();
</style>

View File

@ -46,3 +46,11 @@ const updatedFormatted = computed(() => formatDateLong(task.value.updated))
const doneSince = computed(() => formatDateSince(task.value.doneAt))
const doneFormatted = computed(() => formatDateLong(task.value.doneAt))
</script>
<style lang="scss" scoped>
.created {
font-size: .75rem;
color: var(--grey-500);
text-align: right;
}
</style>

View File

@ -61,8 +61,8 @@ const taskService = shallowReactive(new TaskService())
const task = ref<ITask>()
// We're saving the due date seperately to prevent null errors in very short periods where the task is null.
const dueDate = ref<Date>()
const lastValue = ref<Date>()
const dueDate = ref<Date | null>()
const lastValue = ref<Date | null>()
const changeInterval = ref<ReturnType<typeof setInterval>>()
watch(

View File

@ -5,7 +5,7 @@
<icon icon="align-left"/>
</span>
{{ $t('task.attributes.description') }}
<transition name="fade">
<CustomTransition name="fade">
<span class="is-small is-inline-flex" v-if="loading && saving">
<span class="loader is-inline-block mr-2"></span>
{{ $t('misc.saving') }}
@ -14,7 +14,7 @@
<icon icon="check"/>
{{ $t('misc.saved') }}
</span>
</transition>
</CustomTransition>
</h3>
<editor
:is-edit-enabled="canWrite"
@ -33,6 +33,7 @@
<script setup lang="ts">
import {ref,computed, watch, type PropType} from 'vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask'

View File

@ -1,30 +1,24 @@
<template>
<div
tabindex="-1"
@focus="focus"
<Multiselect
:loading="listUserService.loading"
:placeholder="$t('task.assignee.placeholder')"
:multiple="true"
@search="findUser"
:search-results="foundUsers"
@select="addAssignee"
label="name"
:select-placeholder="$t('task.assignee.selectPlaceholder')"
v-model="assignees"
>
<Multiselect
:loading="listUserService.loading"
:placeholder="$t('task.assignee.placeholder')"
:multiple="true"
@search="findUser"
:search-results="foundUsers"
@select="addAssignee"
label="name"
:select-placeholder="$t('task.assignee.selectPlaceholder')"
v-model="assignees"
ref="multiselect"
>
<template #tag="{item: user}">
<span class="assignee">
<user :avatar-size="32" :show-username="false" :user="user"/>
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
<icon icon="times"/>
</BaseButton>
</span>
</template>
</Multiselect>
</div>
<template #tag="{item: user}">
<span class="assignee">
<user :avatar-size="32" :show-username="false" :user="user"/>
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
<icon icon="times"/>
</BaseButton>
</span>
</template>
</Multiselect>
</template>
<script setup lang="ts">
@ -41,6 +35,7 @@ import {success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import type {IUser} from '@/modelTypes/IUser'
import {getDisplayName} from '@/models/user'
const props = defineProps({
taskId: {
@ -65,7 +60,7 @@ const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'})
const listUserService = shallowReactive(new ListUserService())
const foundUsers = ref([])
const foundUsers = ref<IUser[]>([])
const assignees = ref<IUser[]>([])
let isAdding = false
@ -110,30 +105,21 @@ async function removeAssignee(user: IUser) {
async function findUser(query: string) {
if (query === '') {
clearAllFoundUsers()
foundUsers.value = []
return
}
const response = await listUserService.getAll({listId: props.listId}, {s: query})
const response = await listUserService.getAll({listId: props.listId}, {s: query}) as IUser[]
// Filter the results to not include users who are already assigned
foundUsers.value = response.filter(({id}) => !includesById(assignees.value, id))
foundUsers.value = response
.filter(({id}) => !includesById(assignees.value, id))
.map(u => {
// Users may not have a display name set, so we fall back on the username in that case
u.name = u.name === '' ? u.username : u.name
u.name = getDisplayName(u)
return u
})
}
function clearAllFoundUsers() {
foundUsers.value = []
}
const multiselect = ref()
function focus() {
multiselect.value.focus()
}
</script>
<style lang="scss" scoped>
@ -149,19 +135,20 @@ function focus() {
margin-right: 0;
}
.remove-assignee {
position: absolute;
top: 4px;
left: 2px;
color: var(--danger);
background: var(--white);
padding: 0 4px;
display: block;
border-radius: 100%;
font-size: .75rem;
width: 18px;
height: 18px;
z-index: 100;
}
}
.remove-assignee {
position: absolute;
top: 4px;
left: 2px;
color: var(--danger);
background: var(--white);
padding: 0 4px;
display: block;
border-radius: 100%;
font-size: .75rem;
width: 18px;
height: 18px;
z-index: 100;
}
</style>

View File

@ -128,7 +128,6 @@ async function createAndAddLabel(title: string) {
return
}
const labelStore = useLabelStore()
const newLabel = await labelStore.createLabel(new LabelModel({title}))
addLabel(newLabel, false)
labels.value.push(newLabel)

View File

@ -4,7 +4,7 @@
<Done class="heading__done" :is-done="task.done"/>
<ColorBubble
v-if="task.hexColor !== ''"
:color="task.getHexColor()"
:color="getHexColor(task.hexColor)"
class="mt-1 ml-2"
/>
<h1
@ -17,7 +17,7 @@
>
{{ task.title.trim() }}
</h1>
<transition name="fade">
<CustomTransition name="fade">
<span
v-if="loading && saving"
class="is-inline-flex is-align-items-center"
@ -32,7 +32,7 @@
<icon icon="check" class="mr-2"/>
{{ $t('misc.saved') }}
</span>
</transition>
</CustomTransition>
</div>
</template>
@ -41,6 +41,7 @@ import {ref, computed, type PropType} from 'vue'
import {useRouter} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
import Done from '@/components/misc/Done.vue'
@ -48,6 +49,7 @@ import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {useTaskStore} from '@/stores/tasks'
import type {ITask} from '@/modelTypes/ITask'
import {getHexColor} from '@/models/task'
const props = defineProps({
task: {
@ -84,9 +86,7 @@ const showSavedMessage = ref(false)
async function save(title: string) {
// We only want to save if the title was actually changed.
// Because the contenteditable does not have a change event
// we're building it ourselves and only continue
// if the task title changed.
// so we only continue if the task title changed.
if (title === props.task.title) {
return
}
@ -109,6 +109,36 @@ async function save(title: string) {
</script>
<style lang="scss" scoped>
.heading {
display: flex;
justify-content: space-between;
text-transform: none;
align-items: center;
@media screen and (max-width: $tablet) {
flex-direction: column;
align-items: start;
}
}
.title {
margin-bottom: 0;
}
.title.input {
// 1.8rem is the font-size, 1.125 is the line-height, .3rem padding everywhere, 1px border around the whole thing.
min-height: calc(1.8rem * 1.125 + .6rem + 2px);
@media screen and (max-width: $tablet) {
margin: 0 -.3rem .5rem -.3rem; // the title has 0.3rem padding - this make the text inside of it align with the rest
}
}
.title.task-id {
color: var(--grey-400);
white-space: nowrap;
}
.heading__done {
margin-left: .5rem;
}

View File

@ -2,16 +2,17 @@
<Multiselect
class="control is-expanded"
:placeholder="$t('list.search')"
@search="findLists"
:search-results="foundLists"
@select="select"
label="title"
v-model="list"
:select-placeholder="$t('list.searchSelect')"
:model-value="list"
@update:model-value="Object.assign(list, $event)"
@select="select"
@search="findLists"
>
<template #searchResult="props">
<span class="list-namespace-title search-result">{{ namespace(props.option.namespaceId) }} ></span>
{{ props.option.title }}
<template #searchResult="{option}">
<span class="list-namespace-title search-result">{{ namespace((option as IList).namespaceId) }} ></span>
{{ (option as IList).title }}
</template>
</Multiselect>
</template>
@ -20,12 +21,17 @@
import {reactive, ref, watch} from 'vue'
import type {PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import ListModel from '@/models/list'
import type {IList} from '@/modelTypes/IList'
import Multiselect from '@/components/input/multiselect.vue'
import type {INamespace} from '@/modelTypes/INamespace'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
import ListModel from '@/models/list'
import Multiselect from '@/components/input/multiselect.vue'
const props = defineProps({
modelValue: {
type: Object as PropType<IList>,
@ -65,7 +71,7 @@ function select(l: IList | null) {
emit('update:modelValue', list)
}
function namespace(namespaceId: number) {
function namespace(namespaceId: INamespace['id']) {
const namespace = namespaceStore.getNamespaceById(namespaceId)
return namespace !== null
? namespace.title

View File

@ -28,13 +28,10 @@ const props = defineProps({
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const priority = ref(0)
// FIXME: store value outside
// Set the priority to the :value every time it changes from the outside
watch(
() => props.modelValue,
(value) => {

View File

@ -5,8 +5,8 @@
<ButtonLink @click="() => visible = true">{{ $t('task.quickAddMagic.what') }}</ButtonLink>
</p>
<modal
@close="() => visible = false"
:enabled="visible"
@close="() => visible = false"
transition-name="fade"
:overflow="true"
variant="hint-modal"

View File

@ -14,7 +14,7 @@
<template v-if="editEnabled && showCreate">
<label class="label" key="label">
{{ $t('task.relation.new') }}
<transition name="fade">
<CustomTransition name="fade">
<span class="is-inline-flex" v-if="taskRelationService.loading">
<span class="loader is-inline-block mr-2"></span>
{{ $t('misc.saving') }}
@ -22,7 +22,7 @@
<span class="has-text-success" v-else-if="!taskRelationService.loading && saved">
{{ $t('misc.saved') }}
</span>
</transition>
</CustomTransition>
</label>
<div class="field" key="field-search">
<Multiselect
@ -133,7 +133,7 @@
</p>
<modal
v-if="relationToDelete !== undefined"
:enabled="relationToDelete !== undefined"
@close="relationToDelete = undefined"
@submit="removeTaskRelation()"
>
@ -163,6 +163,7 @@ import {RELATION_KINDS, RELATION_KIND, type IRelationKind} from '@/types/IRelati
import TaskRelationService from '@/services/taskRelation'
import TaskRelationModel from '@/models/taskRelation'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Multiselect from '@/components/input/multiselect.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
@ -442,6 +443,4 @@ async function toggleTaskDone(task: ITask) {
padding: 0;
height: 18px; // The exact height of the checkbox in the container
}
@include modal-transition();
</style>

View File

@ -1,30 +1,38 @@
<template>
<div :class="{'is-loading': taskService.loading}" class="task loader-container">
<fancycheckbox :disabled="(isArchived || disabled) && !canMarkAsDone" @change="markAsDone" v-model="task.done"/>
<fancycheckbox
:disabled="(isArchived || disabled) && !canMarkAsDone"
@change="markAsDone"
v-model="task.done"
/>
<ColorBubble
v-if="showListColor && listColor !== ''"
:color="listColor"
class="mr-1"
/>
<router-link
:to="taskDetailRoute"
:class="{ 'done': task.done}"
class="tasktext">
:class="{ 'done': task.done, 'show-list': showList && taskList !== null}"
class="tasktext"
>
<span>
<router-link
v-if="showList && taskList !== null"
:to="{ name: 'list.list', params: { listId: task.listId } }"
class="task-list"
:class="{'mr-2': task.hexColor !== ''}"
v-if="showList && getListById(task.listId) !== null"
v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
{{ getListById(task.listId).title }}
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})">
{{ taskList.title }}
</router-link>
<ColorBubble
v-if="task.hexColor !== ''"
:color="task.getHexColor()"
:color="getHexColor(task.hexColor)"
class="mr-1"
/>
<!-- Show any parent tasks to make it clear this task is a sub task of something -->
<span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'">
<template v-for="(pt, i) in task.relatedTasks.parenttask">
@ -35,15 +43,22 @@
{{ task.title }}
</span>
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0" />
<user
<labels
v-if="task.labels.length > 0"
class="labels ml-2 mr-1"
:labels="task.labels"
/>
<User
v-for="(a, i) in task.assignees"
:avatar-size="27"
:is-inline="true"
:key="task.id + 'assignee' + a.id + i"
:show-username="false"
:user="a"
v-for="(a, i) in task.assignees"
/>
<!-- FIXME: use popup -->
<BaseButton
v-if="+new Date(task.dueDate) > 0"
class="dueDate"
@ -59,10 +74,12 @@
- {{ $t('task.detail.due', {at: formatDateSince(task.dueDate)}) }}
</time>
</BaseButton>
<transition name="fade">
<CustomTransition name="fade">
<defer-task v-if="+new Date(task.dueDate) > 0 && showDefer" v-model="task" ref="deferDueDate"/>
</transition>
</CustomTransition>
<priority-label :priority="task.priority" :done="task.done"/>
<span>
<span class="list-task-icon" v-if="task.attachments.length > 0">
<icon icon="paperclip"/>
@ -74,184 +91,187 @@
<icon icon="history"/>
</span>
</span>
<checklist-summary :task="task"/>
</router-link>
<progress
class="progress is-small"
v-if="task.percentDone > 0"
:value="task.percentDone * 100" max="100">
:value="task.percentDone * 100" max="100"
>
{{ task.percentDone * 100 }}%
</progress>
<router-link
v-if="!showList && currentList.id !== task.listId && taskList !== null"
:to="{ name: 'list.list', params: { listId: task.listId } }"
class="task-list"
v-if="!showList && currentList.id !== task.listId && getListById(task.listId) !== null"
v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
{{ getListById(task.listId).title }}
v-tooltip="$t('task.detail.belongsToList', {list: taskList.title})"
>
{{ taskList.title }}
</router-link>
<BaseButton
:class="{'is-favorite': task.isFavorite}"
@click="toggleFavorite"
class="favorite">
class="favorite"
>
<icon icon="star" v-if="task.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</BaseButton>
<slot></slot>
<slot />
</div>
</template>
<script lang="ts">
import {defineComponent, type PropType} from 'vue'
import {mapState} from 'pinia'
<script setup lang="ts">
import {ref, watch, shallowReactive, toRef, type PropType, onMounted, onBeforeUnmount, computed} from 'vue'
import {useI18n} from 'vue-i18n'
import TaskModel from '@/models/task'
import TaskModel, { getHexColor } from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from './priorityLabel.vue'
import TaskService from '../../../services/task'
import Labels from '@/components/tasks/partials/labels.vue'
import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import Labels from '@/components/tasks/partials//labels.vue'
import DeferTask from '@/components/tasks/partials//defer-task.vue'
import ChecklistSummary from '@/components/tasks/partials/checklist-summary.vue'
import User from '@/components/misc/user.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Fancycheckbox from '../../input/fancycheckbox.vue'
import DeferTask from './defer-task.vue'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import ChecklistSummary from './checklist-summary.vue'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import TaskService from '@/services/task'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import {success} from '@/message'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks'
export default defineComponent({
name: 'singleTaskInList',
data() {
return {
taskService: new TaskService(),
task: new TaskModel(),
showDefer: false,
}
const props = defineProps({
theTask: {
type: Object as PropType<ITask>,
required: true,
},
components: {
ColorBubble,
BaseButton,
ChecklistSummary,
DeferTask,
Fancycheckbox,
User,
Labels,
PriorityLabel,
isArchived: {
type: Boolean,
default: false,
},
props: {
theTask: {
type: Object as PropType<ITask>,
required: true,
},
isArchived: {
type: Boolean,
default: false,
},
showList: {
type: Boolean,
default: false,
},
disabled: {
type: Boolean,
default: false,
},
showListColor: {
type: Boolean,
default: true,
},
canMarkAsDone: {
type: Boolean,
default: true,
},
showList: {
type: Boolean,
default: false,
},
emits: ['task-updated'],
watch: {
theTask(newVal) {
this.task = newVal
},
disabled: {
type: Boolean,
default: false,
},
mounted() {
this.task = this.theTask
document.addEventListener('click', this.hideDeferDueDatePopup)
showListColor: {
type: Boolean,
default: true,
},
beforeUnmount() {
document.removeEventListener('click', this.hideDeferDueDatePopup)
},
computed: {
...mapState(useListStore, {
getListById: 'getListById',
}),
listColor() {
const list = this.getListById(this.task.listId)
return list !== null ? list.hexColor : ''
},
currentList() {
const baseStore = useBaseStore()
return typeof baseStore.currentList === 'undefined' ? {
id: 0,
title: '',
} : baseStore.currentList
},
taskDetailRoute() {
return {
name: 'task.detail',
params: {id: this.task.id},
// TODO: re-enable opening task detail in modal
// state: { backdropView: this.$router.currentRoute.value.fullPath },
}
},
},
methods: {
formatDateSince,
formatISO,
formatDateLong,
async markAsDone(checked: boolean) {
const updateFunc = async () => {
const task = await useTaskStore().update(this.task)
this.task = task
this.$emit('task-updated', task)
this.$message.success({
message: this.task.done ?
this.$t('task.doneSuccess') :
this.$t('task.undoneSuccess'),
}, [{
title: 'Undo',
callback: () => this.undoDone(checked),
}])
}
if (checked) {
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
} else {
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
}
},
undoDone(checked: boolean) {
this.task.done = !this.task.done
this.markAsDone(!checked)
},
async toggleFavorite() {
this.task.isFavorite = !this.task.isFavorite
this.task = await this.taskService.update(this.task)
this.$emit('task-updated', this.task)
useNamespaceStore().loadNamespacesIfFavoritesDontExist()
},
hideDeferDueDatePopup(e) {
if (!this.showDefer) {
return
}
closeWhenClickedOutside(e, this.$refs.deferDueDate.$el, () => {
this.showDefer = false
})
},
canMarkAsDone: {
type: Boolean,
default: true,
},
})
const emit = defineEmits(['task-updated'])
const {t} = useI18n({useScope: 'global'})
const taskService = shallowReactive(new TaskService())
const task = ref<ITask>(new TaskModel())
const showDefer = ref(false)
const theTask = toRef(props, 'theTask')
watch(
theTask,
newVal => {
task.value = newVal
},
)
onMounted(() => {
task.value = theTask.value
document.addEventListener('click', hideDeferDueDatePopup)
})
onBeforeUnmount(() => {
document.removeEventListener('click', hideDeferDueDatePopup)
})
const baseStore = useBaseStore()
const listStore = useListStore()
const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore()
const taskList = computed(() => listStore.getListById(task.value.listId))
const listColor = computed(() => taskList.value !== null ? taskList.value.hexColor : '')
const currentList = computed(() => {
return typeof baseStore.currentList === 'undefined' ? {
id: 0,
title: '',
} : baseStore.currentList
})
const taskDetailRoute = computed(() => ({
name: 'task.detail',
params: {id: task.value.id},
// TODO: re-enable opening task detail in modal
// state: { backdropView: router.currentRoute.value.fullPath },
}))
async function markAsDone(checked: boolean) {
const updateFunc = async () => {
const newTask = await taskStore.update(task.value)
task.value = newTask
emit('task-updated', newTask)
success({
message: task.value.done ?
t('task.doneSuccess') :
t('task.undoneSuccess'),
}, [{
title: 'Undo',
callback: () => undoDone(checked),
}])
}
if (checked) {
setTimeout(updateFunc, 300) // Delay it to show the animation when marking a task as done
} else {
await updateFunc() // Don't delay it when un-marking it as it doesn't have an animation the other way around
}
}
function undoDone(checked: boolean) {
task.value.done = !task.value.done
markAsDone(!checked)
}
async function toggleFavorite() {
task.value.isFavorite = !task.value.isFavorite
task.value = await taskService.update(task.value)
emit('task-updated', task.value)
namespaceStore.loadNamespacesIfFavoritesDontExist()
}
const deferDueDate = ref<typeof DeferTask | null>(null)
function hideDeferDueDatePopup(e) {
if (!showDefer.value) {
return
}
closeWhenClickedOutside(e, deferDueDate.value.$el, () => {
showDefer.value = false
})
}
</script>
<style lang="scss" scoped>
@ -371,6 +391,10 @@ export default defineComponent({
width: auto;
}
.show-list .parent-tasks {
padding-left: .25rem;
}
.remove {
color: var(--danger);
}

View File

@ -0,0 +1,72 @@
import {ref, unref, watch} from 'vue'
import {debouncedWatch, tryOnMounted, useWindowSize, type MaybeRef} from '@vueuse/core'
// TODO: also add related styles
// OR: replace with vueuse function
export function useAutoHeightTextarea(value: MaybeRef<string>) {
const textarea = ref<HTMLInputElement>()
const minHeight = ref(0)
// adapted from https://github.com/LeaVerou/stretchy/blob/47f5f065c733029acccb755cae793009645809e2/src/stretchy.js#L34
function resize(textareaEl: HTMLInputElement | undefined) {
if (!textareaEl) return
let empty
// the value here is the attribute value
if (!textareaEl.value && textareaEl.placeholder) {
empty = true
textareaEl.value = textareaEl.placeholder
}
const cs = getComputedStyle(textareaEl)
textareaEl.style.minHeight = ''
textareaEl.style.height = '0'
const offset = textareaEl.offsetHeight - parseFloat(cs.paddingTop) - parseFloat(cs.paddingBottom)
const height = textareaEl.scrollHeight + offset + 'px'
textareaEl.style.height = height
// calculate min-height for the first time
if (!minHeight.value) {
minHeight.value = parseFloat(height)
}
textareaEl.style.minHeight = minHeight.value.toString()
if (empty) {
textareaEl.value = ''
}
}
tryOnMounted(() => {
if (textarea.value) {
// we don't want scrollbars
textarea.value.style.overflowY = 'hidden'
}
})
const {width: windowWidth} = useWindowSize()
debouncedWatch(
windowWidth,
() => resize(textarea.value),
{debounce: 200},
)
// It is not possible to get notified of a change of the value attribute of a textarea without workarounds (setTimeout)
// So instead we watch the value that we bound to it.
watch(
() => [textarea.value, unref(value)],
() => resize(textarea.value),
{
immediate: true, // calculate initial size
flush: 'post', // resize after value change is rendered to DOM
},
)
return textarea
}

View File

@ -1,31 +0,0 @@
import {describe, it, expect} from 'vitest'
import {hourToSalutation} from './useDateTimeSalutation'
const dateWithHour = (hours: number): Date => {
const date = new Date()
date.setHours(hours)
return date
}
describe('Salutation', () => {
it('shows the right salutation in the night', () => {
const salutation = hourToSalutation(dateWithHour(4))
expect(salutation).toBe('home.welcomeNight')
})
it('shows the right salutation in the morning', () => {
const salutation = hourToSalutation(dateWithHour(8))
expect(salutation).toBe('home.welcomeMorning')
})
it('shows the right salutation in the day', () => {
const salutation = hourToSalutation(dateWithHour(13))
expect(salutation).toBe('home.welcomeDay')
})
it('shows the right salutation in the night', () => {
const salutation = hourToSalutation(dateWithHour(20))
expect(salutation).toBe('home.welcomeEvening')
})
it('shows the right salutation in the night again', () => {
const salutation = hourToSalutation(dateWithHour(23))
expect(salutation).toBe('home.welcomeNight')
})
})

View File

@ -1,31 +0,0 @@
import {computed} from 'vue'
import {useNow} from '@vueuse/core'
const TRANSLATION_KEY_PREFIX = 'home.welcome'
export function hourToSalutation(now: Date) {
const hours = now.getHours()
if (hours < 5) {
return `${TRANSLATION_KEY_PREFIX}Night`
}
if (hours < 11) {
return `${TRANSLATION_KEY_PREFIX}Morning`
}
if (hours < 18) {
return `${TRANSLATION_KEY_PREFIX}Day`
}
if (hours < 23) {
return `${TRANSLATION_KEY_PREFIX}Evening`
}
return `${TRANSLATION_KEY_PREFIX}Night`
}
export function useDateTimeSalutation() {
const now = useNow()
return computed(() => hourToSalutation(now.value))
}

View File

@ -0,0 +1,26 @@
import {computed} from 'vue'
import {useI18n} from 'vue-i18n'
import {useNow} from '@vueuse/core'
import {useAuthStore} from '@/stores/auth'
import {hourToDaytime} from '@/helpers/hourToDaytime'
export type Daytime = 'night' | 'morning' | 'day' | 'evening'
export function useDaytimeSalutation() {
const {t} = useI18n({useScope: 'global'})
const now = useNow()
const authStore = useAuthStore()
const name = computed(() => authStore.userDisplayName)
const daytime = computed(() => hourToDaytime(now.value))
const salutations = {
'night': () => t('home.welcomeNight', {username: name.value}),
'morning': () => t('home.welcomeMorning', {username: name.value}),
'day': () => t('home.welcomeDay', {username: name.value}),
'evening': () => t('home.welcomeEvening', {username: name.value}),
} as Record<Daytime, () => string>
return computed(() => name.value ? salutations[daytime.value]() : undefined)
}

View File

@ -0,0 +1,26 @@
import {useRouter} from 'vue-router'
import {getLastVisited, clearLastVisited} from '@/helpers/saveLastVisited'
export function useRedirectToLastVisited() {
const router = useRouter()
function redirectIfSaved() {
const last = getLastVisited()
if (last !== null) {
router.push({
name: last.name,
params: last.params,
query: last.query,
})
clearLastVisited()
return
}
router.push({name: 'home'})
}
return {
redirectIfSaved,
}
}

View File

@ -3,6 +3,9 @@ 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'
const SECONDS_TOKEN_VALID = 60 * SECONDS_A_HOUR
export function useRenewTokenOnFocus() {
const router = useRouter()
@ -16,23 +19,23 @@ export function useRenewTokenOnFocus() {
authStore.renewToken()
// Check if the token is still valid if the window gets focus again to maybe renew it
useEventListener('focus', () => {
useEventListener('focus', async () => {
if (!authenticated.value) {
return
}
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - +new Date() / 1000
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - new Date().valueOf() / MILLISECONDS_A_HOUR
// 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) {
authStore.checkAuth()
router.push({name: 'user.login'})
await authStore.checkAuth()
await router.push({name: 'user.login'})
return
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
if (expiresIn < SECONDS_TOKEN_VALID) {
authStore.renewToken()
console.debug('renewed token')
}

View File

@ -0,0 +1,71 @@
import {computed, ref, watch, type Ref} from 'vue'
import {useRouter, type RouteLocationNormalized, type RouteLocationRaw} from 'vue-router'
import equal from 'fast-deep-equal/es6'
export type Filters = Record<string, any>
export function useRouteFilters<CurrentFilters extends Filters>(
route: Ref<RouteLocationNormalized>,
getDefaultFilters: (route: RouteLocationNormalized) => CurrentFilters,
routeToFilters: (route: RouteLocationNormalized) => CurrentFilters,
filtersToRoute: (filters: CurrentFilters) => RouteLocationRaw,
) {
const router = useRouter()
const filters = ref<CurrentFilters>(routeToFilters(route.value))
const routeFromFiltersFullPath = computed(() => router.resolve(filtersToRoute(filters.value)).fullPath)
watch(
route,
(route, oldRoute) => {
if (
route?.name !== oldRoute?.name ||
routeFromFiltersFullPath.value === route.fullPath
) {
return
}
filters.value = routeToFilters(route)
},
{
immediate: true, // set the filter from the initial route
},
)
watch(
filters,
async () => {
if (routeFromFiltersFullPath.value !== route.value.fullPath) {
await router.push(routeFromFiltersFullPath.value)
}
},
// only apply new route after all filters have changed in component cycle
{
deep: true,
flush: 'post',
},
)
const hasDefaultFilters = ref(false)
watch(
[filters, route],
([filters, route]) => {
hasDefaultFilters.value = equal(filters, getDefaultFilters(route))
},
{
deep: true,
immediate: true,
},
)
function setDefaultFilters() {
filters.value = getDefaultFilters(route.value)
}
return {
filters,
hasDefaultFilters,
setDefaultFilters,
}
}

View File

@ -19,7 +19,7 @@ export function useRouteWithModal() {
return
}
// logic from vue-router
// this is adapted from vue-router
// https://github.com/vuejs/vue-router-next/blob/798cab0d1e21f9b4d45a2bd12b840d2c7415f38a/src/RouterView.ts#L125
const routePropsOption = route.matched[0]?.props.default
const routeProps = routePropsOption
@ -28,7 +28,9 @@ export function useRouteWithModal() {
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
: {}
routeProps.backdropView = backdropView.value
const component = route.matched[0]?.components?.default

View File

@ -2,7 +2,8 @@ import {ref, shallowReactive, watch, computed} from 'vue'
import {useRoute} from 'vue-router'
import TaskCollectionService from '@/services/taskCollection'
import type { ITask } from '@/modelTypes/ITask'
import type {ITask} from '@/modelTypes/ITask'
import {error} from '@/message'
// FIXME: merge with DEFAULT_PARAMS in filters.vue
export const getDefaultParams = () => ({
@ -18,23 +19,12 @@ const SORT_BY_DEFAULT = {
id: 'desc',
}
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
const params = ref({...getDefaultParams()})
const search = ref('')
const page = ref(1)
const sortBy = ref({ ...sortByDefault })
// This makes sure an id sort order is always sorted last.
// When tasks would be sorted first by id and then by whatever else was specified, the id sort takes
// precedence over everything else, making any other sort columns pretty useless.
function formatSortOrder(params) {
function formatSortOrder(sortBy, params) {
let hasIdFilter = false
const sortKeys = Object.keys(sortBy.value)
const sortKeys = Object.keys(sortBy)
for (const s of sortKeys) {
if (s === 'id') {
sortKeys.splice(s, 1)
@ -46,11 +36,24 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
sortKeys.push('id')
}
params.sort_by = sortKeys
params.order_by = sortKeys.map(s => sortBy.value[s])
params.order_by = sortKeys.map(s => sortBy[s])
return params
}
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
*/
export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
const params = ref({...getDefaultParams()})
const search = ref('')
const page = ref(1)
const sortBy = ref({ ...sortByDefault })
const getAllTasksParams = computed(() => {
let loadParams = {...params.value}
@ -58,7 +61,7 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
loadParams.s = search.value
}
loadParams = formatSortOrder(loadParams)
loadParams = formatSortOrder(sortBy.value, loadParams)
return [
{listId: listId.value},
@ -74,7 +77,11 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
const tasks = ref<ITask[]>([])
async function loadTasks() {
tasks.value = []
tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value)
try {
tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value)
} catch (e) {
error(e)
}
return tasks.value
}

14
src/constants/date.ts Normal file
View File

@ -0,0 +1,14 @@
export const DATEFNS_DATE_FORMAT_KEBAB = 'yyyy-LL-dd'
export const SECONDS_A_MINUTE = 60
export const SECONDS_A_HOUR = SECONDS_A_MINUTE * 60
export const SECONDS_A_DAY = SECONDS_A_HOUR * 24
export const SECONDS_A_WEEK = SECONDS_A_DAY * 7
export const SECONDS_A_MONTH = SECONDS_A_DAY * 30
export const SECONDS_A_YEAR = SECONDS_A_DAY * 365
export const MILLISECONDS_A_SECOND = 1000
export const MILLISECONDS_A_MINUTE = SECONDS_A_MINUTE * MILLISECONDS_A_SECOND
export const MILLISECONDS_A_HOUR = SECONDS_A_HOUR * MILLISECONDS_A_SECOND
export const MILLISECONDS_A_DAY = SECONDS_A_DAY * MILLISECONDS_A_SECOND
export const MILLISECONDS_A_WEEK = SECONDS_A_WEEK * MILLISECONDS_A_SECOND

View File

@ -2,7 +2,7 @@ import type {Directive} from 'vue'
import {install, uninstall} from '@github/hotkey'
import {isAppleDevice} from '@/helpers/isAppleDevice'
const directive: Directive = {
const directive = <Directive<HTMLElement,string>>{
mounted(el, {value}) {
if(value === '') {
return

View File

@ -1,4 +1,4 @@
import {AuthenticatedHTTPFactory} from '@/http-common'
import {AuthenticatedHTTPFactory} from '@/helpers/fetcher'
import type {AxiosResponse} from 'axios'
let savedToken: string | null = null

Some files were not shown because too many files have changed in this diff Show More