Compare commits

...

416 Commits

Author SHA1 Message Date
kolaente e7ffba5322 [skip ci] Updated translations via Crowdin 2021-09-29 20:53:14 +00:00
kolaente 1b7a6cecfe
chore(ci): temporarily disable cache 2021-09-29 22:48:01 +02:00
kolaente 4a1b402e62
fix: quick add magic always disabled 2021-09-29 22:42:13 +02:00
renovate e20be7b788 chore(deps): update dependency vite to v2.6.1 (#807)
Reviewed-on: vikunja/frontend#807
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-29 20:39:08 +00:00
kolaente 416380025e
fix: more spacing for last viewed tasks headline 2021-09-29 21:29:09 +02:00
kolaente 96ef25ba01
fix: don't enable editing when the user has no rights for it
related: #804
2021-09-29 21:22:44 +02:00
Christoph Schmatzler 7cd3394da5
Allow specifying listen ports (#27) 2021-09-29 21:12:22 +02:00
kolaente 4bd2c94256
fix: don't try to create a task with an empty title when creating multiple tasks at once 2021-09-29 21:01:54 +02:00
konrad 96523f1fbf feat: task checklist improvements (#797)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#797
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2021-09-29 18:31:14 +00:00
konrad d47b13647e feat(natural language): make natural language prefixes configurable (#795)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#795
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2021-09-29 18:30:55 +00:00
renovate 3b58756285 chore(deps): update dependency jest to v27.2.4 (#806)
Reviewed-on: vikunja/frontend#806
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-29 18:12:42 +00:00
renovate 924b77d10f chore(deps): update dependency vite to v2.6.0 (#805)
Reviewed-on: vikunja/frontend#805
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-29 18:12:30 +00:00
renovate 08521d7eb9 chore(deps): update dependency esbuild to v0.13.3 (#802)
Reviewed-on: vikunja/frontend#802
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-28 18:29:30 +00:00
renovate dd74b9f957 fix(deps): update dependency vue-i18n to v8.26.2 (#803)
Reviewed-on: vikunja/frontend#803
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-28 18:29:01 +00:00
renovate 5e8fb32c71 chore(deps): update dependency jest to v27.2.3 (#801)
Reviewed-on: vikunja/frontend#801
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-28 11:50:18 +00:00
renovate 8596d501dd chore(deps): update dependency cypress to v8.5.0 (#800)
Reviewed-on: vikunja/frontend#800
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-28 07:04:29 +00:00
renovate 4c0bdbb908 chore(deps): update typescript-eslint monorepo to v4.32.0 (#799)
Reviewed-on: vikunja/frontend#799
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-27 18:13:33 +00:00
kolaente 2691a84610
feat: don't show bullet points of checklists 2021-09-26 20:53:27 +02:00
kolaente e17116dac1
feat: make checklists work with '-' instead of '*' 2021-09-26 20:51:02 +02:00
konrad c0d744cfaa [skip ci] Updated translations via Crowdin 2021-09-26 18:42:26 +00:00
konrad 442e6b12e0 feat: allow quickly creating multiple tasks at once with multiline input (#796)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#796
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2021-09-26 18:22:28 +00:00
kolaente 3dfa286a12
feat(quick actions): select the item when only one result is available
resolves #703
2021-09-26 20:19:58 +02:00
kolaente a955488cdf
fix: date formatting for non-english languages 2021-09-26 20:07:15 +02:00
dpschen 8cd4bbccf6 chore: some small changes in the cypress README.md (#793)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#793
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-26 17:49:27 +00:00
dpschen 166539c7e8 fix: remove font preload of quicksand 300 (#794)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#794
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-26 16:16:22 +00:00
dpschen 7579222bb0 feat: add example configuration for vscode with volor (#791)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#791
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-26 14:49:06 +00:00
kolaente 8b01dc6b71
fix: run tests with unstable api 2021-09-26 15:54:58 +02:00
kolaente 4b64e27d2b
fix(natural language parser): parts of week days in other words 2021-09-26 15:00:11 +02:00
kolaente 403d77ce14
fix(natural language parser): fix parsing short days
related: #773
2021-09-26 14:38:15 +02:00
kolaente e918b82cfa
feat: don't try to load task comments if they are disabled 2021-09-26 13:44:13 +02:00
renovate 75f1a5a97f chore(deps): update dependency autoprefixer to v10.3.6 (#792)
Reviewed-on: vikunja/frontend#792
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-26 08:37:08 +00:00
dpschen e9978548d8 feat: add types for vite (#790)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#790
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-25 18:33:30 +00:00
dpschen c551bf5836 feat: define node version in .nvmrc file (#789)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#789
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-25 18:33:00 +00:00
dpschen feb34c8cc1 fix: eslint settings (#787)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#787
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-25 15:56:06 +00:00
renovate 72627d13c6 chore(deps): update dependency jest to v27.2.2 (#788)
Reviewed-on: vikunja/frontend#788
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-25 15:55:11 +00:00
kolaente f762d8ad4d
fix: remove unused function 2021-09-24 21:46:42 +02:00
Dominik Pschenitschni 697ea12c8e
fix: breaking attribute coercion behavior
see: https://v3.vuejs.org/guide/migration/attribute-coercion.html#overview
2021-09-24 21:46:42 +02:00
Dominik Pschenitschni fe27a432c7
feat: move unique functions from taskList to List 2021-09-24 21:46:42 +02:00
Dominik Pschenitschni f51371bbe0
feat: move from life cycle to data or watcher
- remove from created / mounted
- initialize component services in data
- use immediate watcher where appropriate
- deep watch for route changes
2021-09-24 21:46:42 +02:00
Dominik Pschenitschni ebeca48be4
fix: namespace collision of global error method with draggable error method 2021-09-24 21:46:42 +02:00
Dominik Pschenitschni 0da7a46612
feat: formatting
- remove unneeded data props
- prepare for compiler warnings vue3; see https://v3.vuejs.org/guide/migration/v-bind.html
2021-09-24 21:46:42 +02:00
Dominik Pschenitschni 4454e6cf22
feat: update to new slot syntax
Prepare for vue 3
2021-09-24 21:46:42 +02:00
kolaente 77f8b27dc6
fix: lists disappearing when updating their namespace 2021-09-24 20:51:59 +02:00
dpschen 1bc6d66895 [skip ci] Updated translations via Crowdin 2021-09-24 18:27:00 +00:00
dpschen 41331c8a86 feature/remove-attachment-upload-mixin (#724)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#724
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-24 18:16:37 +00:00
dpschen 4f2378ff02 feat: add variant hint-modal to modal component (#764)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#764
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-24 18:08:48 +00:00
kolaente 97416ab2d5 [skip ci] Updated translations via Crowdin 2021-09-24 17:57:26 +00:00
kolaente dedf6cbf21
fix: quick add magic assignee prefix in explanation 2021-09-24 19:47:40 +02:00
renovate dab6bc23ed fix(deps): pin dependency ufo to 0.7.9 (#780)
Reviewed-on: vikunja/frontend#780
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-23 18:23:23 +00:00
renovate c44db22b99 chore(deps): update dependency esbuild to v0.13.2 (#782)
Reviewed-on: vikunja/frontend#782
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-23 18:22:51 +00:00
renovate 4e62ab2b79 fix(deps): update dependency vue-i18n to v8.26.1 (#784)
Reviewed-on: vikunja/frontend#784
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-23 18:22:17 +00:00
dpschen 6b1bf27bf8 feat: reduce file size by removing by removing fonts (#759)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#759
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-23 14:07:08 +00:00
dpschen 3245752a80 fix: computed in api-config (#777)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#777
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-23 05:09:47 +00:00
renovate 649bbd8c9b chore(deps): update dependency esbuild to v0.13.1 (#776)
Reviewed-on: vikunja/frontend#776
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-23 05:07:17 +00:00
renovate ab954fcb9a fix(deps): update dependency vue-i18n to v8.26.0 (#779)
Reviewed-on: vikunja/frontend#779
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-23 05:06:43 +00:00
dpschen 17d11c6ce3 fix: prevent vue-shortkey use in elements with contenteditable (#775)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#775
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-22 16:25:09 +00:00
renovate d72d69debb chore(deps): update dependency sass to v1.42.1 (#772)
Reviewed-on: vikunja/frontend#772
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-22 09:55:06 +00:00
renovate 7d59e64c61 chore(deps): update dependency autoprefixer to v10.3.5 (#771)
Reviewed-on: vikunja/frontend#771
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-22 07:13:20 +00:00
renovate 6d72ac1ef3 [skip ci] Updated translations via Crowdin 2021-09-22 06:18:44 +00:00
renovate da4bac09f8 chore(deps): update dependency esbuild to v0.12.29 (#769)
Reviewed-on: vikunja/frontend#769
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-22 06:08:12 +00:00
renovate 64f4852d76 fix(deps): update dependency browserslist to v4.17.1 (#770)
Reviewed-on: vikunja/frontend#770
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-22 06:07:40 +00:00
renovate 1ae8f84b1c [skip ci] Updated translations via Crowdin 2021-09-21 20:18:47 +00:00
renovate 2f506ea653 chore(deps): update dependency sass to v1.42.0 (#751)
Reviewed-on: vikunja/frontend#751
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 20:08:17 +00:00
renovate e5158d9438 chore(deps): update dependency cypress to v8.4.1 (#750)
Reviewed-on: vikunja/frontend#750
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 19:06:48 +00:00
renovate c758c00f54 chore(deps): update dependency vite to v2.5.10 (#746)
Reviewed-on: vikunja/frontend#746
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 19:06:11 +00:00
renovate 1a0b47a581 fix(deps): update dependency date-fns to v2.24.0 (#757)
Reviewed-on: vikunja/frontend#757
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 19:05:48 +00:00
renovate 35a9532b86 chore(deps): update dependency eslint-plugin-vue to v7.18.0 (#761)
Reviewed-on: vikunja/frontend#761
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 19:05:35 +00:00
renovate 68947b77e9 chore(deps): update dependency @types/jest to v27.0.2 (#766)
Reviewed-on: vikunja/frontend#766
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 19:04:58 +00:00
dpschen 7719ef1bef feat: move pagination to dedicated component (#760)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#760
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-21 19:03:38 +00:00
dpschen 2ff0976da6 [skip ci] Updated translations via Crowdin 2021-09-21 17:46:16 +00:00
dpschen 77352e7a8c fix spelling in cypress README (#763)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#763
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-21 16:59:35 +00:00
simon1506 d88e299358 fix: no drag delay when using mouse on touch device (#748)
Reviewed-on: vikunja/frontend#748
Reviewed-by: konrad <k@knt.li>
Co-authored-by: simon1506 <simon.sch.dev@gmail.com>
Co-committed-by: simon1506 <simon.sch.dev@gmail.com>
2021-09-21 16:47:42 +00:00
dpschen 9122a184d6 feat: move fontawesome icons import to dedicated file (#742)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#742
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-21 16:39:56 +00:00
dpschen 728dfc52e5 feat: close modals with esc key (#741)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#741
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-21 16:37:56 +00:00
dpschen 47ad115738 chore: remove obsolete css vendor prefixes (#739)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#739
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-21 16:36:38 +00:00
dpschen 3ac25c9f08 fix: import bulma utilities global (#738)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#738
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-21 16:35:46 +00:00
renovate 7fbd9c6ab0 fix(deps): update dependency dompurify to v2.3.3 (#754)
Reviewed-on: vikunja/frontend#754
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:23:50 +00:00
renovate 681b76b3a5 fix(deps): update dependency marked to v3.0.4 (#753)
Reviewed-on: vikunja/frontend#753
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:23:28 +00:00
renovate 857fdc35e4 chore(deps): update typescript-eslint monorepo to v4.31.2 (#749)
Reviewed-on: vikunja/frontend#749
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:23:12 +00:00
renovate b70c88b3f3 fix(deps): update dependency vue-i18n to v8.25.1 (#747)
Reviewed-on: vikunja/frontend#747
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:22:48 +00:00
renovate b6ad09e3d8 chore(deps): update dependency jest to v27.2.1 (#745)
Reviewed-on: vikunja/frontend#745
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:22:27 +00:00
renovate eddb2cbce0 chore(deps): update dependency esbuild to v0.12.28 (#744)
Reviewed-on: vikunja/frontend#744
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:22:01 +00:00
renovate 1cf0316b3b chore(deps): update dependency typescript to v4.4.3 (#740)
Reviewed-on: vikunja/frontend#740
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-21 16:21:23 +00:00
dpschen 50c1a2e4d5 feat simplify taskList mixin (#728)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#728
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-10 14:39:18 +00:00
dpschen 07a6a31f47 chore: move constants in folder (#732)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#732
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-10 14:21:33 +00:00
dpschen 0295113f50 feat: use store getters to check auth (#731)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#731
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-10 13:04:00 +00:00
dpschen b5df941e39 chore: define default filters and params at one location (#721)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#721
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-10 13:02:52 +00:00
konrad 077fe264f0 fix: use date-fns for gantt years (#734)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#734
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2021-09-10 12:58:23 +00:00
dpschen dae441a373 feat: simplify heading blur logic (#727)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#727
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-10 12:57:59 +00:00
dpschen 0376ef53e3 fix: remove attachment by id (#725)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#725
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-10 12:50:41 +00:00
renovate 7ab308b846 chore(deps): update workbox monorepo to v6.3.0 (#730)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#730
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-10 12:49:50 +00:00
renovate 33447c4a09 chore(deps): update dependency sass to v1.39.2 (#733)
Reviewed-on: vikunja/frontend#733
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-10 11:13:54 +00:00
dpschen d0e46e59e8 chore: make method event independent (#719)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#719
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-09 22:25:08 +00:00
dpschen 0ed3cf2553 feat: import bulma utilities global (#718)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#718
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-09 22:24:08 +00:00
renovate 33f1480284 chore(deps): update dependency esbuild to v0.12.26 (#729)
Reviewed-on: vikunja/frontend#729
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-09 16:17:12 +00:00
dpschen 432c6babf2 feat: use computed for api domain (#722)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#722
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-08 21:48:30 +00:00
dpschen 96ef926dde chore: create progress dots dynamically (#715)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#715
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-08 21:45:28 +00:00
dpschen 87c70cec0e chore: define default label background color once (#713)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#713
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-08 21:44:56 +00:00
renovate 4689de7f1f fix(deps): update dependency marked to v3.0.3 (#726)
Reviewed-on: vikunja/frontend#726
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-08 21:19:21 +00:00
renovate b0f616d784 [skip ci] Updated translations via Crowdin 2021-09-08 20:36:21 +00:00
renovate 11b5d0574d chore(deps): update dependency vite to v2.5.6 (#723)
Reviewed-on: vikunja/frontend#723
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-08 20:31:09 +00:00
kolaente 134a09c9f2
Fix download export user data title 2021-09-08 22:19:45 +02:00
kolaente c7c3ef79be
0.18.1 release preparations 2021-09-08 19:49:23 +02:00
kolaente 2bae8e95e5
Fix task attributes overridden when saving the task title with enter 2021-09-08 19:37:08 +02:00
dpschen c4095327ad feat: make it possible to fake online state via dev env (#720)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#720
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-08 16:30:14 +00:00
dpschen c9631c1e71 fix: call to /null from background image (#714)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#714
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-08 16:28:26 +00:00
dpschen 4fc8858c64 fix: kanban-card mutatation violation (#712)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#712
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-09-08 16:28:13 +00:00
kolaente 9cee720ac9
Fix sort order for table view 2021-09-08 18:13:02 +02:00
kolaente 6f89863c81
Fix missing translation when creating a new task on the kanban board 2021-09-08 17:49:10 +02:00
kolaente debdc83f1b
Fix data export download progress 2021-09-08 17:33:58 +02:00
renovate 033b30d6cd Update dependency @4tw/cypress-drag-drop to v2 (#711)
Reviewed-on: vikunja/frontend#711
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-08 14:27:47 +00:00
renovate c23d3c1488 Update dependency jest to v27.1.1 (#716)
Reviewed-on: vikunja/frontend#716
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-08 14:27:24 +00:00
renovate 4f305b28fd Update dependency vite to v2.5.5 (#709)
Reviewed-on: vikunja/frontend#709
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-08 05:43:13 +00:00
renovate 33926a2d11 Update dependency vite to v2.5.4 (#708)
Reviewed-on: vikunja/frontend#708
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-07 17:44:41 +00:00
kolaente 332dbc1598
Fix rearranging tasks in a kanban bucket when its limit was reached 2021-09-07 18:38:53 +02:00
renovate 28a4b1c533 Update dependency vite-plugin-vue2 to v1.8.2 (#707)
Reviewed-on: vikunja/frontend#707
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-07 16:17:41 +00:00
kolaente 78346f9ac6
Fix translation badge 2021-09-06 21:22:18 +02:00
renovate 8f916c275b Update typescript-eslint monorepo to v4.31.0 (#706)
Reviewed-on: vikunja/frontend#706
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-06 19:02:48 +00:00
renovate cb0e2a7bc1 Update dependency axios to v0.21.4 (#705)
Reviewed-on: vikunja/frontend#705
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-06 16:53:46 +00:00
kolaente 2d804c3af3
0.18.0 release preparations 2021-09-05 17:07:51 +02:00
renovate 6868fb134d Update dependency browserslist to v4.17.0 (#701)
Reviewed-on: vikunja/frontend#701
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-05 12:40:41 +00:00
kolaente b7ec975384
Change building latest docker image 2021-09-05 14:40:14 +02:00
kolaente 75174c2496
Switch the :latest docker image tag to contain the latest release instead of the latest unstable 2021-09-05 13:44:05 +02:00
kolaente e0c9332634
Add proofread languages to available languages 2021-09-05 13:31:53 +02:00
kolaente 7a4e568898 [skip ci] Updated translations via Crowdin 2021-09-05 11:10:28 +00:00
kolaente fb20afae92
Move translated files after downloading them 2021-09-05 13:06:45 +02:00
kolaente db0e023d35 [skip ci] Updated translations via Crowdin 2021-09-05 11:03:35 +00:00
kolaente 13f01cdc30
Add depends_on for push step 2021-09-05 13:03:15 +02:00
kolaente b4919a5662
Add depends_on for upload step 2021-09-05 13:02:43 +02:00
kolaente 240137cb79 [skip ci] Updated translations via Crowdin 2021-09-05 10:02:06 +00:00
kolaente a2e0c7e1f4
Fix git push remote to update crowdin translations 2021-09-05 12:01:53 +02:00
kolaente 2bc85a9de7
Fix setting secret for updating translations 2021-09-05 11:49:53 +02:00
kolaente 54ada3f06e
Automatically update approved translations from crowdin 2021-09-05 11:35:50 +02:00
kolaente c3aae638f7
Add more debug logs for gantt charts 2021-09-05 10:09:58 +02:00
renovate 8dc6d24003 chore(deps): update dependency axios to v0.21.3 (#700)
Reviewed-on: vikunja/frontend#700
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-04 20:12:15 +00:00
renovate 60173ebc59 chore(deps): update dependency axios to v0.21.2 (#698)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#698
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-04 19:57:09 +00:00
kolaente 388a31d95a
Fix gantt months being wrong 2021-09-04 21:56:22 +02:00
konrad f4c552a79f User Data Export and import (#699)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#699
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2021-09-04 19:26:38 +00:00
renovate 44bb7358b6 chore(deps): update dependency autoprefixer to v10.3.4 (#697)
Reviewed-on: vikunja/frontend#697
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-03 05:11:07 +00:00
renovate 67615a3984 chore(deps): update dependency esbuild to v0.12.25 (#696)
Reviewed-on: vikunja/frontend#696
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-02 17:59:44 +00:00
renovate 741f1773a2 chore(deps): update dependency sass to v1.39.0 (#695)
Reviewed-on: vikunja/frontend#695
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-02 05:24:51 +00:00
renovate c941b0a097 chore(deps): update dependency vite to v2.5.3 (#694)
Co-authored-by: konrad <k@knt.li>
Reviewed-on: vikunja/frontend#694
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-01 20:31:36 +00:00
renovate 3487203fa4 chore(deps): update dependency @4tw/cypress-drag-drop to v1.8.1 (#693)
Co-authored-by: konrad <k@knt.li>
Reviewed-on: vikunja/frontend#693
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-09-01 20:31:03 +00:00
kolaente 8b4efeeb11
Another day, another js date edge-case 2021-09-01 22:12:46 +02:00
renovate c42f8ca9f8 chore(deps): update dependency vite to v2.5.2 (#692)
Reviewed-on: vikunja/frontend#692
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-31 19:16:54 +00:00
kolaente f216797d4e
Fix parsing dates on the last day of the month 2021-08-31 20:02:54 +02:00
renovate 6a34262d41 chore(deps): update typescript-eslint monorepo to v4.30.0 (#691)
Reviewed-on: vikunja/frontend#691
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-30 18:22:27 +00:00
renovate daece57467 chore(deps): update dependency sass to v1.38.2 (#690)
Reviewed-on: vikunja/frontend#690
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-28 16:22:51 +00:00
renovate 6c4aacca1e chore(deps): update dependency cypress to v8.3.1 (#689)
Reviewed-on: vikunja/frontend#689
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-28 16:22:30 +00:00
renovate 059ee39772 chore(deps): update dependency esbuild to v0.12.24 (#688)
Reviewed-on: vikunja/frontend#688
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-27 15:26:19 +00:00
renovate 7a3aeab867 chore(deps): update dependency jest to v27.1.0 (#687)
Reviewed-on: vikunja/frontend#687
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-27 12:10:39 +00:00
renovate 5b5916b798 chore(deps): update dependency eslint-plugin-vue to v7.17.0 (#686)
Reviewed-on: vikunja/frontend#686
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-27 05:48:39 +00:00
renovate 1838d57eec chore(deps): update dependency typescript to v4.4.2 (#685)
Reviewed-on: vikunja/frontend#685
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-26 22:12:44 +00:00
renovate 1ace5c12f1 chore(deps): update dependency autoprefixer to v10.3.3 (#684)
Reviewed-on: vikunja/frontend#684
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-26 16:46:47 +00:00
renovate e6b9b9870a chore(deps): update dependency esbuild to v0.12.23 (#683)
Reviewed-on: vikunja/frontend#683
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-26 06:02:51 +00:00
renovate e65da2d217 fix(deps): update dependency marked to v3.0.2 (#682)
Reviewed-on: vikunja/frontend#682
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-25 06:00:17 +00:00
renovate f5bd9df510 chore(deps): update dependency vite-plugin-pwa to v0.11.2 (#681)
Reviewed-on: vikunja/frontend#681
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-24 21:21:33 +00:00
renovate c9a8d5f951 chore(deps): update dependency vite to v2.5.1 (#680)
Reviewed-on: vikunja/frontend#680
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-24 20:54:35 +00:00
renovate d024448402 chore(deps): update dependency sass to v1.38.1 (#679)
Reviewed-on: vikunja/frontend#679
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-24 05:13:28 +00:00
dpschen 0a8505f53c fix: vuex mutation violation from draggable (#674)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#674
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-08-23 19:24:52 +00:00
renovate 2adeb97074 chore(deps): update typescript-eslint monorepo to v4.29.3 (#676)
Reviewed-on: vikunja/frontend#676
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-23 19:21:26 +00:00
renovate 9e22e86d86 fix(deps): update dependency marked to v3.0.1 (#677)
Reviewed-on: vikunja/frontend#677
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-23 19:20:44 +00:00
dpschen 30d699df2d fix: non unique ids (#672)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#672
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-08-23 17:42:42 +00:00
dpschen 19f52eb8d1 chore: discard old font file formats (#673)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#673
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-08-23 17:42:31 +00:00
dpschen 0660129b41 feat: provide global variables in all components (#669)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#669
Reviewed-by: konrad <k@knt.li>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-08-23 16:39:11 +00:00
dpschen ecb3924b09 chore: only import common languages (#671)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#671
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: dpschen <dpschen@noreply.kolaente.de>
Co-committed-by: dpschen <dpschen@noreply.kolaente.de>
2021-08-23 16:38:29 +00:00
kolaente 0947ae9ce9
Fix list settings not being available when list backgrounds are disabled 2021-08-23 18:24:55 +02:00
renovate 9f392c275e Update dependency autoprefixer to v10.3.2 (#670)
Reviewed-on: vikunja/frontend#670
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-21 19:39:51 +00:00
renovate ef734103f1 Update dependency esbuild to v0.12.22 (#668)
Reviewed-on: vikunja/frontend#668
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-21 07:01:15 +00:00
renovate 49ab6c0a17 Update dependency esbuild to v0.12.21 (#666)
Reviewed-on: vikunja/frontend#666
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-20 20:37:18 +00:00
renovate 618125dde6 Update dependency vite-plugin-pwa to v0.11.0 (#667)
Reviewed-on: vikunja/frontend#667
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-20 20:36:55 +00:00
kolaente 65782ca971
Fix test for saving a task description 2021-08-20 19:10:45 +02:00
kolaente 9fda82839b
Fix showing an editor save button in cases where it wasn't required 2021-08-20 18:56:50 +02:00
kolaente 08a34d8a68
Make saving a text edit a button 2021-08-18 22:57:15 +02:00
renovate f8b3c2bb58 Update dependency browserslist to v4.16.8 (#664)
Reviewed-on: vikunja/frontend#664
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-18 17:08:02 +00:00
kolaente 38bc377323
Make sure highlight.js is always lazy-loaded 2021-08-17 21:17:31 +02:00
kolaente 9c6369e8d8
Make editor edit button at the bottom the default and make sure the done button stands out more 2021-08-17 21:10:32 +02:00
renovate 3a46032c63 Update dependency ts-jest to v27.0.5 (#662)
Reviewed-on: vikunja/frontend#662
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-17 13:21:47 +00:00
renovate 66a0f315b8 Update dependency sass to v1.38.0 (#661)
Reviewed-on: vikunja/frontend#661
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-17 06:01:17 +00:00
renovate a7eac8418a Update typescript-eslint monorepo to v4.29.2 (#659)
Reviewed-on: vikunja/frontend#659
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-16 21:17:01 +00:00
renovate d8d4c1dc21 Update dependency cypress to v8.3.0 (#660)
Reviewed-on: vikunja/frontend#660
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-16 21:16:25 +00:00
renovate 2dc5c22dea Update dependency vite to v2.5.0 (#658)
Reviewed-on: vikunja/frontend#658
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-16 10:36:30 +00:00
renovate 2e66d17376 Update dependency marked to v3 (#657)
Reviewed-on: vikunja/frontend#657
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-16 10:36:07 +00:00
kolaente d23fc3be74
Fix redirecting to /login for some routes 2021-08-15 18:52:31 +02:00
renovate cc8665b538 Update dependency vite-plugin-vue2 to v1.8.1 (#656)
Reviewed-on: vikunja/frontend#656
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-15 10:12:54 +00:00
kolaente 3aa316988b
Directly redirect to the openid auth provider if that's the only auth method 2021-08-15 12:02:29 +02:00
kolaente a5687d78f5
Fix changing the repeat mode of a task when no value is entered yet 2021-08-15 11:25:06 +02:00
kolaente f79f4101b6
Show errors from openid provider 2021-08-14 17:31:35 +02:00
kolaente 7bd081efe0
Fix loading & disabled state on inputs when creating a new task 2021-08-13 21:47:15 +02:00
renovate 69b626cba9 Update dependency dompurify to v2.3.1 (#655)
Reviewed-on: vikunja/frontend#655
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-13 19:12:14 +00:00
renovate 3e9a1e0166 Update dependency esbuild to v0.12.20 (#654)
Reviewed-on: vikunja/frontend#654
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-13 19:11:42 +00:00
renovate 49e532826f Update dependency @types/jest to v27.0.1 (#653)
Reviewed-on: vikunja/frontend#653
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-12 21:19:13 +00:00
kolaente bdf7af010c
Make the progress bar color lighter 2021-08-12 20:07:41 +02:00
kolaente fde931eaf4
Fix comment on different task after clicking on a task notification 2021-08-11 23:16:46 +02:00
renovate bf053051f8 Update workbox monorepo to v6.2.4 (#649)
Reviewed-on: vikunja/frontend#649
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-11 19:20:38 +00:00
konrad dc04c1b256 User account deletion (#651)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#651
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-08-11 19:08:18 +00:00
renovate 328c172d40 Update dependency @types/jest to v27 (#650)
Reviewed-on: vikunja/frontend#650
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-10 22:20:38 +00:00
renovate f9529f605c Update dependency eslint-plugin-vue to v7.16.0 (#648)
Reviewed-on: vikunja/frontend#648
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-10 14:32:23 +00:00
renovate 9053c6914e Update typescript-eslint monorepo to v4.29.1 (#647)
Reviewed-on: vikunja/frontend#647
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-09 18:18:12 +00:00
renovate 0c998e1d9e Update dependency vite-plugin-vue2 to v1.8.0 (#646)
Reviewed-on: vikunja/frontend#646
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-09 16:24:00 +00:00
renovate 24d5446ce5 Update dependency vite-plugin-pwa to v0.10.0 (#644)
Reviewed-on: vikunja/frontend#644
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-09 11:46:13 +00:00
renovate 28d9f89a4d Update dependency vue-advanced-cropper to v1.8.2 (#645)
Reviewed-on: vikunja/frontend#645
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-09 10:33:59 +00:00
renovate bd2c2f3977 Update dependency esbuild to v0.12.19 (#643)
Reviewed-on: vikunja/frontend#643
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-08 07:05:31 +00:00
renovate 64137757c9 Update dependency vue-advanced-cropper to v1.8.1 (#642)
Reviewed-onvikunja/frontend#642
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-07 11:31:24 +00:00
renovate 30b83f4012 Update dependency vue-advanced-cropper to v1.8.0 (#641)
Reviewed-on: vikunja/frontend#641
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-07 08:04:00 +00:00
kolaente d21795ffd7
Make sure the task popup view takes up all the space it can on mobile 2021-08-06 23:52:55 +02:00
kolaente f4a4909ead
Fix populating task details ater updating the description 2021-08-06 23:45:46 +02:00
kolaente 187a8f5933
Small cleanups & code improvements 2021-08-06 23:34:37 +02:00
kolaente f2c29d42dd
Fix highlight.js in editor 2021-08-06 22:22:25 +02:00
renovate 5e82e75e1b Update workbox monorepo to v6.2.2 (#640)
Reviewed-on: vikunja/frontend#640
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-06 19:11:00 +00:00
kolaente 85306362d3
Fix multiselect search padding 2021-08-06 19:34:22 +02:00
kolaente 9195becd99
Fix global mutation of has tasks state 2021-08-06 19:29:22 +02:00
kolaente 5b70f8d5d7
Fix showing import tasks cta when tasks are loading 2021-08-06 19:25:17 +02:00
kolaente 3ff749976d
Fix setting delete button for newly created task comments 2021-08-06 18:49:31 +02:00
kolaente 0ce0ad1479
Cleanup drone pipeline 2021-08-06 10:26:16 +02:00
kolaente 33c34655df
Drone debug 2021-08-06 01:01:26 +02:00
kolaente f6aed0fb9f
Change desktop downstream trigger plugin with our own debug build 2021-08-06 00:49:53 +02:00
renovate f4c31a3168 Update workbox monorepo to v6.2.0 (#639)
Reviewed-on: vikunja/frontend#639
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-05 19:13:37 +00:00
renovate 4a8773d806 Update dependency esbuild to v0.12.18 (#638)
Reviewed-on: vikunja/frontend#638
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-05 19:13:09 +00:00
renovate 8edf69a342 Update dependency cypress to v8.2.0 (#637)
Reviewed-on: vikunja/frontend#637
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-04 20:08:00 +00:00
renovate f0a7a9b343 Update Font Awesome (#636)
Reviewed-on: vikunja/frontend#636
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-04 20:07:20 +00:00
renovate c5c83038ae Update dependency sass to v1.37.5 (#635)
Reviewed-on: vikunja/frontend#635
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-04 08:05:04 +00:00
kolaente 493180d442
Fix lint 2021-08-03 23:28:55 +02:00
kolaente e3787b9496
Only add a drag delay if on mobile instead of setting it to 0 2021-08-03 23:26:39 +02:00
renovate 7792cdf8bd Update dependency browserslist to v4.16.7 (#634)
Reviewed-on: vikunja/frontend#634
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-03 10:09:51 +00:00
renovate 304f55e763 Update dependency vite-plugin-pwa to v0.9.3 (#629)
Reviewed-on: vikunja/frontend#629
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-03 09:55:47 +00:00
renovate 4c79ee2372 Update dependency eslint-plugin-vue to v7.15.1 (#633)
Reviewed-on: vikunja/frontend#633
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-03 09:52:46 +00:00
renovate 5f9af2d040 Update dependency sass to v1.37.2 (#632)
Reviewed-on: vikunja/frontend#632
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-03 09:52:21 +00:00
renovate 96d673c32d Update typescript-eslint monorepo to v4.29.0 (#631)
Reviewed-on: vikunja/frontend#631
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-02 20:01:32 +00:00
renovate 40fddd9c56 Update dependency sass to v1.37.0 (#628)
Reviewed-on: vikunja/frontend#628
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-02 07:52:20 +00:00
renovate 7732c84719 Update dependency highlight.js to v11.2.0 (#630)
Reviewed-on: vikunja/frontend#630
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-08-02 07:19:38 +00:00
kolaente e855b1b1e5
Fix CTA spacings 2021-08-02 07:48:00 +02:00
renovate 2dd12f5dae Update dependency eslint to v7.32.0 (#627)
Reviewed-on: vikunja/frontend#627
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-31 05:53:08 +00:00
kolaente 364d42b2b9
Add confirm with enter when setting a new password 2021-07-30 14:46:00 +02:00
kolaente 362ccde425
Fix padding for kanban cards 2021-07-30 14:15:06 +02:00
kolaente 8f84594e92
TOTP UX improvements & translation fixes 2021-07-30 12:23:36 +02:00
renovate f9831f8e47 Update dependency cypress to v8.1.0 (#624)
Reviewed-on: vikunja/frontend#624
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-30 10:02:20 +00:00
renovate 9738694c32 Update dependency eslint-plugin-vue to v7.15.0 (#625)
Reviewed-on: vikunja/frontend#625
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-30 10:02:01 +00:00
renovate 81ab488eaa Update dependency esbuild to v0.12.17 (#623)
Reviewed-on: vikunja/frontend#623
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-29 17:13:56 +00:00
kolaente c323804c7c
Add drag delay on mobile 2021-07-29 13:08:38 +02:00
kolaente 3c2d89a0f6
Don't allow dragging a list when the user does not have the rights 2021-07-29 13:05:33 +02:00
kolaente 8a08a41a3c
Fix setting a task as favorite button 2021-07-29 13:00:48 +02:00
konrad c4067c7c35 PWA improvments (#622)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#622
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-07-28 20:58:12 +00:00
kolaente a1e1fe4eb0
Add missing position property to list and bucket models 2021-07-28 22:46:33 +02:00
kolaente 617a6a0c21
Add making tasks favorite from the task detail view 2021-07-28 22:13:24 +02:00
renovate ccf466c31c Pin dependencies (#621)
Reviewed-on: vikunja/frontend#621
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-28 20:13:00 +00:00
konrad 3c7f8d7aa2 Reorder tasks, lists and kanban buckets (#620)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#620
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-07-28 19:56:29 +00:00
renovate 39ef4b48f2 Update dependency vite to v2.4.4 (#619)
Reviewed-on: vikunja/frontend#619
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-27 14:07:28 +00:00
kolaente 53fe5738c9
Search namespaces locally only when duplicating a list 2021-07-27 15:36:02 +02:00
kolaente ce84067982
Fix llama background url 2021-07-27 15:26:47 +02:00
kolaente dcb846324d
Cleanup broken sw functions 2021-07-26 23:09:49 +02:00
renovate a9f4d0dff9 Update dependency vite-plugin-pwa to v0.8.2 (#612)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#612
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-26 20:26:53 +00:00
renovate bfc2d63b9a Update typescript-eslint monorepo to v4.28.5 (#618)
Reviewed-on: vikunja/frontend#618
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-26 19:06:24 +00:00
renovate f26529420a Update dependency @rollup/plugin-commonjs to v19.0.2 (#617)
Reviewed-on: vikunja/frontend#617
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-26 16:25:19 +00:00
renovate 66e5d03aef Pin dependencies (#616)
Reviewed-on: vikunja/frontend#616
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-26 16:25:09 +00:00
kolaente 91c86fc563
Add express for serve:dev 2021-07-26 17:06:04 +02:00
kolaente c0210bd6b4
Cleanup old vue cli config 2021-07-26 16:59:59 +02:00
kolaente 2af50c7307
Fix user name and avatar alignment in navbar 2021-07-26 15:37:33 +02:00
kolaente 34849d80b7
Fix Gantt layout overflowsing on mobile 2021-07-26 11:25:55 +02:00
kolaente fd5d331eca
Fix sorting labels
Resolves #603
2021-07-26 10:55:19 +02:00
renovate 55436661af Update dependency esbuild to v0.12.16 (#614)
Reviewed-on: vikunja/frontend#614
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-26 07:18:18 +00:00
renovate d4eab2a52c Update dependency vite to v2.4.3 (#611)
Reviewed-on: vikunja/frontend#611
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-25 21:17:18 +00:00
renovate 888bb0fb67 Update dependency vite-plugin-vue2 to v1.7.3 (#613)
Reviewed-on: vikunja/frontend#613
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-25 20:39:56 +00:00
renovate 7bcec2ba31 Update dependency esbuild to v0.12.15 (#610)
Reviewed-on: vikunja/frontend#610
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-25 20:39:24 +00:00
renovate 910d3e87a2 Pin dependencies (#608)
Reviewed-on: vikunja/frontend#608
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-25 20:38:40 +00:00
renovate 623a8aa3f7 Update dependency workbox-cli to v6.1.5 (#609)
Reviewed-on: vikunja/frontend#609
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-25 14:09:48 +00:00
kolaente 5fa03923e9
Improve chunk size 2021-07-25 16:02:49 +02:00
konrad a08306d612 Add vite (#416)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#416
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-07-25 13:27:15 +00:00
kolaente 92c15b435c
Don't prefetch all i18n files 2021-07-25 12:55:31 +02:00
konrad c45911fd36 Fix date parsing parsing words with weekdays in them (#607)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#607
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-07-25 10:45:17 +00:00
kolaente 2a6649d9dc
Only build a bundle for modern browsers 2021-07-25 12:45:05 +02:00
kolaente 8664c4f88c
Fix token in storage not getting renewed 2021-07-25 11:49:15 +02:00
renovate e2aca53253 Update dependency sass to v1.36.0 (#606)
Reviewed-on: vikunja/frontend#606
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-24 07:38:45 +00:00
renovate 272deaa400 Update dependency date-fns to v2.23.0 (#604)
Reviewed-on: vikunja/frontend#604
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-23 12:20:37 +00:00
konrad b547029379 Run frontend-tests with dist in ci (#605)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#605
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-07-23 11:13:15 +00:00
kolaente 2429be7c72
Fix spacing for task detail view in lists with a background 2021-07-23 11:09:23 +02:00
kolaente ac9c854b24
Fix selecting a single value from multiselect
Used when moving a task to other lists among other things
2021-07-22 23:20:57 +02:00
kolaente 83b530b8ba
Sort labels alphabetically on tasks 2021-07-22 22:05:34 +02:00
kolaente fe4a8c17c8
Fix label changes appearing to be saved immediately when editing them 2021-07-22 22:03:49 +02:00
kolaente 926d7938ab
Show labels alphabetically sorted in the overview 2021-07-22 22:00:13 +02:00
kolaente cf25e96c50
Fix other values getting pushed away when creating a new one through multiselect 2021-07-22 21:57:33 +02:00
kolaente 028dbf27ee
Fix table headers wrapping in table view 2021-07-22 21:51:34 +02:00
kolaente 5fd7e3f708
Fix table view scrolling on mobile 2021-07-22 21:48:05 +02:00
renovate caf817f25d Update dependency ts-jest to v27.0.4 (#602)
Reviewed-on: vikunja/frontend#602
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-21 14:28:26 +00:00
kolaente 5913f78dc7
Hide keyboard shortcuts indicator on mobile 2021-07-21 00:05:11 +02:00
kolaente cc53796f1b
Decrease page padding on task detail page 2021-07-21 00:03:41 +02:00
kolaente 2e2887c2ad
Change menu hamburger icon 2021-07-20 23:57:55 +02:00
kolaente 3c548b2153
Fix labels list in saved filter spacing 2021-07-20 22:46:39 +02:00
kolaente 2838ec6148
Fix setting filters for reminders 2021-07-20 22:45:10 +02:00
kolaente 7b16928d81
Fix loading labels when editing a saved filter 2021-07-20 22:42:34 +02:00
kolaente 2779cfc140
Fix not reloading tasks of a saved filter after editing it 2021-07-20 22:16:44 +02:00
kolaente d81b4117f5
Fix quick actions not working when nonexisting lists where left over in history 2021-07-20 18:03:38 +02:00
renovate 176c6462bb Update dependency cypress to v8 (#601)
Reviewed-on: vikunja/frontend#601
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-20 07:35:02 +00:00
renovate a127ac5f66 Pin dependencies (#599)
Reviewed-on: vikunja/frontend#599
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-19 20:27:31 +00:00
renovate aa44082606 Update typescript-eslint monorepo to v4.28.4 (#600)
Reviewed-on: vikunja/frontend#600
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-19 19:50:12 +00:00
konrad fa8492f97c Add typescript support for helper functions (#598)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#598
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-07-19 18:20:49 +00:00
kolaente b812c422f9
Fix sending the user back to the list view they came from when opening a task in detail view
Resolves #589
2021-07-19 11:20:05 +02:00
kolaente 46957c389f
Fix lint 2021-07-19 11:18:22 +02:00
renovate a455fb23d1 Update dependency eslint-plugin-vue to v7.14.0 (#597)
Reviewed-on: vikunja/frontend#597
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-18 10:37:33 +00:00
renovate 27a9f6c665 Update dependency eslint to v7.31.0 (#596)
Reviewed-on: vikunja/frontend#596
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-18 08:23:11 +00:00
sytone 306a926c66 Add default list setting & creating tasks from home (#520)
Co-authored-by: sytone <github@sytone.com>
Co-authored-by: Sytone <github@sytone.com>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#520
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: sytone <kolaente@sytone.com>
Co-committed-by: sytone <kolaente@sytone.com>
2021-07-17 21:21:46 +00:00
renovate bad5e3d0ec Update dependency vue-i18n to v8.25.0 (#595)
Co-authored-by: konrad <konrad@kola-entertainments.de>
Reviewed-on: vikunja/frontend#595
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-17 17:35:20 +00:00
kolaente 15085a7c26
Fix sass division 2021-07-17 19:14:26 +02:00
kolaente be0bc5c84f
Fix lists showing up multiple times in history 2021-07-17 19:10:05 +02:00
kolaente 0e5954cf96
Fix user test fixtures 2021-07-17 19:02:03 +02:00
kolaente 01ed17a0f0
Fix setting task favorite status in test fixtures 2021-07-13 12:18:54 +02:00
kolaente 9e42559d70
Add syncing translations to crowdin 2021-07-13 12:06:50 +02:00
kolaente f0e093b3d6
Remove logout button for link shares 2021-07-10 13:13:10 +02:00
kolaente 7fa94a9bd5
Fix loading a list when it was already partially saved in vuex 2021-07-10 12:45:36 +02:00
kolaente a6842d959b
Fix quick actions not opening 2021-07-10 12:35:29 +02:00
kolaente 20fd25e280
Add timeout to fix race condition when authenticating as a link share and renewing the token simultaneously
Related #587
2021-07-10 12:32:04 +02:00
kolaente a787f6ffc7
Save auth tokens from link shares only in memory, don't persist them to localStorage
Resolves #587
2021-07-09 20:10:57 +02:00
kolaente aebfde0c74
Fix error property already defined as a function 2021-07-09 19:10:25 +02:00
kolaente eac4f88a65
Don't load already loaded task attachments again when saving an edited task description 2021-07-09 18:04:49 +02:00
kolaente 2854d3a0c2
Fix table text alignment in task detail page
Fixes #585
2021-07-09 17:43:59 +02:00
renovate e3d7ebf1b3 Update dependency highlight.js to v11.1.0 (#582)
Reviewed-on: vikunja/frontend#582
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-09 08:44:34 +00:00
andreymal 7355204d2f Improve some translations (#581)
Reviewed-on: vikunja/frontend#581
Reviewed-by: konrad <konrad@kola-entertainments.de>
Co-authored-by: andreymal <andriyano-31@mail.ru>
Co-committed-by: andreymal <andriyano-31@mail.ru>
2021-07-09 08:22:20 +00:00
kolaente 096fc0bbc8
Fix tests failing on thursdays 2021-07-09 09:48:33 +02:00
kolaente 21e96a08c9
Change the docker builder image to a working one on arm 2021-07-07 22:39:42 +02:00
renovate 9f9016e17b Update dependency sass to v1.35.2 (#579)
Reviewed-on: vikunja/frontend#579
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-07 20:38:08 +00:00
kolaente b6bc410346
Add a button to copy an attachment url from the attachment overview 2021-07-07 22:13:21 +02:00
kolaente ac6082a670
Add collapsing kanban buckets 2021-07-07 21:58:41 +02:00
renovate 304ba797a0 Update Node.js to v16.4.2 (#578)
Reviewed-on: vikunja/frontend#578
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-07 19:16:32 +00:00
renovate c3f91a0449 Update dependency cypress to v7.7.0 (#577)
Reviewed-on: vikunja/frontend#577
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-07 15:12:41 +00:00
renovate b8c1086c4a Update Node.js to v16.4.1 (#576)
Reviewed-on: vikunja/frontend#576
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-07 09:26:38 +00:00
kolaente f3d4295049
Add frontend tests for list history 2021-07-07 10:26:31 +02:00
kolaente 9b4efc773b
Fix home page tests 2021-07-07 10:19:37 +02:00
kolaente c094b5175a
Show recently visited lists in quick actions 2021-07-06 22:58:06 +02:00
kolaente 8097ef93c1
Load list background in list card 2021-07-06 22:50:54 +02:00
kolaente d09eff1655
Show last visited list on home page 2021-07-06 22:22:57 +02:00
kolaente c7c9b5ee47
Show salutation based on the time of day 2021-07-06 17:13:13 +02:00
kolaente f3715c7900
Add showing version info in GUI
Resolves #373
2021-07-06 17:05:35 +02:00
kolaente 647b02e989
Fix translating dates 2021-07-06 16:46:45 +02:00
kolaente 21d31f2b40
Indicate done tasks in quick actions 2021-07-06 15:08:55 +02:00
kolaente e640f90488
Don't show archived lists/namespaces in quick actions 2021-07-06 15:00:31 +02:00
kolaente f68794bd01
Change quick add magic characters to be more familiar with the todoist ones 2021-07-06 14:53:32 +02:00
renovate e63dbdd625 Update dependency eslint-plugin-vue to v7.13.0 (#574)
Reviewed-on: vikunja/frontend#574
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-06 12:47:40 +00:00
renovate f9e5a17907 Update dependency dompurify to v2.3.0 (#573)
Reviewed-on: vikunja/frontend#573
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-06 11:10:58 +00:00
renovate 8fca4f4c53 Update dependency vue-flatpickr-component to v8.1.7 (#572)
Reviewed-on: vikunja/frontend#572
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-06 08:16:26 +00:00
konrad c8209c6c10 Quick add magic for tasks (#570)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#570
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-07-05 10:29:04 +00:00
renovate bc73d75a9b Update dependency eslint to v7.30.0 (#571)
Reviewed-on: vikunja/frontend#571
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-07-04 13:10:30 +00:00
renovate 7e48f65ff0 Update dependency jest to v27.0.6 (#569)
Reviewed-on: vikunja/frontend#569
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-28 18:28:01 +00:00
renovate 101a60799a Update dependency wait-on to v6 (#568)
Reviewed-on: vikunja/frontend#568
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-27 11:34:57 +00:00
renovate 34ab4fbf5e Update dependency marked to v2.1.3 (#567)
Reviewed-on: vikunja/frontend#567
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-25 21:38:17 +00:00
kolaente c5e53175cf
Allow failure of the weblate update step 2021-06-25 17:32:48 +02:00
renovate 3b23378a26 Update dependency eslint-plugin-vue to v7.12.1 (#565)
Reviewed-on: vikunja/frontend#565
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-25 10:57:26 +00:00
kolaente 807685028c
Break long list titles in list overview 2021-06-24 17:35:44 +02:00
kolaente 7d9328735c
Fix some translation strings 2021-06-24 15:39:26 +02:00
kolaente b8c7dba0ef
Fix new lists created with quick actions not showing up in the menu 2021-06-24 15:38:25 +02:00
kolaente 2b7e9856d8
Fix flickering pre-loaded search results when focusing the search input 2021-06-24 15:22:48 +02:00
kolaente 7b99d96df0
Move weblate ping to shell script 2021-06-24 13:23:48 +02:00
renovate 1f42d18d47 Update dependency vue-i18n to v8.24.5 (#564)
Reviewed-on: vikunja/frontend#564
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-24 11:17:01 +00:00
renovate 13f02c6165 Pin dependency vue-i18n to v8.24.4 (#563)
Reviewed-on: vikunja/frontend#563
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-24 10:01:43 +00:00
konrad f0498fd767 Add translations (#562)
Reviewed-on: vikunja/frontend#562
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-06-23 23:24:57 +00:00
renovate 5badb65037 Update dependency cypress to v7.6.0 (#561)
Reviewed-on: vikunja/frontend#561
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-23 22:30:19 +00:00
kolaente 543df91aba
Fix header layout for long list titles 2021-06-23 22:08:20 +02:00
kolaente f58a48bc1f
Move general settings to the top 2021-06-23 17:23:51 +02:00
kolaente 0e53745e91
Refactor success and error notifications to prevent html in them 2021-06-22 22:41:29 +02:00
kolaente cdc805c8da
Refactor success and error messages 2021-06-22 22:07:57 +02:00
renovate ab4edc17de Update dependency marked to v2.1.2 (#559)
Reviewed-on: vikunja/frontend#559
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-22 19:32:44 +00:00
renovate 92d3c8521f Update dependency jest to v27.0.5 (#558)
Reviewed-on: vikunja/frontend#558
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-22 12:14:25 +00:00
renovate 7f2fba8bb7 Update dependency vue-router to v3.5.2 (#557)
Reviewed-on: vikunja/frontend#557
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-21 16:10:15 +00:00
renovate 81f9d62ce2 Update dependency cypress-file-upload to v5.0.8 (#556)
Reviewed-on: vikunja/frontend#556
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-21 08:40:48 +00:00
renovate 9abe2a4de2 Update dependency eslint to v7.29.0 (#555)
Reviewed-on: vikunja/frontend#555
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-18 22:09:31 +00:00
renovate fe5ecb4865 Update dependency bulma to v0.9.3 (#554)
Reviewed-on: vikunja/frontend#554
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-18 22:09:00 +00:00
renovate 08708927e6 Update dependency marked to v2.1.1 (#553)
Reviewed-on: vikunja/frontend#553
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-16 18:31:45 +00:00
renovate e9fdf15f23 Update dependency marked to v2.1.0 (#552)
Reviewed-on: vikunja/frontend#552
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-16 08:18:25 +00:00
renovate 3a77326318 Update dependency sass to v1.35.1 (#551)
Reviewed-on: vikunja/frontend#551
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-15 21:19:52 +00:00
renovate 70dd4f8512 Update dependency sass to v1.35.0 (#550)
Reviewed-on: vikunja/frontend#550
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-15 08:59:48 +00:00
renovate f55d1151f4 Update Node.js (#549)
Reviewed-on: vikunja/frontend#549
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-14 20:33:56 +00:00
renovate 37dae144cf Update dependency eslint-plugin-vue to v7.11.1 (#548)
Reviewed-on: vikunja/frontend#548
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-13 15:26:12 +00:00
renovate 264393203f Update dependency eslint-plugin-vue to v7.11.0 (#547)
Reviewed-on: vikunja/frontend#547
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-11 07:02:01 +00:00
renovate 1c01cb3e47 Update dependency vue-advanced-cropper to v1.7.0 (#543)
Reviewed-on: vikunja/frontend#543
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-08 20:28:18 +00:00
renovate 7dda54d675 Update dependency cypress to v7.5.0 (#541)
Reviewed-on: vikunja/frontend#541
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-08 08:07:27 +00:00
renovate 5da6a6bb39 Update vue monorepo to v2.6.14 (#540)
Reviewed-on: vikunja/frontend#540
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-07 10:27:53 +00:00
renovate 34ded051d1 Update dependency eslint to v7.28.0 (#539)
Reviewed-on: vikunja/frontend#539
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-05 08:26:45 +00:00
renovate 65be56eb39 Update dependency highlight.js to v11.0.1 (#538)
Reviewed-on: vikunja/frontend#538
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-04 09:21:03 +00:00
kolaente 3dbd36eef7
Move creation of new items to the bottom of the multiselect list 2021-06-03 22:58:47 +02:00
kolaente a9d3446ce3
Preload labels and use locally stored in vuex 2021-06-03 22:23:04 +02:00
kolaente e37145cd43
Add setting for the first day of the week 2021-06-03 18:12:40 +02:00
kolaente 641ccd1026
Fix resetting date filters from upcoming after viewing a task detail page (popup) 2021-06-03 17:18:38 +02:00
kolaente 0cd9d43a7c
Fix showing edit buttons when the user does not have the rights to use them 2021-06-03 16:27:41 +02:00
kolaente c92062b6a5
Fix list archived notification mobile layout 2021-06-03 15:38:46 +02:00
kolaente 4a3b4982ab
Fix saving showing archived setting 2021-06-03 15:31:39 +02:00
kolaente 0f42ed3cf7
Add filters for quick action bar 2021-06-03 14:20:26 +02:00
renovate 7802cf619f Update dependency sass to v1.34.1 (#534)
Reviewed-on: vikunja/frontend#534
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-03 09:15:34 +00:00
renovate 2a8ecb8a8c Update dependency jest to v27.0.4 (#535)
Reviewed-on: vikunja/frontend#535
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-03 09:15:21 +00:00
renovate ab02c2d896 Update dependency marked to v2.0.7 (#532)
Reviewed-on: vikunja/frontend#532
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-02 14:18:24 +00:00
renovate 324d387d0a Update dependency dompurify to v2.2.9 (#529)
Reviewed-on: vikunja/frontend#529
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-02 08:16:09 +00:00
kolaente 570cfc8610
Configure tests retries 2021-06-02 09:08:24 +02:00
renovate af9fd856eb Update vue monorepo to v2.6.13 (#530)
Reviewed-on: vikunja/frontend#530
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-06-02 07:03:56 +00:00
kolaente 80471f95e2
Improve tests 2021-05-31 21:37:21 +02:00
kolaente 9c2d076f58
Make tests less flaky 2021-05-30 21:40:56 +02:00
kolaente 1969bd8190
Add quick action bar shortcut to shortcut overview 2021-05-30 21:14:15 +02:00
kolaente 7690cada78
Fix menu styles 2021-05-30 21:00:54 +02:00
konrad b85beb06eb Quick Actions & global search (#528)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#528
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-05-30 18:30:08 +00:00
renovate dff84209f0 Update dependency highlight.js to v11 (#527)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#527
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-30 10:12:59 +00:00
renovate 30e53d7aa2 Update dependency jest to v27.0.3 (#526)
Reviewed-on: vikunja/frontend#526
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-30 09:26:10 +00:00
renovate 3834f961de Update dependency eslint-plugin-vue to v7.10.0 (#525)
Reviewed-on: vikunja/frontend#525
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-30 08:41:41 +00:00
renovate 6e3ac2a7c0 Update dependency date-fns to v2.22.1 (#524)
Reviewed-on: vikunja/frontend#524
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-28 13:26:56 +00:00
renovate 9e75d2b461 Update dependency date-fns to v2.22.0 (#523)
Reviewed-on: vikunja/frontend#523
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-28 08:57:02 +00:00
renovate a4ec322ce2 Update dependency marked to v2.0.6 (#522)
Reviewed-on: vikunja/frontend#522
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-27 18:09:08 +00:00
konrad 8c3dd387a3 Add more global state tests (#521)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#521
Co-authored-by: konrad <konrad@kola-entertainments.de>
Co-committed-by: konrad <konrad@kola-entertainments.de>
2021-05-27 06:51:42 +00:00
kolaente 1fa42f047c
Fix usage of / in sass 2021-05-26 22:10:50 +02:00
kolaente 9c799ab161
Fix removing a namespace from state after it was deleted 2021-05-26 17:39:57 +02:00
kolaente 188134ae2e
Fix users not removed from the list in settings when unshared 2021-05-26 17:32:03 +02:00
kolaente 9d818921a7
Fix not updating list name in store when changing it 2021-05-26 16:46:16 +02:00
renovate 912cb66970 Update dependency jest to v27 (#519)
Reviewed-on: vikunja/frontend#519
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-25 11:18:06 +00:00
renovate 2b7b8d695f Update dependency vue-advanced-cropper to v1.6.0 (#516)
Reviewed-on: vikunja/frontend#516
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-24 21:43:20 +00:00
renovate 62004556ad Update dependency cypress to v7.4.0 (#517)
Reviewed-on: vikunja/frontend#517
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-24 21:42:52 +00:00
renovate e3ede85b79 Update dependency marked to v2.0.5 (#513)
Reviewed-on: vikunja/frontend#513
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-24 11:57:04 +00:00
renovate 7d6361713f Update dependency eslint to v7.27.0 (#514)
Reviewed-on: vikunja/frontend#514
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-24 10:24:24 +00:00
renovate 2fc23b081f Update dependency sass to v1.34.0 (#515)
Reviewed-on: vikunja/frontend#515
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-24 10:24:09 +00:00
renovate d0c819e280 Update dependency cypress to v7.3.0 (#507)
Reviewed-on: vikunja/frontend#507
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-21 08:41:59 +00:00
renovate 2cbc0b1f0a Update dependency sass to v1.33.0 (#512)
Reviewed-on: vikunja/frontend#512
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-21 08:41:32 +00:00
renovate 0f2ee69f2a Update dependency marked to v2.0.4 (#510)
Reviewed-on: vikunja/frontend#510
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-05-20 14:52:48 +00:00
282 changed files with 23941 additions and 15420 deletions

View File

@ -12,75 +12,97 @@ trigger:
services:
- name: api
image: vikunja/api
image: vikunja/api:unstable
environment:
VIKUNJA_SERVICE_TESTINGTOKEN: averyLongSecretToSe33dtheDB
VIKUNJA_LOG_LEVEL: DEBUG
steps:
- name: restore-cache
image: meltwater/drone-cache:dev
pull: true
environment:
AWS_ACCESS_KEY_ID:
from_secret: cache_aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: cache_aws_secret_access_key
settings:
restore: true
bucket: kolaente.dev-drone-dependency-cache
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
path_style: true
cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
mount:
- '.cache'
# Disabled until we figure out why it is so slow
# - name: restore-cache
# image: meltwater/drone-cache:dev
# pull: true
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
# AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key
# settings:
# restore: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
# - '.cache'
- name: dependencies
image: node:12
image: node:16
pull: true
environment:
YARN_CACHE_FOLDER: .cache/yarn/
CYPRESS_CACHE_FOLDER: .cache/cypress/
commands:
- yarn --frozen-lockfile --network-timeout 100000
depends_on:
- restore-cache
# depends_on:
# - restore-cache
- name: rebuild-cache
image: meltwater/drone-cache:dev
pull: true
environment:
AWS_ACCESS_KEY_ID:
from_secret: cache_aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: cache_aws_secret_access_key
settings:
rebuild: true
bucket: kolaente.dev-drone-dependency-cache
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
path_style: true
cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
mount:
- '.cache'
depends_on:
- dependencies
# - name: rebuild-cache
# image: meltwater/drone-cache:dev
# pull: true
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
# AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key
# settings:
# rebuild: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
# - '.cache'
# depends_on:
# - dependencies
- name: build
image: node:12
- name: lint
image: node:16
pull: true
environment:
YARN_CACHE_FOLDER: .cache/yarn/
CYPRESS_CACHE_FOLDER: .cache/cypress/
commands:
- yarn run lint
- yarn run build
depends_on:
- dependencies
# Building in dev mode to avoid the service worker for testing
- name: build-dev
image: node:16
pull: true
environment:
YARN_CACHE_FOLDER: .cache/yarn/
CYPRESS_CACHE_FOLDER: .cache/cypress/
commands:
- yarn build:dev
depends_on:
- dependencies
- name: build-prod
image: node:16
pull: true
environment:
YARN_CACHE_FOLDER: .cache/yarn/
commands:
- yarn build --dest dist-prod
depends_on:
- dependencies
- name: test-unit
image: node:12
image: node:16
pull: true
commands:
- yarn test:unit
@ -88,20 +110,21 @@ steps:
- dependencies
- name: test-frontend
image: cypress/browsers:node12.18.3-chrome87-ff82
image: cypress/browsers:node14.17.0-chrome91-ff89
pull: true
environment:
CYPRESS_API_URL: http://api:3456/api/v1
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
YARN_CACHE_FOLDER: .cache/yarn/
CYPRESS_CACHE_FOLDER: .cache/cypress/
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 10000
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
commands:
- sed -i 's/localhost/api/g' public/index.html
- yarn serve & npx wait-on http://localhost:8080
- sed -i 's/localhost/api/g' dist-dev/index.html
- yarn serve:dist-dev & npx wait-on http://localhost:5000
- yarn test:frontend --browser chrome
depends_on:
- dependencies
- build-dev
- name: upload-test-results
image: plugins/s3:1
@ -144,26 +167,26 @@ steps:
commands:
- git fetch --tags
- name: restore-cache
image: meltwater/drone-cache:dev
pull: true
environment:
AWS_ACCESS_KEY_ID:
from_secret: cache_aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: cache_aws_secret_access_key
settings:
restore: true
bucket: kolaente.dev-drone-dependency-cache
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
path_style: true
cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
mount:
- '.cache'
# - name: restore-cache
# image: meltwater/drone-cache:dev
# pull: true
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
# AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key
# settings:
# restore: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
# - '.cache'
- name: build
image: node:12
image: node:16
pull: true
group: build-static
environment:
@ -174,8 +197,8 @@ steps:
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- yarn run build
- sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing
depends_on:
- restore-cache
# depends_on:
# - restore-cache
- name: static
image: kolaente/zip
@ -219,26 +242,26 @@ steps:
commands:
- git fetch --tags
- name: restore-cache
image: meltwater/drone-cache:dev
pull: true
environment:
AWS_ACCESS_KEY_ID:
from_secret: cache_aws_access_key_id
AWS_SECRET_ACCESS_KEY:
from_secret: cache_aws_secret_access_key
settings:
restore: true
bucket: kolaente.dev-drone-dependency-cache
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
path_style: true
cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
mount:
- '.cache'
# - name: restore-cache
# image: meltwater/drone-cache:dev
# pull: true
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
# AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key
# settings:
# restore: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
# - '.cache'
- name: build
image: node:12
image: node:16
pull: true
group: build-static
environment:
@ -249,8 +272,8 @@ steps:
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- yarn run build
- sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing
depends_on:
- restore-cache
# depends_on:
# - restore-cache
- name: static
image: kolaente/zip
@ -288,7 +311,7 @@ trigger:
- push
depends_on:
- release-latest
- release-latest
steps:
- name: trigger
@ -319,7 +342,7 @@ trigger:
- "refs/tags/**"
steps:
- name: docker-latest
- name: docker-unstable
image: plugins/docker:linux-arm
pull: true
settings:
@ -328,7 +351,7 @@ steps:
password:
from_secret: docker_password
repo: vikunja/frontend
tags: latest-linux-arm
tags: unstable-linux-arm
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=unstable
@ -358,7 +381,7 @@ steps:
depends_on:
- clone
- name: docker-latest-arm64
- name: docker-unstable-arm64
image: plugins/docker:linux-arm64
pull: true
settings:
@ -367,7 +390,7 @@ steps:
password:
from_secret: docker_password
repo: vikunja/frontend
tags: latest-linux-arm64
tags: unstable-linux-arm64
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=unstable
@ -416,7 +439,7 @@ trigger:
- "refs/tags/**"
steps:
- name: docker-latest
- name: docker-unstable
image: plugins/docker:linux-amd64
pull: true
settings:
@ -425,7 +448,7 @@ steps:
password:
from_secret: docker_password
repo: vikunja/frontend
tags: latest-linux-amd64
tags: unstable-linux-amd64
build_args:
- USE_RELEASE=true
- RELEASE_VERSION=unstable
@ -466,12 +489,12 @@ depends_on:
- docker-arm-release
steps:
- name: manifest-latest
- name: manifest-unstable
pull: always
image: plugins/manifest
settings:
tags: latest
spec: docker-manifest-latest.tmpl
tags: unstable
spec: docker-manifest-unstable.tmpl
password:
from_secret: docker_password
username:
@ -494,6 +517,23 @@ steps:
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
when:
ref:
- "refs/tags/**"
---
kind: pipeline
@ -528,3 +568,66 @@ steps:
status:
- success
- failure
---
kind: pipeline
type: docker
name: update-translations
depends_on:
- build
trigger:
branch:
- main
event:
- push
steps:
- name: download
pull: always
image: jonasfranz/crowdin
settings:
download: true
export_dir: src/i18n/lang/
ignore_branch: true
project_identifier: vikunja
environment:
CROWDIN_KEY:
from_secret: crowdin_key
- name: move-files
pull: always
image: bash
depends_on:
- download
commands:
- mv src/i18n/lang/*/*.json src/i18n/lang
- name: push
pull: always
image: appleboy/drone-git-push
depends_on:
- move-files
settings:
author_email: "frederik@vikunja.io"
author_name: Frederick [Bot]
branch: main
commit: true
commit_message: "[skip ci] Updated translations via Crowdin"
remote: "ssh://git@kolaente.dev:9022/vikunja/frontend.git"
ssh_key:
from_secret: translation_git_push_ssh_key
- name: upload
pull: always
image: jonasfranz/crowdin
depends_on:
- clone
settings:
files:
en.json: src/i18n/lang/en.json
ignore_branch: true
project_identifier: vikunja
environment:
CROWDIN_KEY:
from_secret: crowdin_key

25
.editorconfig Normal file
View File

@ -0,0 +1,25 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = tab
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = false
insert_final_newline = false
[*.vue]
indent_style = tab
[*.{yaml,yml}]
indent_style = space
indent_size = 2
[*.json]
indent_style = space
indent_size = 2
[.nvmrc]
insert_final_newline = false

4
.gitignore vendored
View File

@ -1,6 +1,6 @@
.DS_Store
node_modules
/dist
/dist*
*.zip
# local env files
@ -11,6 +11,7 @@ node_modules
npm-debug.log*
yarn-debug.log*
yarn-error.log*
stats.html
# Editor directories and files
.idea
@ -20,6 +21,7 @@ yarn-error.log*
*.njsproj
*.sln
*.sw*
!rollup.sw.js
# Test files
cypress/screenshots

1
.nvmrc Normal file
View File

@ -0,0 +1 @@
v16

View File

@ -0,0 +1,12 @@
{
"recommendations": [
"codezombiech.gitignore",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"johnsoncodehk.volar",
"lokalise.i18n-ally",
"mgmcdermott.vscode-language-babel",
"mikestead.dotenv",
"Syler.sass-indented"
]
}

View File

@ -0,0 +1,30 @@
{
"eslint.packageManager": "yarn",
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": true
},
"eslint.format.enable": true,
"[javascript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
"[typescript]": {
"editor.defaultFormatter": "dbaeumer.vscode-eslint"
},
// https://eslint.vuejs.org/user-guide/#editor-integrations
"eslint.validate": [
"javascript",
"javascriptreact",
"vue"
],
"vetur.validation.template": false,
// i18n ally
"i18n-ally.localesPaths": [
"src/i18n/lang"
],
"i18n-ally.sortKeys": true,
"i18n-ally.keepFulfilled": true,
"i18n-ally.keystyle": "nested",
}

View File

@ -2,13 +2,347 @@
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
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.18.1] - 2021-09-08
### Added
* feat: make it possible to fake online state via dev env (#720)
### Fixed
* fix: call to /null from background image (#714)
* Fix data export download progress
* fix: kanban-card mutatation violation (#712)
* Fix missing translation when creating a new task on the kanban board
* Fix rearranging tasks in a kanban bucket when its limit was reached
* Fix sort order for table view
* Fix task attributes overridden when saving the task title with enter
* Fix translation badge
### Dependency Updates
* Update dependency @4tw/cypress-drag-drop to v2 (#711)
* Update dependency axios to v0.21.4 (#705)
* Update dependency jest to v27.1.1 (#716)
* Update dependency vite-plugin-vue2 to v1.8.2 (#707)
* Update dependency vite to v2.5.4 (#708)
* Update dependency vite to v2.5.5 (#709)
* Update typescript-eslint monorepo to v4.31.0 (#706)
## [0.18.0] - 2021-09-05
### Added
* Add a button to copy an attachment url from the attachment overview
* Add collapsing kanban buckets
* Add confirm with enter when setting a new password
* Add default list setting & creating tasks from home (#520)
* Add depends_on for push step
* Add depends_on for upload step
* Add drag delay on mobile
* Add express for serve:dev
* Add filters for quick action bar
* Add frontend tests for list history
* Add making tasks favorite from the task detail view
* Add missing position property to list and bucket models
* Add more debug logs for gantt charts
* Add more global state tests (#521)
* Add proofread languages to available languages
* Add quick action bar shortcut to shortcut overview
* Add setting for the first day of the week
* Add showing version info in GUI
* Add syncing translations to crowdin
* Add timeout to fix race condition when authenticating as a link share and renewing the token simultaneously
* Add translations (#562)
* Add typescript support for helper functions (#598)
* Add vite (#416)
* Allow failure of the weblate update step
* Always set the kanban board to full width for share links
* Another day, another js date edge-case
* Automatically update approved translations from crowdin
* Break long list titles in list overview
* Preload labels and use locally stored in vuex
* PWA improvments (#622)
* Quick Actions & global search (#528)
* Quick add magic for tasks (#570)
* Reorder tasks, lists and kanban buckets (#620)
* Show last visited list on home page
* Show recently visited lists in quick actions
* Show salutation based on the time of day
* Sort labels alphabetically on tasks
* Switch the :latest docker image tag to contain the latest release instead of the latest unstable
### Changed
* Change building latest docker image
* Change desktop downstream trigger plugin with our own debug build
* Change menu hamburger icon
* Change quick add magic characters to be more familiar with the todoist ones
* Change the docker builder image to a working one on arm
* chore: discard old font file formats (#673)
* chore: only import common languages (#671)
* Cleanup broken sw functions
* Cleanup drone pipeline
* Cleanup old vue cli config
* Configure tests retries
* Decrease page padding on task detail page
* Directly redirect to the openid auth provider if that's the only auth method
* Don't allow dragging a list when the user does not have the rights
* Don't load already loaded task attachments again when saving an edited task description
* Don't prefetch all i18n files
* Don't show archived lists/namespaces in quick actions
* feat: provide global variables in all components (#669)
* Hide favorite list edit menu
* Hide keyboard shortcuts indicator on mobile
* Improve chunk size
* Improve some translations (#581)
* Improve tests
* Indicate done tasks in quick actions
* Load list background in list card
* Make editor edit button at the bottom the default and make sure the done button stands out more
* Make saving a text edit a button
* Make sure highlight.js is always lazy-loaded
* Make sure the task popup view takes up all the space it can on mobile
* Make tests less flaky
* Make the logo smaller on link shared lists
* Make the progress bar color lighter
* Move creation of new items to the bottom of the multiselect list
* Move general settings to the top
* Move translated files after downloading them
* Move weblate ping to shell script
* Only add a drag delay if on mobile instead of setting it to 0
* Only build a bundle for modern browsers
* Refactor success and error messages
* Refactor success and error notifications to prevent html in them
* Remove logout button for link shares
* Run frontend-tests with dist in ci (#605)
* Save auth tokens from link shares only in memory, don't persist them to localStorage
* Search namespaces locally only when duplicating a list
* Show errors from openid provider
* Show labels alphabetically sorted in the overview
* Small cleanups & code improvements
* TOTP UX improvements & translation fixes
### Fixed
* Fix changing the repeat mode of a task when no value is entered yet
* Fix comment on different task after clicking on a task notification
* Fix CTA spacings
* Fix date parsing parsing words with weekdays in them (#607)
* fix(deps): update dependency marked to v3.0.1 (#677)
* fix(deps): update dependency marked to v3.0.2 (#682)
* Fix error property already defined as a function
* Fix flickering pre-loaded search results when focusing the search input
* Fix Gantt layout overflowsing on mobile
* Fix gantt months being wrong
* Fix git push remote to update crowdin translations
* Fix global mutation of has tasks state
* Fix header layout for long list titles
* Fix highlight.js in editor
* Fix home page tests
* Fix keyboard shortcuts not working on the task detail page
* Fix label changes appearing to be saved immediately when editing them
* Fix labels list in saved filter spacing
* Fix lint
* Fix list archived notification mobile layout
* Fix list settings not being available when list backgrounds are disabled
* Fix lists showing up multiple times in history
* Fix llama background url
* Fix loading a list when it was already partially saved in vuex
* Fix loading & disabled state on inputs when creating a new task
* Fix loading labels when editing a saved filter
* Fix menu styles
* Fix missing background for tasks on a shared list with a background
* Fix multiselect search padding
* Fix new lists created with quick actions not showing up in the menu
* fix: non unique ids (#672)
* Fix not reloading tasks of a saved filter after editing it
* Fix not updating list name in store when changing it
* Fix other values getting pushed away when creating a new one through multiselect
* Fix padding for kanban cards
* Fix parsing dates on the last day of the month
* Fix populating task details ater updating the description
* Fix quick actions not opening
* Fix quick actions not working when nonexisting lists where left over in history
* Fix redirecting to /login for some routes
* Fix removing a namespace from state after it was deleted
* Fix resetting date filters from upcoming after viewing a task detail page (popup)
* Fix sass division
* Fix saving showing archived setting
* Fix selecting a single value from multiselect
* Fix sending openid scopes when authenticating
* Fix sending the user back to the list view they came from when opening a task in detail view
* Fix setting a task as favorite button
* Fix setting delete button for newly created task comments
* Fix setting filters for reminders
* Fix setting secret for updating translations
* Fix setting task favorite status in test fixtures
* Fix showing an editor save button in cases where it wasn't required
* Fix showing edit buttons when the user does not have the rights to use them
* Fix showing import tasks cta when tasks are loading
* Fix some translation strings
* Fix sorting labels
* Fix spacing for task detail view in lists with a background
* Fix table headers wrapping in table view
* Fix table text alignment in task detail page
* Fix table view scrolling on mobile
* Fix test for saving a task description
* Fix tests failing on thursdays
* Fix token in storage not getting renewed
* Fix translating dates
* Fix usage of / in sass
* Fix user name and avatar alignment in navbar
* Fix users not removed from the list in settings when unshared
* Fix user test fixtures
* fix: vuex mutation violation from draggable (#674)
### Dependency Updates
* chore(deps): update dependency @4tw/cypress-drag-drop to v1.8.1 (#693)
* chore(deps): update dependency autoprefixer to v10.3.3 (#684)
* chore(deps): update dependency autoprefixer to v10.3.4 (#697)
* chore(deps): update dependency axios to v0.21.2 (#698)
* chore(deps): update dependency axios to v0.21.3 (#700)
* chore(deps): update dependency cypress to v8.3.1 (#689)
* chore(deps): update dependency esbuild to v0.12.23 (#683)
* chore(deps): update dependency esbuild to v0.12.24 (#688)
* chore(deps): update dependency esbuild to v0.12.25 (#696)
* chore(deps): update dependency eslint-plugin-vue to v7.17.0 (#686)
* chore(deps): update dependency jest to v27.1.0 (#687)
* chore(deps): update dependency sass to v1.38.1 (#679)
* chore(deps): update dependency sass to v1.38.2 (#690)
* chore(deps): update dependency sass to v1.39.0 (#695)
* chore(deps): update dependency typescript to v4.4.2 (#685)
* chore(deps): update dependency vite-plugin-pwa to v0.11.2 (#681)
* chore(deps): update dependency vite to v2.5.1 (#680)
* chore(deps): update dependency vite to v2.5.2 (#692)
* chore(deps): update dependency vite to v2.5.3 (#694)
* chore(deps): update typescript-eslint monorepo to v4.29.3 (#676)
* chore(deps): update typescript-eslint monorepo to v4.30.0 (#691)
* Update dependency autoprefixer to v10.3.2 (#670)
* Update dependency browserslist to v4.16.7 (#634)
* Update dependency browserslist to v4.16.8 (#664)
* Update dependency browserslist to v4.17.0 (#701)
* Update dependency bulma to v0.9.3 (#554)
* Update dependency cypress-file-upload to v5.0.8 (#556)
* Update dependency cypress to v7.3.0 (#507)
* Update dependency cypress to v7.4.0 (#517)
* Update dependency cypress to v7.5.0 (#541)
* Update dependency cypress to v7.6.0 (#561)
* Update dependency cypress to v7.7.0 (#577)
* Update dependency cypress to v8.1.0 (#624)
* Update dependency cypress to v8.2.0 (#637)
* Update dependency cypress to v8.3.0 (#660)
* Update dependency cypress to v8 (#601)
* Update dependency date-fns to v2.22.0 (#523)
* Update dependency date-fns to v2.22.1 (#524)
* Update dependency date-fns to v2.23.0 (#604)
* Update dependency dompurify to v2.2.9 (#529)
* Update dependency dompurify to v2.3.0 (#573)
* Update dependency dompurify to v2.3.1 (#655)
* Update dependency esbuild to v0.12.15 (#610)
* Update dependency esbuild to v0.12.16 (#614)
* Update dependency esbuild to v0.12.17 (#623)
* Update dependency esbuild to v0.12.18 (#638)
* Update dependency esbuild to v0.12.19 (#643)
* Update dependency esbuild to v0.12.20 (#654)
* Update dependency esbuild to v0.12.21 (#666)
* Update dependency esbuild to v0.12.22 (#668)
* Update dependency eslint-plugin-vue to v7.10.0 (#525)
* Update dependency eslint-plugin-vue to v7.11.0 (#547)
* Update dependency eslint-plugin-vue to v7.11.1 (#548)
* Update dependency eslint-plugin-vue to v7.12.1 (#565)
* Update dependency eslint-plugin-vue to v7.13.0 (#574)
* Update dependency eslint-plugin-vue to v7.14.0 (#597)
* Update dependency eslint-plugin-vue to v7.15.0 (#625)
* Update dependency eslint-plugin-vue to v7.15.1 (#633)
* Update dependency eslint-plugin-vue to v7.16.0 (#648)
* Update dependency eslint to v7.27.0 (#514)
* Update dependency eslint to v7.28.0 (#539)
* Update dependency eslint to v7.29.0 (#555)
* Update dependency eslint to v7.30.0 (#571)
* Update dependency eslint to v7.31.0 (#596)
* Update dependency eslint to v7.32.0 (#627)
* Update dependency highlight.js to v11.0.1 (#538)
* Update dependency highlight.js to v11.1.0 (#582)
* Update dependency highlight.js to v11.2.0 (#630)
* Update dependency highlight.js to v11 (#527)
* Update dependency jest to v27.0.3 (#526)
* Update dependency jest to v27.0.4 (#535)
* Update dependency jest to v27.0.5 (#558)
* Update dependency jest to v27.0.6 (#569)
* Update dependency jest to v27 (#519)
* Update dependency marked to v2.0.4 (#510)
* Update dependency marked to v2.0.5 (#513)
* Update dependency marked to v2.0.6 (#522)
* Update dependency marked to v2.0.7 (#532)
* Update dependency marked to v2.1.0 (#552)
* Update dependency marked to v2.1.1 (#553)
* Update dependency marked to v2.1.2 (#559)
* Update dependency marked to v2.1.3 (#567)
* Update dependency marked to v3 (#657)
* Update dependency @rollup/plugin-commonjs to v19.0.2 (#617)
* Update dependency sass to v1.33.0 (#512)
* Update dependency sass to v1.34.0 (#515)
* Update dependency sass to v1.34.1 (#534)
* Update dependency sass to v1.35.0 (#550)
* Update dependency sass to v1.35.1 (#551)
* Update dependency sass to v1.35.2 (#579)
* Update dependency sass to v1.36.0 (#606)
* Update dependency sass to v1.37.0 (#628)
* Update dependency sass to v1.37.2 (#632)
* Update dependency sass to v1.37.5 (#635)
* Update dependency sass to v1.38.0 (#661)
* Update dependency ts-jest to v27.0.4 (#602)
* Update dependency ts-jest to v27.0.5 (#662)
* Update dependency @types/jest to v27.0.1 (#653)
* Update dependency @types/jest to v27 (#650)
* Update dependency vite-plugin-pwa to v0.10.0 (#644)
* Update dependency vite-plugin-pwa to v0.11.0 (#667)
* Update dependency vite-plugin-pwa to v0.8.2 (#612)
* Update dependency vite-plugin-pwa to v0.9.3 (#629)
* Update dependency vite-plugin-vue2 to v1.7.3 (#613)
* Update dependency vite-plugin-vue2 to v1.8.0 (#646)
* Update dependency vite-plugin-vue2 to v1.8.1 (#656)
* Update dependency vite to v2.4.3 (#611)
* Update dependency vite to v2.4.4 (#619)
* Update dependency vite to v2.5.0 (#658)
* Update dependency vue-advanced-cropper to v1.6.0 (#516)
* Update dependency vue-advanced-cropper to v1.7.0 (#543)
* Update dependency vue-advanced-cropper to v1.8.0 (#641)
* Update dependency vue-advanced-cropper to v1.8.1 (#642)
* Update dependency vue-advanced-cropper to v1.8.2 (#645)
* Update dependency vue-flatpickr-component to v8.1.7 (#572)
* Update dependency vue-i18n to v8.24.5 (#564)
* Update dependency vue-i18n to v8.25.0 (#595)
* Update dependency vue-router to v3.5.2 (#557)
* Update dependency wait-on to v6 (#568)
* Update dependency workbox-cli to v6.1.5 (#609)
* Update Font Awesome (#636)
* Update Node.js (#549)
* Update Node.js to v16.4.1 (#576)
* Update Node.js to v16.4.2 (#578)
* Update typescript-eslint monorepo to v4.28.4 (#600)
* Update typescript-eslint monorepo to v4.28.5 (#618)
* Update typescript-eslint monorepo to v4.29.0 (#631)
* Update typescript-eslint monorepo to v4.29.1 (#647)
* Update typescript-eslint monorepo to v4.29.2 (#659)
* Update vue monorepo to v2.6.13 (#530)
* Update vue monorepo to v2.6.14 (#540)
* Update workbox monorepo to v6.2.0 (#639)
* Update workbox monorepo to v6.2.2 (#640)
* Update workbox monorepo to v6.2.4 (#649)
* User account deletion (#651)
* User Data Export and import (#699)
## [0.17.0 - 2021-05-14]
### Added
@ -148,7 +482,8 @@ The releases aim at the api versions which is why there are missing versions.
* Make sure all arm64 build steps run in parallel
* Make sure all empty pages have a call to action
* Make sure all popups & dropdowns are animated
* Make sure attachements are only added once to the list after uploading + Make sure the attachment list shows up every time after adding an attachment
* Make sure attachements are only added once to the list after uploading + Make sure the attachment list shows up every
time after adding an attachment
* Make sure no cta's are visible while the page is loading
* Make sure the loading spinner is always visible at the end of the page
* Make the button shadow lighter
@ -675,7 +1010,7 @@ The releases aim at the api versions which is why there are missing versions.
* Hide totp settings if it is disabled server side
* Increase network timeout when building docker image
* Make sure the version includes the tag when building docker images
* #PrideMonth
* # PrideMonth
* Only renew user token on tab focus events
* Redirect the user to login page if the token expired when the tab gets focus again
* Remove title length restrictions
@ -710,7 +1045,7 @@ The releases aim at the api versions which is why there are missing versions.
## [0.13] - 2020-05-12
#### Added
#### Added
* Add docker run script to change api url on startup
* Add github token for renovate (#89)
@ -1055,6 +1390,7 @@ The releases aim at the api versions which is why there are missing versions.
* Use email instead of username when resetting a password
### Fixed
* Fixed trying to verify an email when there was none
* Fixed loading tasks when the user was not authenticated

View File

@ -1,5 +1,5 @@
# Stage 1: Build application
FROM node:13.14.0 AS compile-image
FROM node:16 AS compile-image
WORKDIR /build

View File

@ -4,7 +4,8 @@
[![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.17.0-brightgreen.svg)](https://dl.vikunja.io)
[![Download](https://img.shields.io/badge/download-v0.18.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.
@ -19,21 +20,25 @@ If you find any security-related issues you don't want to disclose publicly, ple
There is a [docker image available](https://hub.docker.com/r/vikunja/api) with support for http/2 and aggressive caching enabled.
## Project setup
```
```shell
yarn install
```
### Compiles and hot-reloads for development
```
```shell
yarn run serve
```
### Compiles and minifies for production
```
```shell
yarn run build
```
### Lints and fixes files
```
```shell
yarn run lint
```

View File

@ -1,5 +1,5 @@
module.exports = {
presets: [
'@vue/app'
]
'@vue/app',
],
}

View File

@ -1,8 +1,11 @@
{
"baseUrl": "http://localhost:8080",
"baseUrl": "http://localhost:5000",
"env": {
"API_URL": "http://localhost:3456/api/v1",
"TEST_SECRET": "testingS3cr3et"
},
"video": false
"video": false,
"retries": {
"runMode": 2
}
}

View File

@ -9,8 +9,8 @@
## Fixtures
We're using the [test endpoint](https://vikunja.io/docs/config-options/#testingtoken) of the vikunja api to
seed the database with test data before running the tests.
We're using the [test endpoint](https://vikunja.io/docs/config-options/#testingtoken) of the vikunja api to
seed the database with test data before running the tests.
This ensures better reproducability of tests.
## Running The Tests Locally
@ -22,20 +22,20 @@ It uses the same configuration as the CI.
To use it, run
```
```shell
docker-compose up -d
```
Then, once all containers are started, run
```
docker-composer run cypress bash
```shell
docker-compose run cypress bash
```
to get a shell inside the cypress container.
In that shell you can then execute the tests with
```
```shell
yarn test:frontend
```
@ -43,6 +43,6 @@ yarn test:frontend
To open the Cypress Dashboard and run tests from there, run
```
```shell
yarn cypress:open
```

View File

@ -14,7 +14,6 @@ export class TaskFactory extends Factory {
done: false,
list_id: 1,
created_by_id: 1,
is_favorite: false,
index: '{increment}',
created: formatISO(now),
updated: formatISO(now)

View File

@ -13,7 +13,7 @@ export class UserFactory extends Factory {
id: '{increment}',
username: faker.lorem.word(10) + faker.random.uuid(),
password: '$2a$14$dcadBoMBL9jQoOcZK8Fju.cy0Ptx2oZECkKLnaa8ekRoTFe1w7To.', // 1234
is_active: true,
status: 0,
created: formatISO(now),
updated: formatISO(now)
}

View File

@ -10,10 +10,12 @@ import {BucketFactory} from '../../factories/bucket'
import '../../support/authenticateUser'
describe('Lists', () => {
let lists
beforeEach(() => {
UserFactory.create(1)
NamespaceFactory.create(1)
const lists = ListFactory.create(1, {
lists = ListFactory.create(1, {
title: 'First List'
})
TaskFactory.truncate()
@ -54,6 +56,64 @@ describe('Lists', () => {
.should('contain', '/lists/1/kanban')
})
it('Should rename the list in all places', () => {
const tasks = TaskFactory.create(5, {
id: '{increment}',
list_id: 1,
})
const newListName = 'New list name'
cy.visit('/lists/1')
cy.get('.list-title h1')
.should('contain', 'First List')
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-trigger')
.click()
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content')
.contains('Edit')
.click()
cy.get('#title')
.type(`{selectall}${newListName}`)
cy.get('footer.modal-card-foot .button')
.contains('Save')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.list-title h1')
.should('contain', newListName)
.should('not.contain', lists[0].title)
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child')
.should('contain', newListName)
.should('not.contain', lists[0].title)
cy.visit('/')
cy.get('.card-content .tasks')
.should('contain', newListName)
.should('not.contain', lists[0].title)
})
it('Should remove a list', () => {
cy.visit(`/lists/${lists[0].id}`)
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-trigger')
.click()
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list li:first-child .dropdown .dropdown-content')
.contains('Delete')
.click()
cy.url()
.should('contain', '/settings/delete')
cy.get('.modal-mask .modal-container .modal-content .actions a.button')
.contains('Do it')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.namespace-container .menu.namespaces-lists .more-container .menu-list')
.should('not.contain', lists[0].title)
cy.location('pathname')
.should('equal', '/')
})
describe('List View', () => {
it('Should be an empty list', () => {
cy.visit('/lists/1')
@ -193,20 +253,24 @@ describe('Lists', () => {
describe('Gantt View', () => {
it('Hides tasks with no dates', () => {
TaskFactory.create(1)
const tasks = TaskFactory.create(1)
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart-container .gantt-chart .tasks')
.should('be.empty')
.should('not.contain', tasks[0].title)
})
it('Shows tasks from the current and next month', () => {
const now = new Date()
const nextMonth = now
nextMonth.setDate(1)
nextMonth.setMonth(now.getMonth() + 1)
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart-container .gantt-chart .months')
.should('contain', format(now, 'MMMM'))
.should('contain', format(now.setMonth(now.getMonth() + 1), 'MMMM'))
.should('contain', format(nextMonth, 'MMMM'))
})
it('Shows tasks with dates', () => {
@ -324,7 +388,7 @@ describe('Lists', () => {
.first()
.click()
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
.contains('Limit: Not set')
.contains('Limit: Not Set')
.click()
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input')
.first()
@ -372,26 +436,23 @@ describe('Lists', () => {
.should('exist')
})
it('Can drag tasks around', () => {
const tasks = TaskFactory.create(2, {
list_id: 1,
bucket_id: 1,
})
cy.visit('/lists/1/kanban')
// The following test does not work. It seems like vue-smooth-dnd does not use either mousemove or dragstart
// (not sure why this actually works at all?) and as I'm planning to swap that out for vuedraggable/sortable.js
// anyway, I figured it wouldn't be worth the hassle right now.
// it('Can drag tasks around', () => {
// const tasks = TaskFactory.create(2, {
// list_id: 1,
// bucket_id: 1,
// })
// cy.visit('/lists/1/kanban')
//
// cy.get('.kanban .bucket .tasks .task')
// .contains(tasks[0].title)
// .first()
// .drag('.kanban .bucket:nth-child(2) .tasks .smooth-dnd-container.vertical')
// .trigger('mousedown', {which: 1})
// .trigger('mousemove', {clientX: 500, clientY: 0})
// .trigger('mouseup', {force: true})
// })
cy.get('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
.first()
.drag('.kanban .bucket:nth-child(2) .tasks .dropper div')
cy.get('.kanban .bucket:nth-child(2) .tasks')
.should('contain', tasks[0].title)
cy.get('.kanban .bucket:nth-child(1) .tasks')
.should('not.contain', tasks[0].title)
})
it('Should navigate to the task when the task card is clicked', () => {
const tasks = TaskFactory.create(5, {
@ -401,7 +462,7 @@ describe('Lists', () => {
})
cy.visit('/lists/1/kanban')
cy.getAttached('.kanban .bucket .tasks .task')
cy.getSettled('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
.should('be.visible')
.click()
@ -423,7 +484,7 @@ describe('Lists', () => {
const task = tasks[0]
cy.visit('/lists/1/kanban')
cy.getAttached('.kanban .bucket .tasks .task')
cy.getSettled('.kanban .bucket .tasks .task')
.contains(task.title)
.should('be.visible')
.click()
@ -447,4 +508,34 @@ describe('Lists', () => {
.should('not.contain', task.title)
})
})
describe('List history', () => {
it('should show a list history on the home page', () => {
const lists = ListFactory.create(6)
cy.visit('/')
cy.get('h3')
.contains('Last viewed')
.should('not.exist')
cy.visit(`/lists/${lists[0].id}`)
cy.visit(`/lists/${lists[1].id}`)
cy.visit(`/lists/${lists[2].id}`)
cy.visit(`/lists/${lists[3].id}`)
cy.visit(`/lists/${lists[4].id}`)
cy.visit(`/lists/${lists[5].id}`)
cy.visit('/')
cy.get('h3')
.contains('Last viewed')
.should('exist')
cy.get('.list-cards-wrapper-2-rows')
.should('not.contain', lists[0].title)
.should('contain', lists[1].title)
.should('contain', lists[2].title)
.should('contain', lists[3].title)
.should('contain', lists[4].title)
.should('contain', lists[5].title)
})
})
})

View File

@ -20,20 +20,126 @@ describe('Namepaces', () => {
})
it('Should create a new Namespace', () => {
const newNamespaceTitle = 'New Namespace'
cy.visit('/namespaces')
cy.get('a.button')
.contains('Create namespace')
.contains('Create a new namespace')
.click()
cy.url()
.should('contain', '/namespaces/new')
cy.get('.card-header-title')
.should('contain', 'Create a new namespace')
cy.get('input.input')
.type('New Namespace')
.type(newNamespaceTitle)
cy.get('.button')
.contains('Create')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.namespace-container')
.should('contain', newNamespaceTitle)
cy.url()
.should('contain', '/namespaces')
})
it('Should rename the namespace all places', () => {
const newNamespaces = NamespaceFactory.create(5)
const newNamespaceName = 'New namespace name'
cy.visit('/namespaces')
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
.click()
cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content')
.contains('Edit')
.click()
cy.url()
.should('contain', '/settings/edit')
cy.get('#namespacetext')
.invoke('val')
.should('equal', newNamespaces[0].title) // wait until the namespace data is loaded
cy.get('#namespacetext')
.type(`{selectall}${newNamespaceName}`)
cy.get('footer.modal-card-foot .button')
.contains('Save')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.namespace-container .menu.namespaces-lists')
.should('contain', newNamespaceName)
.should('not.contain', newNamespaces[0].title)
cy.get('.content.namespaces-list')
.should('contain', newNamespaceName)
.should('not.contain', newNamespaces[0].title)
})
it('Should remove a namespace when deleting it', () => {
const newNamespaces = NamespaceFactory.create(5)
cy.visit('/')
cy.get(`.namespace-container .menu.namespaces-lists .namespace-title:contains(${newNamespaces[0].title}) .dropdown .dropdown-trigger`)
.click()
cy.get('.namespace-container .menu.namespaces-lists .namespace-title .dropdown .dropdown-content')
.contains('Delete')
.click()
cy.url()
.should('contain', '/settings/delete')
cy.get('.modal-mask .modal-container .modal-content .actions a.button')
.contains('Do it')
.click()
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.namespace-container .menu.namespaces-lists')
.should('not.contain', newNamespaces[0].title)
})
it('Should not show archived lists & namespaces if the filter is not checked', () => {
const n = NamespaceFactory.create(1, {
id: 2,
is_archived: true,
}, false)
ListFactory.create(1, {
id: 2,
namespace_id: n[0].id,
}, false)
ListFactory.create(1, {
id: 3,
is_archived: true,
}, false)
// Initial
cy.visit('/namespaces')
cy.get('.namespaces-list .namespace')
.should('not.contain', 'Archived')
// Show archived
cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span')
.should('be.visible')
.click()
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
.should('be.checked')
cy.get('.namespaces-list .namespace')
.should('contain', 'Archived')
// Don't show archived
cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span')
.should('be.visible')
.click()
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
.should('not.be.checked')
// Second time visiting after unchecking
cy.visit('/namespaces')
cy.get('.namespaces-list .fancycheckbox.show-archived-check input')
.should('not.be.checked')
cy.get('.namespaces-list .namespace')
.should('not.contain', 'Archived')
})
})

View File

@ -0,0 +1,35 @@
import '../../support/authenticateUser'
const setHours = hours => {
const date = new Date()
date.setHours(hours)
cy.clock(+date)
}
describe('Home Page', () => {
it('shows the right salutation in the night', () => {
setHours(4)
cy.visit('/')
cy.get('h2').should('contain', 'Good Night')
})
it('shows the right salutation in the morning', () => {
setHours(8)
cy.visit('/')
cy.get('h2').should('contain', 'Good Morning')
})
it('shows the right salutation in the day', () => {
setHours(13)
cy.visit('/')
cy.get('h2').should('contain', 'Hi')
})
it('shows the right salutation in the night', () => {
setHours(20)
cy.visit('/')
cy.get('h2').should('contain', 'Good Evening')
})
it('shows the right salutation in the night again', () => {
setHours(23)
cy.visit('/')
cy.get('h2').should('contain', 'Good Night')
})
})

View File

@ -11,7 +11,7 @@ describe('Team', () => {
const newTeamName = 'New Team'
cy.get('a.button')
.contains('New Team')
.contains('Create a new team')
.click()
cy.url()
.should('contain', '/teams/new')
@ -113,7 +113,7 @@ describe('Team', () => {
cy.get('.card')
.contains('Team Members')
.get('.card-content .button')
.contains('Add To Team')
.contains('Add to team')
.click()
cy.get('table.table td')

View File

@ -11,6 +11,7 @@ import '../../support/authenticateUser'
import {TaskAssigneeFactory} from '../../factories/task_assignee'
import {LabelFactory} from '../../factories/labels'
import {LabelTaskFactory} from '../../factories/label_task'
import {BucketFactory} from '../../factories/bucket'
describe('Task', () => {
let namespaces
@ -26,7 +27,7 @@ describe('Task', () => {
it('Should be created new', () => {
cy.visit('/lists/1/list')
cy.get('input.input[placeholder="Add a new task..."')
cy.get('.input[placeholder="Add a new task…"')
.type('New Task')
cy.get('.button')
.contains('Add')
@ -42,7 +43,7 @@ describe('Task', () => {
cy.visit('/lists/1/list')
cy.get('.list-is-empty-notice')
.should('not.exist')
cy.get('input.input[placeholder="Add a new task..."')
cy.get('.input[placeholder="Add a new task…"')
.type('New Task')
cy.get('.button')
.contains('Add')
@ -112,9 +113,10 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .heading .is-done')
.should('exist')
.should('be.visible')
.should('contain', 'Done')
cy.get('.task-view .action-buttons p.created')
.should('be.visible')
.should('contain', 'Done')
})
@ -167,7 +169,7 @@ describe('Task', () => {
cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
.type('{selectall}New Description')
cy.get('.task-view .details.content.description .editor a')
.contains('Done')
.contains('Save')
.click()
cy.get('.task-view .details.content.description h3 span.is-small.has-text-success')
@ -182,9 +184,11 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .comments .media.comment .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
.should('be.visible')
.type('{selectall}New Comment')
cy.get('.task-view .comments .media.comment .button:not([disabled])')
.contains('Comment')
.should('be.visible')
.click()
cy.get('.task-view .comments .media.comment .editor')
@ -195,6 +199,9 @@ describe('Task', () => {
it('Can move a task to another list', () => {
const lists = ListFactory.create(2)
BucketFactory.create(2, {
list_id: '{increment}'
})
const tasks = TaskFactory.create(1, {
id: 1,
list_id: lists[0].id,
@ -228,6 +235,7 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button')
.should('be.visible')
.contains('Delete task')
.click()
cy.get('.modal-mask .modal-container .modal-content .header')
@ -310,6 +318,7 @@ describe('Task', () => {
cy.get('.task-view .action-buttons .button')
.contains('Add labels')
.should('be.visible')
.click()
cy.get('.task-view .details.labels-list .multiselect input')
.type(newLabelText)
@ -365,6 +374,7 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
.should('be.visible')
.should('contain', labels[0].title)
cy.get('.task-view .details.labels-list .multiselect .input-wrapper')
.children()

View File

@ -34,6 +34,7 @@ context('Login', () => {
cy.get('input[id=password]').type(fixture.password)
cy.get('.button').contains('Login').click()
cy.url().should('include', '/')
cy.clock(1625656161057) // 13:00
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
})

View File

@ -28,6 +28,7 @@ context('Registration', () => {
cy.get('#password2').type(fixture.password)
cy.get('#register-submit').click()
cy.url().should('include', '/')
cy.clock(1625656161057) // 13:00
cy.get('h2').should('contain', `Hi ${fixture.username}!`)
})

View File

@ -36,7 +36,6 @@ describe('User Settings', () => {
.contains('Save')
.click()
cy.wait(3000) // Wait for the request to finish
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.navbar .user .username')

View File

@ -1,17 +1,33 @@
/**
* getAttached(selector)
* getAttached(selectorFn)
* Recursively gets an element, returning only after it's determined to be attached to the DOM for good.
*
* Waits until the selector finds an attached element, then yields it (wrapped).
* selectorFn, if provided, is passed $(document). Don't use cy methods inside selectorFn.
*
* Source: https://github.com/cypress-io/cypress/issues/5743#issuecomment-650421731
* Source: https://github.com/cypress-io/cypress/issues/7306#issuecomment-850621378
*/
Cypress.Commands.add('getAttached', selector => {
const getElement = typeof selector === 'function' ? selector : $d => $d.find(selector);
let $el = null;
return cy.document().should($d => {
$el = getElement(Cypress.$($d));
expect(Cypress.dom.isDetached($el)).to.be.false;
}).then(() => cy.wrap($el));
});
Cypress.Commands.add('getSettled', (selector, opts = {}) => {
const retries = opts.retries || 3
const delay = opts.delay || 100
const isAttached = (resolve, count = 0) => {
const el = Cypress.$(selector)
// is element attached to the DOM?
count = Cypress.dom.isAttached(el) ? count + 1 : 0
// hit our base case, return the element
if (count >= retries) {
return resolve(el)
}
// retry after a bit of a delay
setTimeout(() => isAttached(resolve, count), delay)
}
// wrap, so we can chain cypress commands off the result
return cy.wrap(null).then(() => {
return new Cypress.Promise((resolve) => {
return isAttached(resolve, 0)
}).then((el) => {
return cy.wrap(el)
})
})
})

View File

@ -21,7 +21,7 @@ export class Factory {
* @param override
* @returns {[]}
*/
static create(count = 1, override = {}) {
static create(count = 1, override = {}, truncate = true) {
const data = []
for (let i = 1; i <= count; i++) {
@ -38,7 +38,7 @@ export class Factory {
data.push(entry)
}
seed(this.table, data)
seed(this.table, data, truncate)
return data
}

View File

@ -1,3 +1,4 @@
import './commands'
import 'cypress-file-upload'
import '@4tw/cypress-drag-drop'

View File

@ -8,14 +8,14 @@
* @param table
* @param data
*/
export function seed(table, data = {}) {
if(data === null) {
export function seed(table, data = {}, truncate = true) {
if (data === null) {
data = []
}
cy.request({
method: 'PATCH',
url: `${Cypress.env('API_URL')}/test/${table}`,
url: `${Cypress.env('API_URL')}/test/${table}?truncate=${truncate ? 'true' : 'false'}`,
headers: {
'Authorization': Cypress.env('TEST_SECRET'),
},

View File

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

36
index.html Normal file
View File

@ -0,0 +1,36 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Vikunja</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="Vikunja (/vɪˈkuːnjə/) - The to-do app to organize your life.">
<meta name="theme-color" content="#1973ff"/>
<link rel="icon" href="/favicon.ico">
<link rel="apple-touch-icon" href="/images/icons/apple-touch-icon-180x180.png"/>
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700italic.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-italic.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-500.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-700.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-regular.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/open-sans-v15-latin-700.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="/fonts/quicksand-v7-latin-regular.woff2" as="font">
</head>
<body>
<noscript>
<strong>We're sorry but Vikunja doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
<script>
//
// This variable points the frontend to the api.
// It has to be the full url, including the last /api/v1 part and port.
// You can change this if your api is not reachable on the same port as the frontend.
window.API_URL = 'http://localhost:3456/api/v1'
//
</script>
</body>
</html>

View File

@ -3,77 +3,116 @@
"version": "0.10.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"serve:dist": "node scripts/serve-dist.js",
"build": "vue-cli-service build --modern",
"build:report": "vue-cli-service build --report",
"lint": "vue-cli-service lint --ignore-pattern '*.test.*'",
"serve": "vite",
"serve:dist-dev": "node scripts/serve-dist.js",
"serve:dist": "vite preview",
"build": "vite build && workbox copyLibraries dist/",
"build:dev": "vite build -m development --outDir dist-dev/",
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
"cypress:open": "cypress open",
"test:unit": "jest",
"test:frontend": "cypress run"
},
"dependencies": {
"browserslist": "4.16.6",
"bulma": "0.9.2",
"browserslist": "4.17.1",
"bulma": "0.9.3",
"camel-case": "4.1.2",
"copy-to-clipboard": "3.3.1",
"date-fns": "2.21.3",
"dompurify": "2.2.8",
"highlight.js": "10.7.2",
"date-fns": "2.24.0",
"dompurify": "2.3.3",
"highlight.js": "11.2.0",
"is-touch-device": "1.0.1",
"lodash": "4.17.21",
"marked": "2.0.3",
"marked": "3.0.4",
"register-service-worker": "1.7.2",
"sass": "1.32.13",
"snake-case": "3.0.4",
"ufo": "0.7.9",
"verte": "0.0.12",
"vue": "2.6.12",
"vue-advanced-cropper": "1.5.2",
"vue": "2.6.14",
"vue-advanced-cropper": "1.8.2",
"vue-drag-resize": "1.5.4",
"vue-easymde": "1.4.0",
"vue-i18n": "8.26.2",
"vue-shortkey": "3.1.7",
"vue-smooth-dnd": "0.8.1",
"vuex": "3.6.2"
"vuedraggable": "2.24.3",
"vuex": "3.6.2",
"workbox-precaching": "6.3.0"
},
"devDependencies": {
"@fortawesome/fontawesome-svg-core": "1.2.35",
"@fortawesome/free-regular-svg-icons": "5.15.3",
"@fortawesome/free-solid-svg-icons": "5.15.3",
"@4tw/cypress-drag-drop": "2.0.0",
"@fortawesome/fontawesome-svg-core": "1.2.36",
"@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/vue-fontawesome": "2.0.2",
"@vue/cli": "4.5.13",
"@vue/cli-plugin-babel": "4.5.13",
"@vue/cli-plugin-eslint": "4.5.13",
"@vue/cli-plugin-pwa": "4.5.13",
"@vue/cli-service": "4.5.13",
"axios": "0.21.1",
"@types/jest": "27.0.2",
"@typescript-eslint/eslint-plugin": "4.32.0",
"@typescript-eslint/parser": "4.32.0",
"@vue/babel-preset-app": "4.5.13",
"@vue/eslint-config-typescript": "7.0.0",
"@vue/runtime-dom": "latest",
"autoprefixer": "10.3.6",
"axios": "0.21.4",
"babel-eslint": "10.1.0",
"cypress": "7.2.0",
"cypress-file-upload": "5.0.7",
"eslint": "7.26.0",
"eslint-plugin-vue": "7.9.0",
"cypress": "8.5.0",
"cypress-file-upload": "5.0.8",
"esbuild": "0.13.3",
"eslint": "7.32.0",
"eslint-plugin-vue": "7.18.0",
"express": "4.17.1",
"faker": "5.5.3",
"jest": "26.6.3",
"sass-loader": "10.2.0",
"vue-flatpickr-component": "8.1.6",
"jest": "27.2.4",
"rollup-plugin-terser": "7.0.2",
"rollup-plugin-visualizer": "5.5.2",
"sass": "1.42.1",
"ts-jest": "27.0.5",
"typescript": "4.4.3",
"vite": "2.6.1",
"vite-plugin-pwa": "0.11.2",
"vite-plugin-vue2": "1.8.2",
"vue-flatpickr-component": "8.1.7",
"vue-notification": "1.3.20",
"vue-router": "3.5.1",
"vue-template-compiler": "2.6.12",
"wait-on": "5.3.0"
"vue-router": "3.5.2",
"vue-template-compiler": "2.6.14",
"wait-on": "6.0.0",
"workbox-cli": "6.3.0"
},
"eslintConfig": {
"root": true,
"env": {
"browser": true,
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:vue/essential",
"eslint:recommended"
"@vue/typescript"
],
"rules": {},
"rules": {
"vue/html-quotes": [
"error",
"double"
],
"quotes": [
"error",
"single"
],
"comma-dangle": [
"error",
"always-multiline"
],
"semi": [
"error",
"never"
]
},
"parser": "vue-eslint-parser",
"parserOptions": {
"parser": "babel-eslint"
"parser": "@typescript-eslint/parser",
"ecmaVersion": 2021
},
"ignorePatterns": [
"*.test.js",
"*.test.*",
"cypress/*"
]
},
@ -85,12 +124,27 @@
"browserslist": [
"> 1%",
"last 2 versions",
"not ie < 11"
"not ie > 0",
"not dead",
"Firefox ESR"
],
"license": "AGPL-3.0-or-later",
"jest": {
"testPathIgnorePatterns": [
"cypress"
],
"testEnvironment": "jsdom",
"preset": "ts-jest",
"roots": [
"<rootDir>/src"
],
"transform": {
"^.+\\.(js|tsx?)$": "ts-jest"
},
"moduleFileExtensions": [
"ts",
"js",
"json"
]
}
},
"license": "AGPL-3.0-or-later"
}

8
ping-weblate.sh Executable file
View File

@ -0,0 +1,8 @@
#!/bin/sh
set -e
# Shell script because yaml doesn't understand the header is a string literal and not a yaml symbol
curl -d operation=pull -H "Authorization: Token $WEBLATE_TOKEN" https://hosted.weblate.org/api/projects/vikunja/repository/
curl -d operation=push -H "Authorization: Token $WEBLATE_TOKEN" https://hosted.weblate.org/api/projects/vikunja/repository/

View File

@ -16,9 +16,6 @@
height="1066.6667"
viewBox="0 0 1066.6667 1066.6667"
sodipodi:docname="llama-nightscape.svg"
inkscape:export-filename="/home/konrad/www/vikunja/frontend/public/images/llama-nightscape.png"
inkscape:export-xdpi="172.8"
inkscape:export-ydpi="172.8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata8"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type

Before

Width:  |  Height:  |  Size: 174 KiB

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -1,36 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Vikunja</title>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<meta name="description" content="Vikunja (/vɪˈkuːnjə/) - The to-do app to organize your life.">
<meta name="hash" content="<%= webpack.hash %>"/>
<link rel="icon" href="<%= BASE_URL %>favicon.ico">
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/open-sans-v15-latin-700italic.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/open-sans-v15-latin-italic.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/quicksand-v7-latin-300.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/quicksand-v7-latin-500.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/quicksand-v7-latin-700.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/open-sans-v15-latin-regular.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/open-sans-v15-latin-700.woff2" as="font">
<link rel="preload" crossorigin="anonymous" href="<%= BASE_URL %>fonts/quicksand-v7-latin-regular.woff2" as="font">
</head>
<body>
<noscript>
<strong>We're sorry but Vikunja doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!-- built files will be auto injected -->
<script>
//
// This variable points the frontend to the api.
// It has to be the full url, including the last /api/v1 part and port.
// You can change this if your api is not reachable on the same port as the frontend.
window.API_URL = 'http://localhost:3456/api/v1'
//
</script>
</body>
</html>

10
run.sh
View File

@ -2,15 +2,19 @@
# This shell script sets the api url based on an environment variable and starts nginx in foreground.
if [ -z "$VIKUNJA_API_URL" ]; then
VIKUNJA_API_URL="/api/v1"
fi
VIKUNJA_API_URL="${VIKUNJA_API_URL:-"/api/v1"}"
VIKUNJA_HTTP_PORT="${VIKUNJA_HTTP_PORT:-80}"
VIKUNJA_HTTPS_PORT="${VIKUNJA_HTTP_PORT:-443}"
# Escape the variable to prevent sed from complaining
VIKUNJA_API_URL=$(echo $VIKUNJA_API_URL |sed 's/\//\\\//g')
sed -i "s/http\:\/\/localhost\:3456\/api\/v1/$VIKUNJA_API_URL/g" /usr/share/nginx/html/index.html
sed -i "s/listen 80/listen $VIKUNJA_HTTP_PORT/g" nginx.conf
sed -i "s/listen 443/listen $VIKUNJA_HTTPS_PORT/g" nginx.conf
# Set the uid and gid of the nginx run user
usermod --non-unique --uid ${PUID} nginx
groupmod --non-unique --gid ${PGID} nginx

View File

@ -2,8 +2,8 @@ const path = require('path')
const express = require('express')
const app = express()
const p = path.join(__dirname, '..', 'dist')
const port = 8080
const p = path.join(__dirname, '..', 'dist-dev')
const port = 5000
app.use(express.static(p))
// Handle urls set by the frontend

View File

@ -1,5 +1,5 @@
<template>
<div>
<div :class="{'is-touch': isTouch}">
<div :class="{'is-hidden': !online}">
<!-- This is a workaround to get the sw to "see" the to-be-cached version of the offline background image -->
<div class="offline" style="height: 0;width: 0;"></div>
@ -23,17 +23,18 @@
</template>
<script>
import {mapState} from 'vuex'
import authTypes from './models/authTypes'
import {mapState, mapGetters} from 'vuex'
import isTouchDevice from 'is-touch-device'
import Notification from './components/misc/notification'
import {KEYBOARD_SHORTCUTS_ACTIVE, ONLINE} from './store/mutation-types'
import KeyboardShortcuts from './components/misc/keyboard-shortcuts'
import TopNavigation from '@/components/home/topNavigation'
import ContentAuth from '@/components/home/contentAuth'
import ContentLinkShare from '@/components/home/contentLinkShare'
import ContentNoAuth from '@/components/home/contentNoAuth'
import TopNavigation from './components/home/topNavigation'
import ContentAuth from './components/home/contentAuth'
import ContentLinkShare from './components/home/contentLinkShare'
import ContentNoAuth from './components/home/contentNoAuth'
import {setLanguage} from './i18n/setup'
import AccountDeleteService from '@/services/accountDelete'
export default {
name: 'app',
@ -49,10 +50,16 @@ export default {
this.setupOnlineStatus()
this.setupPasswortResetRedirect()
this.setupEmailVerificationRedirect()
this.setupAccountDeletionVerification()
},
beforeCreate() {
this.$store.dispatch('config/update')
.then(() => {
this.$store.dispatch('auth/checkAuth')
})
this.$store.dispatch('auth/checkAuth')
setLanguage()
},
created() {
// Make sure to always load the home route when running with electron
@ -60,12 +67,19 @@ export default {
this.$router.push({name: 'home'})
}
},
computed: mapState({
authUser: state => state.auth.authenticated && (state.auth.info && state.auth.info.type === authTypes.USER),
authLinkShare: state => state.auth.authenticated && (state.auth.info && state.auth.info.type === authTypes.LINK_SHARE),
online: ONLINE,
keyboardShortcutsActive: KEYBOARD_SHORTCUTS_ACTIVE,
}),
computed: {
isTouch() {
return isTouchDevice()
},
...mapState({
online: ONLINE,
keyboardShortcutsActive: KEYBOARD_SHORTCUTS_ACTIVE,
}),
...mapGetters('auth', [
'authUser',
'authLinkShare',
]),
},
methods: {
setupOnlineStatus() {
this.$store.commit(ONLINE, navigator.onLine)
@ -86,6 +100,17 @@ export default {
this.$router.push({name: 'user.login'})
}
},
setupAccountDeletionVerification() {
if (typeof this.$route.query.accountDeletionConfirm !== 'undefined') {
const accountDeletionService = new AccountDeleteService()
accountDeletionService.confirm(this.$route.query.accountDeletionConfirm)
.then(() => {
this.$message.success({message: this.$t('user.deletion.confirmSuccess')})
this.$store.dispatch('auth/refreshUserInfo')
})
.catch(e => this.$message.error(e))
}
},
},
}
</script>

View File

@ -1,3 +0,0 @@
{
"SW_UPDATED": "swUpdated"
}

View File

@ -1,112 +0,0 @@
/* eslint-disable no-console */
/* eslint-disable no-undef */
// Cache assets
workbox.routing.registerRoute(
// This regexp matches all files in precache-manifest
new RegExp('.+\\.(css|json|js|svg|woff2|png|html|txt|wav)$'),
new workbox.strategies.StaleWhileRevalidate(),
)
// Always send api reqeusts through the network
workbox.routing.registerRoute(
new RegExp('api\\/v1\\/.*$'),
new workbox.strategies.NetworkOnly(),
)
// This code listens for the user's confirmation to update the app.
self.addEventListener('message', (e) => {
if (!e.data) {
return
}
switch (e.data) {
case 'skipWaiting':
self.skipWaiting()
break
default:
// NOOP
break
}
})
const getBearerToken = async () => {
// we can't get a client that sent the current request, therefore we need
// to ask any controlled page for auth token
const allClients = await self.clients.matchAll()
const client = allClients.filter(client => client.type === 'window')[0]
// if there is no page in scope, we can't get any token
// and we indicate it with null value
if (!client) {
return null
}
// to communicate with a page we will use MessageChannels
// they expose pipe-like interface, where a receiver of
// a message uses one end of a port for messaging and
// we use the other end for listening
const channel = new MessageChannel()
client.postMessage({
'action': 'getBearerToken',
}, [channel.port1])
// ports support only onmessage callback which
// is cumbersome to use, so we wrap it with Promise
return new Promise((resolve, reject) => {
channel.port2.onmessage = event => {
if (event.data.error) {
console.error('Port error', event.error)
reject(event.data.error)
}
resolve(event.data.authToken)
}
})
}
// Notification action
self.addEventListener('notificationclick', function (event) {
const taskId = event.notification.data.taskId
event.notification.close()
switch (event.action) {
case 'mark-as-done':
// FIXME: Ugly as hell, but no other way of doing this, since we can't use modules
// in service workers for now.
fetch('/config.json')
.then(r => r.json())
.then(config => {
getBearerToken()
.then(token => {
fetch(`${config.VIKUNJA_API_BASE_URL}tasks/${taskId}`, {
method: 'post',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
'Authorization': `Bearer ${token}`,
},
body: JSON.stringify({id: taskId, done: true}),
})
.then(r => r.json())
.then(r => {
console.debug('Task marked as done from notification', r)
})
.catch(e => {
console.debug('Error marking task as done from notification', e)
})
})
})
break
case 'show-task':
clients.openWindow(`/tasks/${taskId}`)
break
}
})
workbox.core.clientsClaim()
// The precaching code provided by Workbox.
self.__precacheManifest = [].concat(self.__precacheManifest || [])
workbox.precaching.precacheAndRoute(self.__precacheManifest, {})

View File

@ -5,21 +5,21 @@
</a>
<div
:class="{'has-background': background}"
:style="{'background-image': `url(${background})`}"
:style="{'background-image': background && `url(${background})`}"
class="app-container"
>
<navigation/>
<div
:class="[
{
'is-menu-enabled': menuActive,
},
{ 'is-menu-enabled': menuActive },
$route.name,
]"
class="app-content"
>
<a @click="$store.commit('menuActive', false)" class="mobile-overlay" v-if="menuActive"></a>
<quick-actions/>
<router-view/>
<transition name="modal">
@ -42,22 +42,23 @@
<script>
import {mapState} from 'vuex'
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
import Navigation from '@/components/home/navigation'
import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue'
export default {
name: 'contentAuth',
components: {Navigation},
components: {QuickActions, Navigation},
watch: {
'$route': 'doStuffAfterRoute',
'$route': {
handler: 'doStuffAfterRoute',
deep: true,
},
},
created() {
this.renewTokenOnFocus()
this.loadLabels()
},
computed: mapState({
namespaces(state) {
return state.namespaces.namespaces.filter(n => !n.isArchived)
},
currentList: CURRENT_LIST,
background: 'background',
menuActive: MENU_ACTIVE,
userInfo: state => state.auth.info,
@ -83,7 +84,7 @@ export default {
this.$route.name === 'user.settings' ||
this.$route.name === 'namespaces.index'
) {
this.$store.commit(CURRENT_LIST, {})
this.$store.commit(CURRENT_LIST, null)
}
},
renewTokenOnFocus() {
@ -123,6 +124,12 @@ export default {
showKeyboardShortcuts() {
this.$store.commit(KEYBOARD_SHORTCUTS_ACTIVE, true)
},
loadLabels() {
this.$store.dispatch('labels/loadAllLabels')
.catch(e => {
this.$message.error(e)
})
},
},
}
</script>

View File

@ -10,20 +10,12 @@
<h1
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
class="title">
{{ currentList.title === '' ? 'Loading...' : currentList.title }}
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
</h1>
<div class="box has-text-left view">
<div class="logout">
<x-button @click="logout()" type="secondary">
<span>Logout</span>
<span class="icon is-small">
<icon icon="sign-out-alt"/>
</span>
</x-button>
</div>
<router-view/>
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank">
Powered by Vikunja
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank" rel="noreferrer noopener nofollow">
{{ $t('misc.poweredBy') }}
</a>
</div>
</div>

View File

@ -1,10 +1,10 @@
<template>
<div class="no-auth-wrapper">
<div class="noauth-container">
<img alt="Vikunja" src="/images/logo-full.svg"/>
<img alt="Vikunja" src="/images/logo-full.svg" width="400" height="117"/>
<div class="message is-info" v-if="motd !== ''">
<div class="message-header">
<p>Info</p>
<p>{{ $t('misc.info') }}</p>
</div>
<div class="message-body">
{{ motd }}

View File

@ -2,7 +2,7 @@
<div :class="{'is-active': menuActive}" class="namespace-container">
<div class="menu top-menu">
<router-link :to="{name: 'home'}" class="logo">
<img alt="Vikunja" src="/images/logo-full.svg"/>
<img alt="Vikunja" src="/images/logo-full.svg" width="164" height="48"/>
</router-link>
<ul class="menu-list">
<li>
@ -10,7 +10,7 @@
<span class="icon">
<icon icon="calendar"/>
</span>
Overview
{{ $t('navigation.overview') }}
</router-link>
</li>
<li>
@ -18,7 +18,7 @@
<span class="icon">
<icon :icon="['far', 'calendar-alt']"/>
</span>
Upcoming
{{ $t('navigation.upcoming') }}
</router-link>
</li>
<li>
@ -26,7 +26,7 @@
<span class="icon">
<icon icon="layer-group"/>
</span>
Namespaces & Lists
{{ $t('namespace.title') }}
</router-link>
</li>
<li>
@ -34,7 +34,7 @@
<span class="icon">
<icon icon="tags"/>
</span>
Labels
{{ $t('label.title') }}
</router-link>
</li>
<li>
@ -42,26 +42,26 @@
<span class="icon">
<icon icon="users"/>
</span>
Teams
{{ $t('team.title') }}
</router-link>
</li>
</ul>
</div>
<aside class="menu namespaces-lists loader-container" :class="{'is-loading': loading}">
<template v-for="n in namespaces">
<template v-for="(n, nk) in namespaces">
<div :key="n.id" class="namespace-title" :class="{'has-menu': n.id > 0}">
<span
@click="toggleLists(n.id)"
class="menu-label"
v-tooltip="n.title + ' (' + n.lists.filter(l => !l.isArchived).length + ')'">
v-tooltip="getNamespaceTitle(n) + ' (' + n.lists.filter(l => !l.isArchived).length + ')'">
<span class="name">
<span
:style="{ backgroundColor: n.hexColor }"
class="color-bubble"
v-if="n.hexColor !== ''">
</span>
{{ n.title }} ({{ n.lists.filter(l => !l.isArchived).length }})
{{ getNamespaceTitle(n) }} ({{ n.lists.filter(l => !l.isArchived).length }})
</span>
</span>
<a
@ -73,25 +73,54 @@
</a>
<namespace-settings-dropdown :namespace="n" v-if="n.id > 0"/>
</div>
<div :key="n.id + 'child'" class="more-container" v-if="typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true">
<ul class="menu-list can-be-hidden">
<template v-for="l in n.lists">
<!-- This is a bit ugly but vue wouldn't want to let me filter this - probably because the lists
are nested inside of the namespaces makes it a lot harder.-->
<li :key="l.id" v-if="!l.isArchived">
<div
:key="n.id + 'child'"
class="more-container"
v-if="typeof listsVisible[n.id] !== 'undefined' ? listsVisible[n.id] : true"
>
<!--
NOTE: a v-model / computed setter is not possible, since the updateActiveLists function
triggered by the change needs to have access to the current namespace
-->
<draggable
v-bind="dragOptions"
:value="activeLists[nk]"
@input="(lists) => updateActiveLists(n, lists)"
:group="`namespace-${n.id}-lists`"
@start="() => drag = true"
@end="e => saveListPosition(e, nk)"
handle=".handle"
:disabled="n.id < 0"
:class="{'dragging-disabled': n.id < 0}"
>
<transition-group
type="transition"
:name="!drag ? 'flip-list' : null"
tag="ul"
class="menu-list can-be-hidden"
>
<li
v-for="l in activeLists[nk]"
:key="l.id"
class="loader-container"
:class="{'is-loading': listUpdating[l.id]}"
>
<router-link
class="list-menu-link"
:class="{'router-link-exact-active': currentList.id === l.id}"
:to="{ name: 'list.index', params: { listId: l.id} }"
tag="span"
>
<span class="icon handle">
<icon icon="grip-lines"/>
</span>
<span
:style="{ backgroundColor: l.hexColor }"
class="color-bubble"
v-if="l.hexColor !== ''">
</span>
<span class="list-menu-title">
{{ l.title }}
{{ getListTitle(l) }}
</span>
<span
:class="{'is-favorite': l.isFavorite}"
@ -104,41 +133,55 @@
<list-settings-dropdown :list="l" v-if="l.id > 0"/>
<span class="list-setting-spacer" v-else></span>
</li>
</template>
</ul>
</transition-group>
</draggable>
</div>
</template>
</aside>
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank">Powered by Vikunja</a>
<a class="menu-bottom-link" href="https://vikunja.io" target="_blank" rel="noreferrer noopener nofollow">
{{ $t('misc.poweredBy') }}
</a>
</div>
</template>
<script>
import {mapState} from 'vuex'
import {CURRENT_LIST, MENU_ACTIVE, LOADING, LOADING_MODULE} from '@/store/mutation-types'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
import draggable from 'vuedraggable'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
export default {
name: 'navigation',
data() {
return {
listsVisible: {},
drag: false,
dragOptions: {
animation: 100,
ghostClass: 'ghost',
},
listUpdating: {},
}
},
components: {
ListSettingsDropdown,
NamespaceSettingsDropdown,
draggable,
},
computed: mapState({
namespaces(state) {
return state.namespaces.namespaces.filter(n => !n.isArchived)
computed: {
...mapState({
namespaces: state => state.namespaces.namespaces.filter(n => !n.isArchived),
currentList: CURRENT_LIST,
background: 'background',
menuActive: MENU_ACTIVE,
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
}),
activeLists() {
return this.namespaces.map(({lists}) => lists.filter(item => !item.isArchived))
},
currentList: CURRENT_LIST,
background: 'background',
menuActive: MENU_ACTIVE,
loading: state => state[LOADING] && state[LOADING_MODULE] === 'namespaces',
}),
},
beforeCreate() {
this.$store.dispatch('namespaces/loadNamespaces')
.then(namespaces => {
@ -163,7 +206,7 @@ export default {
return
}
this.$store.dispatch('lists/toggleListFavorite', list)
.catch(e => this.error(e, this))
.catch(e => this.$message.error(e))
},
resize() {
// Hide the menu by default on mobile
@ -176,6 +219,45 @@ export default {
toggleLists(namespaceId) {
this.$set(this.listsVisible, namespaceId, !this.listsVisible[namespaceId] ?? false)
},
updateActiveLists(namespace, activeLists) {
// this is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it.
// instead we iterate over the non archived items in the old list and replace them with the ones in their new order
const lists = namespace.lists.map((item) => {
if (item.isArchived) {
return item
}
return activeLists.shift()
})
const newNamespace = {
...namespace,
lists,
}
this.$store.commit('namespaces/setNamespaceById', newNamespace)
},
saveListPosition(e, namespaceIndex) {
const listsActive = this.activeLists[namespaceIndex]
const list = listsActive[e.newIndex]
const listBefore = listsActive[e.newIndex - 1] ?? null
const listAfter = listsActive[e.newIndex + 1] ?? null
this.$set(this.listUpdating, list.id, true)
const position = calculateItemPosition(listBefore !== null ? listBefore.position : null, listAfter !== null ? listAfter.position : null)
// create a copy of the list in order to not violate vuex mutations
this.$store.dispatch('lists/updateList', {
...list,
position,
})
.catch(e => {
this.$message.error(e)
})
.finally(() => {
this.$set(this.listUpdating, list.id, false)
})
},
},
}
</script>

View File

@ -7,8 +7,8 @@
>
<div class="navbar-brand">
<router-link :to="{name: 'home'}" class="navbar-item logo">
<img alt="Vikunja" src="/images/logo-full-pride.svg" v-if="(new Date()).getMonth() === 5"/>
<img alt="Vikunja" src="/images/logo-full.svg" v-else/>
<img width="164" height="48" alt="Vikunja" src="/images/logo-full-pride.svg" v-if="(new Date()).getMonth() === 5"/>
<img width="164" height="48" alt="Vikunja" src="/images/logo-full.svg" v-else/>
</router-link>
<a
@click="$store.commit('toggleMenu')"
@ -16,32 +16,40 @@
@shortkey="() => $store.commit('toggleMenu')"
v-shortkey="['ctrl', 'e']"
>
<icon icon="bars"></icon>
</a>
</div>
<a
@click="$store.commit('toggleMenu')"
class="menu-show-button"
>
<icon icon="bars"></icon>
</a>
<div class="list-title" v-if="currentList.id">
<h1
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
class="title">
{{ currentList.title === '' ? 'Loading...' : currentList.title }}
</h1>
<div class="list-title" ref="listTitle" :style="{'display': currentList.id ? '': 'none'}">
<template v-if="currentList.id">
<h1
:style="{ 'opacity': currentList.title === '' ? '0': '1' }"
class="title">
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
</h1>
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
</template>
</div>
<div class="navbar-end">
<update/>
<a
@click="openQuickActions"
class="trigger-button pr-0"
@shortkey="openQuickActions"
v-shortkey="['ctrl', 'k']"
>
<icon icon="search"/>
</a>
<notifications/>
<div class="user">
<img :src="userAvatar" alt="" class="avatar"/>
<dropdown class="is-right">
<template v-slot:trigger>
<img :src="userAvatar" alt="" class="avatar" width="40" height="40"/>
<dropdown class="is-right" ref="usernameDropdown">
<template #trigger>
<x-button
type="secondary"
:shadow="false">
@ -53,27 +61,32 @@
</template>
<router-link :to="{name: 'user.settings'}" class="dropdown-item">
Settings
{{ $t('user.settings.title') }}
</router-link>
<a
:href="imprintUrl"
class="dropdown-item"
target="_blank"
rel="noreferrer noopener nofollow"
v-if="imprintUrl">
Imprint
{{ $t('navigation.imprint') }}
</a>
<a
:href="privacyPolicyUrl"
class="dropdown-item"
target="_blank"
rel="noreferrer noopener nofollow"
v-if="privacyPolicyUrl">
Privacy policy
{{ $t('navigation.privacy') }}
</a>
<a @click="$store.commit('keyboardShortcutsActive', true)" class="dropdown-item">
Keyboard Shortcuts
{{ $t('keyboardShortcuts.title') }}
</a>
<router-link :to="{name: 'about'}" class="dropdown-item">
{{ $t('about.title') }}
</router-link>
<a @click="logout()" class="dropdown-item">
Logout
{{ $t('user.auth.logout') }}
</a>
</dropdown>
</div>
@ -83,12 +96,12 @@
<script>
import {mapState} from 'vuex'
import {CURRENT_LIST} from '@/store/mutation-types'
import Rights from '@/models/rights.json'
import Update from '@/components/home/update'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown'
import Dropdown from '@/components/misc/dropdown'
import Notifications from '@/components/notifications/notifications'
import {CURRENT_LIST, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import Rights from '@/models/constants/rights.json'
import Update from '@/components/home/update.vue'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import Notifications from '@/components/notifications/notifications.vue'
export default {
name: 'topNavigation',
@ -108,11 +121,24 @@ export default {
privacyPolicyUrl: state => state.config.legal.privacyPolicyUrl,
canWriteCurrentList: state => state.currentList.maxRight > Rights.READ,
}),
mounted() {
this.$nextTick(() => {
if (typeof this.$refs.usernameDropdown === 'undefined' || typeof this.$refs.listTitle === 'undefined') {
return
}
const usernameWidth = this.$refs.usernameDropdown.$el.clientWidth
this.$refs.listTitle.style.setProperty('--nav-username-width', `${usernameWidth}px`)
})
},
methods: {
logout() {
this.$store.dispatch('auth/logout')
this.$router.push({name: 'user.login'})
},
openQuickActions() {
this.$store.commit(QUICK_ACTIONS_ACTIVE, true)
},
},
}
</script>

View File

@ -1,15 +1,13 @@
<template>
<div class="update-notification" v-if="updateAvailable">
<p>There is an update for Vikunja available!</p>
<p>{{ $t('update.available') }}</p>
<x-button @click="refreshApp()" :shadow="false">
Update Now
{{ $t('update.do') }}
</x-button>
</div>
</template>
<script>
import swEvents from '@/ServiceWorker/events.json'
export default {
name: 'update',
data() {
@ -20,7 +18,7 @@ export default {
}
},
created() {
document.addEventListener(swEvents.SW_UPDATED, this.showRefreshUI, {once: true})
document.addEventListener('swUpdated', this.showRefreshUI, {once: true})
if (navigator && navigator.serviceWorker) {
navigator.serviceWorker.addEventListener(

View File

@ -9,9 +9,9 @@
'is-text is-inverted has-no-shadow underline-none':
type === 'tertary',
}"
:disabled="disabled"
:disabled="disabled || null"
@click="click"
:href="href !== '' ? href : false"
:href="href !== '' ? href : null"
>
<icon :icon="icon" v-if="showIconOnly"/>
<span class="icon is-small" v-else-if="icon !== ''">
@ -55,7 +55,7 @@ export default {
computed: {
showIconOnly() {
return this.icon !== '' && typeof this.$slots.default === 'undefined'
}
},
},
methods: {
click(e) {

View File

@ -19,14 +19,13 @@
:class="{'is-empty': empty}"
/>
<x-button @click="reset" class="is-small ml-2" :shadow="false" type="secondary">
Reset Color
{{ $t('input.resetColor') }}
</x-button>
</div>
</template>
<script>
import verte from 'verte'
import 'verte/dist/verte.css'
export default {
name: 'colorPicker',
@ -49,16 +48,16 @@ export default {
},
},
watch: {
value(newVal) {
this.color = newVal
value: {
handler(value) {
this.color = value
},
immediate: true,
},
color() {
this.update()
},
},
mounted() {
this.color = this.value
},
computed: {
empty() {
return this.color === '#000000' || this.color === ''
@ -91,6 +90,8 @@ export default {
</script>
<style lang="scss">
@import 'verte/dist/verte.css';
.verte.is-empty {
.verte__icon {
opacity: 0;

View File

@ -18,7 +18,7 @@
</span>
<span class="text">
<span>
Today
{{ $t('input.datepicker.today') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('today') }}
@ -31,7 +31,7 @@
</span>
<span class="text">
<span>
Tomorrow
{{ $t('input.datepicker.tomorrow') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('tomorrow') }}
@ -44,7 +44,7 @@
</span>
<span class="text">
<span>
Next Monday
{{ $t('input.datepicker.nextMonday') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('nextMonday') }}
@ -57,7 +57,7 @@
</span>
<span class="text">
<span>
This Weekend
{{ $t('input.datepicker.thisWeekend') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('thisWeekend') }}
@ -70,7 +70,7 @@
</span>
<span class="text">
<span>
Later This Week
{{ $t('input.datepicker.laterThisWeek') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('laterThisWeek') }}
@ -83,7 +83,7 @@
</span>
<span class="text">
<span>
Next Week
{{ $t('input.datepicker.nextWeek') }}
</span>
<span class="weekday">
{{ getWeekdayFromStringInterval('nextWeek') }}
@ -102,7 +102,7 @@
:shadow="false"
@click="close"
>
Confirm
{{ $t('misc.confirm') }}
</x-button>
</div>
</transition>
@ -127,14 +127,6 @@ export default {
show: false,
changed: false,
flatPickerConfig: {
altFormat: 'j M Y H:i',
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
},
// Since flatpickr dates are strings, we need to convert them to native date objects.
// To make that work, we need a separate variable since flatpickr does not have a change event.
flatPickrDate: null,
@ -145,33 +137,50 @@ export default {
},
props: {
value: {
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string'
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
},
chooseDateLabel: {
type: String,
default: 'Choose a date'
default() {
return this.$t('input.datepicker.chooseDate')
},
},
disabled: {
type: Boolean,
default: false,
}
},
},
mounted() {
this.setDateValue(this.value)
document.addEventListener('click', this.hideDatePopup)
},
beforeDestroy() {
document.removeEventListener('click', this.hideDatePopup)
},
watch: {
value(newVal) {
this.setDateValue(newVal)
value: {
handler: 'setDateValue',
immediate: true,
},
flatPickrDate(newVal) {
this.date = createDateFromString(newVal)
this.updateData()
},
},
computed: {
flatPickerConfig() {
return {
altFormat: this.$t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: this.$store.state.auth.settings.weekStart,
},
}
},
},
methods: {
setDateValue(newVal) {
if(newVal === null) {

View File

@ -1,26 +1,7 @@
<template>
<div :class="{'is-pulled-up': isEditEnabled}" class="editor">
<div class="is-pulled-right mb-4" v-if="hasPreview && isEditEnabled && !hasEditBottom">
<x-button
v-if="!isEditActive"
@click="toggleEdit"
:shadow="false"
type="secondary"
>
<icon icon="pen"/>
</x-button>
<x-button
v-else
@click="toggleEdit"
:shadow="false"
type="secondary"
>
Done
</x-button>
</div>
<div class="editor">
<div class="clear"></div>
<vue-easymde
:configs="config"
@change="bubble"
@ -32,22 +13,34 @@
<div class="preview content" v-html="preview" v-if="isPreviewActive && text !== ''">
</div>
<p class="has-text-centered has-text-grey is-italic" v-if="isPreviewActive && text === '' && emptyText !== ''">
<p class="has-text-centered has-text-grey is-italic" v-if="showPreviewText">
{{ emptyText }}
<a @click="toggleEdit">Edit</a>.
<template v-if="isEditEnabled">
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>.
</template>
</p>
<ul class="actions">
<template v-if="hasEditBottom">
<ul class="actions" v-if="bottomActions.length > 0">
<template v-if="isEditEnabled && !showPreviewText && showSave">
<li>
<a v-if="!isEditActive" @click="toggleEdit">Edit</a>
<a v-else @click="toggleEdit">Done</a>
<a v-if="!isEditActive" @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
<a v-else @click="toggleEdit" class="done-edit">{{ $t('misc.save') }}</a>
</li>
</template>
<li v-for="(action, k) in bottomActions" :key="k">
<a @click="action.action">{{ action.title }}</a>
</li>
</ul>
<template v-else-if="isEditEnabled && showSave">
<ul v-if="!isEditActive" class="actions">
<li>
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
</li>
</ul>
<x-button v-else @click="toggleEdit" type="secondary" :shadow="false">
{{ $t('misc.save') }}
</x-button>
</template>
</div>
</template>
@ -56,9 +49,11 @@ import VueEasymde from 'vue-easymde'
import EasyMDE from 'easymde'
import marked from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js/lib/common'
import AttachmentModel from '../../models/attachment'
import AttachmentService from '../../services/attachment'
import {findCheckboxesInText} from '../../helpers/checklistFromText'
export default {
name: 'editor',
@ -94,10 +89,6 @@ export default {
isEditEnabled: {
default: true,
},
hasEditBottom: {
type: Boolean,
default: false,
},
bottomActions: {
default: () => [],
},
@ -105,6 +96,15 @@ export default {
type: String,
default: () => '',
},
showSave: {
type: Boolean,
default: false,
},
},
computed: {
showPreviewText() {
return this.isPreviewActive && this.text === '' && this.emptyText !== ''
},
},
data() {
return {
@ -115,6 +115,7 @@ export default {
preview: '',
attachmentService: null,
loadedAttachments: {},
config: {
autoDownloadFontAwesome: false,
@ -127,112 +128,112 @@ export default {
{
name: 'heading-1',
action: EasyMDE.toggleHeading1,
title: 'Heading 1',
title: this.$t('input.editor.heading1'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
{
name: 'heading-2',
action: EasyMDE.toggleHeading2,
title: 'Heading 2',
title: this.$t('input.editor.heading2'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
{
name: 'heading-3',
action: EasyMDE.toggleHeading3,
title: 'Heading 3',
title: this.$t('input.editor.heading3'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
{
name: 'heading-smaller',
action: EasyMDE.toggleHeadingSmaller,
title: 'Heading Smaller',
title: this.$t('input.editor.headingSmaller'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
{
name: 'heading-bigger',
action: EasyMDE.toggleHeadingBigger,
title: 'Heading Bigger',
title: this.$t('input.editor.headingBigger'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.2773 19.25L12.5773 4.34995C12.5773 4.34995 12.5773 4.24995 12.4773 4.24995C12.4773 4.24995 12.4773 4.14995 12.3773 4.14995C12.3773 4.14995 12.2773 4.14995 12.2773 4.04995L12.1773 3.94995H12.0773H11.9773C11.8773 3.94995 11.8773 3.94995 11.8773 3.94995H11.7773C11.6773 4.04995 11.6773 4.14995 11.5773 4.14995C11.5773 4.14995 11.5773 4.14995 11.4773 4.14995C11.4773 4.14995 11.4773 4.24995 11.3773 4.24995L11.2773 4.34995L5.67733 19.25C5.57733 19.55 5.67733 19.95 5.97733 20.05C6.07733 20.05 6.07733 20.05 6.17733 20.05C6.37733 20.05 6.67733 19.95 6.77733 19.65L7.87733 16.85H16.1773L17.2773 19.65C17.3773 19.85 17.5773 20.05 17.8773 20.05C17.9773 20.05 17.9773 20.05 18.0773 20.05C18.2773 19.85 18.4773 19.55 18.2773 19.25ZM8.27733 15.65L11.9773 6.24995L15.6773 15.65H8.27733Z"/></svg>',
},
'|',
{
name: 'bold',
action: EasyMDE.toggleBold,
title: 'Bold',
title: this.$t('input.editor.bold'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M3.5 3H6.5H15.25C18.15 3 20.5 5.36 20.5 8.25C20.5 9.8 19.81 11.19 18.73 12.15C20.37 13.04 21.5 14.76 21.5 16.75C21.5 19.64 19.15 22 16.25 22H6.5H3.5C2.95 22 2.5 21.55 2.5 21C2.5 20.45 2.95 20 3.5 20H5.5V5H3.5C2.95 5 2.5 4.55 2.5 4C2.5 3.45 2.95 3 3.5 3ZM7.5 20H16.25C18.04 20 19.5 18.54 19.5 16.75C19.5 14.96 18.04 13.5 16.25 13.5H7.5V20ZM7.5 11.5H15.25C17.04 11.5 18.5 10.04 18.5 8.25C18.5 6.46 17.04 5 15.25 5H7.5V11.5Z"/></svg>',
},
{
name: 'italic',
action: EasyMDE.toggleItalic,
title: 'Italic',
title: this.$t('input.editor.italic'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M14.0967 4.2H17.0001C17.3301 4.2 17.6001 3.93 17.6001 3.6C17.6001 3.27 17.3301 3 17.0001 3H10.2001C9.8701 3 9.6001 3.27 9.6001 3.6C9.6001 3.93 9.8701 4.2 10.2001 4.2H12.8748L9.90335 19.8H6.9999C6.6699 19.8 6.3999 20.07 6.3999 20.4C6.3999 20.73 6.6699 21 6.9999 21H13.7999C14.1299 21 14.3999 20.73 14.3999 20.4C14.3999 20.07 14.1299 19.8 13.7999 19.8H11.1253L14.0967 4.2Z"/></svg>',
},
{
name: 'strikethrough',
action: EasyMDE.toggleStrikethrough,
title: 'Strikethrough',
title: this.$t('input.editor.strikethrough'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.25 7.17005C18.25 7.50005 17.98 7.77005 17.65 7.77005C17.32 7.77005 17.05 7.50005 17.05 7.17005V5.96005C15.97 5.12005 14.17 4.56005 12.79 4.31005C11.1 4.00005 9.51 4.30005 8.41 5.12005C7.2 6.03005 6.67 7.67005 7.19 8.88005C7.56 9.73005 8.37 10.31 8.98 10.64C9.57215 10.9644 10.1961 11.2013 10.8465 11.3999H20.4C20.73 11.3999 21 11.6699 21 11.9999C21 12.3299 20.73 12.5999 20.4 12.5999H15.3012C16.6583 13.0929 17.5255 13.7765 17.95 14.69C18.73 16.36 17.74 18.33 16.36 19.41C15.05 20.4401 13.35 21 11.54 21H11.16C9.78 20.9401 8.34 20.5301 6.95 19.85V20.3601C6.95 20.6901 6.68 20.96 6.35 20.96C6.02 20.96 5.75 20.6901 5.75 20.3601V17.36C5.75 17.03 6.02 16.76 6.35 16.76C6.68 16.76 6.95 17.03 6.95 17.36V18.5C8.35 19.2801 9.81 19.74 11.21 19.8C12.86 19.89 14.46 19.39 15.62 18.48C16.6 17.71 17.37 16.3 16.86 15.21C16.55 14.54 15.8 14.0201 14.58 13.63C13.9711 13.4331 13.3222 13.2762 12.6906 13.1235C12.6168 13.1056 12.5432 13.0878 12.47 13.07C12.4313 13.0607 12.3925 13.0514 12.3537 13.0421C11.7861 12.9055 11.2108 12.767 10.6413 12.5999H3.6C3.27 12.5999 3 12.3299 3 11.9999C3 11.6699 3.27 11.3999 3.6 11.3999H7.90288C7.04984 10.8343 6.42752 10.1363 6.09 9.36005C5.34 7.63005 6.03 5.40005 7.69 4.16005C9.05 3.15005 10.99 2.77005 13 3.13005C13.64 3.25005 15.53 3.66005 17.05 4.53005V4.17005C17.05 3.84005 17.32 3.57005 17.65 3.57005C17.98 3.57005 18.25 3.84005 18.25 4.17005V7.17005Z"/></svg>',
},
{
name: 'code',
action: EasyMDE.toggleCodeBlock,
title: 'Code',
title: this.$t('input.editor.code'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M8.57 20.9601C8.64 20.9901 8.71 21.0001 8.78 21.0001C9.02 21.0001 9.24 20.8501 9.34 20.6101L15.79 3.81005C15.9 3.50005 15.75 3.15005 15.44 3.03005C15.14 2.92005 14.79 3.07005 14.67 3.38005L8.22 20.1801C8.11 20.4901 8.26 20.8401 8.57 20.9601ZM7.00007 18.0001C6.85007 18.0001 6.69007 17.9401 6.58007 17.8201L1.18007 12.4201C0.950068 12.1901 0.950068 11.8101 1.18007 11.5701L6.58007 6.17006C6.81007 5.94006 7.19007 5.94006 7.43007 6.17006C7.66007 6.40006 7.66007 6.78006 7.43007 7.02006L2.45007 12.0001L7.43007 16.9801C7.66007 17.2101 7.66007 17.5901 7.43007 17.8301C7.31007 17.9401 7.15007 18.0001 7.00007 18.0001ZM17 18.0001C16.85 18.0001 16.69 17.9401 16.58 17.8201C16.35 17.5901 16.35 17.2101 16.58 16.9701L21.55 12.0001L16.57 7.02006C16.34 6.79006 16.34 6.41006 16.57 6.17006C16.81 5.94006 17.19 5.94006 17.42 6.17006L22.82 11.5701C23.05 11.8001 23.05 12.1801 22.82 12.4201L17.42 17.8201C17.31 17.9401 17.15 18.0001 17 18.0001Z"/></svg>',
},
{
name: 'quote',
action: EasyMDE.toggleBlockquote,
title: 'Quote',
title: this.$t('input.editor.quote'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M19.373 5.16357H5.62695C4.79102 5.16357 4.11133 5.84326 4.11133 6.6792V16.2095C4.11133 17.0464 4.79102 17.7261 5.62695 17.7261H6.8877V21.1245C6.8877 21.3667 7.0332 21.5854 7.25684 21.6782C7.33203 21.7095 7.41016 21.7241 7.4873 21.7241C7.64258 21.7241 7.7959 21.6636 7.91113 21.5493L11.748 17.7261H19.373C20.209 17.7261 20.8887 17.0464 20.8887 16.2095V6.6792C20.8887 5.84326 20.209 5.16357 19.373 5.16357ZM19.6895 16.2095C19.6895 16.3843 19.5469 16.5269 19.373 16.5269H11.5C11.3408 16.5269 11.1895 16.5894 11.0762 16.7017L8.08691 19.6802V17.1265C8.08691 16.7954 7.81836 16.5269 7.4873 16.5269H5.62695C5.45312 16.5269 5.31055 16.3843 5.31055 16.2095V6.6792C5.31055 6.50537 5.45312 6.36279 5.62695 6.36279H19.373C19.5469 6.36279 19.6895 6.50537 19.6895 6.6792V16.2095ZM10.3431 8.45264C9.46326 8.45264 8.75 9.16589 8.75 10.0458C8.75 10.9257 9.46326 11.639 10.3431 11.639C10.4775 11.639 10.6058 11.6173 10.7305 11.5861V11.6195C10.7305 12.1322 10.3135 12.5492 9.75586 12.5492C9.4248 12.5492 9.17871 12.8177 9.17871 13.1488C9.17871 13.4799 9.46973 13.7484 9.80078 13.7484C10.9746 13.7484 11.9297 12.7933 11.9297 11.6195V10.1176L11.9294 10.1165L11.9292 10.1155C11.9297 10.1049 11.9312 10.0946 11.9326 10.0843L11.9326 10.0843C11.9345 10.0716 11.9363 10.059 11.9363 10.0458C11.9363 9.16589 11.223 8.45264 10.3431 8.45264ZM13.0637 10.0458C13.0637 9.16589 13.7771 8.45264 14.657 8.45264C15.5369 8.45264 16.2501 9.16589 16.2501 10.0458C16.2501 10.0584 16.2484 10.0706 16.2466 10.0828C16.2452 10.0929 16.2437 10.103 16.2433 10.1134C16.2433 10.1149 16.2441 10.1161 16.2441 10.1176V11.6195C16.2441 12.7933 15.2891 13.7484 14.1152 13.7484C13.7842 13.7484 13.4922 13.4799 13.4922 13.1488C13.4922 12.8177 13.7383 12.5492 14.0693 12.5492C14.6279 12.5492 15.0449 12.1322 15.0449 11.6195V11.5858C14.9202 11.6173 14.7915 11.639 14.657 11.639C13.7771 11.639 13.0637 10.9257 13.0637 10.0458Z"/></svg>',
},
{
name: 'unordered-list',
action: EasyMDE.toggleUnorderedList,
title: 'Unordered List',
title: this.$t('input.editor.unorderedList'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.5281 3.7002H3.5281C3.1981 3.7002 2.9281 3.9702 2.9281 4.3002V7.3002C2.9281 7.6302 3.1981 7.9002 3.5281 7.9002H6.5281C6.8581 7.9002 7.1281 7.6302 7.1281 7.3002V4.3002C7.1281 3.9702 6.8581 3.7002 6.5281 3.7002ZM5.9281 6.7002H4.1281V4.9002H5.9281V6.7002ZM3.5281 9.90015H6.5281C6.8581 9.90015 7.1281 10.1701 7.1281 10.5001V13.5001C7.1281 13.8301 6.8581 14.1001 6.5281 14.1001H3.5281C3.1981 14.1001 2.9281 13.8301 2.9281 13.5001V10.5001C2.9281 10.1701 3.1981 9.90015 3.5281 9.90015ZM4.1281 12.9001H5.9281V11.1001H4.1281V12.9001ZM3.5281 16.1001H6.5281C6.8581 16.1001 7.1281 16.3701 7.1281 16.7001V19.7001C7.1281 20.0301 6.8581 20.3001 6.5281 20.3001H3.5281C3.1981 20.3001 2.9281 20.0301 2.9281 19.7001V16.7001C2.9281 16.3701 3.1981 16.1001 3.5281 16.1001ZM4.1281 19.1001H5.9281V17.3001H4.1281V19.1001ZM9.72817 6.4002H20.7282C21.0582 6.4002 21.3282 6.1302 21.3282 5.8002C21.3282 5.4702 21.0582 5.2002 20.7282 5.2002H9.72817C9.39817 5.2002 9.12817 5.4702 9.12817 5.8002C9.12817 6.1302 9.39817 6.4002 9.72817 6.4002ZM9.72817 11.4001H20.7282C21.0582 11.4001 21.3282 11.6701 21.3282 12.0001C21.3282 12.3301 21.0582 12.6001 20.7282 12.6001H9.72817C9.39817 12.6001 9.12817 12.3301 9.12817 12.0001C9.12817 11.6701 9.39817 11.4001 9.72817 11.4001ZM9.72817 17.6001H20.7282C21.0582 17.6001 21.3282 17.8701 21.3282 18.2001C21.3282 18.5301 21.0582 18.8001 20.7282 18.8001H9.72817C9.39817 18.8001 9.12817 18.5301 9.12817 18.2001C9.12817 17.8701 9.39817 17.6001 9.72817 17.6001Z"/></svg>',
},
{
name: 'ordered-list',
action: EasyMDE.toggleOrderedList,
title: 'Ordered List',
title: this.$t('input.editor.orderedList'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M4.19494 8.29994H5.99494C6.26494 8.29994 6.49494 8.07995 6.49494 7.79994C6.49494 7.51995 6.27494 7.29994 5.99494 7.29994H5.59494V3.79994C5.59494 3.62994 5.50494 3.46994 5.36494 3.37994C5.22494 3.28994 5.04494 3.26994 4.89494 3.33994L3.89494 3.76994C3.64494 3.87994 3.52494 4.17994 3.63494 4.42994C3.74494 4.67994 4.03494 4.79994 4.29494 4.68994L4.59494 4.55994V7.29994H4.19494C3.91494 7.29994 3.69494 7.51995 3.69494 7.79994C3.69494 8.07995 3.91494 8.29994 4.19494 8.29994ZM20.195 6.39995H9.19497C8.86497 6.39995 8.59497 6.12995 8.59497 5.79995C8.59497 5.46995 8.86497 5.19995 9.19497 5.19995H20.195C20.525 5.19995 20.795 5.46995 20.795 5.79995C20.795 6.12995 20.525 6.39995 20.195 6.39995ZM3.78486 14.36H6.37486C6.65486 14.36 6.87486 14.14 6.87486 13.86C6.87486 13.58 6.65486 13.36 6.37486 13.36H4.88486C5.00486 13.23 5.12486 13.09 5.23486 12.95C5.26626 12.9151 5.29645 12.8802 5.32626 12.8458L5.32629 12.8457C5.38192 12.7814 5.43627 12.7186 5.49486 12.66C5.73486 12.4 5.98486 12.12 6.17486 11.79C6.47486 11.25 6.41486 10.63 6.01486 10.17C5.57486 9.66 4.86486 9.5 4.24486 9.74C3.74486 9.95 3.39486 10.35 3.22486 10.91C3.14486 11.18 3.29486 11.46 3.56486 11.54C3.82486 11.61 4.10486 11.46 4.18486 11.2C4.29486 10.85 4.48486 10.73 4.62486 10.67C4.88486 10.57 5.13486 10.68 5.26486 10.82C5.38486 10.96 5.40486 11.12 5.30486 11.29C5.17595 11.5202 4.99618 11.7165 4.80458 11.9257L4.75486 11.98C4.67298 12.0801 4.58283 12.1801 4.49946 12.2727L4.49945 12.2727L4.47486 12.3C4.12486 12.72 3.76486 13.13 3.40486 13.53C3.27486 13.68 3.23486 13.9 3.32486 14.07C3.41486 14.24 3.58486 14.36 3.78486 14.36ZM3.68486 20.3699C4.04486 20.5899 4.46486 20.6999 4.87486 20.6999C5.13486 20.6999 5.38486 20.6499 5.61486 20.5499C6.31486 20.2799 6.73486 19.5599 6.60486 18.8799C6.53486 18.5499 6.35486 18.2899 6.12486 18.0899C6.32486 17.8999 6.45486 17.6499 6.50486 17.3799C6.57486 17.0099 6.49486 16.6299 6.27486 16.3099C5.85486 15.6899 5.07486 15.5199 4.10486 15.8299C3.83486 15.9199 3.69486 16.1999 3.77486 16.4599C3.86486 16.7299 4.14486 16.8699 4.40486 16.7899C4.70486 16.6999 5.24486 16.5799 5.45486 16.8899C5.51486 16.9899 5.54486 17.0999 5.52486 17.1999C5.51486 17.2699 5.47486 17.3599 5.36486 17.4299C5.26486 17.4999 5.12486 17.5399 4.95486 17.5799L4.77486 17.6299C4.54486 17.6999 4.40486 17.9099 4.41486 18.1499C4.42486 18.3899 4.61486 18.5799 4.84486 18.6099C5.20486 18.6599 5.58486 18.8299 5.63486 19.0799C5.67486 19.2999 5.46486 19.5499 5.25486 19.6299C4.94486 19.7599 4.52486 19.7099 4.21486 19.5199C3.97486 19.3699 3.67486 19.4399 3.52486 19.6799C3.37486 19.9199 3.44486 20.2299 3.68486 20.3699ZM20.195 18.7999H9.19497C8.86497 18.7999 8.59497 18.5299 8.59497 18.1999C8.59497 17.8699 8.86497 17.5999 9.19497 17.5999H20.195C20.525 17.5999 20.795 17.8699 20.795 18.1999C20.795 18.5299 20.525 18.7999 20.195 18.7999ZM9.19497 12.5999H20.195C20.525 12.5999 20.795 12.3299 20.795 11.9999C20.795 11.6699 20.525 11.3999 20.195 11.3999H9.19497C8.86497 11.3999 8.59497 11.6699 8.59497 11.9999C8.59497 12.3299 8.86497 12.5999 9.19497 12.5999Z"/></svg>',
},
'|',
{
name: 'clean-block',
action: EasyMDE.cleanBlock,
title: 'Clean Block',
title: this.$t('input.editor.cleanBlock'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M9.25989 6.18384H20.4513C20.7823 6.18384 21.0509 6.45239 21.0509 6.78345V17.9749C21.0509 18.3059 20.7823 18.5745 20.4513 18.5745H9.25989C9.0929 18.5745 8.93469 18.5061 8.82043 18.384L3.6095 12.7883C3.39563 12.5579 3.39563 12.2004 3.6095 11.97L8.82043 6.37427C8.93469 6.2522 9.0929 6.18384 9.25989 6.18384ZM9.52063 17.3752H19.8517V7.38306H9.52063L4.86926 12.3792L9.52063 17.3752ZM12.7755 15.0686C12.6222 15.0686 12.4679 15.01 12.3517 14.8928C12.1173 14.6584 12.1173 14.2786 12.3517 14.0452L14.0503 12.3469L12.3517 10.6487C12.1173 10.4153 12.1173 10.0354 12.3517 9.80103C12.5841 9.56665 12.965 9.56665 13.1993 9.80103L14.8981 11.4994L16.5968 9.80103C16.8312 9.56665 17.212 9.56665 17.4445 9.80103C17.6788 10.0354 17.6788 10.4153 17.4445 10.6487L15.7458 12.3469L17.4445 14.0452C17.6788 14.2786 17.6788 14.6584 17.4445 14.8928C17.3282 15.01 17.174 15.0686 17.0206 15.0686C16.8673 15.0686 16.714 15.01 16.5968 14.8928L14.8981 13.1945L13.1993 14.8928C13.0822 15.01 12.9288 15.0686 12.7755 15.0686Z"/></svg>',
},
{
name: 'link',
action: EasyMDE.drawLink,
title: 'Link',
title: this.$t('input.editor.link'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M11.4399 15.3452C11.4999 15.3652 11.5699 15.3752 11.6299 15.3752C11.8799 15.3752 12.1199 15.2152 12.1999 14.9652C12.2999 14.6452 12.1299 14.3052 11.8199 14.2052C11.3499 14.0452 10.9299 13.7852 10.5699 13.4152C10.1999 13.0552 9.9399 12.6452 9.7799 12.1552C9.6599 11.8252 9.5999 11.4652 9.5999 11.0952C9.5999 10.2152 9.9399 9.38518 10.5699 8.75518L15.1599 4.15518C16.4499 2.87518 18.5399 2.87518 19.8299 4.15518C20.4499 4.78518 20.7899 5.61518 20.7899 6.49518C20.7899 7.37518 20.4499 8.20518 19.8299 8.82518L16.7399 11.9052C16.5099 12.1452 16.5099 12.5252 16.7399 12.7552C16.9799 12.9852 17.3599 12.9852 17.5899 12.7552L20.6799 9.67518C21.5299 8.83518 21.9999 7.69518 21.9999 6.49518C21.9999 5.29518 21.5299 4.16518 20.6899 3.30518C18.9299 1.55518 16.0799 1.55518 14.3199 3.30518L9.7299 7.90518C8.8699 8.75518 8.3999 9.88518 8.3999 11.0952C8.3999 11.6152 8.4899 12.1152 8.6499 12.5552C8.8599 13.1952 9.2399 13.7952 9.7199 14.2652C10.1999 14.7552 10.7999 15.1352 11.4399 15.3452ZM3.32 20.6851C4.2 21.5551 5.35 21.9951 6.5 21.9951C7.65 21.9951 8.81 21.5551 9.69 20.7051L14.28 16.1051C15.14 15.2551 15.61 14.1251 15.61 12.9151C15.61 12.4551 15.54 11.9951 15.4 11.5551C15.17 10.8651 14.8 10.2551 14.28 9.73509C13.76 9.21509 13.15 8.84509 12.46 8.61509C12.14 8.51509 11.8 8.68509 11.7 8.99509C11.6 9.30509 11.77 9.64509 12.1 9.75509C12.61 9.91509 13.06 10.1951 13.44 10.5751C13.82 10.9551 14.09 11.4051 14.26 11.9151C14.36 12.2351 14.41 12.5651 14.41 12.9051C14.41 13.7951 14.06 14.6251 13.43 15.2451L8.84 19.8451C7.55 21.1251 5.46 21.1251 4.17 19.8451C3.55 19.2151 3.21 18.3951 3.21 17.5051C3.21 16.6151 3.55 15.7851 4.17 15.1651L7.35 11.9851C7.58 11.7451 7.59 11.3651 7.35 11.1351C7.11 10.9051 6.73 10.9051 6.5 11.1351L3.32 14.3151C2.47 15.1551 2 16.2851 2 17.4951C2 18.7051 2.47 19.8351 3.32 20.6851Z"/></svg>',
},
{
name: 'image',
action: EasyMDE.drawImage,
title: 'Image',
title: this.$t('input.editor.image'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M4 4C2.89543 4 2 4.89543 2 6V16V17.5152V18C2 19.1046 2.89543 20 4 20H20C21.0528 20 21.9156 19.1866 21.9942 18.1539L22 18.1632V18V16V6C22 4.89543 21.1046 4 20 4H4ZM3.2 17.7V16.5642L6.78192 13.7254C6.8616 13.6622 6.97597 13.6689 7.04776 13.7409L10.3126 17.0146C10.7026 17.4056 11.3357 17.4065 11.7268 17.0165C11.7606 16.9827 11.792 16.9465 11.8207 16.9083L16.736 10.352C16.8023 10.2636 16.9277 10.2457 17.016 10.312C17.0355 10.3265 17.0521 10.3445 17.0651 10.365L20.8 16.2669V17.7C20.8 18.3075 20.3075 18.8 19.7 18.8H4.3C3.69249 18.8 3.2 18.3075 3.2 17.7ZM17.3865 8.61836L20.8 14.08V6.3C20.8 5.69249 20.3075 5.2 19.7 5.2H4.3C3.69249 5.2 3.2 5.69249 3.2 6.3V15.04L6.65054 12.2796C6.84949 12.1204 7.13629 12.1363 7.31645 12.3164L10.8369 15.8369C10.915 15.915 11.0417 15.915 11.1198 15.8369C11.1265 15.8302 16.5625 8.58336 16.5625 8.58336C16.7282 8.36245 17.0416 8.31768 17.2625 8.48336C17.3118 8.52034 17.3538 8.56611 17.3865 8.61836ZM8 8.5C8 9.32843 7.32843 10 6.5 10C5.67157 10 5 9.32843 5 8.5C5 7.67157 5.67157 7 6.5 7C7.32843 7 8 7.67157 8 8.5Z"/></svg>',
},
{
name: 'table',
action: EasyMDE.drawTable,
title: 'Table',
title: this.$t('input.editor.table'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M6.18524 3.08496H19.4152C20.6752 3.08496 21.7152 4.11496 21.7152 5.38496V18.615C21.7152 19.885 20.6852 20.915 19.4152 20.915H6.18524C4.91524 20.915 3.88525 19.885 3.88525 18.615V5.38496C3.88525 4.11496 4.91524 3.08496 6.18524 3.08496ZM19.4052 19.705C20.0152 19.705 20.5052 19.215 20.5052 18.605H20.5153V5.38496C20.5153 4.77496 20.0252 4.28496 19.4152 4.28496H6.18524C5.57524 4.28496 5.08521 4.77496 5.08521 5.38496V18.605C5.08521 19.215 5.57524 19.705 6.18524 19.705H19.4052ZM17.4453 9.15503H8.15527C7.82527 9.15503 7.5553 9.42503 7.5553 9.75503C7.5553 10.085 7.82527 10.355 8.15527 10.355H17.4453C17.7753 10.355 18.0453 10.085 18.0453 9.75503C18.0453 9.42503 17.7753 9.15503 17.4453 9.15503ZM17.4453 13.635H8.15527C7.82527 13.635 7.5553 13.905 7.5553 14.235C7.5553 14.565 7.82527 14.835 8.15527 14.835H17.4453C17.7753 14.835 18.0453 14.565 18.0453 14.235C18.0453 13.905 17.7753 13.635 17.4453 13.635Z"/></svg>',
},
{
name: 'horizontal-rule',
action: EasyMDE.drawHorizontalRule,
title: 'Horizontal Rule',
title: this.$t('input.editor.horizontalRule'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M21 13H3C2.45 13 2 12.55 2 12C2 11.45 2.45 11 3 11H21C21.55 11 22 11.45 22 12C22 12.55 21.55 13 21 13Z"/></svg>',
},
'|',
{
name: 'side-by-side',
action: EasyMDE.toggleSideBySide,
title: 'Side By Side',
title: this.$t('input.editor.sideBySide'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M18.4787 14.58C18.3587 14.69 18.2987 14.85 18.2987 15C18.2987 15.15 18.3587 15.31 18.4787 15.42C18.7187 15.65 19.0987 15.65 19.3287 15.42L22.3287 12.42C22.5587 12.18 22.5587 11.8 22.3287 11.57L19.3287 8.56996C19.0887 8.33996 18.7087 8.33996 18.4787 8.56996C18.2487 8.80996 18.2487 9.18996 18.4787 9.41996L20.451 11.3999L14.4487 11.3999L14.4487 4.6C14.4487 4.27 14.1787 4 13.8487 4C13.5187 4 13.2487 4.27 13.2487 4.6L13.2487 19.4C13.2487 19.73 13.5187 20 13.8487 20C14.1787 20 14.4487 19.73 14.4487 19.4L14.4487 12.5999L20.4511 12.5999L18.4787 14.58ZM9.54878 19.4L9.54878 12.5999L3.5486 12.5999L5.52867 14.58C5.75867 14.81 5.75867 15.19 5.52867 15.43C5.29867 15.66 4.91867 15.66 4.67867 15.43L1.67867 12.43C1.63274 12.384 1.5956 12.3323 1.56725 12.2774C1.53058 12.2077 1.50724 12.1299 1.50068 12.0477C1.49934 12.0317 1.49867 12.0158 1.49867 12C1.49867 11.9841 1.49933 11.9682 1.50067 11.9522C1.51454 11.778 1.60365 11.6242 1.73526 11.5234L4.67867 8.57997C4.90867 8.34997 5.28867 8.34997 5.52867 8.57997C5.75867 8.80997 5.75867 9.18997 5.52867 9.42997L3.55107 11.3999L9.54878 11.3999L9.54878 4.6C9.54878 4.27 9.81878 4 10.1488 4C10.4788 4 10.7488 4.27 10.7488 4.6L10.7488 11.9999L10.7488 19.4C10.7488 19.73 10.4788 20 10.1488 20C9.81878 20 9.54878 19.73 9.54878 19.4Z"/></svg>',
},
{
@ -240,7 +241,7 @@ export default {
action: () => {
window.open('https://www.markdownguide.org/basic-syntax/', '_blank')
},
title: 'Guide',
title: this.$t('input.editor.guide'),
icon: '<svg viewBox="0 0 24 24"><rect fill="none" rx="0" ry="0"/><path fill-rule="evenodd" clip-rule="evenodd" d="M19.4999 2.3999H6.4999C5.0699 2.3999 3.8999 3.5699 3.8999 4.9999V18.9999C3.8999 20.4299 5.0699 21.5999 6.4999 21.5999H19.4999C19.8299 21.5999 20.0999 21.3299 20.0999 20.9999V16.9999V2.9999C20.0999 2.6699 19.8299 2.3999 19.4999 2.3999ZM5.0999 4.9999V16.8118C5.50468 16.5513 5.98546 16.3999 6.4999 16.3999H18.8999V3.5999H6.4999C5.7299 3.5999 5.0999 4.2299 5.0999 4.9999ZM6.4999 17.5999H18.8999V20.3999H6.4999C5.7299 20.3999 5.0999 19.7699 5.0999 18.9999C5.0999 18.2299 5.7299 17.5999 6.4999 17.5999ZM8.4999 8.5999H15.4999C15.8299 8.5999 16.0999 8.3299 16.0999 7.9999C16.0999 7.6699 15.8299 7.3999 15.4999 7.3999H8.4999C8.1699 7.3999 7.8999 7.6699 7.8999 7.9999C7.8999 8.3299 8.1699 8.5999 8.4999 8.5999ZM15.4999 11.3999H8.4999C8.1699 11.3999 7.8999 11.6699 7.8999 11.9999C7.8999 12.3299 8.1699 12.5999 8.4999 12.5999H15.4999C15.8299 12.5999 16.0999 12.3299 16.0999 11.9999C16.0999 11.6699 15.8299 11.3999 15.4999 11.3999Z"/></svg>',
},
],
@ -282,7 +283,7 @@ export default {
// that in the end, only one change event is triggered to the outside per change.
handleInput(val) {
// Don't bubble if the text is up to date
if(val === this.text) {
if (val === this.text) {
return
}
@ -303,34 +304,7 @@ export default {
return str.substr(0, index) + replacement + str.substr(index + replacement.length)
},
findNthIndex(str, n) {
const searchChecked = '* [x] '
const searchUnchecked = '* [ ] '
let inChecked, inUnchecked, startIndex = 0
// We're building an array with all checkboxes, checked or unchecked.
// I've found this to be the best way to always get the results I need.
// The difficulty without an index is that we need to get all checkboxes, checked and unchecked
// and calculate our index based off that to compare it and find the checkbox we need.
let checkboxes = []
// Searching in two different loops for each search term since that is way easier and more predicatble
// More "intelligent" solutions sometimes don't have all values or duplicates.
// Because we're sorting and removing duplicates of them, we can safely put everything in one giant array.
while ((inChecked = str.indexOf(searchChecked, startIndex)) > -1) {
checkboxes.push(inChecked)
startIndex = startIndex + searchChecked.length
}
startIndex = 0
while ((inUnchecked = str.indexOf(searchUnchecked, startIndex)) > -1) {
checkboxes.push(inUnchecked)
startIndex = startIndex + searchUnchecked.length
}
checkboxes.sort((a, b) => a - b)
checkboxes = checkboxes.filter((v, i, s) => s.indexOf(v) === i && v > -1)
const checkboxes = findCheckboxesInText(str)
return checkboxes[n]
},
renderPreview() {
@ -363,17 +337,16 @@ export default {
link: (href, title, text) => {
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
const html = linkRenderer.call(renderer, href, title, text)
return isLocal ? html : html.replace(/^<a /, `<a target="_blank" rel="noreferrer noopener nofollow" `)
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
},
},
highlight: function (code, language) {
const hljs = require('highlight.js')
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
return hljs.highlight(validLanguage, code).value
return hljs.highlight(code, {language: validLanguage}).value
},
})
this.preview = DOMPurify.sanitize(marked(this.text), { ADD_ATTR: ['target'] })
this.preview = DOMPurify.sanitize(marked(this.text), {ADD_ATTR: ['target']})
// Since the render function is synchronous, we can't do async http requests in it.
// Therefore, we can't resolve the blob url at (markdown) compile time.
@ -390,6 +363,13 @@ export default {
const parts = img.dataset.src.substr(window.API_URL.length + 1).split('/')
const taskId = parseInt(parts[1])
const attachmentId = parseInt(parts[3])
const cacheKey = `${taskId}-${attachmentId}`
if (typeof this.loadedAttachments[cacheKey] !== 'undefined') {
img.src = this.loadedAttachments[cacheKey]
continue
}
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
if (this.attachmentService === null) {
@ -399,6 +379,7 @@ export default {
this.attachmentService.getBlobUrl(attachment)
.then(url => {
img.src = url
this.loadedAttachments[cacheKey] = url
})
}
}
@ -408,6 +389,7 @@ export default {
for (const check of textCheckbox) {
check.removeEventListener('change', this.handleCheckboxClick)
check.addEventListener('change', this.handleCheckboxClick)
check.parentElement.classList.add('has-checkbox')
}
}
})
@ -424,10 +406,12 @@ export default {
}
console.debug(index, this.text.substr(index, 9))
const listPrefix = this.text.substr(index, 1)
if (checked) {
this.text = this.replaceAt(this.text, index, '* [x] ')
this.text = this.replaceAt(this.text, index, `${listPrefix} [x] `)
} else {
this.text = this.replaceAt(this.text, index, '* [ ] ')
this.text = this.replaceAt(this.text, index, `${listPrefix} [ ] `)
}
this.bubble()
this.renderPreview()
@ -448,17 +432,27 @@ export default {
</script>
<style lang="scss">
@import '../../../node_modules/highlight.js/scss/atelier-heath-light';
@import '../../../node_modules/highlight.js/scss/base16/equilibrium-gray-light';
@import '../../../node_modules/easymde/dist/easymde.min.css';
@import '../../styles/theme/variables/all';
.editor {
.clear {
clear: both;
}
.preview.content ul li input[type="checkbox"] {
margin-right: .5rem;
.preview.content {
margin-bottom: .5rem;
ul li {
input[type="checkbox"] {
margin-right: .5rem;
}
&.has-checkbox {
margin-left: -2em;
list-style: none;
}
}
}
}
@ -537,6 +531,10 @@ ul.actions {
&, a {
color: $grey-500;
&.done-edit {
color: $primary;
}
}
a:hover {

View File

@ -4,7 +4,7 @@
:checked="checked"
:disabled="disabled"
:id="checkBoxId"
@change="updateData"
@change="(event) => updateData(event.target.checked)"
style="display: none;"
type="checkbox"/>
<label :for="checkBoxId" class="check">
@ -26,7 +26,7 @@ export default {
data() {
return {
checked: false,
checkBoxId: '',
checkBoxId: 'fancycheckbox' + Math.random(),
}
},
props: {
@ -40,21 +40,19 @@ export default {
},
},
watch: {
value(newVal) {
this.checked = newVal
value: {
handler(value) {
this.checked = value
},
immediate: true,
},
},
mounted() {
this.checked = this.value
},
created() {
this.checkBoxId = 'fancycheckbox' + Math.random()
},
methods: {
updateData(e) {
this.checked = e.target.checked
this.$emit('input', this.checked)
this.$emit('change', e.target.checked)
updateData(checked) {
this.checked = checked
this.$emit('input', checked)
this.$emit('change', checked)
},
},
}

View File

@ -26,36 +26,15 @@
@keyup="search"
@keyup.enter.exact.prevent="() => createOrSelectOnEnter()"
:placeholder="placeholder"
@keydown.down.exact.prevent="() => preSelect(0, true)"
@keydown.down.exact.prevent="() => preSelect(0)"
ref="searchInput"
@focus="() => showSearchResults = true"
@focus="handleFocus"
/>
</div>
</div>
<transition name="fade">
<div class="search-results" v-if="searchResultsVisible">
<button
v-if="creatableAvailable"
class="is-fullwidth"
ref="result--1"
@keydown.up.prevent="() => preSelect(-2)"
@keydown.down.prevent="() => preSelect(0)"
@keyup.enter.prevent="create"
@click.prevent.stop="create"
>
<span>
<slot name="searchResult" :option="query">
<span class="search-result">
{{ query }}
</span>
</slot>
</span>
<span class="hint-text">
{{ createPlaceholder }}
</span>
</button>
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
<button
class="is-fullwidth"
v-for="(data, key) in filteredSearchResults"
@ -74,6 +53,27 @@
{{ selectPlaceholder }}
</span>
</button>
<button
v-if="creatableAvailable"
class="is-fullwidth"
:ref="`result-${filteredSearchResults.length}`"
@keydown.up.prevent="() => preSelect(filteredSearchResults.length - 1)"
@keydown.down.prevent="() => preSelect(filteredSearchResults.length + 1)"
@keyup.enter.prevent="create"
@click.prevent.stop="create"
>
<span>
<slot name="searchResult" :option="query">
<span class="search-result">
{{ query }}
</span>
</slot>
</span>
<span class="hint-text">
{{ createPlaceholder }}
</span>
</button>
</div>
</transition>
@ -108,21 +108,21 @@ export default {
type: Boolean,
default() {
return false
}
},
},
// The placeholder of the search input
placeholder: {
type: String,
default() {
return ''
}
},
},
// The search results where the @search listener needs to put the results into
searchResults: {
type: Array,
default() {
return []
}
},
},
// 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.
@ -130,13 +130,13 @@ export default {
type: String,
default() {
return ''
}
},
},
// The object with the value, updated every time an entry is selected.
value: {
default() {
return null
}
},
},
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
creatable: {
@ -149,14 +149,14 @@ export default {
createPlaceholder: {
type: String,
default() {
return 'Create new'
return this.$t('input.multiselect.createPlaceholder')
},
},
// The text shown next to an option.
selectPlaceholder: {
type: String,
default() {
return 'Click or press enter to select'
return this.$t('input.multiselect.selectPlaceholder')
},
},
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
@ -166,21 +166,48 @@ export default {
return false
},
},
// If true, displays the search results inline instead of using a dropdown.
inline: {
type: Boolean,
default() {
return false
},
},
// If true, shows search results when no query is specified.
showEmpty: {
type: Boolean,
default() {
return true
},
},
// 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() {
return 200
},
},
},
mounted() {
document.addEventListener('click', this.hideSearchResultsHandler)
this.setSelectedObject(this.value)
},
beforeDestroy() {
document.removeEventListener('click', this.hideSearchResultsHandler)
},
watch: {
value(newVal) {
this.setSelectedObject(newVal)
value: {
handler(value) {
this.setSelectedObject(value)
},
immediate: true,
},
},
computed: {
searchResultsVisible() {
if (this.query === '' && !this.showEmpty) {
return false
}
return this.showSearchResults && (
(this.filteredSearchResults.length > 0) ||
(this.creatable && this.query !== '')
@ -197,7 +224,7 @@ export default {
})
},
filteredSearchResults() {
if (this.multiple && this.internalValue !== null) {
if (this.multiple && this.internalValue !== null && Array.isArray(this.internalValue)) {
return this.searchResults.filter(item => !this.internalValue.some(e => e === item))
}
@ -211,7 +238,7 @@ export default {
// Updating the query with a binding does not work on mobile for some reason,
// getting the value manual does.
this.query = this.$refs.searchInput.value
if (this.searchTimeout !== null) {
clearTimeout(this.searchTimeout)
this.searchTimeout = null
@ -225,7 +252,7 @@ export default {
this.localLoading = false
}, 100) // The duration of the loading timeout of the services
this.showSearchResults = true
}, 200)
}, this.searchDelay)
},
hideSearchResultsHandler(e) {
closeWhenClickedOutside(e, this.$refs.multiselectRoot, this.closeSearchResults)
@ -233,6 +260,13 @@ export default {
closeSearchResults() {
this.showSearchResults = false
},
handleFocus() {
// We need the timeout to avoid the hideSearchResultsHandler hiding the search results right after the input
// is focused. That would lead to flickering pre-loaded search results and hiding them right after showing.
setTimeout(() => {
this.showSearchResults = true
}, 10)
},
select(object) {
if (this.multiple) {
if (this.internalValue === null) {
@ -270,13 +304,8 @@ export default {
this.query = this.label !== '' ? object[this.label] : object
},
preSelect(index, lookForCreatable = false) {
if (index === 0 && this.creatable && lookForCreatable) {
index = -1
}
if (index < -1) {
preSelect(index) {
if (index < 0) {
this.$refs.searchInput.focus()
return
}

View File

@ -5,13 +5,13 @@
:to="{ name: `${listRoutePrefix}.settings.edit`, params: { listId: list.id } }"
icon="pen"
>
Edit
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.delete`, params: { listId: list.id } }"
icon="trash-alt"
>
Delete
{{ $t('misc.delete') }}
</dropdown-item>
</template>
<template v-else-if="list.isArchived">
@ -19,7 +19,7 @@
:to="{ name: `${listRoutePrefix}.settings.archive`, params: { listId: list.id } }"
icon="archive"
>
Un-Archive
{{ $t('menu.unarchive') }}
</dropdown-item>
</template>
<template v-else>
@ -27,32 +27,32 @@
:to="{ name: `${listRoutePrefix}.settings.edit`, params: { listId: list.id } }"
icon="pen"
>
Edit
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.background`, params: { listId: list.id } }"
v-if="backgroundsEnabled"
icon="image"
>
Set background
{{ $t('menu.setBackground') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.share`, params: { listId: list.id } }"
icon="share-alt"
>
Share
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.duplicate`, params: { listId: list.id } }"
icon="paste"
>
Duplicate
{{ $t('menu.duplicate') }}
</dropdown-item>
<dropdown-item
:to="{ name: `${listRoutePrefix}.settings.archive`, params: { listId: list.id } }"
icon="archive"
>
Archive
{{ $t('menu.archive') }}
</dropdown-item>
<task-subscription
class="dropdown-item has-no-shadow"
@ -67,7 +67,7 @@
icon="trash-alt"
class="has-text-danger"
>
Delete
{{ $t('menu.delete') }}
</dropdown-item>
</template>
</dropdown>
@ -75,9 +75,9 @@
<script>
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown'
import DropdownItem from '@/components/misc/dropdown-item'
import TaskSubscription from '@/components/misc/subscription'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
export default {
name: 'list-settings-dropdown',
@ -101,7 +101,7 @@ export default {
},
computed: {
backgroundsEnabled() {
return this.$store.state.config.enabledBackgroundProviders.length > 0
return this.$store.state.config.enabledBackgroundProviders !== null && this.$store.state.config.enabledBackgroundProviders.length > 0
},
listRoutePrefix() {
let name = 'list'

View File

@ -25,15 +25,17 @@ export default {
Filters,
},
mounted() {
this.params = this.value
document.addEventListener('click', this.hidePopup)
},
beforeDestroy() {
document.removeEventListener('click', this.hidePopup)
},
watch: {
value(newVal) {
this.$set(this, 'params', newVal)
value: {
handler(value) {
this.params = value
},
immediate: true,
},
visible() {
this.visibleInternal = !this.visibleInternal

View File

@ -1,28 +1,30 @@
<template>
<card class="filters has-overflow">
<fancycheckbox v-model="params.filter_include_nulls">
Include Tasks which don't have a value set
{{ $t('filters.attributes.includeNulls') }}
</fancycheckbox>
<fancycheckbox
v-model="filters.requireAllFilters"
@change="setFilterConcat()"
>
Require all filters to be true for a task to show up
{{ $t('filters.attributes.requireAll') }}
</fancycheckbox>
<div class="field">
<label class="label">Show Done Tasks</label>
<label class="label">
{{ $t('filters.attributes.showDoneTasks') }}
</label>
<div class="control">
<fancycheckbox @change="setDoneFilter" v-model="filters.done">
Show Done Tasks
{{ $t('filters.attributes.showDoneTasks') }}
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Search</label>
<label class="label">{{ $t('misc.search') }}</label>
<div class="control">
<input
class="input"
placeholder="Search"
:placeholder="$t('misc.search')"
v-model="params.s"
@blur="change()"
@keyup.enter="change()"
@ -30,7 +32,7 @@
</div>
</div>
<div class="field">
<label class="label">Priority</label>
<label class="label">{{ $t('task.attributes.priority') }}</label>
<div class="control single-value-control">
<priority-select
:disabled="!filters.usePriority"
@ -41,12 +43,12 @@
v-model="filters.usePriority"
@change="setPriority"
>
Enable Filter By Priority
{{ $t('filters.attributes.enablePriority') }}
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Percent Done</label>
<label class="label">{{ $t('task.attributes.percentDone') }}</label>
<div class="control single-value-control">
<percent-done-select
v-model.number="filters.percentDone"
@ -57,65 +59,65 @@
v-model="filters.usePercentDone"
@change="setPercentDoneFilter"
>
Enable Filter By Percent Done
{{ $t('filters.attributes.enablePercentDone') }}
</fancycheckbox>
</div>
</div>
<div class="field">
<label class="label">Due Date</label>
<label class="label">{{ $t('task.attributes.dueDate') }}</label>
<div class="control">
<flat-pickr
:config="flatPickerConfig"
@on-close="setDueDateFilter"
class="input"
placeholder="Due Date Range"
:placeholder="$t('filters.attributes.dueDateRange')"
v-model="filters.dueDate"
/>
</div>
</div>
<div class="field">
<label class="label">Start Date</label>
<label class="label">{{ $t('task.attributes.startDate') }}</label>
<div class="control">
<flat-pickr
:config="flatPickerConfig"
@on-close="setStartDateFilter"
class="input"
placeholder="Start Date Range"
:placeholder="$t('filters.attributes.startDateRange')"
v-model="filters.startDate"
/>
</div>
</div>
<div class="field">
<label class="label">End Date</label>
<label class="label">{{ $t('task.attributes.endDate') }}</label>
<div class="control">
<flat-pickr
:config="flatPickerConfig"
@on-close="setEndDateFilter"
class="input"
placeholder="End Date Range"
:placeholder="$t('filters.attributes.endDateRange')"
v-model="filters.endDate"
/>
</div>
</div>
<div class="field">
<label class="label">Reminders</label>
<label class="label">{{ $t('task.attributes.reminders') }}</label>
<div class="control">
<flat-pickr
:config="flatPickerConfig"
@on-close="setReminderFilter"
class="input"
placeholder="Reminder Date Range"
:placeholder="$t('filters.attributes.reminderRange')"
v-model="filters.reminders"
/>
</div>
</div>
<div class="field">
<label class="label">Assignees</label>
<label class="label">{{ $t('task.attributes.assignees') }}</label>
<div class="control">
<multiselect
:loading="usersService.loading"
placeholder="Type to search for a user..."
:placeholder="$t('team.edit.search')"
@search="query => find('users', query)"
:search-results="foundusers"
@select="() => add('users', 'assignees')"
@ -128,37 +130,19 @@
</div>
<div class="field">
<label class="label">Labels</label>
<div class="control">
<multiselect
:loading="labelService.loading"
placeholder="Type to search for a label..."
@search="findLabels"
:search-results="foundLabels"
@select="label => addLabel(label)"
label="title"
:multiple="true"
v-model="labels"
>
<template v-slot:tag="props">
<span
:style="{'background': props.item.hexColor, 'color': props.item.textColor}"
class="tag ml-2 mt-2">
<span>{{ props.item.title }}</span>
<a @click="removeLabel(props.item)" class="delete is-small"></a>
</span>
</template>
</multiselect>
<label class="label">{{ $t('task.attributes.labels') }}</label>
<div class="control labels-list">
<edit-labels v-model="labels" @change="changeLabelFilter"/>
</div>
</div>
<template v-if="$route.name === 'filters.create' || $route.name === 'list.edit'">
<div class="field">
<label class="label">Lists</label>
<label class="label">{{ $t('list.lists') }}</label>
<div class="control">
<multiselect
:loading="listsService.loading"
placeholder="Type to search for a list..."
:placeholder="$t('list.search')"
@search="query => find('lists', query)"
:search-results="foundlists"
@select="() => add('lists', 'list_id')"
@ -170,11 +154,11 @@
</div>
</div>
<div class="field">
<label class="label">Namespaces</label>
<label class="label">{{ $t('namespace.namespaces') }}</label>
<div class="control">
<multiselect
:loading="namespaceService.loading"
placeholder="Type to search for a namespace..."
:placeholder="$t('namespace.search')"
@search="query => find('namespace', query)"
:search-results="foundnamespace"
@select="() => add('namespace', 'namespace')"
@ -197,18 +181,48 @@ import 'flatpickr/dist/flatpickr.css'
import {formatISO} from 'date-fns'
import differenceWith from 'lodash/differenceWith'
import PrioritySelect from '@/components/tasks/partials/prioritySelect'
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect'
import Multiselect from '@/components/input/multiselect'
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
import PercentDoneSelect from '@/components/tasks/partials/percentDoneSelect.vue'
import Multiselect from '@/components/input/multiselect.vue'
import UserService from '@/services/user'
import LabelService from '@/services/label'
import ListService from '@/services/list'
import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue'
// FIXME: merge with DEFAULT_PARAMS in taskList.js
const DEFAULT_PARAMS = {
sort_by: [],
order_by: [],
filter_by: [],
filter_value: [],
filter_comparator: [],
filter_include_nulls: true,
filter_concat: 'or',
s: '',
}
const DEFAULT_FILTERS = {
done: false,
dueDate: '',
requireAllFilters: false,
priority: 0,
usePriority: false,
startDate: '',
endDate: '',
percentDone: 0,
usePercentDone: false,
reminders: '',
assignees: '',
labels: '',
list_id: '',
namespace: '',
}
export default {
name: 'filters',
components: {
EditLabels,
PrioritySelect,
Fancycheckbox,
flatPickr,
@ -217,68 +231,27 @@ export default {
},
data() {
return {
params: {
sort_by: [],
order_by: [],
filter_by: [],
filter_value: [],
filter_comparator: [],
filter_include_nulls: true,
filter_concat: 'or',
s: '',
},
filters: {
done: false,
dueDate: '',
requireAllFilters: false,
priority: 0,
usePriority: false,
startDate: '',
endDate: '',
percentDone: 0,
usePercentDone: false,
reminders: '',
assignees: '',
labels: '',
list_id: '',
namespace: '',
},
flatPickerConfig: {
altFormat: 'j M Y H:i',
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
mode: 'range',
},
params: DEFAULT_PARAMS,
filters: DEFAULT_FILTERS,
usersService: UserService,
usersService: new UserService(),
foundusers: [],
users: [],
labelService: LabelService,
foundLabels: [],
labelQuery: '',
labels: [],
listsService: ListService,
listsService: new ListService(),
foundlists: [],
lists: [],
namespaceService: NamespaceService,
namespaceService: new NamespaceService(),
foundnamespace: [],
namespace: [],
}
},
created() {
this.usersService = new UserService()
this.labelService = new LabelService()
this.listsService = new ListService()
this.namespaceService = new NamespaceService()
},
mounted() {
this.params = this.value
this.filters.requireAllFilters = this.params.filter_concat === 'and'
this.prepareFilters()
},
props: {
value: {
@ -286,9 +259,36 @@ export default {
},
},
watch: {
value(newVal) {
this.$set(this, 'params', newVal)
this.prepareFilters()
value: {
handler(value) {
this.params = value
this.prepareFilters()
},
immediate: true,
},
},
computed: {
foundLabels() {
const labels = (Object.values(this.$store.state.labels.labels).filter(l => {
return l.title.toLowerCase().includes(this.labelQuery.toLowerCase())
}) ?? [])
return differenceWith(labels, this.labels, (first, second) => {
return first.id === second.id
})
},
flatPickerConfig() {
return {
altFormat: this.$t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
mode: 'range',
locale: {
firstDayOfWeek: this.$store.state.auth.settings.weekStart,
},
}
},
},
methods: {
@ -305,9 +305,12 @@ export default {
this.prepareSingleValue('percent_done', 'percentDone', 'usePercentDone', true)
this.prepareDate('reminders')
this.prepareRelatedObjectFilter('users', 'assignees')
this.prepareRelatedObjectFilter('labels', 'labels', 'label')
this.prepareRelatedObjectFilter('lists', 'list_id')
this.prepareRelatedObjectFilter('namespace')
this.prepareSingleValue('labels')
const labelIds = (typeof this.filters.labels === 'string' ? this.filters.labels : '').split(',').map(i => parseInt(i))
this.labels = (Object.values(this.$store.state.labels.labels).filter(l => labelIds.includes(l.id)) ?? [])
},
removePropertyFromFilter(propertyName) {
// Because of the way arrays work, we can only ever remove one element at once.
@ -321,7 +324,11 @@ export default {
}
}
},
setDateFilter(filterName, variableName) {
setDateFilter(filterName, variableName = null) {
if (variableName === null) {
variableName = filterName
}
// Only filter if we have a start and end due date
if (this.filters[variableName] !== '') {
@ -477,7 +484,7 @@ export default {
.then(r => {
this.$set(this, kind, r)
})
.catch(e => this.error(e, this))
.catch(e => this.$message.error(e))
}
},
setDoneFilter() {
@ -532,7 +539,7 @@ export default {
}))
})
.catch(e => {
this.error(e, this)
this.$message.error(e)
})
},
add(kind, filterName) {
@ -560,25 +567,8 @@ export default {
this.$set(this.filters, filterName, ids.join(','))
this.setSingleValueFilter(filterName, filterName, '', 'in')
},
clearLabels() {
this.$set(this, 'foundLabels', [])
},
findLabels(query) {
if (query === '') {
this.clearLabels()
}
this.labelService.getAll({}, {s: query})
.then(response => {
// Filter the results to not include labels already selected
this.$set(this, 'foundLabels', differenceWith(response, this.labels, (first, second) => {
return first.id === second.id
}))
})
.catch(e => {
this.error(e, this)
})
this.labelQuery = query
},
addLabel() {
this.$nextTick(() => {

View File

@ -0,0 +1,87 @@
<template>
<router-link
:class="{
'has-light-text': !colorIsDark(list.hexColor),
'has-background': background !== null
}"
:style="{
'background-color': list.hexColor,
'background-image': background !== null ? `url(${background})` : false,
}"
:to="{ name: 'list.index', params: { listId: list.id} }"
class="list-card"
tag="span"
v-if="list !== null && (showArchived ? true : !list.isArchived)"
>
<div class="is-archived-container">
<span class="is-archived" v-if="list.isArchived">
{{ $t('namespace.archived') }}
</span>
<span
:class="{'is-favorite': list.isFavorite, 'is-archived': list.isArchived}"
@click.stop="toggleFavoriteList(list)"
class="favorite">
<icon icon="star" v-if="list.isFavorite"/>
<icon :icon="['far', 'star']" v-else/>
</span>
</div>
<div class="title">{{ list.title }}</div>
</router-link>
</template>
<script>
import ListService from '@/services/list'
export default {
name: 'list-card',
data() {
return {
background: null,
backgroundLoading: false,
}
},
props: {
list: {
required: true,
},
showArchived: {
default: false,
type: Boolean,
},
},
watch: {
list: {
handler: 'loadBackground',
immediate: true,
},
},
methods: {
loadBackground() {
if (this.list === null || !this.list.backgroundInformation || this.backgroundLoading) {
return
}
this.backgroundLoading = true
const listService = new ListService()
listService.background(this.list)
.then(b => {
this.$set(this, 'background', b)
})
.catch(e => {
this.$message.error(e)
})
.finally(() => this.backgroundLoading = false)
},
toggleFavoriteList(list) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
this.$store.dispatch('lists/toggleListFavorite', list)
.catch(e => this.$message.error(e))
},
},
}
</script>

View File

@ -1,16 +1,34 @@
<template>
<div class="content">
<h1>Import your data from {{ name }} to Vikunja</h1>
<p>Vikunja will import all lists, tasks, notes, reminders and files you have access to.</p>
<h1>{{ $t('migrate.titleService', {name: name}) }}</h1>
<p>{{ $t('migrate.descriptionDo') }}</p>
<template v-if="isMigrating === false && message === '' && lastMigrationDate === null">
<p>To authorize Vikunja to access your {{ name }} Account, click the button below.</p>
<x-button
:loading="migrationService.loading"
:disabled="migrationService.loading"
:href="authUrl"
>
Get Started
</x-button>
<template v-if="isFileMigrator">
<p>{{ $t('migrate.importUpload', {name: name}) }}</p>
<input
@change="migrate"
class="is-hidden"
ref="uploadInput"
type="file"
/>
<x-button
:loading="migrationService.loading"
:disabled="migrationService.loading"
@click="$refs.uploadInput.click()"
>
{{ $t('migrate.upload') }}
</x-button>
</template>
<template v-else>
<p>{{ $t('migrate.authorize', {name: name}) }}</p>
<x-button
:loading="migrationService.loading"
:disabled="migrationService.loading"
:href="authUrl"
>
{{ $t('migrate.getStarted') }}
</x-button>
</template>
</template>
<div
class="migration-in-progress-container"
@ -18,28 +36,20 @@
<div class="migration-in-progress">
<img :alt="name" :src="`/images/migration/${identifier}.png`"/>
<div class="progress-dots">
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span></span>
<span v-for="i in progressDotsCount" :key="i" />
</div>
<img alt="Vikunja" src="/images/logo.svg">
</div>
<p>Importing in progress, hang tight...</p>
<p>{{ $t('migrate.inProgress') }}</p>
</div>
<div v-else-if="lastMigrationDate">
<p>
It looks like you've already imported your stuff from {{ name }} at {{ formatDate(lastMigrationDate) }}.<br/>
Importing again is possible, but might create duplicates.
Are you sure?
{{ $t('migrate.alreadyMigrated1', {name: name, date: formatDate(lastMigrationDate)}) }}<br/>
{{ $t('migrate.alreadyMigrated2') }}
</p>
<div class="buttons">
<x-button @click="migrate">I am sure, please start migrating now!</x-button>
<x-button :to="{name: 'home'}" type="tertary" class="has-text-danger">Cancel</x-button>
<x-button @click="migrate">{{ $t('migrate.confirm') }}</x-button>
<x-button :to="{name: 'home'}" type="tertary" class="has-text-danger">{{ $t('misc.cancel') }}</x-button>
</div>
</div>
<div v-else>
@ -48,23 +58,28 @@
{{ message }}
</div>
</div>
<x-button :to="{name: 'home'}">Refresh</x-button>
<x-button :to="{name: 'home'}">{{ $t('misc.refresh') }}</x-button>
</div>
</div>
</template>
<script>
import AbstractMigrationService from '../../services/migrator/abstractMigrationService'
import AbstractMigrationService from '../../services/migrator/abstractMigration'
import AbstractMigrationFileService from '../../services/migrator/abstractMigrationFile'
const PROGRESS_DOTS_COUNT = 8
export default {
name: 'migration',
data() {
return {
progressDotsCount: PROGRESS_DOTS_COUNT,
authUrl: '',
isMigrating: false,
lastMigrationDate: null,
message: '',
migratorAuthCode: '',
migrationService: null,
}
},
props: {
@ -76,11 +91,21 @@ export default {
type: String,
required: true,
},
isFileMigrator: {
type: Boolean,
default: false,
},
},
created() {
this.message = ''
if (this.isFileMigrator) {
this.migrationService = new AbstractMigrationFileService(this.identifier)
return
}
this.migrationService = new AbstractMigrationService(this.identifier)
this.getAuthUrl()
this.message = ''
if (typeof this.$route.query.code !== 'undefined' || location.hash.startsWith('#token=')) {
if (location.hash.startsWith('#token=')) {
@ -105,7 +130,7 @@ export default {
this.migrate()
})
.catch(e => {
this.error(e, this)
this.$message.error(e)
})
}
},
@ -116,20 +141,42 @@ export default {
this.authUrl = r.url
})
.catch(e => {
this.error(e, this)
this.$message.error(e)
})
},
migrate() {
this.isMigrating = true
this.lastMigrationDate = null
this.message = ''
if (this.isFileMigrator) {
return this.migrateFile()
}
this.migrationService.migrate({code: this.migratorAuthCode})
.then(r => {
this.message = r.message
this.$store.dispatch('namespaces/loadNamespaces')
})
.catch(e => {
this.error(e, this)
this.$message.error(e)
})
.finally(() => {
this.isMigrating = false
})
},
migrateFile() {
if (this.$refs.uploadInput.files.length === 0) {
return
}
this.migrationService.migrate(this.$refs.uploadInput.files[0])
.then(r => {
this.message = r.message
this.$store.dispatch('namespaces/loadNamespaces')
})
.catch(e => {
this.$message.error(e)
})
.finally(() => {
this.isMigrating = false

View File

@ -1,13 +1,13 @@
<template>
<div class="api-config">
<div v-if="configureApi">
<label class="label" for="api-url">Vikunja URL</label>
<label class="label" for="api-url">{{ $t('apiConfig.url') }}</label>
<div class="field has-addons">
<div class="control is-expanded">
<input
class="input"
id="api-url"
placeholder="eg. https://localhost:3456"
:placeholder="$t('apiConfig.urlPlaceholder')"
required
type="url"
v-focus
@ -17,16 +17,17 @@
</div>
<div class="control">
<x-button @click="setApiUrl" :disabled="apiUrl === ''">
Change
{{ $t('apiConfig.change') }}
</x-button>
</div>
</div>
</div>
<div class="api-url-info" v-else>
Sign in to your Vikunja account on
<span v-tooltip="apiUrl"> {{ apiDomain() }} </span>
<i18n path="apiConfig.signInOn">
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
</i18n>
<br />
<a @click="() => (configureApi = true)">change</a>
<a @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</a>
</div>
<div
@ -45,32 +46,31 @@
</template>
<script>
import { parseURL } from 'ufo'
const API_DEFAULT_PORT = 3456
export default {
name: 'apiConfig',
data() {
return {
configureApi: false,
apiUrl: '',
apiUrl: window.API_URL,
errorMsg: '',
successMsg: '',
}
},
created() {
this.apiUrl = window.API_URL
if (this.apiUrl === '') {
this.configureApi = true
}
},
methods: {
computed: {
apiDomain() {
if (window.API_URL.startsWith('/api/v1')) {
return window.location.host
}
const urlParts = window.API_URL.replace('http://', '')
.replace('https://', '')
.split(/[/?#]/)
return urlParts[0]
return parseURL(this.apiUrl).host
},
},
methods: {
setApiUrl() {
if (this.apiUrl === '') {
return
@ -130,17 +130,17 @@ export default {
return Promise.reject(e)
})
.catch((e) => {
// Check if it is reachable at port 3456 and https
if (urlToCheck.port !== 3456) {
// Check if it is reachable at port API_DEFAULT_PORT and https
if (urlToCheck.port !== API_DEFAULT_PORT) {
urlToCheck.protocol = 'https:'
urlToCheck.port = 3456
urlToCheck.port = API_DEFAULT_PORT
window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update')
}
return Promise.reject(e)
})
.catch((e) => {
// Check if it is reachable at :3456 and /api/v1 and https
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and https
urlToCheck.pathname = origUrlToCheck.pathname
if (
!urlToCheck.pathname.endsWith('/api/v1') &&
@ -153,17 +153,17 @@ export default {
return Promise.reject(e)
})
.catch((e) => {
// Check if it is reachable at port 3456 and http
if (urlToCheck.port !== 3456) {
// Check if it is reachable at port API_DEFAULT_PORT and http
if (urlToCheck.port !== API_DEFAULT_PORT) {
urlToCheck.protocol = 'http:'
urlToCheck.port = 3456
urlToCheck.port = API_DEFAULT_PORT
window.API_URL = urlToCheck.toString()
return this.$store.dispatch('config/update')
}
return Promise.reject(e)
})
.catch((e) => {
// Check if it is reachable at :3456 and /api/v1 and http
// Check if it is reachable at :API_DEFAULT_PORT and /api/v1 and http
urlToCheck.pathname = origUrlToCheck.pathname
if (
!urlToCheck.pathname.endsWith('/api/v1') &&
@ -178,14 +178,14 @@ export default {
.catch(() => {
// Still not found, url is still invalid
this.successMsg = ''
this.errorMsg = `Could not find or use Vikunja installation at "${this.apiDomain()}".`
this.errorMsg = this.$t('apiConfig.error', {domain: this.apiDomain})
window.API_URL = oldUrl
})
.then((r) => {
if (typeof r !== 'undefined') {
// Set it + save it to local storage to save us the hoops
this.errorMsg = ''
this.successMsg = `Using Vikunja installation at "${this.apiDomain()}".`
this.successMsg = this.$t('apiConfig.success', {domain: this.apiDomain})
localStorage.setItem('API_URL', window.API_URL)
this.configureApi = false
this.apiUrl = window.API_URL

View File

@ -26,7 +26,7 @@
type="secondary"
@click.prevent.stop="$router.back()"
>
Cancel
{{ $t('misc.cancel') }}
</x-button>
<x-button
type="primary"
@ -52,7 +52,9 @@ export default {
},
primaryLabel: {
type: String,
default: 'Create',
default() {
return this.$t('misc.create')
},
},
primaryIcon: {
type: String,

View File

@ -22,7 +22,7 @@ export default {
type: String,
required: false,
default: '',
}
},
},
}
</script>

View File

@ -1,7 +1,9 @@
<template>
<div class="notification is-danger">
Loading failed, please <a @click="() => location.reload()">try again</a>.
If the error persists, please <a href="https://vikunja.io/contact/">contact us</a>.
<i18n path="loadingError.failed">
<a @click="() => location.reload()">{{ $t('loadingError.tryAgain') }}</a>
<a href="https://vikunja.io/contact/" rel="noreferrer noopener nofollow" target="_blank">{{ $t('loadingError.contact') }}</a>
</i18n>
</div>
</template>

View File

@ -1,59 +1,64 @@
<template>
<div class="modal-mask keyboard-shortcuts-modal">
<div @click.self="close()" class="modal-container">
<div class="modal-content">
<card class="has-background-white has-no-shadow" title="Available Keyboard Shortcuts">
<p>
<strong>Toggle The Menu</strong>
<shortcut :keys="['ctrl', 'e']"/>
</p>
<h3>Kanban</h3>
<div class="message is-primary" v-if="$route.name === 'list.kanban'">
<modal @close="close()">
<card class="has-background-white has-no-shadow" :title="$t('keyboardShortcuts.title')">
<div class="message is-primary">
<div class="message-body">
These shortcuts work on the current page.
{{ $t('keyboardShortcuts.allPages') }}
</div>
</div>
<p>
<strong>Mark a task as done</strong>
<strong>{{ $t('keyboardShortcuts.toggleMenu') }}</strong>
<shortcut :keys="['ctrl', 'e']"/>
</p>
<p>
<strong>{{ $t('keyboardShortcuts.quickSearch') }}</strong>
<shortcut :keys="['ctrl', 'k']"/>
</p>
<h3>{{ $t('list.kanban.title') }}</h3>
<div class="message is-primary" v-if="$route.name === 'list.kanban'">
<div class="message-body">
{{ $t('keyboardShortcuts.currentPageOnly') }}
</div>
</div>
<p>
<strong>{{ $t('keyboardShortcuts.task.done') }}</strong>
<shortcut :keys="['ctrl', 'click']"/>
</p>
<h3>Task Page</h3>
<h3>{{ $t('keyboardShortcuts.task.title') }}</h3>
<div
class="message is-primary"
v-if="$route.name === 'task.detail' || $route.name === 'task.list.detail' || $route.name === 'task.gantt.detail' || $route.name === 'task.kanban.detail' || $route.name === 'task.detail'">
<div class="message-body">
These shortcuts work on the current page.
{{ $t('keyboardShortcuts.currentPageOnly') }}
</div>
</div>
<p>
<strong>Assign this task to a user</strong>
<strong>{{ $t('keyboardShortcuts.task.assign') }}</strong>
<shortcut :keys="['a']"/>
</p>
<p>
<strong>Add labels to this task</strong>
<strong>{{ $t('keyboardShortcuts.task.labels') }}</strong>
<shortcut :keys="['l']"/>
</p>
<p>
<strong>Change the due date of this task</strong>
<strong>{{ $t('keyboardShortcuts.task.dueDate') }}</strong>
<shortcut :keys="['d']"/>
</p>
<p>
<strong>Add an attachment to this task</strong>
<strong>{{ $t('keyboardShortcuts.task.attachment') }}</strong>
<shortcut :keys="['f']"/>
</p>
<p>
<strong>Modify related tasks of this task</strong>
<strong>{{ $t('keyboardShortcuts.task.related') }}</strong>
<shortcut :keys="['r']"/>
</p>
</card>
</div>
</div>
</div>
</modal>
</template>
<script>
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import Shortcut from '@/components/misc/shortcut'
import Shortcut from '@/components/misc/shortcut.vue'
export default {
name: 'keyboard-shortcuts',
@ -64,4 +69,4 @@ export default {
},
},
}
</script>
</script>

View File

@ -1,8 +1,8 @@
<template>
<div class="legal-links">
<a :href="imprintUrl" target="_blank" v-if="imprintUrl">Imprint</a>
<a :href="imprintUrl" rel="noreferrer noopener nofollow" target="_blank" v-if="imprintUrl">{{ $t('navigation.imprint') }}</a>
<span v-if="imprintUrl && privacyPolicyUrl"> | </span>
<a :href="privacyPolicyUrl" target="_blank" v-if="privacyPolicyUrl">Privacy policy</a>
<a :href="privacyPolicyUrl" rel="noreferrer noopener nofollow" target="_blank" v-if="privacyPolicyUrl">{{ $t('navigation.privacy') }}</a>
</div>
</template>

View File

@ -6,6 +6,6 @@
<script>
export default {
name: 'nothing'
name: 'nothing',
}
</script>

View File

@ -1,29 +1,24 @@
<template>
<notifications position="bottom left" :max="2" class="global-notification">
<template slot="body" slot-scope="props">
<template #body="{ item, close }">
<div
:class="[
'vue-notification-template',
'vue-notification',
props.item.type,
item.type,
]"
@click="close(props)"
@click="close()"
>
<div
class="notification-title"
v-html="props.item.title"
v-if="props.item.title"
></div>
<div
class="notification-content"
v-html="props.item.text"
></div>
<div v-if="item.title" class="notification-title">{{ item.title }}</div>
<div class="notification-content">
<template v-for="(t, k) in item.text">{{ t }}<br :key="k"/></template>
</div>
<div
class="buttons is-right"
v-if="
props.item.data &&
props.item.data.actions &&
props.item.data.actions.length > 0
item.data &&
item.data.actions &&
item.data.actions.length > 0
"
>
<x-button
@ -32,7 +27,7 @@
:shadow="false"
class="is-small"
type="secondary"
v-for="(action, i) in props.item.data.actions"
v-for="(action, i) in item.data.actions"
>
{{ action.title }}
</x-button>
@ -45,11 +40,6 @@
<script>
export default {
name: 'notification',
methods: {
close(props) {
props.close()
},
},
}
</script>

View File

@ -0,0 +1,125 @@
<template>
<nav
aria-label="pagination"
class="pagination is-centered p-4"
role="navigation"
v-if="totalPages > 1"
>
<router-link
:disabled="currentPage === 1"
:to="getRouteForPagination(currentPage - 1)"
class="pagination-previous"
tag="button">
{{ $t('misc.previous') }}
</router-link>
<router-link
:disabled="currentPage === totalPages"
:to="getRouteForPagination(currentPage + 1)"
class="pagination-next"
tag="button">
{{ $t('misc.next') }}
</router-link>
<ul class="pagination-list">
<template v-for="(p, i) in pages">
<li :key="'page' + i" v-if="p.isEllipsis">
<span class="pagination-ellipsis">&hellip;</span>
</li>
<li :key="'page' + i" v-else>
<router-link
:aria-label="'Goto page ' + p.number"
:class="{ 'is-current': p.number === currentPage }"
:to="getRouteForPagination(p.number)"
class="pagination-link"
>
{{ p.number }}
</router-link>
</li>
</template>
</ul>
</nav>
</template>
<script>
function createPagination(totalPages, currentPage) {
const pages = []
for (let i = 0; i < totalPages; i++) {
// Show ellipsis instead of all pages
if (
i > 0 && // Always at least the first page
(i + 1) < totalPages && // And the last page
(
// And the current with current + 1 and current - 1
(i + 1) > currentPage + 1 ||
(i + 1) < currentPage - 1
)
) {
// Only add an ellipsis if the last page isn't already one
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
pages.push({
number: 0,
isEllipsis: true,
})
}
continue
}
pages.push({
number: i + 1,
isEllipsis: false,
})
}
return pages
}
function getRouteForPagination(page = 1, type = 'list') {
return {
name: 'list.' + type,
params: {
type: type,
},
query: {
page: page,
},
}
}
export default {
name: 'Pagination',
props: {
totalPages: {
type: Number,
required: true,
},
currentPage: {
type: Number,
required: true,
},
},
computed: {
pages() {
return createPagination(this.totalPages, this.currentPage)
},
},
methods: {
getRouteForPagination,
},
}
</script>
<style lang="scss" scoped>
.pagination {
padding-bottom: 1rem;
.pagination-previous,
.pagination-next {
&:not(:disabled):hover {
background: $scheme-main;
cursor: pointer;
}
}
}
</style>

View File

@ -1,8 +1,8 @@
<template>
<span class="shortcuts">
<template v-for="(k, i) in keys">
<span :key="i">{{ k }}</span>
<i v-if="i < keys.length - 1" :key="`plus${i}`">+</i>
<kbd :key="i">{{ k }}</kbd>
<span v-if="i < keys.length - 1" :key="`plus${i}`">+</span>
</template>
</span>
</template>
@ -14,7 +14,26 @@ export default {
keys: {
type: Array,
required: true,
}
},
},
}
</script>
<style lang="scss" scoped>
.shortcuts {
display: flex;
align-items: center;
}
kbd {
padding: .1rem .35rem;
border: 1px solid $grey-300;
background: $grey-100;
border-radius: 3px;
font-size: .75rem;
}
span {
padding: 0 .25rem;
}
</style>

View File

@ -30,7 +30,7 @@ export default {
name: 'task-subscription',
data() {
return {
subscriptionService: SubscriptionService,
subscriptionService: new SubscriptionService(),
}
},
props: {
@ -49,21 +49,21 @@ export default {
default: true,
},
},
created() {
this.subscriptionService = new SubscriptionService()
},
computed: {
tooltipText() {
if(this.disabled) {
return `You can't unsubscribe here because you are subscribed to this ${this.entity} through its ${this.subscription.entity}.`
if (this.disabled) {
return this.$t('task.subscription.subscribedThroughParent', {
entity: this.entity,
parent: this.subscription.entity,
})
}
return this.subscription !== null ?
`You are currently subscribed to this ${this.entity} and will receive notifications for changes.` :
`You are not subscribed to this ${this.entity} and won't receive notifications for changes.`
this.$t('task.subscription.subscribed', {entity: this.entity}) :
this.$t('task.subscription.notSubscribed', {entity: this.entity})
},
buttonText() {
return this.subscription !== null ? 'Unsubscribe' : 'Subscribe'
return this.subscription !== null ? this.$t('task.subscription.unsubscribe') : this.$t('task.subscription.subscribe')
},
icon() {
return this.subscription !== null ? ['far', 'bell-slash'] : 'bell'
@ -78,7 +78,7 @@ export default {
},
methods: {
changeSubscription() {
if(this.disabled) {
if (this.disabled) {
return
}
@ -96,10 +96,10 @@ export default {
this.subscriptionService.create(subscription)
.then(() => {
this.$emit('change', subscription)
this.success({message: `You are now subscribed to this ${this.entity}`}, this)
this.$message.success({message: this.$t('task.subscription.subscribeSuccess', {entity: this.entity})})
})
.catch(e => {
this.error(e, this)
this.$message.error(e)
})
},
unsubscribe() {
@ -110,12 +110,12 @@ export default {
this.subscriptionService.delete(subscription)
.then(() => {
this.$emit('change', null)
this.success({message: `You are now unsubscribed to this ${this.entity}`}, this)
this.$message.success({message: this.$t('task.subscription.unsubscribeSuccess', {entity: this.entity})})
})
.catch(e => {
this.error(e, this)
this.$message.error(e)
})
}
},
},
}
</script>

View File

@ -47,8 +47,6 @@ export default {
}
img {
-webkit-border-radius: 100%;
-moz-border-radius: 100%;
border-radius: 100%;
vertical-align: middle;

View File

@ -1,8 +1,27 @@
<template>
<transition name="modal">
<div class="modal-mask">
<div class="modal-container" @mousedown.self.prevent.stop="$emit('close')">
<div class="modal-content" :class="{'has-overflow': overflow, 'is-wide': wide}">
<section
v-if="enabled"
class="modal-mask"
:class="[
{ 'has-overflow': overflow },
variant,
]"
>
<div
class="modal-container"
:class="{'has-overflow': overflow}"
@click.self.prevent.stop="$emit('close')"
@shortkey="$emit('close')"
v-shortkey="['esc']"
>
<div
class="modal-content"
:class="{
'has-overflow': overflow,
'is-wide': wide
}"
>
<slot>
<div class="header">
<slot name="header"></slot>
@ -16,27 +35,42 @@
type="tertary"
class="has-text-danger"
>
Cancel
{{ $t('misc.cancel') }}
</x-button>
<x-button
@click="$emit('submit')"
type="primary"
:shadow="false"
>
Do it!
{{ $t('misc.doit') }}
</x-button>
</div>
</slot>
</div>
</div>
</div>
</section>
</transition>
</template>
<script>
export const TRANSITION_NAMES = {
MODAL: 'modal',
FADE: 'fade',
}
export const VARIANTS = {
DEFAULT: 'default',
HINT_MODAL: 'hint-modal',
SCROLLING: 'scrolling',
}
function validValue(values) {
return (value) => Object.values(values).includes(value)
}
export default {
name: 'modal',
mounted: function () {
mounted() {
document.addEventListener('keydown', (e) => {
// Close the model when escape is pressed
if (e.keyCode === 27) {
@ -45,6 +79,10 @@ export default {
})
},
props: {
enabled: {
type: Boolean,
default: true,
},
overflow: {
type: Boolean,
default: false,
@ -53,6 +91,126 @@ export default {
type: Boolean,
default: false,
},
transitionName: {
type: String,
default: TRANSITION_NAMES.MODAL,
validator: validValue(TRANSITION_NAMES),
},
variant: {
type: String,
default: VARIANTS.DEFAULT,
validator: validValue(VARIANTS),
},
},
}
</script>
<style lang="scss" scoped>
.modal-mask {
position: fixed;
z-index: 4000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, .8);
transition: opacity 150ms ease;
color: #fff;
}
.modal-container {
transition: all 150ms ease;
position: relative;
width: 100%;
height: 100%;
max-height: 100vh;
overflow: auto;
}
.default .modal-content,
.hint-modal .modal-content {
text-align: center;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
@media screen and (max-width: $tablet) {
margin: 0;
top: 25%;
transform: translate(-50%, -25%);
}
.header {
font-size: 2rem;
font-weight: 700;
}
.button {
margin: 0 0.5rem;
}
}
// scrolling-content
// used e.g. for <TaskDetailViewModal>
.scrolling .modal-content {
max-width: 1024px;
width: 100%;
margin: 4rem 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
width: 100%;
}
@media screen and (max-width: $desktop) {
margin: 0;
}
}
.is-wide {
max-width: $desktop;
width: calc(100% - 2rem);
}
.hint-modal {
z-index: 4600;
::v-deep.card-content {
text-align: left;
.info {
font-style: italic;
}
p {
display: flex;
justify-content: space-between;
align-items: center;
}
.message-body {
padding: .5rem .75rem;
}
}
}
/* Transitions */
.modal-enter,
.modal-leave-active {
opacity: 0;
}
.modal-enter .modal-container,
.modal-leave-active .modal-container {
transform: scale(0.9);
}
</style>

View File

@ -1,53 +1,43 @@
<template>
<multiselect
:loading="namespaceService.loading"
placeholder="Search for a namespace..."
:placeholder="$t('namespace.search')"
@search="findNamespaces"
:search-results="namespaces"
@select="select"
label="title"
v-model="namespace"
:search-delay="10"
/>
</template>
<script>
import NamespaceService from '../../services/namespace'
import NamespaceModel from '../../models/namespace'
import Multiselect from '@/components/input/multiselect'
import Multiselect from '@/components/input/multiselect.vue'
export default {
name: 'namespace-search',
data() {
return {
namespaceService: NamespaceService,
namespace: NamespaceModel,
namespaces: [],
query: '',
}
},
components: {
Multiselect,
},
created() {
this.namespaceService = new NamespaceService()
computed: {
namespaces() {
if (this.query === '') {
return []
}
return this.$store.state.namespaces.namespaces.filter(n => {
return !n.isArchived &&
n.id > 0 &&
n.title.toLowerCase().includes(this.query.toLowerCase())
})
},
},
methods: {
findNamespaces(query) {
if (query === '') {
this.clearAll()
return
}
this.namespaceService.getAll({}, {s: query})
.then(response => {
this.$set(this, 'namespaces', response)
})
.catch(e => {
this.error(e, this)
})
},
clearAll() {
this.$set(this, 'namespaces', [])
this.query = query
},
select(namespace) {
this.$emit('selected', namespace)

View File

@ -5,7 +5,7 @@
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
icon="archive"
>
Un-Archive
{{ $t('menu.unarchive') }}
</dropdown-item>
</template>
<template v-else>
@ -13,25 +13,25 @@
:to="{ name: 'namespace.settings.edit', params: { id: namespace.id } }"
icon="pen"
>
Edit
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.share', params: { id: namespace.id } }"
icon="share-alt"
>
Share
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'list.create', params: { id: namespace.id } }"
icon="plus"
>
New list
{{ $t('menu.newList') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
icon="archive"
>
Archive
{{ $t('menu.archive') }}
</dropdown-item>
<task-subscription
class="dropdown-item has-no-shadow"
@ -46,16 +46,16 @@
icon="trash-alt"
class="has-text-danger"
>
Delete
{{ $t('menu.delete') }}
</dropdown-item>
</template>
</dropdown>
</template>
<script>
import Dropdown from '@/components/misc/dropdown'
import DropdownItem from '@/components/misc/dropdown-item'
import TaskSubscription from '@/components/misc/subscription'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue'
export default {
name: 'namespace-settings-dropdown',

View File

@ -1,13 +1,15 @@
<template>
<div class="notifications">
<a @click.stop="showNotifications = !showNotifications" class="trigger">
<span class="unread-indicator" v-if="unreadNotifications > 0"></span>
<icon icon="bell"/>
</a>
<div class="is-flex is-justify-content-center">
<a @click.stop="showNotifications = !showNotifications" class="trigger-button">
<span class="unread-indicator" v-if="unreadNotifications > 0"></span>
<icon icon="bell"/>
</a>
</div>
<transition name="fade">
<div class="notifications-list" v-if="showNotifications" ref="popup">
<span class="head">Notifications</span>
<span class="head">{{ $t('notification.title') }}</span>
<div
v-for="(n, index) in notifications"
:key="n.id"
@ -35,9 +37,9 @@
</span>
</div>
<p class="nothing" v-if="notifications.length === 0">
You don't have any notifications. Have a nice day!<br/>
{{ $t('notification.none') }}<br/>
<span class="explainer">
Notifications will appear here when actions on namespaces, lists or tasks you subscribed to happen.
{{ $t('notification.explainer') }}
</span>
</p>
</div>
@ -47,29 +49,28 @@
<script>
import NotificationService from '@/services/notification'
import User from '@/components/misc/user'
import names from '@/models/notificationNames.json'
import User from '@/components/misc/user.vue'
import names from '@/models/constants/notificationNames.json'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {mapState} from 'vuex'
const LOAD_NOTIFICATIONS_INTERVAL = 10000
export default {
name: 'notifications',
components: {User},
data() {
return {
notificationService: NotificationService,
notificationService: new NotificationService(),
allNotifications: [],
showNotifications: false,
interval: null,
}
},
created() {
this.notificationService = new NotificationService()
},
mounted() {
this.loadNotifications()
document.addEventListener('click', this.hidePopup)
this.interval = setInterval(this.loadNotifications, 10000)
this.interval = setInterval(this.loadNotifications, LOAD_NOTIFICATIONS_INTERVAL)
},
beforeDestroy() {
document.removeEventListener('click', this.hidePopup)
@ -98,7 +99,7 @@ export default {
this.$set(this, 'allNotifications', r)
})
.catch(e => {
this.error(e, this)
this.$message.error(e)
})
},
to(n, index) {
@ -136,7 +137,7 @@ export default {
.then(r => {
this.$set(this.allNotifications, index, r)
})
.catch(e => this.error(e, this))
.catch(e => this.$message.error(e))
}
},
},

View File

@ -0,0 +1,505 @@
<template>
<modal v-if="active" class="quick-actions" @close="closeQuickActions" :overflow="isNewTaskCommand">
<div class="card">
<div class="action-input" :class="{'has-active-cmd': selectedCmd !== null}">
<div class="active-cmd tag" v-if="selectedCmd !== null">
{{ selectedCmd.title }}
</div>
<input
v-focus
class="input"
:class="{'is-loading': loading}"
v-model="query"
:placeholder="placeholder"
@keyup="search"
ref="searchInput"
@keydown.down.prevent="() => select(0, 0)"
@keyup.prevent.delete="unselectCmd"
@keyup.prevent.enter="doCmd"
@keyup.prevent.esc="closeQuickActions"
/>
</div>
<div class="help has-text-grey-light p-2" v-if="hintText !== '' && !isNewTaskCommand">
{{ hintText }}
</div>
<quick-add-magic class="p-2 modal-container-smaller" v-if="isNewTaskCommand"/>
<div class="results" v-if="selectedCmd === null">
<div v-for="(r, k) in results" :key="k" class="result">
<span class="result-title">
{{ r.title }}
</span>
<div class="result-items">
<button
v-for="(i, key) in r.items"
:key="key"
:ref="`result-${k}_${key}`"
@keydown.up.prevent="() => select(k, key - 1)"
@keydown.down.prevent="() => select(k, key + 1)"
@click.prevent.stop="() => doAction(r.type, i)"
@keyup.prevent.enter="() => doAction(r.type, i)"
@keyup.prevent.esc="() => $refs.searchInput.focus()"
:class="{'is-strikethrough': i.done}"
>
{{ i.title }}
</button>
</div>
</div>
</div>
</div>
</modal>
</template>
<script>
import TaskService from '@/services/task'
import TeamService from '@/services/team'
import NamespaceModel from '@/models/namespace'
import TeamModel from '@/models/team'
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import ListModel from '@/models/list'
import createTask from '@/components/tasks/mixins/createTask'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import {getHistory} from '../../modules/listHistory'
const TYPE_LIST = 'list'
const TYPE_TASK = 'task'
const TYPE_CMD = 'cmd'
const TYPE_TEAM = 'team'
const CMD_NEW_TASK = 'newTask'
const CMD_NEW_LIST = 'newList'
const CMD_NEW_NAMESPACE = 'newNamespace'
const CMD_NEW_TEAM = 'newTeam'
const SEARCH_MODE_ALL = 'all'
const SEARCH_MODE_TASKS = 'tasks'
const SEARCH_MODE_LISTS = 'lists'
const SEARCH_MODE_TEAMS = 'teams'
export default {
name: 'quick-actions',
components: {QuickAddMagic},
data() {
return {
query: '',
selectedCmd: null,
foundTasks: [],
taskSearchTimeout: null,
taskService: new TaskService(),
foundTeams: [],
teamSearchTimeout: null,
teamService: new TeamService(),
}
},
mixins: [
createTask,
],
computed: {
active() {
const active = this.$store.state[QUICK_ACTIONS_ACTIVE]
if (!active) {
this.reset()
}
return active
},
results() {
let lists = []
if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) {
let query = this.query
if (this.searchMode === SEARCH_MODE_LISTS) {
query = query.substr(1)
}
const ncache = {}
const history = getHistory()
// Puts recently visited lists at the top
const allLists = [...new Set([
...history.map(l => {
return this.$store.getters['lists/getListById'](l.id)
}),
...Object.values(this.$store.state.lists)])]
lists = (allLists.filter(l => {
if (typeof l === 'undefined' || l === null) {
return false
}
if (l.isArchived) {
return false
}
if (typeof ncache[l.namespaceId] === 'undefined') {
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId)
}
if (ncache[l.namespaceId].isArchived) {
return false
}
return l.title.toLowerCase().includes(query.toLowerCase())
}) ?? [])
}
const cmds = this.availableCmds
.filter(a => a.title.toLowerCase().includes(this.query.toLowerCase()))
return [
{
type: TYPE_CMD,
title: this.$t('quickActions.commands'),
items: cmds,
},
{
type: TYPE_TASK,
title: this.$t('quickActions.tasks'),
items: this.foundTasks,
},
{
type: TYPE_LIST,
title: this.$t('quickActions.lists'),
items: lists,
},
{
type: TYPE_TEAM,
title: this.$t('quickActions.teams'),
items: this.foundTeams,
},
].filter(i => i.items.length > 0)
},
nothing() {
return this.search === '' || Object.keys(this.results).length === 0
},
loading() {
return this.taskService.loading ||
(this.$store.state[LOADING] && this.$store.state[LOADING_MODULE] === 'namespaces') ||
(this.$store.state[LOADING] && this.$store.state[LOADING_MODULE] === 'lists') ||
this.teamService.loading
},
placeholder() {
if (this.selectedCmd !== null) {
switch (this.selectedCmd.action) {
case CMD_NEW_TASK:
return this.$t('quickActions.newTask')
case CMD_NEW_LIST:
return this.$t('quickActions.newList')
case CMD_NEW_NAMESPACE:
return this.$t('quickActions.newNamespace')
case CMD_NEW_TEAM:
return this.$t('quickActions.newTeam')
}
}
return this.$t('quickActions.placeholder')
},
hintText() {
let namespace
if (this.selectedCmd !== null && this.currentList !== null) {
switch (this.selectedCmd.action) {
case CMD_NEW_TASK:
return this.$t('quickActions.createTask', {title: this.currentList.title})
case CMD_NEW_LIST:
namespace = this.$store.getters['namespaces/getNamespaceById'](this.currentList.namespaceId)
return this.$t('quickActions.createList', {title: namespace.title})
}
}
return this.$t('quickActions.hint')
},
currentList() {
return Object.keys(this.$store.state[CURRENT_LIST]).length === 0 ? null : this.$store.state[CURRENT_LIST]
},
availableCmds() {
const cmds = []
if (this.currentList !== null) {
cmds.push({
title: this.$t('quickActions.cmds.newTask'),
action: CMD_NEW_TASK,
})
cmds.push({
title: this.$t('quickActions.cmds.newList'),
action: CMD_NEW_LIST,
})
}
cmds.push({
title: this.$t('quickActions.cmds.newNamespace'),
action: CMD_NEW_NAMESPACE,
})
cmds.push({
title: this.$t('quickActions.cmds.newTeam'),
action: CMD_NEW_TEAM,
})
return cmds
},
searchMode() {
if (this.query === '') {
return SEARCH_MODE_ALL
}
if (this.query.startsWith('#')) {
return SEARCH_MODE_TASKS
}
if (this.query.startsWith('*')) {
return SEARCH_MODE_LISTS
}
if (this.query.startsWith('@')) {
return SEARCH_MODE_TEAMS
}
return SEARCH_MODE_ALL
},
isNewTaskCommand() {
return this.selectedCmd !== null && this.selectedCmd.action === CMD_NEW_TASK
},
},
methods: {
search() {
this.searchTasks()
this.searchTeams()
},
searchTasks() {
if (this.searchMode !== SEARCH_MODE_ALL && this.searchMode !== SEARCH_MODE_TASKS) {
this.foundTasks = []
return
}
let query = this.query
if (this.searchMode === SEARCH_MODE_TASKS) {
query = query.substr(1)
}
if (query === '' || this.selectedCmd !== null) {
return
}
if (this.taskSearchTimeout !== null) {
clearTimeout(this.taskSearchTimeout)
this.taskSearchTimeout = null
}
this.taskSearchTimeout = setTimeout(() => {
this.taskService.getAll({}, {s: query})
.then(r => {
r = r.map(t => {
t.type = TYPE_TASK
const list = this.$store.getters['lists/getListById'](t.listId) === null ? null : this.$store.getters['lists/getListById'](t.listId)
if (list !== null) {
t.title = `${t.title} (${list.title})`
}
return t
})
this.$set(this, 'foundTasks', r)
})
}, 150)
},
searchTeams() {
if (this.searchMode !== SEARCH_MODE_ALL && this.searchMode !== SEARCH_MODE_TEAMS) {
this.foundTeams = []
return
}
let query = this.query
if (this.searchMode === SEARCH_MODE_TEAMS) {
query = query.substr(1)
}
if (query === '' || this.selectedCmd !== null) {
return
}
if (this.teamSearchTimeout !== null) {
clearTimeout(this.teamSearchTimeout)
this.teamSearchTimeout = null
}
this.teamSearchTimeout = setTimeout(() => {
this.teamService.getAll({}, {s: query})
.then(r => {
r = r.map(t => {
t.title = t.name
return t
})
this.$set(this, 'foundTeams', r)
})
}, 150)
},
closeQuickActions() {
this.$store.commit(QUICK_ACTIONS_ACTIVE, false)
},
doAction(type, item) {
switch (type) {
case TYPE_LIST:
this.$router.push({name: 'list.index', params: {listId: item.id}})
this.closeQuickActions()
break
case TYPE_TASK:
this.$router.push({name: 'task.detail', params: {id: item.id}})
this.closeQuickActions()
break
case TYPE_CMD:
this.query = ''
this.selectedCmd = item
this.$refs.searchInput.focus()
break
}
},
doCmd() {
if (this.results.length === 1 && this.results[0].items.length === 1) {
this.doAction(this.results[0].type, this.results[0].items[0])
return
}
if (this.selectedCmd === null) {
return
}
if (this.query === '') {
return
}
switch (this.selectedCmd.action) {
case CMD_NEW_TASK:
this.newTask()
break
case CMD_NEW_LIST:
this.newList()
break
case CMD_NEW_NAMESPACE:
this.newNamespace()
break
case CMD_NEW_TEAM:
this.newTeam()
break
}
},
newTask() {
if (this.currentList === null) {
return
}
this.createNewTask(this.query, 0, this.currentList.id)
.then(r => {
this.$message.success({message: this.$t('task.createSuccess')})
this.$router.push({name: 'task.detail', params: {id: r.id}})
this.closeQuickActions()
})
.catch((e) => {
this.$message.error(e)
})
},
newList() {
if (this.currentList === null) {
return
}
const newList = new ListModel({
title: this.query,
namespaceId: this.currentList.namespaceId,
})
this.$store.dispatch('lists/createList', newList)
.then(r => {
this.$message.success({message: this.$t('list.create.createdSuccess')})
this.$router.push({name: 'list.index', params: {listId: r.id}})
this.closeQuickActions()
})
.catch((e) => {
this.$message.error(e)
})
},
newNamespace() {
const newNamespace = new NamespaceModel({title: this.query})
this.$store.dispatch('namespaces/createNamespace', newNamespace)
.then(() => {
this.$message.success({message: this.$t('namespace.create.success')})
this.closeQuickActions()
})
.catch((e) => {
this.$message.error(e)
})
},
newTeam() {
const newTeam = new TeamModel({name: this.query})
this.teamService.create(newTeam)
.then(r => {
this.$router.push({
name: 'teams.edit',
params: {id: r.id},
})
this.$message.success({message: this.$t('team.create.success')})
this.closeQuickActions()
})
.catch((e) => {
this.$message.error(e)
})
},
select(parentIndex, index) {
if (index < 0 && parentIndex === 0) {
this.$refs.searchInput.focus()
return
}
if (index < 0) {
parentIndex--
index = this.results[parentIndex].items.length - 1
}
let elems = this.$refs[`result-${parentIndex}_${index}`]
if (this.results[parentIndex].items.length === index) {
elems = this.$refs[`result-${parentIndex + 1}_0`]
}
if (typeof elems === 'undefined' || elems.length === 0) {
return
}
if (Array.isArray(elems)) {
elems[0].focus()
return
}
elems.focus()
},
unselectCmd() {
if (this.query !== '') {
return
}
this.selectedCmd = null
},
reset() {
this.query = ''
this.selectedCmd = null
},
},
}
</script>
<style lang="scss" scoped>
.quick-actions {
// FIXME: changed position should be an option of the modal
::v-deep.modal-content {
top: 3rem;
transform: translate(-50%, 0);
}
}
// HACK:
// FIXME:
.modal-container-smaller ::v-deep.hint-modal .modal-container {
height: calc(100vh - 5rem);
}
</style>

View File

@ -1,72 +1,76 @@
<template>
<div>
<p class="has-text-weight-bold">
Share Links
{{ $t('list.share.links.title') }}
<span
class="is-size-7"
v-tooltip="'Share Links allow you to easily share a list with other users who don\'t have an account on Vikunja.'">
What is a share link?
v-tooltip="$t('list.share.links.explanation')">
{{ $t('list.share.links.what') }}
</span>
</p>
<div class="sharables-list">
<x-button
v-if="!(linkShares.length === 0 || showNewForm)"
@click="showNewForm = true"
icon="plus"
class="mb-4">
Create a new link share
{{ $t('list.share.links.create') }}
</x-button>
<div class="p-4" v-if="linkShares.length === 0 || showNewForm">
<div class="field">
<label class="label" for="linkShareRight">
Right
{{ $t('list.share.right.title') }}
</label>
<div class="control">
<div class="select">
<select v-model="selectedRight" id="linkShareRight">
<option :value="rights.READ">Read only</option>
<option :value="rights.READ_WRITE">
Read & write
<option :value="rights.READ">
{{ $t('list.share.right.read') }}
</option>
<option :value="rights.READ_WRITE">
{{ $t('list.share.right.readWrite') }}
</option>
<option :value="rights.ADMIN">
{{ $t('list.share.right.admin') }}
</option>
<option :value="rights.ADMIN">Admin</option>
</select>
</div>
</div>
</div>
<div class="field">
<label class="label" for="linkShareName">
Name (optional)
{{ $t('list.share.links.name') }}
</label>
<div class="control">
<input
id="linkShareName"
class="input"
placeholder="e.g. Lorem Ipsum"
v-tooltip="'All actions done by this link share will show up with the name.'"
:placeholder="$t('list.share.links.namePlaceholder')"
v-tooltip="$t('list.share.links.nameExplanation')"
v-model="name"
/>
</div>
</div>
<div class="field">
<label class="label" for="linkSharePassword">
Password (optional)
{{ $t('list.share.links.password') }}
</label>
<div class="control">
<input
id="linkSharePassword"
type="password"
class="input"
placeholder="e.g. ••••••••••••"
v-tooltip="'When authenticating, the user will be required to enter this password.'"
:placeholder="$t('user.auth.passwordPlaceholder')"
v-tooltip="$t('list.share.links.passwordExplanation')"
v-model="password"
/>
</div>
</div>
<x-button @click="add" icon="plus">Share</x-button>
<x-button @click="add" icon="plus">
{{ $t('list.share.share') }}
</x-button>
</div>
<table
@ -75,11 +79,11 @@
>
<thead>
<tr>
<th>Link</th>
<th>Name</th>
<th>Shared&nbsp;by</th>
<th>Right</th>
<th>Delete</th>
<th>{{ $t('list.share.attributes.link') }}</th>
<th>{{ $t('list.share.attributes.name') }}</th>
<th>{{ $t('list.share.attributes.sharedBy') }}</th>
<th>{{ $t('list.share.attributes.right') }}</th>
<th>{{ $t('list.share.attributes.delete') }}</th>
</tr>
</thead>
<tbody>
@ -98,7 +102,7 @@
<x-button
@click="copy(getShareLink(s.hash))"
:shadow="false"
v-tooltip="'Copy to clipboard'"
v-tooltip="$t('misc.copy')"
>
<span class="icon">
<icon icon="paste"/>
@ -111,7 +115,7 @@
<template v-if="s.name !== ''">
{{ s.name }}
</template>
<i v-else>No name set</i>
<i v-else>{{ $t('list.share.links.noName') }}</i>
</td>
<td>
{{ s.sharedBy.getDisplayName() }}
@ -121,19 +125,19 @@
<span class="icon is-small">
<icon icon="lock"/>
</span>&nbsp;
Admin
{{ $t('list.share.right.admin') }}
</template>
<template v-else-if="s.right === rights.READ_WRITE">
<span class="icon is-small">
<icon icon="pen"/>
</span>&nbsp;
Write
{{ $t('list.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users"/>
</span>&nbsp;
Read-only
{{ $t('list.share.right.read') }}
</template>
</td>
<td class="actions">
@ -159,20 +163,20 @@
@submit="remove()"
v-if="showDeleteModal"
>
<span slot="header">Remove a link share</span>
<p slot="text">
Are you sure you want to remove this link share?<br/>
It will no longer be possible to access this list with this link
share.<br/>
<b>This CANNOT BE UNDONE!</b>
</p>
<template #header>
<span>{{ $t('list.share.links.remove') }}</span>
</template>
<template #text>
<p>{{ $t('list.share.links.removeText') }}</p>
</template>
</modal>
</transition>
</div>
</template>
<script>
import rights from '../../models/rights'
import rights from '../../models/constants/rights'
import LinkShareService from '../../services/linkShare'
import LinkShareModel from '../../models/linkShare'
@ -191,8 +195,7 @@ export default {
data() {
return {
linkShares: [],
linkShareService: LinkShareService,
newLinkShare: LinkShareModel,
linkShareService: new LinkShareService(),
rights: rights,
selectedRight: rights.READ,
name: '',
@ -202,17 +205,10 @@ export default {
showNewForm: false,
}
},
beforeMount() {
this.linkShareService = new LinkShareService()
},
created() {
this.linkShareService = new LinkShareService()
this.load()
},
watch: {
listId() {
// watch it
this.load()
listId: {
handler: 'load',
immediate: true,
},
},
computed: mapState({
@ -231,7 +227,7 @@ export default {
this.linkShares = r
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
add() {
@ -248,14 +244,11 @@ export default {
this.name = ''
this.password = ''
this.showNewForm = false
this.success(
{message: 'The link share was successfully created'},
this
)
this.$message.success({message: this.$t('list.share.links.createSuccess')})
this.load()
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
remove() {
@ -266,22 +259,17 @@ export default {
this.linkShareService
.delete(linkshare)
.then(() => {
this.success(
{message: 'The link share was successfully deleted'},
this
)
this.$message.success({message: this.$t('list.share.links.deleteSuccess')})
this.load()
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
.finally(() => {
this.showDeleteModal = false
})
},
copy(text) {
copy(text)
},
copy,
getShareLink(hash) {
return this.frontendUrl + 'share/' + hash + '/auth'
},

View File

@ -1,6 +1,8 @@
<template>
<div>
<p class="has-text-weight-bold">Shared with these {{ shareType }}s</p>
<p class="has-text-weight-bold">
{{ $t('list.share.userTeam.shared', {type: shareTypeNames}) }}
</p>
<div v-if="userIsAdmin">
<div class="field has-addons">
<p
@ -9,7 +11,7 @@
>
<multiselect
:loading="searchService.loading"
placeholder="Type to search..."
:placeholder="$t('misc.searchPlaceholder')"
@search="find"
:search-results="found"
:label="searchLabel"
@ -17,7 +19,7 @@
/>
</p>
<p class="control">
<x-button @click="add()"> Share</x-button>
<x-button @click="add()">{{ $t('list.share.share') }}</x-button>
</p>
</div>
</div>
@ -29,7 +31,7 @@
<td>{{ s.getDisplayName() }}</td>
<td>
<template v-if="s.id === userInfo.id">
<b class="is-success">You</b>
<b class="is-success">{{ $t('list.share.userTeam.you') }}</b>
</template>
</td>
</template>
@ -50,19 +52,19 @@
<span class="icon is-small">
<icon icon="lock"/>
</span>
Admin
{{ $t('list.share.right.admin') }}
</template>
<template v-else-if="s.right === rights.READ_WRITE">
<span class="icon is-small">
<icon icon="pen"/>
</span>
Write
{{ $t('list.share.right.readWrite') }}
</template>
<template v-else>
<span class="icon is-small">
<icon icon="users"/>
</span>
Read-only
{{ $t('list.share.right.read') }}
</template>
</td>
<td class="actions" v-if="userIsAdmin">
@ -76,19 +78,19 @@
:selected="s.right === rights.READ"
:value="rights.READ"
>
Read only
{{ $t('list.share.right.read') }}
</option>
<option
:selected="s.right === rights.READ_WRITE"
:value="rights.READ_WRITE"
>
Read & write
{{ $t('list.share.right.readWrite') }}
</option>
<option
:selected="s.right === rights.ADMIN"
:value="rights.ADMIN"
>
Admin
{{ $t('list.share.right.admin') }}
</option>
</select>
</div>
@ -108,7 +110,7 @@
</table>
<nothing v-else>
Not shared with any {{ shareType }} yet.
{{ $t('list.share.userTeam.notShared', {type: shareTypeNames}) }}
</nothing>
<transition name="modal">
@ -117,22 +119,18 @@
@submit="deleteSharable()"
v-if="showDeleteModal"
>
<span slot="header"
>Remove a {{ shareType }} from the {{ typeString }}</span
>
<p slot="text">
Are you sure you want to remove this {{ shareType }} from the
{{ typeString }}?<br/>
<b>This CANNOT BE UNDONE!</b>
</p>
<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>
</div>
</template>
<script>
import {mapState} from 'vuex'
import UserNamespaceService from '../../services/userNamespace'
import UserNamespaceModel from '../../models/userNamespace'
import UserListModel from '../../models/userList'
@ -147,9 +145,9 @@ import TeamListService from '../../services/teamList'
import TeamService from '../../services/team'
import TeamModel from '../../models/team'
import rights from '../../models/rights'
import Multiselect from '@/components/input/multiselect'
import Nothing from '@/components/misc/nothing'
import rights from '../../models/constants/rights.json'
import Multiselect from '@/components/input/multiselect.vue'
import Nothing from '@/components/misc/nothing.vue'
export default {
name: 'userTeamShare',
@ -192,9 +190,44 @@ export default {
Nothing,
Multiselect,
},
computed: mapState({
userInfo: (state) => state.auth.info,
}),
computed: {
userInfo() {
return this.$store.state.auth.info
},
shareTypeNames() {
if (this.shareType === 'user') {
return this.$tc('list.share.userTeam.typeUser', 2)
}
if (this.shareType === 'team') {
return this.$tc('list.share.userTeam.typeTeam', 2)
}
return ''
},
shareTypeName() {
if (this.shareType === 'user') {
return this.$tc('list.share.userTeam.typeUser', 1)
}
if (this.shareType === 'team') {
return this.$tc('list.share.userTeam.typeTeam', 1)
}
return ''
},
sharableName() {
if (this.type === 'list') {
return this.$t('list.list.title')
}
if (this.shareType === 'namespace') {
return this.$t('namespace.namespace')
}
return ''
},
},
created() {
if (this.shareType === 'user') {
this.searchService = new UserService()
@ -202,11 +235,11 @@ export default {
this.searchLabel = 'username'
if (this.type === 'list') {
this.typeString = `list`
this.typeString = 'list'
this.stuffService = new UserListService()
this.stuffModel = new UserListModel({listId: this.id})
} else if (this.type === 'namespace') {
this.typeString = `namespace`
this.typeString = 'namespace'
this.stuffService = new UserNamespaceService()
this.stuffModel = new UserNamespaceModel({
namespaceId: this.id,
@ -220,11 +253,11 @@ export default {
this.searchLabel = 'name'
if (this.type === 'list') {
this.typeString = `list`
this.typeString = 'list'
this.stuffService = new TeamListService()
this.stuffModel = new TeamListModel({listId: this.id})
} else if (this.type === 'namespace') {
this.typeString = `namespace`
this.typeString = 'namespace'
this.stuffService = new TeamNamespaceService()
this.stuffModel = new TeamNamespaceModel({
namespaceId: this.id,
@ -245,11 +278,11 @@ export default {
.then((r) => {
this.$set(this, 'sharables', r)
r.forEach((s) =>
this.$set(this.selectedRight, s.id, s.right)
this.$set(this.selectedRight, s.id, s.right),
)
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
deleteSharable() {
@ -258,34 +291,23 @@ export default {
} else if (this.shareType === 'team') {
this.stuffModel.teamId = this.sharable.id
}
this.stuffService
.delete(this.stuffModel)
.then(() => {
this.showDeleteModal = false
for (const i in this.sharables) {
if (
(this.sharables[i].id === this.stuffModel.userId &&
this.shareType === 'user') ||
(this.sharables[i].id === this.stuffModel.teamId &&
this.shareType === 'team')
(this.sharables[i].username === this.stuffModel.userId && this.shareType === 'user') ||
(this.sharables[i].id === this.stuffModel.teamId && this.shareType === 'team')
) {
this.sharables.splice(i, 1)
}
}
this.success(
{
message:
'The ' +
this.shareType +
' was successfully deleted from the ' +
this.typeString +
'.',
},
this
)
this.$message.success({message: this.$t('list.share.userTeam.removeSuccess', {type: this.shareTypeName, sharable: this.sharableName})})
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
add(admin) {
@ -306,19 +328,11 @@ export default {
this.stuffService
.create(this.stuffModel)
.then(() => {
this.success(
{
message:
'The ' +
this.shareType +
' was successfully added.',
},
this
)
this.$message.success({message: this.$t('list.share.userTeam.addedSuccess', {type: this.shareTypeName})})
this.load()
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
toggleType(sharable) {
@ -351,18 +365,10 @@ export default {
this.$set(this.sharables[i], 'right', r.right)
}
}
this.success(
{
message:
'The ' +
this.shareType +
' right was successfully updated.',
},
this
)
this.$message.success({message: this.$t('list.share.userTeam.updatedSuccess', {type: this.shareTypeName})})
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
find(query) {
@ -377,7 +383,7 @@ export default {
this.$set(this, 'found', response)
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
clearAll() {

View File

@ -0,0 +1,148 @@
<template>
<div class="task-add">
<div class="field is-grouped">
<p class="control has-icons-left is-expanded">
<textarea
:disabled="taskService.loading || null"
class="input"
:placeholder="$t('list.list.addPlaceholder')"
type="text"
v-focus
v-model="newTaskTitle"
ref="newTaskInput"
:style="{'height': `${textAreaHeight}px`}"
@keyup="errorMessage = ''"
@keydown.enter="handleEnter"
/>
<span class="icon is-small is-left">
<icon icon="tasks"/>
</span>
</p>
<p class="control">
<x-button
:disabled="newTaskTitle === '' || taskService.loading || null"
@click="addTask()"
icon="plus"
:loading="taskService.loading"
>
{{ $t('list.list.add') }}
</x-button>
</p>
</div>
<p class="help is-danger" v-if="errorMessage !== ''">
{{ errorMessage }}
</p>
<quick-add-magic v-if="errorMessage === ''"/>
</div>
</template>
<script>
import TaskService from '../../services/task'
import createTask from '@/components/tasks/mixins/createTask'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
const INITIAL_SCROLL_HEIGHT = 40
const cleanupTitle = title => {
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '')
}
export default {
name: 'add-task',
data() {
return {
newTaskTitle: '',
taskService: new TaskService(),
errorMessage: '',
textAreaHeight: INITIAL_SCROLL_HEIGHT,
}
},
mixins: [
createTask,
],
components: {
QuickAddMagic,
},
props: {
defaultPosition: {
type: Number,
required: false,
},
},
watch: {
newTaskTitle(newVal) {
let scrollHeight = this.$refs.newTaskInput.scrollHeight
if (scrollHeight < INITIAL_SCROLL_HEIGHT || newVal === '') {
scrollHeight = INITIAL_SCROLL_HEIGHT
}
this.textAreaHeight = scrollHeight
},
},
methods: {
addTask() {
if (this.newTaskTitle === '') {
this.errorMessage = this.$t('list.create.addTitleRequired')
return
}
this.errorMessage = ''
if (this.taskService.loading) {
return
}
const newTasks = []
this.newTaskTitle.split(/[\r\n]+/).forEach(t => {
const title = cleanupTitle(t)
if (title === '') {
return
}
newTasks.push(
this.createNewTask(title, 0, this.$store.state.auth.settings.defaultListId, this.defaultPosition)
.then(task => {
this.$emit('taskAdded', task)
return task
}),
)
})
Promise.all(newTasks)
.then(() => {
this.newTaskTitle = ''
})
.catch(e => {
if (e === 'NO_LIST') {
this.errorMessage = this.$t('list.create.addListRequired')
return
}
this.$message.error(e)
})
},
handleEnter(e) {
// when pressing shift + enter we want to continue as we normally would. Otherwise, we want to create
// the new task(s). The vue event modifier don't allow this, hence this method.
if (e.shiftKey) {
return
}
e.preventDefault()
this.addTask()
},
},
}
</script>
<style lang="scss" scoped>
.task-add {
margin-bottom: 0;
.button {
height: 2.5rem;
}
}
.input, .textarea {
transition: border-color $transition;
}
</style>

View File

@ -1,15 +1,14 @@
<template>
<form @submit.prevent="editTaskSubmit()">
<div class="field">
<label class="label" for="tasktext">Title</label>
<label class="label" for="tasktext">{{ $t('task.attributes.title') }}</label>
<div class="control">
<input
:class="{ disabled: taskService.loading }"
:disabled="taskService.loading"
:disabled="taskService.loading || null"
@change="editTaskSubmit()"
class="input"
id="tasktext"
placeholder="The task text is here..."
type="text"
v-focus
v-model="taskEditTask.title"
@ -17,26 +16,26 @@
</div>
</div>
<div class="field">
<label class="label" for="taskdescription">Description</label>
<label class="label" for="taskdescription">{{ $t('task.attributes.description') }}</label>
<div class="control">
<editor
:preview-is-default="false"
id="taskdescription"
placeholder="The tasks description goes here..."
:placeholder="$t('task.description.placeholder')"
v-if="editorActive"
v-model="taskEditTask.description"
/>
</div>
</div>
<strong>Reminders</strong>
<strong>{{ $t('task.attributes.reminders') }}</strong>
<reminders
@change="editTaskSubmit()"
v-model="taskEditTask.reminderDates"
/>
<div class="field">
<label class="label">Labels</label>
<label class="label">{{ $t('task.attributes.labels') }}</label>
<div class="control">
<edit-labels
:task-id="taskEditTask.id"
@ -46,7 +45,7 @@
</div>
<div class="field">
<label class="label">Color</label>
<label class="label">{{ $t('task.attributes.color') }}</label>
<div class="control">
<color-picker v-model="taskEditTask.hexColor" />
</div>
@ -57,14 +56,14 @@
class="is-fullwidth"
@click="editTaskSubmit()"
>
Save
{{ $t('misc.save') }}
</x-button>
<router-link
class="mt-2 has-text-centered is-block"
:to="{name: 'task.detail', params: {id: taskEditTask.id}}"
>
Open task detail view
{{ $t('task.openDetail') }}
</router-link>
</form>
</template>
@ -73,7 +72,7 @@
import ListService from '../../services/list'
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import priorities from '../../models/priorities'
import priorities from '../../models/constants/priorities'
import EditLabels from './partials/editLabels'
import Reminders from './partials/reminders'
import ColorPicker from '../input/colorPicker'
@ -85,23 +84,15 @@ export default {
data() {
return {
listId: this.$route.params.id,
listService: ListService,
taskService: TaskService,
listService: new ListService(),
taskService: new TaskService(),
priorities: priorities,
list: {},
editorActive: false,
newTask: TaskModel,
newTask: new TaskModel(),
isTaskEdit: false,
taskEditTask: TaskModel,
flatPickerConfig: {
altFormat: 'j M Y H:i',
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
onOpen: this.updateLastReminderDate,
onClose: this.addReminderDate,
},
}
},
components: {
@ -109,7 +100,7 @@ export default {
Reminders,
EditLabels,
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../components/input/editor'),
component: import('../../components/input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
@ -122,18 +113,14 @@ export default {
},
},
watch: {
task() {
this.taskEditTask = this.task
this.initTaskFields()
task: {
handler() {
this.taskEditTask = this.task
this.initTaskFields()
},
immediate: true,
},
},
created() {
this.listService = new ListService()
this.taskService = new TaskService()
this.newTask = new TaskModel()
this.taskEditTask = this.task
this.initTaskFields()
},
methods: {
initTaskFields() {
this.taskEditTask.dueDate =
@ -158,10 +145,10 @@ export default {
.then((r) => {
this.$set(this, 'taskEditTask', r)
this.initTaskFields()
this.success({message: 'The task has been saved successfully.'}, this)
this.$message.success({message: this.$t('task.detail.updateSuccess')})
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
},

View File

@ -7,11 +7,11 @@
type="secondary"
icon="filter"
>
Filters
{{ $t('filters.title') }}
</x-button>
</div>
<filter-popup
@change="loadTasks"
@change="loadTasks()"
:visible="showTaskFilter"
v-model="params"
/>
@ -24,18 +24,10 @@
class="month"
v-for="(m, mk) in days[yk]"
>
{{
new Date(
new Date(yk).setMonth(mk)
).toLocaleString('en-us', {month: 'long'})
}},
{{ new Date(yk).getFullYear() }}
{{ formatYear(new Date(`${yk}-${parseInt(mk) + 1}-01`)) }}
<div class="days">
<div
:class="{
today:
d.toDateString() === now.toDateString(),
}"
:class="{ today: d.toDateString() === now.toDateString() }"
:key="dk + 'day'"
:style="{ width: dayWidth + 'px' }"
class="day"
@ -108,8 +100,9 @@
'has-super-high-priority':
t.priority === priorities.DO_NOW,
}"
>{{ t.title }}</span
>
{{ t.title }}
</span>
<priority-label :priority="t.priority"/>
<!-- using the key here forces vue to use the updated version model and not the response returned by the api -->
<a @click="editTask(theTasks[k])" class="edit-toggle">
@ -148,7 +141,7 @@
@resizestop="resizeTask"
axis="x"
class="task nodate"
v-tooltip="'This task has no dates set.'"
v-tooltip="$t('list.gantt.noDates')"
>
<span>{{ t.title }}</span>
</VueDragResize>
@ -172,14 +165,14 @@
/>
</transition>
<x-button @click="showCreateNewTask" :shadow="false" icon="plus">
Add a new task
{{ $t('list.list.newTaskCta') }}
</x-button>
</form>
<transition name="fade">
<card
v-if="isTaskEdit"
class="taskedit"
title="Edit Task"
:title="$t('list.list.editTask')"
@close="() => {isTaskEdit = false;taskToEdit = null}"
:has-close="true"
>
@ -195,12 +188,13 @@ import EditTask from './edit-task'
import TaskService from '../../services/task'
import TaskModel from '../../models/task'
import priorities from '../../models/priorities'
import priorities from '../../models/constants/priorities'
import PriorityLabel from './partials/priorityLabel'
import TaskCollectionService from '../../services/taskCollection'
import {mapState} from 'vuex'
import Rights from '../../models/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup'
import Rights from '../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue'
import {format} from 'date-fns'
export default {
name: 'GanttChart',
@ -238,17 +232,17 @@ export default {
endDate: null,
theTasks: [], // Pretty much a copy of the prop, since we cant mutate the prop directly
tasksWithoutDates: [],
taskService: TaskService,
taskService: new TaskService(),
taskDragged: null, // Saves to currently dragged task to be able to update it
fullWidth: 0,
now: null,
now: new Date(),
dayOffsetUntilToday: 0,
isTaskEdit: false,
taskToEdit: null,
newTaskTitle: '',
newTaskFieldActive: false,
priorities: {},
taskCollectionService: TaskCollectionService,
priorities: priorities,
taskCollectionService: new TaskCollectionService(),
showTaskFilter: false,
params: {
@ -266,12 +260,6 @@ export default {
dateTo: 'buildTheGanttChart',
listId: 'parseTasks',
},
created() {
this.now = new Date()
this.taskCollectionService = new TaskCollectionService()
this.taskService = new TaskService()
this.priorities = priorities
},
mounted() {
this.buildTheGanttChart()
},
@ -287,12 +275,12 @@ export default {
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
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]]
let years = {}
for (
@ -304,15 +292,13 @@ export default {
if (years[date.getFullYear() + ''] === undefined) {
years[date.getFullYear() + ''] = {}
}
if (
years[date.getFullYear() + ''][date.getMonth() + ''] ===
undefined
) {
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.$set(this, 'days', years)
},
parseTasks() {
@ -362,7 +348,7 @@ export default {
})
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
addGantAttributes(t) {
@ -387,7 +373,7 @@ export default {
let startDate = new Date(this.startDate)
startDate.setDate(
startDate.getDate() + newRect.left / this.dayWidth
startDate.getDate() + newRect.left / this.dayWidth,
)
startDate.setUTCHours(0)
startDate.setUTCMinutes(0)
@ -396,7 +382,7 @@ export default {
this.taskDragged.startDate = startDate
let endDate = new Date(startDate)
endDate.setDate(
startDate.getDate() + newRect.width / this.dayWidth
startDate.getDate() + newRect.width / this.dayWidth,
)
this.taskDragged.startDate = startDate
this.taskDragged.endDate = endDate
@ -439,7 +425,7 @@ export default {
this.$set(
this.theTasks,
tt,
this.addGantAttributes(r)
this.addGantAttributes(r),
)
break
}
@ -447,7 +433,7 @@ export default {
}
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
editTask(task) {
@ -484,9 +470,12 @@ export default {
this.hideCrateNewTask()
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
formatYear(date) {
return format(date, 'MMMM, yyyy')
},
},
}
</script>

View File

@ -1,38 +0,0 @@
import AttachmentModel from '../../../models/attachment'
import AttachmentService from '../../../services/attachment'
export default {
methods: {
attachmentUpload(file, onSuccess) {
const files = [file]
const attachmentService = new AttachmentService()
this.createAttachment(attachmentService, files, onSuccess)
},
createAttachment(attachmentService, files, onSuccess = () => {}) {
const attachmentModel = new AttachmentModel({taskId: this.taskId})
attachmentService.create(attachmentModel, files)
.then(r => {
console.debug(`Uploaded attachments for task ${this.taskId}, response was`, r)
if (r.success !== null) {
r.success.forEach(a => {
this.$store.dispatch('tasks/addTaskAttachment', {
taskId: this.taskId,
attachment: a,
})
onSuccess(`${window.API_URL}/tasks/${this.taskId}/attachments/${a.id}`)
})
}
if (r.errors !== null) {
r.errors.forEach(m => {
this.error(m)
})
}
})
.catch(e => {
this.error(e, this)
})
},
},
}

View File

@ -0,0 +1,141 @@
import {parseTaskText} from '@/modules/parseTaskText'
import TaskModel from '@/models/task'
import {formatISO} from 'date-fns'
import LabelTask from '@/models/labelTask'
import LabelModel from '@/models/label'
import LabelTaskService from '@/services/labelTask'
import {mapState} from 'vuex'
import UserService from '@/services/user'
import TaskService from '@/services/task'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
export default {
data() {
return {
taskService: TaskService,
labelTaskService: LabelTaskService,
userService: UserService,
}
},
created() {
this.labelTaskService = new LabelTaskService()
this.userService = new UserService()
this.taskService = new TaskService()
},
computed: mapState({
labels: state => state.labels.labels,
}),
methods: {
createNewTask(newTaskTitle, bucketId = 0, lId = 0, position = 0) {
const parsedTask = parseTaskText(newTaskTitle, getQuickAddMagicMode())
const assignees = []
// Uses the following ways to get the list id of the new task:
// 1. If specified in quick add magic, look in store if it exists and use it if it does
// 2. Else check if a list was passed as parameter
// 3. Otherwise use the id from the route parameter
// 4. If none of the above worked, reject the promise with an error.
let listId = null
if (parsedTask.list !== null) {
const list = this.$store.getters['lists/findListByExactname'](parsedTask.list)
listId = list === null ? null : list.id
}
if (lId !== 0) {
listId = lId
}
if (typeof this.$route.params.listId !== 'undefined') {
listId = parseInt(this.$route.params.listId)
}
if (typeof listId === 'undefined' || listId === null) {
return Promise.reject('NO_LIST')
}
// Separate closure because we need to wait for the results of the user search if users were entered in the
// task create request. Because _that_ happens in a promise, we'll need something to call when it resolves.
const createTask = () => {
const task = new TaskModel({
title: parsedTask.text,
listId: listId,
dueDate: parsedTask.date !== null ? formatISO(parsedTask.date) : null, // I don't know why, but it all goes up in flames when I just pass in the date normally.
priority: parsedTask.priority,
assignees: assignees,
bucketId: bucketId,
position: position,
})
return this.taskService.create(task)
.then(task => {
if (parsedTask.labels.length > 0) {
const labelAddsToWaitFor = []
const addLabelToTask = label => {
const labelTask = new LabelTask({
taskId: task.id,
labelId: label.id,
})
return this.labelTaskService.create(labelTask)
.then(result => {
task.labels.push(label)
return Promise.resolve(result)
})
.catch(e => Promise.reject(e))
}
// Then do everything that is involved in finding, creating and adding the label to the task
parsedTask.labels.forEach(labelTitle => {
// Check if the label exists
const label = Object.values(this.labels).find(l => {
return l.title.toLowerCase() === labelTitle.toLowerCase()
})
// Label found, use it
if (typeof label !== 'undefined') {
labelAddsToWaitFor.push(addLabelToTask(label))
} else {
// label not found, create it
const label = new LabelModel({title: labelTitle})
labelAddsToWaitFor.push(this.$store.dispatch('labels/createLabel', label)
.then(res => {
return addLabelToTask(res)
})
.catch(e => Promise.reject(e)),
)
}
})
// This waits until all labels are created and added to the task
return Promise.all(labelAddsToWaitFor)
.then(() => {
return Promise.resolve(task)
})
}
return Promise.resolve(task)
})
.catch(e => Promise.reject(e))
}
if (parsedTask.assignees.length > 0) {
const searches = []
parsedTask.assignees.forEach(a => {
searches.push(this.userService.getAll({}, {s: a})
.then(users => {
const user = users.find(u => u.username.toLowerCase() === a.toLowerCase())
if (typeof user !== 'undefined') {
assignees.push(user)
}
return Promise.resolve(users)
}),
)
})
return Promise.all(searches)
.then(() => createTask())
}
return createTask()
},
},
}

View File

@ -1,5 +1,15 @@
import TaskCollectionService from '../../../services/taskCollection'
import {cloneDeep} from 'lodash'
import TaskCollectionService from '@/services/taskCollection'
import cloneDeep from 'lodash/cloneDeep'
// FIXME: merge with DEFAULT_PARAMS in filters.vue
const DEFAULT_PARAMS = {
sort_by: ['position', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
}
/**
* This mixin provides a base set of methods and properties to get tasks on a list.
@ -7,51 +17,41 @@ import {cloneDeep} from 'lodash'
export default {
data() {
return {
taskCollectionService: TaskCollectionService,
taskCollectionService: new TaskCollectionService(),
tasks: [],
pages: [],
currentPage: 0,
loadedList: null,
showTaskSearch: false,
searchTerm: '',
showTaskFilter: false,
params: {
sort_by: ['done', 'id'],
order_by: ['asc', 'desc'],
filter_by: ['done'],
filter_value: ['false'],
filter_comparator: ['equals'],
filter_concat: 'and',
},
params: DEFAULT_PARAMS,
}
},
watch: {
'$route.query': 'loadTasksForPage', // Only listen for query path changes
},
beforeMount() {
// Triggering loading the tasks in beforeMount lets the component maintain the current page, therefore the page
// is not lost after navigating back from a task detail page for example.
this.loadTasksForPage(this.$route.query)
},
created() {
this.taskCollectionService = new TaskCollectionService()
// Only listen for query path changes
'$route.query': {
handler: 'loadTasksForPage',
immediate: true,
},
'$route.path': 'loadTasksOnSavedFilter',
},
methods: {
loadTasks(
page,
search = '',
params = null,
forceLoading = false,
) {
// Because this function is triggered every time on topNavigation, we're putting a condition here to only load it when we actually want to show tasks
// FIXME: This is a bit hacky -> Cleanup.
if (
this.$route.name !== 'list.list' &&
this.$route.name !== 'list.table'
this.$route.name !== 'list.table' &&
!forceLoading
) {
return
}
@ -68,56 +68,28 @@ export default {
const currentList = {
id: list.listId,
params: params,
search: search,
page: page,
params,
search,
page,
}
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList)) {
if (JSON.stringify(currentList) === JSON.stringify(this.loadedList) && !forceLoading) {
return
}
this.$set(this, 'tasks', [])
this.tasks = []
this.taskCollectionService.getAll(list, params, page)
.then(r => {
this.$set(this, 'tasks', r)
this.$set(this, 'pages', [])
this.tasks = r
this.currentPage = page
for (let i = 0; i < this.taskCollectionService.totalPages; i++) {
// Show ellipsis instead of all pages
if (
i > 0 && // Always at least the first page
(i + 1) < this.taskCollectionService.totalPages && // And the last page
(
// And the current with current + 1 and current - 1
(i + 1) > this.currentPage + 1 ||
(i + 1) < this.currentPage - 1
)
) {
// Only add an ellipsis if the last page isn't already one
if (this.pages[i - 1] && !this.pages[i - 1].isEllipsis) {
this.pages.push({
number: 0,
isEllipsis: true,
})
}
continue
}
this.pages.push({
number: i + 1,
isEllipsis: false,
})
}
this.loadedList = cloneDeep(currentList)
})
.catch(e => {
this.error(e, this)
this.$message.error(e)
})
},
loadTasksForPage(e) {
// The page parameter can be undefined, in the case where the user loads a new list from the side bar menu
let page = Number(e.page)
@ -130,53 +102,9 @@ export default {
}
this.initTasks(page, search)
},
sortTasks() {
if (this.tasks === null || this.tasks === []) {
return
}
return this.tasks.sort(function (a, b) {
if (a.done < b.done)
return -1
if (a.done > b.done)
return 1
if (a.id > b.id)
return -1
if (a.id < b.id)
return 1
return 0
})
},
searchTasks() {
// Only search if the search term changed
if (this.$route.query === this.searchTerm) {
return
}
this.$router.push({
name: 'list.list',
query: {search: this.searchTerm},
})
},
hideSearchBar() {
// This is a workaround.
// When clicking on the search button, @blur from the input is fired. If we
// would then directly hide the whole search bar directly, no click event
// from the button gets fired. To prevent this, we wait 200ms until we hide
// everything so the button has a chance of firering the search event.
setTimeout(() => {
this.showTaskSearch = false
}, 200)
},
getRouteForPagination(page = 1, type = 'list') {
return {
name: 'list.' + type,
params: {
type: type,
},
query: {
page: page,
},
loadTasksOnSavedFilter() {
if(typeof this.$route.params.listId !== 'undefined' && parseInt(this.$route.params.listId) < 0) {
this.loadTasks(1, '', null, true)
}
},
},

View File

@ -4,11 +4,11 @@
<span class="icon is-grey">
<icon icon="paperclip"/>
</span>
Attachments
{{ $t('task.attachment.title') }}
</h3>
<input
:disabled="attachmentService.loading"
:disabled="attachmentService.loading || null"
@change="uploadNewAttachment()"
id="files"
multiple
@ -35,18 +35,16 @@
<div class="filename">{{ a.file.name }}</div>
<div class="info">
<p class="collapses">
<span>
created
<span v-tooltip="formatDate(a.created)">{{
formatDateSince(a.created)
}}</span>
by
<i18n path="task.attachment.createdBy">
<span v-tooltip="formatDate(a.created)">
{{ formatDateSince(a.created) }}
</span>
<user
:avatar-size="24"
:user="a.createdBy"
:is-inline="true"
/>
</span>
</i18n>
<span>
{{ a.file.getHumanSize() }}
</span>
@ -57,16 +55,22 @@
<p>
<a
@click.prevent.stop="downloadAttachment(a)"
v-tooltip="'Download this attachment'"
v-tooltip="$t('task.attachment.downloadTooltip')"
>
Download
{{ $t('misc.download') }}
</a>
<a
@click.stop="copyUrl(a)"
v-tooltip="$t('task.attachment.copyUrlTooltip')"
>
{{ $t('task.attachment.copyUrl') }}
</a>
<a
@click.prevent.stop="() => {attachmentToDelete = a; showDeleteModal = true}"
v-if="editEnabled"
v-tooltip="'Delete this attachment'"
v-tooltip="$t('task.attachment.deleteTooltip')"
>
Delete
{{ $t('misc.delete') }}
</a>
</p>
</div>
@ -82,7 +86,7 @@
type="secondary"
:shadow="false"
>
Upload attachment
{{ $t('task.attachment.upload') }}
</x-button>
<!-- Dropzone -->
@ -95,7 +99,7 @@
<div class="icon">
<icon icon="cloud-upload-alt"/>
</div>
<div class="hint">Drop files here to upload</div>
<div class="hint">{{ $t('task.attachment.drop') }}</div>
</div>
</div>
@ -106,12 +110,12 @@
v-if="showDeleteModal"
@submit="deleteAttachment()"
>
<span slot="header">Delete attachment</span>
<p slot="text">
Are you sure you want to delete the attachment
{{ attachmentToDelete.file.name }}?<br/>
<b>This CANNOT BE UNDONE!</b>
</p>
<template #header><span>{{ $t('task.attachment.delete') }}</span></template>
<template #text>
<p>{{ $t('task.attachment.deleteText1', {filename: attachmentToDelete.file.name}) }}<br/>
<strong>{{ $t('task.attachment.deleteText2') }}</strong></p>
</template>
</modal>
</transition>
@ -135,20 +139,19 @@
import AttachmentService from '../../../services/attachment'
import AttachmentModel from '../../../models/attachment'
import User from '../../misc/user'
import attachmentUpload from '@/components/tasks/mixins/attachmentUpload'
import {mapState} from 'vuex'
import copy from 'copy-to-clipboard'
import { uploadFiles, generateAttachmentUrl } from '@/helpers/attachments'
export default {
name: 'attachments',
components: {
User,
},
mixins: [
attachmentUpload,
],
data() {
return {
attachmentService: AttachmentService,
attachmentService: new AttachmentService(),
showDropzone: false,
showDeleteModal: false,
@ -170,9 +173,6 @@ export default {
default: true,
},
},
created() {
this.attachmentService = new AttachmentService()
},
computed: mapState({
attachments: (state) => state.attachments.attachments,
}),
@ -216,7 +216,7 @@ export default {
this.uploadFiles(this.$refs.files.files)
},
uploadFiles(files) {
this.createAttachment(this.attachmentService, files)
uploadFiles(this.attachmentService, this.taskId, files)
},
deleteAttachment() {
this.attachmentService
@ -224,12 +224,12 @@ export default {
.then((r) => {
this.$store.commit(
'attachments/removeById',
this.attachmentToDelete.id
this.attachmentToDelete.id,
)
this.success(r, this)
this.$message.success(r)
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
.finally(() => {
this.showDeleteModal = false
@ -250,6 +250,9 @@ export default {
this.downloadAttachment(attachment)
}
},
copyUrl(attachment) {
copy(generateAttachmentUrl(this.taskId, attachment.id))
},
},
}
</script>

View File

@ -0,0 +1,60 @@
<template>
<span v-if="checklist.total > 0" class="checklist-summary">
<svg width="12" height="12">
<circle stroke-width="2" fill="transparent" cx="50%" cy="50%" r="5"></circle>
<circle stroke-width="2" stroke-dasharray="31" :stroke-dashoffset="checklistCircleDone"
stroke-linecap="round" fill="transparent" cx="50%" cy="50%" r="5"></circle>
</svg>
<span>
{{ $t(checklist.total === checklist.checked ? 'task.checklistAllDone' : 'task.checklistTotal', checklist) }}
</span>
</span>
</template>
<script>
import {getChecklistStatistics} from '../../../helpers/checklistFromText'
export default {
name: 'checklist-summary',
props: {
task: {
required: true,
},
},
computed: {
checklist() {
return getChecklistStatistics(this.task.description)
},
checklistCircleDone() {
const r = 5
const c = Math.PI * (r * 2)
const progress = this.checklist.checked / this.checklist.total * 100
return ((100 - progress) / 100) * c
},
},
}
</script>
<style scoped lang="scss">
.checklist-summary {
color: $grey-500;
display: inline-flex;
align-items: center;
svg {
transform: rotate(-90deg);
transition: stroke-dashoffset 0.35s;
margin-right: .25rem;
circle {
stroke: $grey-400;
&:last-child {
stroke: $primary;
}
}
}
}
</style>

View File

@ -1,20 +1,18 @@
<template>
<div class="content details">
<div class="content details" v-if="enabled">
<h3 v-if="canWrite || comments.length > 0">
<span class="icon is-grey">
<icon :icon="['far', 'comments']"/>
</span>
Comments
{{ $t('task.comment.title') }}
</h3>
<div class="comments">
<span
class="is-inline-flex is-align-items-center"
v-if="
taskCommentService.loading && saving === null && !creating
"
v-if="taskCommentService.loading && saving === null && !creating"
>
<span class="loader is-inline-block mr-2"></span>
Loading comments...
{{ $t('task.comment.loading') }}
</span>
<div :key="c.id" class="media comment" v-for="c in comments">
<figure class="media-left is-hidden-mobile">
@ -35,8 +33,7 @@
height="20"
width="20"
/>
<strong>{{ c.author.getDisplayName() }}</strong
>&nbsp;
<strong>{{ c.author.getDisplayName() }}</strong>&nbsp;
<span v-tooltip="formatDate(c.created)" class="has-text-grey">
{{ formatDateSince(c.created) }}
</span>
@ -44,7 +41,7 @@
v-if="+new Date(c.created) !== +new Date(c.updated)"
v-tooltip="formatDate(c.updated)"
>
· edited {{ formatDateSince(c.updated) }}
· {{ $t('task.comment.edited', {date: formatDateSince(c.updated)}) }}
</span>
<transition name="fade">
<span
@ -54,19 +51,17 @@
saving === c.id
"
>
<span
class="loader is-inline-block mr-2"
></span>
Saving...
<span class="loader is-inline-block mr-2"></span>
{{ $t('misc.saving') }}
</span>
<span
class="has-text-success"
v-if="
v-else-if="
!taskCommentService.loading &&
saved === c.id
"
>
Saved!
{{ $t('misc.saved') }}
</span>
</transition>
</div>
@ -82,8 +77,8 @@
}
"
v-model="c.comment"
:has-edit-bottom="true"
:bottom-actions="actions[c.id]"
:show-save="true"
/>
</div>
</div>
@ -104,10 +99,8 @@
class="is-inline-flex"
v-if="taskCommentService.loading && creating"
>
<span
class="loader is-inline-block mr-2"
></span>
Creating comment...
<span class="loader is-inline-block mr-2"></span>
{{ $t('task.comment.creating') }}
</span>
</transition>
<div class="field">
@ -120,20 +113,18 @@
:has-preview="false"
:upload-callback="attachmentUpload"
:upload-enabled="true"
placeholder="Add your comment..."
:placeholder="$t('task.comment.placeholder')"
v-if="editorActive"
v-model="newComment.comment"
/>
</div>
<div class="field">
<x-button
:loading="
taskCommentService.loading && !isCommentEdit
"
:loading="taskCommentService.loading && !isCommentEdit"
:disabled="newComment.comment === ''"
@click="addComment()"
>
Comment
{{ $t('task.comment.comment') }}
</x-button>
</div>
</div>
@ -147,11 +138,14 @@
@submit="deleteComment()"
v-if="showDeleteModal"
>
<span slot="header">Delete this comment</span>
<p slot="text">
Are you sure you want to delete this comment? <br/>This
<b>CANNOT BE UNDONE!</b>
</p>
<template #header><span>{{ $t('task.comment.delete') }}</span></template>
<template #text>
<p>
{{ $t('task.comment.deleteText1') }}<br/>
<strong>{{ $t('task.comment.deleteText2') }}</strong>
</p>
</template>
</modal>
</transition>
</div>
@ -160,21 +154,21 @@
<script>
import TaskCommentService from '../../../services/taskComment'
import TaskCommentModel from '../../../models/taskComment'
import attachmentUpload from '../mixins/attachmentUpload'
import LoadingComponent from '../../misc/loading'
import ErrorComponent from '../../misc/error'
import {uploadFile} from '@/helpers/attachments'
import {mapState} from 'vuex'
export default {
name: 'comments',
components: {
editor: () => ({
component: import(/* webpackChunkName: "editor" */ '../../input/editor'),
component: import('../../input/editor'),
loading: LoadingComponent,
error: ErrorComponent,
timeout: 60000,
}),
},
mixins: [attachmentUpload],
props: {
taskId: {
type: Number,
@ -189,13 +183,13 @@ export default {
comments: [],
showDeleteModal: false,
commentToDelete: TaskCommentModel,
commentToDelete: new TaskCommentModel(),
isCommentEdit: false,
commentEdit: TaskCommentModel,
commentEdit: new TaskCommentModel(),
taskCommentService: TaskCommentService,
newComment: TaskCommentModel,
taskCommentService: new TaskCommentService(),
newComment: new TaskCommentModel(),
editorActive: true,
actions: {},
@ -204,30 +198,33 @@ export default {
creating: false,
}
},
created() {
this.taskCommentService = new TaskCommentService()
this.newComment = new TaskCommentModel({taskId: this.taskId})
this.commentEdit = new TaskCommentModel({taskId: this.taskId})
this.commentToDelete = new TaskCommentModel({taskId: this.taskId})
this.comments = []
},
mounted() {
this.loadComments()
},
watch: {
taskId() {
this.loadComments()
taskId: {
handler(taskId) {
if (!this.enabled) {
return
}
this.loadComments()
this.newComment.taskId = taskId
this.commentEdit.taskId = taskId
this.commentToDelete.taskId = taskId
},
immediate: true,
},
canWrite() {
this.makeActions()
},
},
computed: {
userAvatar() {
return this.$store.state.auth.info.getAvatarUrl(48)
},
},
computed: mapState({
userAvatar: state => state.auth.info.getAvatarUrl(48),
enabled: state => state.config.taskCommentsEnabled,
}),
methods: {
attachmentUpload(...args) {
return uploadFile(this.taskId, ...args)
},
loadComments() {
this.taskCommentService
.getAll({taskId: this.taskId})
@ -236,7 +233,7 @@ export default {
this.makeActions()
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
},
addComment() {
@ -258,13 +255,11 @@ export default {
.then((r) => {
this.comments.push(r)
this.newComment.comment = ''
this.success(
{message: 'The comment was added successfully.'},
this
)
this.$message.success({message: this.$t('task.comment.addedSuccess')})
this.makeActions()
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
.finally(() => {
this.creating = false
@ -300,7 +295,7 @@ export default {
}, 2000)
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
.finally(() => {
this.isCommentEdit = false
@ -318,7 +313,7 @@ export default {
}
})
.catch((e) => {
this.error(e, this)
this.$message.error(e)
})
.finally(() => {
this.showDeleteModal = false
@ -330,7 +325,7 @@ export default {
this.$set(this.actions, c.id, [
{
action: () => this.toggleDelete(c.id),
title: 'Remove',
title: this.$t('misc.delete'),
},
])
})

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