Compare commits

...

199 Commits

Author SHA1 Message Date
renovate 66e60a4e6a chore(deps): update dependency @vitejs/plugin-legacy to v2.3.1 (#2641)
Reviewed-on: vikunja/frontend#2641
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 10:47:56 +00:00
renovate 0f67a78ec8 chore(deps): update dependency vite to v3.2.3 2022-11-07 09:04:32 +00:00
renovate 722802fb2e chore(deps): update dependency netlify-cli to v12.1.0 (#2640)
Reviewed-on: vikunja/frontend#2640
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 07:14:15 +00:00
renovate 752ead3a75 chore(deps): update dependency caniuse-lite to v1.0.30001430 (#2639)
Reviewed-on: vikunja/frontend#2639
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-07 06:35:25 +00:00
renovate b0b261d647 chore(deps): update dependency eslint to v8.27.0 2022-11-06 07:04:02 +00:00
renovate 442a14242c fix(deps): update dependency marked to v4.2.2 (#2636)
Reviewed-on: vikunja/frontend#2636
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-05 09:43:08 +00:00
Dominik Pschenitschni 66c0c322a2 [skip ci] Updated translations via Crowdin 2022-11-05 00:29:13 +00:00
Dominik Pschenitschni f4bc2b94f0 feat: sticky action buttons (#2622)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2622
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-04 13:49:28 +00:00
Dominik Pschenitschni f7728e5384 fix: remove wrong loadTask params (#2635)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2635
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-04 13:39:14 +00:00
kolaente 78b765ddc4 fix(kanban): don't allow dragging a bucket if a task input is focused
Resolves vikunja/frontend#2452
2022-11-04 12:16:25 +00:00
kolaente f967bcb205
fix(auth): always redirect to external openid provider if only one is enabled 2022-11-04 13:08:12 +01:00
Dominik Pschenitschni e49f960aea chore: inline simple helper (#2631)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2631
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-04 08:22:58 +00:00
renovate 98cb878250 chore(deps): update dependency sass to v1.56.0 (#2633)
Reviewed-on: vikunja/frontend#2633
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-04 08:17:28 +00:00
renovate 03f2b253b8 chore(deps): update dependency vite-plugin-pwa to v0.13.2 (#2632)
Reviewed-on: vikunja/frontend#2632
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-04 07:54:53 +00:00
renovate 69c0726b9d chore(deps): pin dependency @types/codemirror to 5.60.5 2022-11-03 15:03:57 +00:00
Dominik Pschenitschni eb59ca5836 fix: resolve issues with vue-easymde (#2629)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2629
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-03 14:37:24 +00:00
Dominik Pschenitschni 8b7b4d61a3 feat: MigrateService script setup (#2432)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2432
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-03 14:19:42 +00:00
renovate 0ed7114260 fix(deps): update sentry-javascript monorepo to v7.17.4 (#2628)
Reviewed-on: vikunja/frontend#2628
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-03 11:14:18 +00:00
renovate dda162c16f chore(deps): update dependency esbuild to v0.15.13 (#2627)
Reviewed-on: vikunja/frontend#2627
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-03 07:44:19 +00:00
Dominik Pschenitschni eeb562314e [skip ci] Updated translations via Crowdin 2022-11-03 00:29:00 +00:00
Dominik Pschenitschni 7f00c7dabd chore: remove unused processModel in services (#2624)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2624
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-02 16:19:26 +00:00
konrad 0ff0d8c5b8 feat: improved types (#2547)
Reviewed-on: vikunja/frontend#2547
Reviewed-by: konrad <k@knt.li>
2022-11-02 16:06:55 +00:00
renovate 9c9a5d08ff chore(deps): update pnpm to v7.14.2 2022-11-02 10:04:26 +00:00
renovate 9a2b88d295 fix(deps): update dependency marked to v4.2.1 (#2625)
Reviewed-on: vikunja/frontend#2625
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-02 09:45:05 +00:00
kolaente 09b76b7bd4
fix: don't show user deletion menu entry in user settings if the server disabled it 2022-11-02 10:44:14 +01:00
renovate f72c847e99 chore(deps): update dependency @cypress/vue to v4.2.2 2022-11-01 21:04:04 +00:00
renovate 8ea899fa26 chore(deps): update dependency vitest to v0.24.5 (#2621)
Reviewed-on: vikunja/frontend#2621
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 18:15:29 +00:00
Dominik Pschenitschni e01df4d369
fix: coverImageAttachmentId 2022-11-01 14:27:35 +01:00
Dominik Pschenitschni 096daad80a feat: rename http-common to fetcher (#2620)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2620
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-01 13:06:27 +00:00
Dominik Pschenitschni 3c5bfcc6f3
fix: potential issue with refs in Avatar 2022-11-01 13:12:13 +01:00
Dominik Pschenitschni 0182695cda
feat: add type info 2022-11-01 13:12:12 +01:00
Dominik Pschenitschni 44e6981759 feat: singleTaskInList script setup (#2463)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2463
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-11-01 10:43:01 +00:00
renovate 15b64c7e8a chore(deps): update dependency @types/node to v18.11.9 (#2619)
Reviewed-on: vikunja/frontend#2619
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 10:13:19 +00:00
renovate 9c357cb83e fix(deps): update dependency marked to v4.2.0 (#2616)
Reviewed-on: vikunja/frontend#2616
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 07:27:30 +00:00
renovate 0cf9b7595a chore(deps): update dependency @cypress/vite-dev-server to v3.4.0 (#2617)
Reviewed-on: vikuja/frontend#2617
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 07:26:42 +00:00
renovate 21dce0d8a8 chore(deps): update dependency rollup to v3.2.5 (#2618)
Reviewed-on: vikunja/frontend#2618
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 07:25:55 +00:00
renovate 218b96b230 fix(deps): update dependency @kyvg/vue3-notification to v2.6.1 (#2615)
Reviewed-on: vikunja/frontend#2615
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-11-01 07:25:14 +00:00
Dominik Pschenitschni d19c48a4f5 [skip ci] Updated translations via Crowdin 2022-11-01 00:28:44 +00:00
Dominik Pschenitschni 480aa8813e
fix: Multiselect modelValue prop type 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni caa29c152d
chore: improve multiselect hover types
when hovering over props you can only see comments if written with JSDoc
2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 1101fcb3ff
chore: remove comment 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 5d601ca4b3
fix: missing href 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 53c9a9bc9c
jsx templates 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni d6cb965ea7
fix: disable props destructure error 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 964aba4824
fix: better kanban updateBucket types 2022-10-31 22:42:30 +01:00
Dominik Pschenitschni 35f4bb1385
fix: setModuleLoading LoadingState type 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni 0b58973d87
feat: rework popup 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni 02deb0bedd
feat: rework dropdown-item 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni 4cd0e90fea
feat: rework XButton 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni e8c6afce72
feat: rework BaseButton 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni a2c1702eef
feat: type global components and especially icon prop 2022-10-31 22:42:29 +01:00
Dominik Pschenitschni 599e28e5e5
feat: type improvements 2022-10-31 22:42:28 +01:00
Dominik Pschenitschni 1002579173 feat: label store with composition api (#2605)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2605
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:45:36 +00:00
Dominik Pschenitschni 5ae8bace82 feat: lists store with composition api (#2606)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2606
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:41:23 +00:00
Dominik Pschenitschni 0832184222 feat: namespaces store with composition api (#2607)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2607
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:40:55 +00:00
Dominik Pschenitschni a50eca852f feat: attachments store with composition api (#2603)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2603
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:40:26 +00:00
Dominik Pschenitschni b4f4fd45a4 feat: base store with composition api (#2601)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2601
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:31:58 +00:00
Dominik Pschenitschni 15ef86d597 feat: config store with composition api (#2604)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2604
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:29:56 +00:00
Dominik Pschenitschni 825ba100f0 feat: auth store with composition api (#2602)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2602
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:25:35 +00:00
Dominik Pschenitschni 839d331bf5 feat: task store with composition api (#2610)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2610
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-31 20:22:00 +00:00
renovate 1798388e31 chore(deps): update dependency rollup to v3.2.4 (#2614)
Reviewed-on: vikunja/frontend#2614
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-31 20:17:07 +00:00
renovate c3f8dcefb6 chore(deps): update typescript-eslint monorepo to v5.42.0 2022-10-31 18:04:06 +00:00
renovate 816292e86a fix(deps): update dependency @kyvg/vue3-notification to v2.6.0 (#2612)
Reviewed-on: vikunja/frontend#2612
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-31 17:51:22 +00:00
renovate ea1c7f1a7e fix(deps): update dependency @kyvg/vue3-notification to v2.5.1 2022-10-31 15:04:09 +00:00
renovate 6cb17c1267 fix(deps): update dependency @kyvg/vue3-notification to v2.5.0 2022-10-31 14:29:35 +00:00
renovate cbb2cf2951 chore(deps): update dependency vite to v3.2.2 2022-10-31 14:04:16 +00:00
renovate 85e1b36b00 chore(deps): update dependency @types/node to v18.11.8 2022-10-31 13:15:55 +00:00
renovate c9b9367c0b chore(deps): update dependency vitest to v0.24.4 2022-10-31 13:14:46 +00:00
renovate a14644c156 fix(deps): update dependency blurhash to v2.0.4 2022-10-31 13:09:02 +00:00
renovate 189b5ee8aa chore(deps): update dependency caniuse-lite to v1.0.30001427 2022-10-31 13:08:20 +00:00
renovate 61ed47fab4 chore(deps): update dependency eslint-plugin-vue to v9.7.0 2022-10-31 13:07:35 +00:00
renovate f18c03fa4d fix(deps): update sentry-javascript monorepo to v7.17.3 2022-10-31 12:04:02 +00:00
renovate 0e219b48a3 fix(deps): update dependency vue-flatpickr-component to v11 2022-10-30 12:48:43 +00:00
renovate 9ee05d5583 chore(deps): update pnpm to v7.14.1 (#2593)
Reviewed-on: vikunja/frontend#2593
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-30 10:25:37 +00:00
renovate 6d20e762ee chore(deps): update dependency @vue/test-utils to v2.2.1 (#2591)
Reviewed-on: vikunja/frontend#2591
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-30 10:24:54 +00:00
drone 5143e09d2b [skip ci] Updated translations via Crowdin 2022-10-30 00:29:14 +00:00
drone b428523c89 [skip ci] Updated translations via Crowdin 2022-10-29 00:29:10 +00:00
renovate 6ef0a0ded9 chore(deps): update dependency vite to v3.2.1 2022-10-28 11:03:47 +00:00
kolaente bd7fc44722
chore: release preparations 2022-10-28 12:21:18 +02:00
kolaente 549e7b4310
chore: add git-cliff to flake 2022-10-28 12:21:18 +02:00
renovate 89a125599e fix(deps): update sentry-javascript monorepo to v7.17.2 (#2587)
Reviewed-on: vikunja/frontend#2587
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-28 09:25:47 +00:00
kolaente da2a7a224e
fix: label multiselect styling on focus 2022-10-28 11:23:55 +02:00
drone da478a49d1 [skip ci] Updated translations via Crowdin 2022-10-28 00:21:53 +00:00
kolaente 98943377b8
fix: lint 2022-10-27 23:18:59 +02:00
renovate d28bbb7dc0 chore(deps): update dependency autoprefixer to v10.4.13 (#2586)
Reviewed-on: vikunja/frontend#2586
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 21:14:28 +00:00
Dominik Pschenitschni 386fd79b49 feat: quick-actions script setup (#2478)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2478
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-27 21:10:27 +00:00
Dominik Pschenitschni 9807858436 feat: unify savedFilter logic in service (#2491)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2491
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-27 19:56:14 +00:00
Dominik Pschenitschni 9ded3d0cd6 fix: improve notifications (#2583)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#2583
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-27 19:49:17 +00:00
konrad d5258b7315 feat: improve useTaskList (#2582)
Reviewed-on: vikunja/frontend#2582
2022-10-27 19:48:26 +00:00
renovate eccaeae9e9 fix(deps): update sentry-javascript monorepo to v7.17.1 (#2585)
Reviewed-on: vikunja/frontend#2585
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 19:28:28 +00:00
konrad fd3e7e655d feat: replace our home-grown gantt implementation with ganttastic (#2180)
Reviewed-on: vikunja/frontend#2180
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-27 16:03:25 +00:00
kolaente 5271166120
chore(gantt): pnpm install after merge 2022-10-27 17:54:56 +02:00
Dominik Pschenitschni 61a89117d2
chore(gantt): upgrade packages 2022-10-27 17:51:44 +02:00
Dominik Pschenitschni 066553838a
fix: improve return type 2022-10-27 17:51:09 +02:00
Dominik Pschenitschni 443e1a063d
chore: refactor parseTimeLabel to own function 2022-10-27 17:51:09 +02:00
Dominik Pschenitschni 9a84fb6d7f
feat(gantt): disable useDayjsLanguageSync 2022-10-27 17:51:04 +02:00
Dominik Pschenitschni d8d3e4c8a6
fix(gantt): useDayjsLanguageSync and move to separate file 2022-10-27 17:50:47 +02:00
Dominik Pschenitschni b4f88bd4a6
fix: remove IE fallback 2022-10-27 17:50:31 +02:00
Dominik Pschenitschni abc26496cf
fix: do not change language to the current one 2022-10-27 17:50:31 +02:00
Dominik Pschenitschni b8cc828bc0
chore(gantt): upgrade packages 2022-10-27 17:50:31 +02:00
Dominik Pschenitschni 874dc1e5fc
feat: align with vue-flatpickr-component 10 2022-10-27 17:49:50 +02:00
Dominik Pschenitschni e74e6fcc99
feat: move config preparation in separate function 2022-10-27 17:49:50 +02:00
Dominik Pschenitschni 52d4d0bdb9
feat(gantt): reset gantt filter 2022-10-27 17:49:45 +02:00
Dominik Pschenitschni 6bf6357cbd
feat: use plural for filters consequently 2022-10-27 17:49:35 +02:00
Dominik Pschenitschni cf0eaf9ba1
chore: don't pass other params to ListGantt than route 2022-10-27 17:49:35 +02:00
Dominik Pschenitschni 8dea4082bb
fix: parseBooleanProp 2022-10-27 17:49:35 +02:00
Dominik Pschenitschni 51dc123d89
feat: use ref for filters 2022-10-27 17:49:35 +02:00
Dominik Pschenitschni acb3ddc73f
feat: use PascalCase for component name 2022-10-27 17:49:34 +02:00
Dominik Pschenitschni 407f5f2ef8
fix: initial transformation of ganttBars 2022-10-27 17:49:34 +02:00
Dominik Pschenitschni 73eab6c5b5
fix: scope ListGantt styles 2022-10-27 17:49:34 +02:00
Dominik Pschenitschni aefda38bdd
feat: remove gantt-chart wrapper 2022-10-27 17:49:34 +02:00
Dominik Pschenitschni a70a2e3ba6
feat(gantt): use time constants 2022-10-27 17:49:29 +02:00
Dominik Pschenitschni db611ab2d3
fix(gantt): only unmount chart if there aren't any loaded tasks yet 2022-10-27 17:48:30 +02:00
kolaente e1f49f2ff1
fix: disable dayjsLanguageSync function 2022-10-27 17:47:53 +02:00
kolaente b8e7b87f96
fix: don't try to dynamically load dayjs locales 2022-10-27 17:47:53 +02:00
kolaente 6c619072b4
chore: use vue-ganttastic release 2022-10-27 17:47:53 +02:00
kolaente 26e522cf8c
chore: pnpm install after merge 2022-10-27 17:47:53 +02:00
Dominik Pschenitschni 7f4114b703
feat: move useGanttTaskList in separate file 2022-10-27 17:47:53 +02:00
Dominik Pschenitschni c7dd20ef57
feat: simplify ListGantt styles 2022-10-27 17:47:53 +02:00
Dominik Pschenitschni c1da04eda1
feat(gantt): add task collection to useGanttFilter 2022-10-27 17:47:47 +02:00
Dominik Pschenitschni 2c732eb0d5
feat: abstract to useGanttFilter / and useRouteFilter 2022-10-27 17:47:15 +02:00
Dominik Pschenitschni 2acb70c562
chore: clean up 2022-10-27 17:47:15 +02:00
Dominik Pschenitschni eaf777864a
feat: working gantt-chart 2022-10-27 17:47:15 +02:00
Dominik Pschenitschni 0b194bb0cf
fix: update eslint env to 2022 2022-10-27 17:47:15 +02:00
Dominik Pschenitschni e968c88cfd
feat(gantt): trying to load gantt-chart 2022-10-27 17:47:09 +02:00
Dominik Pschenitschni df02dd5291
chore: better naming for input 2022-10-27 17:46:36 +02:00
Dominik Pschenitschni acdbf2f8f5
feat: working route sync 2022-10-27 17:46:36 +02:00
Dominik Pschenitschni 9f146c8c7f
chore(gantt): wip daterange 2022-10-27 17:46:28 +02:00
Dominik Pschenitschni 3b244dfdbe
feat: improve types 2022-10-27 17:45:29 +02:00
Dominik Pschenitschni 2f820e517f
feat: update ganttastic version 2022-10-27 17:45:29 +02:00
kolaente 56b88218b3
fix(tests): adjust gantt rows identifier 2022-10-27 17:45:29 +02:00
kolaente 957d8f05a5
chore: update lockfile 2022-10-27 17:45:26 +02:00
kolaente 31f2065d20
fix: correctly import all components 2022-10-27 17:45:09 +02:00
kolaente f5fd14124f
fix: use base store 2022-10-27 17:45:09 +02:00
Dominik Pschenitschni d91bc5090a
fix imports 2022-10-27 17:45:09 +02:00
Dominik Pschenitschni f21a4e1e9f
feat: review changes
move TaskForm in separate component, improve types
2022-10-27 17:45:09 +02:00
kolaente 970a04d973
fix: remove precision setting 2022-10-27 17:45:09 +02:00
kolaente fd9d0ad155
chore: don't use ref when not nessecary 2022-10-27 17:45:09 +02:00
kolaente 4be0977014
chore: add types for template ref 2022-10-27 17:45:09 +02:00
kolaente 6975a2b286
chore: don't use for..in 2022-10-27 17:45:09 +02:00
kolaente 64fdae81ec
feat: only use one watcher 2022-10-27 17:45:09 +02:00
kolaente 56a25734d7
chore: define types 2022-10-27 17:45:09 +02:00
kolaente ed5d3be7cb
chore: don't set required if there's a default value 2022-10-27 17:45:08 +02:00
kolaente 98d0398ca8
chore: uppercase const 2022-10-27 17:45:08 +02:00
kolaente d3925b8d80
chore: use @/models 2022-10-27 17:45:08 +02:00
kolaente b7b4530a11
fix: use inherit for font family 2022-10-27 17:45:08 +02:00
kolaente 766b4c669f
chore: use Loading component 2022-10-27 17:45:08 +02:00
kolaente 5f7159ebc4
feat: increase the default date range 2022-10-27 17:44:53 +02:00
kolaente 0a9588e097
feat: create task when pressing the button 2022-10-27 17:44:53 +02:00
kolaente 091beecc19
fix: make tests work again with new selectors 2022-10-27 17:44:53 +02:00
kolaente 6cb331ee0f
chore: remove old component and dependencies 2022-10-27 17:44:50 +02:00
kolaente 8c62a9e198
feat: loading animation 2022-10-27 17:44:30 +02:00
kolaente 29dcc02217
feat: handle changing props 2022-10-27 17:44:30 +02:00
kolaente 3eacc0754f
feat: show done tasks strikethrough 2022-10-27 17:44:30 +02:00
kolaente ebd824bddf
feat: update task in gantt bar after dragging to make sure it changes its color 2022-10-27 17:44:30 +02:00
kolaente 2c012e1a08
fix: make sure the date format is actually valid 2022-10-27 17:44:30 +02:00
kolaente 10c6db3849
fix: handle bar styling so they can actually be used 2022-10-27 17:44:30 +02:00
kolaente 80c151ca6c
feat: styling 2022-10-27 17:44:30 +02:00
kolaente 7a7a1c985e
chore: use width property 2022-10-27 17:44:30 +02:00
kolaente c8eac914d1
feat: scroll 2022-10-27 17:44:29 +02:00
kolaente d2c40926de
feat: add open task detail when double clicking 2022-10-27 17:44:29 +02:00
kolaente c3cae78213
fix: new task input styling 2022-10-27 17:44:29 +02:00
kolaente c289a6ae18
chore: use flatpickr range instead of two datepickers 2022-10-27 17:44:29 +02:00
kolaente ef4689335b
feat: create new tasks 2022-10-27 17:44:29 +02:00
kolaente 3b48adad67
feat: dynamically set default date 2022-10-27 17:44:29 +02:00
kolaente 736e5a8bf5
feat: dynamically set default date 2022-10-27 17:44:29 +02:00
kolaente ed241d21be
feat: only load tasks which start in the currently selected range 2022-10-27 17:44:29 +02:00
kolaente 49a24977f9
feat: allow passing props down to the gantt component 2022-10-27 17:44:29 +02:00
kolaente 2b0df8c237
feat: add basic implementation of ganttastic 2022-10-27 17:44:26 +02:00
renovate ef3f19d046 fix(deps): update sentry-javascript monorepo to v7.17.0 2022-10-27 14:04:07 +00:00
Dominik Pschenitschni 7ce880239e
feat: rename useTaskList 2022-10-27 15:47:48 +02:00
Dominik Pschenitschni aa2278a564
chore: move helper function outside of composable 2022-10-27 15:47:03 +02:00
renovate 96e44bf225 chore(deps): update dependency @vitejs/plugin-vue to v3.2.0 (#2579)
Reviewed-on: vikunja/frontend#2579
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 08:11:58 +00:00
renovate 4ad99bdad1 chore(deps): update dependency @vitejs/plugin-legacy to v2.3.0 (#2578)
Reviewed-on: vikunja/frontend#2578
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 07:14:45 +00:00
renovate 5e7fe3280c chore(deps): update dependency @types/node to v18.11.7 (#2581)
Reviewed-on: vikunja/frontend#2581
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 06:30:24 +00:00
renovate 7ec31363c3 chore(deps): update dependency vite to v3.2.0 (#2580)
Reviewed-on: vikunja/frontend#2580
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-27 06:29:25 +00:00
drone c40c1fb10a [skip ci] Updated translations via Crowdin 2022-10-27 00:21:10 +00:00
renovate 59be904d4a chore(deps): update dependency @types/node to v18.11.6 2022-10-26 13:03:57 +00:00
drone 1d9d093b31 [skip ci] Updated translations via Crowdin 2022-10-26 00:21:30 +00:00
renovate ef6bc3cbab chore(deps): update dependency cypress to v10.11.0 (#2576)
Reviewed-on: vikunja/frontend#2576
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-25 21:02:21 +00:00
kolaente e13e477682
fix: lint 2022-10-25 21:16:55 +02:00
kolaente 8a5b1ab3e3
fix(sharing): correctly check if the user has admin rights when sharing 2022-10-25 18:48:51 +02:00
renovate 70e81ee682 fix(deps): update vueuse to v9.4.0 (#2575)
Reviewed-on: vikunja/frontend#2575
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-25 13:12:51 +00:00
kolaente a0795db040
fix: building version into releases 2022-10-25 09:16:37 +02:00
renovate 35649d0e87 chore(deps): update dependency @types/node to v18 (#2574)
Reviewed-on: vikunja/frontend#2574
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-25 05:08:10 +00:00
drone 67145fe00b [skip ci] Updated translations via Crowdin 2022-10-25 00:14:16 +00:00
renovate 22d93a1a3c chore(deps): update typescript-eslint monorepo to v5.41.0 (#2573)
Reviewed-on: vikunja/frontend#2573
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 18:14:23 +00:00
kolaente 51471b9551
fix(i18n): rename "right" to permission so that it's clearer what it is used for 2022-10-24 19:07:08 +02:00
kolaente 22a18f8437
fix(subscription): make sure list subscription state is propagated everywhere for the current list 2022-10-24 19:01:01 +02:00
kolaente f17bbeddec
fix(subscription): don't remove every namespace but the one subscribing to 2022-10-24 18:56:50 +02:00
kolaente eae555475d
fix(teams): show an error message when no user is selected to add to a team 2022-10-24 18:52:31 +02:00
kolaente 12faafbe7c
fix(i18n): spelling typo 2022-10-24 18:41:12 +02:00
kolaente 5ddce387fe
fix: show frontend version in about dialog 2022-10-24 15:41:12 +02:00
renovate 05d000fc50 fix(deps): update dependency vue-router to v4.1.6 (#2572)
Reviewed-on: vikunja/frontend#2572
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 09:14:24 +00:00
renovate 333df9b247 chore(deps): update dependency netlify-cli to v12.0.11 (#2569)
Reviewed-on: vikunja/frontend#2569
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 08:31:17 +00:00
renovate 8d368c552d chore(deps): update dependency caniuse-lite to v1.0.30001423 (#2568)
Reviewed-on: vikunja/frontend#2568
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 07:53:01 +00:00
renovate 57cc7b8f37 chore(deps): update dependency @vue/test-utils to v2.2.0 (#2570)
Reviewed-on: vikunja/frontend#2570
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 07:52:25 +00:00
renovate 527873dad4 chore(deps): update dependency happy-dom to v7.6.0 (#2571)
Reviewed-on: vikunja/frontend#2571
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-24 07:51:38 +00:00
renovate d67dca4a81 chore(deps): update dependency @types/node to v16.18.0 (#2567)
Reviewed-on: vikunja/frontend#2567
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-23 21:44:54 +00:00
182 changed files with 6063 additions and 4494 deletions

View File

@ -202,8 +202,9 @@ steps:
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
- apk add git
- corepack enable && pnpm config set store-dir .cache/.pnp
- pnpm install --fetch-timeout 100000
- pnpm install --fetch-timeout 100000 --frozen-lockfile
- pnpm run lint
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- pnpm run build
@ -278,8 +279,9 @@ steps:
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
- apk add git
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000
- pnpm install --fetch-timeout 100000 --frozen-lockfile
- pnpm run lint
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- pnpm run build
@ -659,6 +661,6 @@ steps:
from_secret: crowdin_key
---
kind: signature
hmac: c885a0e50db729842402494aa645dd3ac662828b691108550f6bf302158295ba
hmac: 5dc7ab785b6e4d1611fc2851971e23c444d93d4988517f116e02e8c4d1af82f3
...

View File

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

View File

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

View File

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

View File

@ -11,7 +11,7 @@ describe('List View Gantt', () => {
const tasks = TaskFactory.create(1)
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart .tasks')
cy.get('.g-gantt-rows-container')
.should('not.contain', tasks[0].title)
})
@ -25,7 +25,7 @@ describe('List View Gantt', () => {
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart .months')
cy.get('.g-timeunits-container')
.should('contain', format(now, 'MMMM'))
.should('contain', format(nextMonth, 'MMMM'))
})
@ -38,14 +38,13 @@ describe('List View Gantt', () => {
})
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart .tasks')
cy.get('.g-gantt-rows-container')
.should('not.be.empty')
cy.get('.gantt-chart .tasks')
.should('contain', tasks[0].title)
})
it('Shows tasks with no dates after enabling them', () => {
TaskFactory.create(1, {
const tasks = TaskFactory.create(1, {
start_date: null,
end_date: null,
})
@ -55,13 +54,15 @@ describe('List View Gantt', () => {
.contains('Show tasks which don\'t have dates set')
.click()
cy.get('.gantt-chart .tasks')
cy.get('.g-gantt-rows-container')
.should('not.be.empty')
cy.get('.gantt-chart .tasks .task.nodate')
.should('exist')
.should('contain', tasks[0].title)
})
it('Drags a task around', () => {
cy.intercept('**/api/v1/tasks/*')
.as('taskUpdate')
const now = new Date()
TaskFactory.create(1, {
start_date: formatISO(now),
@ -69,10 +70,11 @@ describe('List View Gantt', () => {
})
cy.visit('/lists/1/gantt')
cy.get('.gantt-chart .tasks .task')
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
.first()
.trigger('mousedown', {which: 1})
.trigger('mousemove', {clientX: 500, clientY: 0})
.trigger('mouseup', {force: true})
cy.wait('@taskUpdate')
})
})

View File

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

View File

@ -23,22 +23,25 @@
"@fortawesome/free-solid-svg-icons": "6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@github/hotkey": "2.0.1",
"@kyvg/vue3-notification": "2.4.1",
"@sentry/tracing": "7.16.0",
"@sentry/vue": "7.16.0",
"@infectoone/vue-ganttastic": "2.1.2",
"@kyvg/vue3-notification": "2.6.1",
"@sentry/tracing": "7.17.4",
"@sentry/vue": "7.17.4",
"@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.15.0",
"@vueuse/core": "9.3.1",
"@vueuse/router": "9.3.1",
"@vueuse/core": "9.4.0",
"@vueuse/router": "9.4.0",
"axios": "0.27.2",
"blurhash": "2.0.3",
"blurhash": "2.0.4",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"codemirror": "5.65.9",
"date-fns": "2.29.3",
"dayjs": "1.11.6",
"dompurify": "2.4.0",
"easymde": "2.18.0",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.21",
"floating-vue": "2.0.0-beta.20",
@ -46,7 +49,7 @@
"is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8",
"marked": "4.1.1",
"marked": "4.2.2",
"minimist": "1.2.7",
"pinia": "2.0.23",
"register-service-worker": "1.7.2",
@ -55,56 +58,57 @@
"ufo": "0.8.6",
"vue": "3.2.41",
"vue-advanced-cropper": "2.8.6",
"vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "10.0.0",
"vue-flatpickr-component": "11.0.1",
"vue-i18n": "9.2.2",
"vue-router": "4.1.5",
"vue-router": "4.1.6",
"workbox-precaching": "6.5.4",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.1",
"@cypress/vite-dev-server": "3.3.1",
"@cypress/vue": "4.2.1",
"@cypress/vite-dev-server": "3.4.0",
"@cypress/vue": "4.2.2",
"@faker-js/faker": "7.6.0",
"@rushstack/eslint-patch": "1.2.0",
"@types/codemirror": "5.60.5",
"@types/dompurify": "2.3.4",
"@types/flexsearch": "0.7.3",
"@types/lodash.debounce": "4.0.7",
"@types/marked": "4.0.7",
"@types/node": "16.11.68",
"@types/node": "18.11.9",
"@types/postcss-preset-env": "7.7.0",
"@typescript-eslint/eslint-plugin": "5.40.1",
"@typescript-eslint/parser": "5.40.1",
"@vitejs/plugin-legacy": "2.2.0",
"@vitejs/plugin-vue": "3.1.2",
"@typescript-eslint/eslint-plugin": "5.42.0",
"@typescript-eslint/parser": "5.42.0",
"@vitejs/plugin-legacy": "2.3.1",
"@vitejs/plugin-vue": "3.2.0",
"@vue/eslint-config-typescript": "11.0.2",
"@vue/test-utils": "2.1.0",
"@vue/test-utils": "2.2.1",
"@vue/tsconfig": "0.1.3",
"autoprefixer": "10.4.12",
"autoprefixer": "10.4.13",
"browserslist": "4.21.4",
"caniuse-lite": "1.0.30001420",
"cypress": "10.10.0",
"esbuild": "0.15.12",
"eslint": "8.26.0",
"eslint-plugin-vue": "9.6.0",
"caniuse-lite": "1.0.30001430",
"csstype": "3.1.1",
"cypress": "10.11.0",
"esbuild": "0.15.13",
"eslint": "8.27.0",
"eslint-plugin-vue": "9.7.0",
"express": "4.18.2",
"happy-dom": "7.5.12",
"netlify-cli": "12.0.9",
"happy-dom": "7.6.6",
"netlify-cli": "12.1.0",
"postcss": "8.4.18",
"postcss-preset-env": "7.8.2",
"rollup": "3.2.3",
"rollup": "3.2.5",
"rollup-plugin-visualizer": "5.8.3",
"sass": "1.55.0",
"sass": "1.56.0",
"typescript": "4.8.4",
"vite": "3.1.8",
"vite-plugin-pwa": "0.13.1",
"vite": "3.2.3",
"vite-plugin-pwa": "0.13.2",
"vite-svg-loader": "3.6.0",
"vitest": "0.24.3",
"vitest": "0.24.5",
"vue-tsc": "1.0.9",
"wait-on": "6.0.1",
"workbox-cli": "6.5.4"
},
"license": "AGPL-3.0-or-later",
"packageManager": "pnpm@7.14.0"
"packageManager": "pnpm@7.14.2"
}

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

@ -193,7 +193,7 @@ function toggleDatePopup() {
}
const datepickerPopup = ref<HTMLElement | null>(null)
function hideDatePopup(e) {
function hideDatePopup(e: MouseEvent) {
if (show.value) {
closeWhenClickedOutside(e, datepickerPopup.value, close)
}

View File

@ -115,6 +115,7 @@ const props = defineProps({
default: true,
},
bottomActions: {
type: Array,
default: () => [],
},
emptyText: {

View File

@ -100,37 +100,52 @@ function elementInResults(elem: string | any, label: string, query: string): boo
}
const props = defineProps({
// When true, shows a loading spinner
/**
* When true, shows a loading spinner
*/
loading: {
type: Boolean,
default: false,
},
// The placeholder of the search input
/**
* The placeholder of the search input
*/
placeholder: {
type: String,
default: '',
},
// The search results where the @search listener needs to put the results into
/**
* The search results where the @search listener needs to put the results into
*/
searchResults: {
type: Array as PropType<{[id: string]: any}>,
default: () => [],
},
// The name of the property of the searched object to show the user.
// If empty the component will show all raw data of an entry.
/**
* The name of the property of the searched object to show the user.
* If empty the component will show all raw data of an entry.
*/
label: {
type: String,
default: '',
},
// The object with the value, updated every time an entry is selected.
/**
* The object with the value, updated every time an entry is selected.
*/
modelValue: {
type: [Object] as PropType<{[key: string]: any}>,
default: null,
},
// If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
/**
* If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
*/
creatable: {
type: Boolean,
default: false,
},
// The text shown next to the new value option.
/**
* The text shown next to the new value option.
*/
createPlaceholder: {
type: String,
default() {
@ -138,7 +153,9 @@ const props = defineProps({
return t('input.multiselect.createPlaceholder')
},
},
// The text shown next to an option.
/**
* The text shown next to an option.
*/
selectPlaceholder: {
type: String,
default() {
@ -146,22 +163,30 @@ const props = defineProps({
return t('input.multiselect.selectPlaceholder')
},
},
// If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
/**
* If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
*/
multiple: {
type: Boolean,
default: false,
},
// If true, displays the search results inline instead of using a dropdown.
/**
* If true, displays the search results inline instead of using a dropdown.
*/
inline: {
type: Boolean,
default: false,
},
// If true, shows search results when no query is specified.
/**
* If true, shows search results when no query is specified.
*/
showEmpty: {
type: Boolean,
default: true,
},
// The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke.
/**
* The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke.
*/
searchDelay: {
type: Number,
default: 200,
@ -174,17 +199,25 @@ const props = defineProps({
const emit = defineEmits<{
(e: 'update:modelValue', value: null): void
// @search: Triggered every time the search query input changes
/**
* Triggered every time the search query input changes
*/
(e: 'search', query: string): void
// @select: Triggered every time an option from the search results is selected. Also triggers a change in v-model.
(e: 'select', value: null): void
// @create: If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
/**
* Triggered every time an option from the search results is selected. Also triggers a change in v-model.
*/
(e: 'select', value: {[key: string]: any}): void
/**
* If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
*/
(e: 'create', query: string): void
// @remove: If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
/**
* If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
*/
(e: 'remove', value: null): void
}>()
const query = ref('')
const query = ref<string | {[key: string]: any}>('')
const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
const localLoading = ref(false)
const showSearchResults = ref(false)
@ -414,8 +447,8 @@ function focus() {
.input-wrapper {
padding: 0;
background: var(--white) !important;
border-color: var(--grey-200) !important;
background: var(--white);
border-color: var(--grey-200);
flex-wrap: wrap;
height: auto;

View File

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

View File

@ -78,12 +78,13 @@
<script setup lang="ts">
import {ref, computed, watchEffect, type PropType} from 'vue'
import {isSavedFilter} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import Subscription from '@/components/misc/subscription.vue'
import type {IList} from '@/modelTypes/IList'
import type {ISubscription} from '@/modelTypes/ISubscription'
import {isSavedFilter} from '@/services/savedFilter'
import {useConfigStore} from '@/stores/config'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'

View File

@ -34,7 +34,7 @@ import {computed, ref, watch} from 'vue'
import Filters from '@/components/list/partials/filters.vue'
import {getDefaultParams} from '@/composables/taskList'
import {getDefaultParams} from '@/composables/useTaskList'
const props = defineProps({
modelValue: {

View File

@ -212,7 +212,7 @@ import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
import {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/composables/taskList'
import {getDefaultParams} from '@/composables/useTaskList'
import {camelCase} from 'camel-case'
// FIXME: merge with DEFAULT_PARAMS in taskList.js

View File

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

View File

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

View File

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

View File

@ -46,6 +46,9 @@
</template>
<script setup lang="ts">
import type {PropType} from 'vue'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
defineProps({
title: {
type: String,
@ -55,7 +58,7 @@ defineProps({
type: String,
},
primaryIcon: {
type: String,
type: String as PropType<IconProp>,
default: 'plus',
},
primaryDisabled: {

View File

@ -1,46 +1,24 @@
<template>
<component
:is="componentNodeName"
v-bind="elementBindings"
:to="to"
class="dropdown-item">
<BaseButton class="dropdown-item">
<span class="icon" v-if="icon">
<icon :icon="icon"/>
<Icon :icon="icon"/>
</span>
<span>
<slot></slot>
<slot />
</span>
</component>
</BaseButton>
</template>
<script lang="ts" setup>
import {ref, useAttrs, watchEffect} from 'vue'
import BaseButton, { type BaseButtonProps } from '@/components/base//BaseButton.vue'
import Icon from '@/components/misc/Icon'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const props = defineProps<{
to?: object,
icon?: string | string[],
}>()
export interface DropDownItemProps extends BaseButtonProps {
icon?: IconProp,
}
const componentNodeName = ref<Node['nodeName']>('a')
const elementBindings = ref({})
const attrs = useAttrs()
watchEffect(() => {
let nodeName = 'a'
if (props.to) {
nodeName = 'router-link'
}
if ('href' in attrs) {
nodeName = 'BaseButton'
}
componentNodeName.value = nodeName
elementBindings.value = {
...attrs,
}
})
defineProps<DropDownItemProps>()
</script>
<style scoped lang="scss">
@ -91,5 +69,4 @@ button.dropdown-item {
}
}
}
</style>

View File

@ -17,14 +17,15 @@
</template>
<script setup lang="ts">
import {ref} from 'vue'
import {ref, type PropType} from 'vue'
import {onClickOutside} from '@vueuse/core'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
import BaseButton from '@/components/base/BaseButton.vue'
defineProps({
triggerIcon: {
type: String,
type: String as PropType<IconProp>,
default: 'ellipsis-h',
},
})

View File

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

View File

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

View File

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

View File

@ -99,6 +99,9 @@ watchEffect(() => {
</script>
<style lang="scss" scoped>
$modal-margin: 4rem;
$modal-width: 1024px;
.modal-mask {
position: fixed;
z-index: 4000;
@ -147,16 +150,16 @@ watchEffect(() => {
// scrolling-content
// used e.g. for <TaskDetailViewModal>
.scrolling .modal-content {
max-width: 1024px;
max-width: $modal-width;
width: 100%;
margin: 4rem auto;
margin: $modal-margin auto;
max-height: none; // reset bulma
overflow: visible; // reset bulma
@media screen and (min-width: $tablet) {
max-height: none; // reset bulma
margin: 4rem auto; // reset bulma
margin: $modal-margin auto; // reset bulma
width: 100%;
}
@ -189,14 +192,23 @@ watchEffect(() => {
}
.close {
$close-button-min-space: 84px;
$close-button-padding: 26px;
position: fixed;
top: 5px;
right: 26px;
color: var(--white);
right: $close-button-padding;
color: var(--grey-900);
font-size: 2rem;
@media screen and (max-width: $desktop) {
color: var(--grey-900);
@media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
top: calc(5px + $modal-margin);
right: 50%;
// we align the close button to the modal until there is enough space outside for it
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
}
// we can only use light color when there is enough space for the close button next to the modal
@media screen and (min-width: calc(#{$desktop } + #{$close-button-min-space})) {
color: var(--white);
}
}
</style>

View File

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

View File

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

View File

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

View File

@ -79,11 +79,9 @@ onMounted(() => {
function setSubscriptionInStore(sub: ISubscription) {
subscription.value = sub
namespaceStore.setNamespaces([
{
...props.namespace,
subscription: sub,
},
])
namespaceStore.setNamespaceById({
...props.namespace,
subscription: sub,
})
}
</script>

View File

@ -76,7 +76,7 @@ const notifications = computed(() => {
})
const userInfo = computed(() => authStore.info)
let interval: number
let interval: ReturnType<typeof setInterval>
onMounted(() => {
loadNotifications()

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@ -214,7 +214,7 @@ async function addTask() {
return rel
})
await Promise.all(relations)
} catch (e: { message?: string }) {
} catch (e: any) {
newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_LIST') {
errorMessage.value = t('list.create.addListRequired')

View File

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

View File

@ -165,7 +165,6 @@ import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import type AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask'
@ -227,9 +226,9 @@ function uploadFilesToTask(files: File[] | FileList) {
uploadFiles(attachmentService, props.task.id, files)
}
const attachmentToDelete = ref<AttachmentModel | null>(null)
const attachmentToDelete = ref<IAttachment | null>(null)
function setAttachmentToDelete(attachment: AttachmentModel | null) {
function setAttachmentToDelete(attachment: IAttachment | null) {
attachmentToDelete.value = attachment
}
@ -250,7 +249,7 @@ async function deleteAttachment() {
const attachmentImageBlobUrl = ref<string | null>(null)
async function viewOrDownload(attachment: AttachmentModel) {
async function viewOrDownload(attachment: IAttachment) {
if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} else {

View File

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

View File

@ -4,7 +4,7 @@
<Done class="heading__done" :is-done="task.done"/>
<ColorBubble
v-if="task.hexColor !== ''"
:color="task.getHexColor()"
:color="getHexColor(task.hexColor)"
class="mt-1 ml-2"
/>
<h1
@ -48,6 +48,7 @@ import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {useTaskStore} from '@/stores/tasks'
import type {ITask} from '@/modelTypes/ITask'
import {getHexColor} from '@/models/task'
const props = defineProps({
task: {

View File

@ -9,9 +9,9 @@
v-model="list"
:select-placeholder="$t('list.searchSelect')"
>
<template #searchResult="props">
<span class="list-namespace-title search-result">{{ namespace(props.option.namespaceId) }} ></span>
{{ props.option.title }}
<template #searchResult="{option}">
<span class="list-namespace-title search-result">{{ namespace((option as IList).namespaceId) }} ></span>
{{ (option as IList).title }}
</template>
</Multiselect>
</template>
@ -25,6 +25,7 @@ import type {IList} from '@/modelTypes/IList'
import Multiselect from '@/components/input/multiselect.vue'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
import type { INamespace } from '@/modelTypes/INamespace'
const props = defineProps({
modelValue: {
@ -65,7 +66,7 @@ function select(l: IList | null) {
emit('update:modelValue', list)
}
function namespace(namespaceId: number) {
function namespace(namespaceId: INamespace['id']) {
const namespace = namespaceStore.getNamespaceById(namespaceId)
return namespace !== null
? namespace.title

View File

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

View File

@ -3,6 +3,9 @@ import {useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
import {useAuthStore} from '@/stores/auth'
import {MILLISECONDS_A_HOUR, SECONDS_A_HOUR} from '@/constants/date'
const SECONDS_TOKEN_VALID = 60 * SECONDS_A_HOUR
export function useRenewTokenOnFocus() {
const router = useRouter()
@ -21,7 +24,7 @@ export function useRenewTokenOnFocus() {
return
}
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - +new Date() / 1000
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - new Date().valueOf() / MILLISECONDS_A_HOUR
// If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page
@ -32,7 +35,7 @@ export function useRenewTokenOnFocus() {
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
if (expiresIn < SECONDS_TOKEN_VALID) {
authStore.renewToken()
console.debug('renewed token')
}

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

@ -3,17 +3,15 @@ import {snakeCase} from 'snake-case'
/**
* Transforms field names to camel case.
* @param object
* @returns {*}
*/
export function objectToCamelCase(object) {
export function objectToCamelCase(object: Record<string, any>) {
// When calling recursively, this can be called without being and object or array in which case we just return the value
if (typeof object !== 'object') {
return object
}
const parsedObject = {}
const parsedObject: Record<string, any> = {}
for (const m in object) {
parsedObject[camelCase(m)] = object[m]
@ -25,7 +23,7 @@ export function objectToCamelCase(object) {
// Call it again for arrays
if (Array.isArray(object[m])) {
parsedObject[camelCase(m)] = object[m].map(o => objectToCamelCase(o))
parsedObject[camelCase(m)] = object[m].map((o: Record<string, any>) => objectToCamelCase(o))
// Because typeof [] === 'object' is true for arrays, we leave the loop here to prevent converting arrays to objects.
continue
}
@ -40,17 +38,15 @@ export function objectToCamelCase(object) {
/**
* Transforms field names to snake case - used before making an api request.
* @param object
* @returns {*}
*/
export function objectToSnakeCase(object) {
export function objectToSnakeCase(object: Record<string, any>) {
// When calling recursively, this can be called without being and object or array in which case we just return the value
if (typeof object !== 'object') {
return object
}
const parsedObject = {}
const parsedObject: Record<string, any> = {}
for (const m in object) {
parsedObject[snakeCase(m)] = object[m]
@ -65,7 +61,7 @@ export function objectToSnakeCase(object) {
// Call it again for arrays
if (Array.isArray(object[m])) {
parsedObject[snakeCase(m)] = object[m].map(o => objectToSnakeCase(o))
parsedObject[snakeCase(m)] = object[m].map((o: Record<string, any>) => objectToSnakeCase(o))
// Because typeof [] === 'object' is true for arrays, we leave the loop here to prevent converting arrays to objects.
continue
}

View File

@ -5,11 +5,11 @@
* @param rootElement
* @param closeCallback A closure function to call when the click event happened outside of the rootElement.
*/
export const closeWhenClickedOutside = (event, rootElement, closeCallback) => {
export const closeWhenClickedOutside = (event: MouseEvent, rootElement: HTMLElement, closeCallback: () => void) => {
// We walk up the tree to see if any parent of the clicked element is the root element.
// If it is not, we call the close callback. We're doing all this hassle to only call the
// closing callback when a click happens outside of the rootElement.
let parent = event.target.parentElement
let parent = (event.target as HTMLElement)?.parentElement
while (parent !== rootElement) {
if (parent === null || parent.parentElement === null) {
parent = null

View File

@ -4,7 +4,7 @@
* @param color
* @returns {string}
*/
export function colorFromHex(color) {
export function colorFromHex(color: string) {
if (color.substring(0, 1) === '#') {
color = color.substring(1, 7)
}

View File

@ -0,0 +1,21 @@
import { defineAsyncComponent, type AsyncComponentLoader, type AsyncComponentOptions, type Component, type ComponentPublicInstance } from 'vue'
import ErrorComponent from '@/components/misc/error.vue'
import LoadingComponent from '@/components/misc/loading.vue'
const DEFAULT_TIMEOUT = 60000
export function createAsyncComponent<T extends Component = {
new (): ComponentPublicInstance;
}>(source: AsyncComponentLoader<T> | AsyncComponentOptions<T>): T {
if (typeof source === 'function') {
source = { loader: source }
}
return defineAsyncComponent({
...source,
loadingComponent: LoadingComponent,
errorComponent: ErrorComponent,
timeout: DEFAULT_TIMEOUT,
})
}

View File

@ -35,7 +35,7 @@ export function setupMarkdownRenderer(checkboxId: string) {
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
},
},
highlight(code, language) {
highlight(code: string, language: string) {
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
return hljs.highlight(code, {language: validLanguage}).value
},

View File

@ -1,6 +0,0 @@
// https://stackoverflow.com/a/32108184/10924593
export function objectIsEmpty(obj: Record<string, unknown>): boolean {
return obj
&& Object.keys(obj).length === 0
&& Object.getPrototypeOf(obj) === Object.prototype
}

View File

@ -1,12 +1,12 @@
/**
* Make date objects from timestamps
*/
export function parseDateOrNull(date) {
export function parseDateOrNull(date: string | Date) {
if (date instanceof Date) {
return date
}
if ((typeof date === 'string' || date instanceof String) && !date.startsWith('0001')) {
if ((typeof date === 'string') && !date.startsWith('0001')) {
return new Date(date)
}

View File

@ -1,21 +1,29 @@
// Save the current list view to local storage
// We use local storage and not a store here to make it persistent across reloads.
export const saveListView = (listId, routeName) => {
import type { IList } from '@/modelTypes/IList'
type ListView = Record<IList['id'], string>
const DEFAULT_LIST_VIEW = 'list.list' as const
/**
* Save the current list view to local storage
*/
export function saveListView(listId: IList['id'], routeName: string) {
if (routeName.includes('settings.')) {
return
}
if (!listId) {
return
}
// We use local storage and not the store here to make it persistent across reloads.
const savedListView = localStorage.getItem('listView')
let savedListViewJson = false
let savedListViewJson: ListView | false = false
if (savedListView !== null) {
savedListViewJson = JSON.parse(savedListView)
savedListViewJson = JSON.parse(savedListView) as ListView
}
let listView = {}
let listView: ListView = {}
if (savedListViewJson) {
listView = savedListViewJson
}
@ -24,7 +32,7 @@ export const saveListView = (listId, routeName) => {
localStorage.setItem('listView', JSON.stringify(listView))
}
export const getListView = listId => {
export const getListView = (listId: IList['id']) => {
// Remove old stored settings
const savedListView = localStorage.getItem('listView')
if (savedListView !== null && savedListView.startsWith('list.')) {
@ -32,13 +40,13 @@ export const getListView = listId => {
}
if (!savedListView) {
return 'list.list'
return DEFAULT_LIST_VIEW
}
const savedListViewJson = JSON.parse(savedListView)
const savedListViewJson: ListView = JSON.parse(savedListView)
if (!savedListViewJson[listId]) {
return 'list.list'
return DEFAULT_LIST_VIEW
}
return savedListViewJson[listId]

View File

@ -1,14 +0,0 @@
import type {IList} from '@/modelTypes/IList'
export function getSavedFilterIdFromListId(listId: IList['id']) {
let filterId = listId * -1 - 1
// FilterIds from listIds are always positive
if (filterId < 0) {
filterId = 0
}
return filterId
}
export function isSavedFilter(list: IList) {
return getSavedFilterIdFromListId(list.id) > 0
}

View File

@ -10,7 +10,7 @@ const days = {
friday: 5,
saturday: 6,
sunday: 0,
}
} as Record<string, number>
for (const n in days) {
test(`today on a ${n}`, () => {
@ -32,7 +32,7 @@ const nextMonday = {
friday: 3,
saturday: 2,
sunday: 1,
}
} as Record<string, number>
for (const n in nextMonday) {
test(`next monday on a ${n}`, () => {
@ -48,7 +48,7 @@ const thisWeekend = {
friday: 1,
saturday: 0,
sunday: 0,
}
} as Record<string, number>
for (const n in thisWeekend) {
test(`this weekend on a ${n}`, () => {
@ -64,7 +64,7 @@ const laterThisWeek = {
friday: 0,
saturday: 0,
sunday: 0,
}
} as Record<string, number>
for (const n in laterThisWeek) {
test(`later this week on a ${n}`, () => {
@ -80,7 +80,7 @@ const laterNextWeek = {
friday: 7 + 0,
saturday: 7 + 0,
sunday: 7 + 0,
}
} as Record<string, number>
for (const n in laterNextWeek) {
test(`later next week on a ${n} (this week)`, () => {

View File

@ -1,4 +1,6 @@
export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())) {
type Day<T extends number = number> = T
export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())): Day {
switch (dateString) {
case 'today':
return 0

View File

@ -6,7 +6,7 @@
* @param dateString
* @returns {Date}
*/
export const createDateFromString = dateString => {
export function createDateFromString(dateString: string | Date) {
if (dateString instanceof Date) {
return dateString
}

View File

@ -1,12 +1,14 @@
import {createDateFromString} from '@/helpers/time/createDateFromString'
import {format, formatDistanceToNow, formatISO as formatISOfns} from 'date-fns'
// FIXME: support all locales and load dynamically
import {enGB, de, fr, ru} from 'date-fns/locale'
import {i18n} from '@/i18n'
const locales = {en: enGB, de, ch: de, fr, ru}
export function dateIsValid(date) {
export function dateIsValid(date: Date | null) {
if (date === null) {
return false
}

View File

@ -1,3 +1,5 @@
import {MILLISECONDS_A_WEEK} from '@/constants/date'
export function getNextWeekDate(): Date {
return new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000)
return new Date((new Date()).getTime() + MILLISECONDS_A_WEEK)
}

View File

@ -0,0 +1,8 @@
import {format} from 'date-fns'
import {DATEFNS_DATE_FORMAT_KEBAB} from '@/constants/date'
import type {DateISO} from '@/types/DateISO'
import type {DateKebab} from '@/types/DateKebab'
export function isoToKebabDate(isoDate: DateISO) {
return format(new Date(isoDate), DATEFNS_DATE_FORMAT_KEBAB) as DateKebab
}

View File

@ -0,0 +1,5 @@
export function parseBooleanProp(booleanProp: string | undefined) {
return (booleanProp === 'false' || booleanProp === '0')
? false
: Boolean(booleanProp)
}

View File

@ -349,9 +349,7 @@ const getMonthFromText = (text: string, date: Date) => {
const getDateFromInterval = (interval: number): Date => {
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0)
newDate.setSeconds(0)
newDate.setHours(calculateNearestHours(newDate), 0, 0)
return newDate
}

View File

@ -0,0 +1,30 @@
import type {DateISO} from '@/types/DateISO'
import type {DateKebab} from '@/types/DateKebab'
export function parseDateProp(kebabDate: DateKebab | undefined): string | undefined {
try {
if (!kebabDate) {
throw new Error('No value')
}
const dateValues = kebabDate.split('-')
const [, monthString, dateString] = dateValues
const [year, month, date] = dateValues.map(val => Number(val))
const dateValuesAreValid = (
!Number.isNaN(year) &&
monthString.length >= 1 && monthString.length <= 2 &&
!Number.isNaN(month) &&
month >= 1 && month <= 12 &&
dateString.length >= 1 && dateString.length <= 31 &&
!Number.isNaN(date) &&
date >= 1 && date <= 31
)
if (!dateValuesAreValid) {
throw new Error('Invalid date values')
}
return new Date(year, month, date).toISOString() as DateISO
} catch(e) {
// ignore nonsense route queries
return
}
}

View File

@ -0,0 +1,7 @@
import {parse} from 'date-fns'
import {DATEFNS_DATE_FORMAT_KEBAB} from '@/constants/date'
import type {DateKebab} from '@/types/DateKebab'
export function parseKebabDate(date: DateKebab): Date {
return parse(date, DATEFNS_DATE_FORMAT_KEBAB, new Date())
}

View File

@ -1,19 +1,8 @@
import {createI18n} from 'vue-i18n'
import langEN from './lang/en.json'
export const i18n = createI18n({
locale: 'en', // set locale
fallbackLocale: 'en',
legacy: true,
globalInjection: true,
allowComposition: true,
messages: {
en: langEN,
},
})
export const availableLanguages = {
en: 'English',
export const SUPPORTED_LOCALES = {
'en': 'English',
'de-DE': 'Deutsch',
'de-swiss': 'Schwizertütsch',
'ru-RU': 'Русский',
@ -24,62 +13,72 @@ export const availableLanguages = {
'pl-PL': 'Polski',
'nl-NL': 'Nederlands',
'pt-PT': 'Português',
}
'zh-CN': 'Chinese',
} as Record<string, string>
const loadedLanguages = ['en'] // our default language that is preloaded
export type SupportedLocale = keyof typeof SUPPORTED_LOCALES
const setI18nLanguage = (lang: string) => {
export const DEFAULT_LANGUAGE: SupportedLocale= 'en'
export type ISOLanguage = string
export const i18n = createI18n({
locale: DEFAULT_LANGUAGE, // set locale
fallbackLocale: DEFAULT_LANGUAGE,
legacy: true,
globalInjection: true,
allowComposition: true,
inheritLocale: true,
messages: {
en: langEN,
} as Record<SupportedLocale, any>,
})
function setI18nLanguage(lang: SupportedLocale): SupportedLocale {
i18n.global.locale = lang
document.documentElement.lang =lang
document.documentElement.lang = lang
return lang
}
export const loadLanguageAsync = lang => {
export async function loadLanguageAsync(lang: SupportedLocale) {
if (!lang) {
throw new Error()
}
// do not change language to the current one
if (i18n.global.locale === lang) {
return
}
if (
// If the same language
i18n.global.locale === lang ||
// If the language was already loaded
loadedLanguages.includes(lang)
) {
return setI18nLanguage(lang)
// If the language hasn't been loaded yet
if (!i18n.global.availableLocales.includes(lang)) {
const messages = await import(`./lang/${lang}.json`)
i18n.global.setLocaleMessage(lang, messages.default)
}
// If the language hasn't been loaded yet
return import(`./lang/${lang}.json`).then(
messages => {
i18n.global.setLocaleMessage(lang, messages.default)
loadedLanguages.push(lang)
return setI18nLanguage(lang)
},
)
return setI18nLanguage(lang)
}
export const getCurrentLanguage = () => {
export function getCurrentLanguage(): SupportedLocale {
const savedLanguage = localStorage.getItem('language')
if (savedLanguage !== null) {
return savedLanguage
}
const browserLanguage = navigator.language || navigator.userLanguage
const browserLanguage = navigator.language
for (const k in availableLanguages) {
if (browserLanguage[k] === browserLanguage || k.startsWith(browserLanguage + '-')) {
return k
}
}
const language: SupportedLocale | undefined = Object.keys(SUPPORTED_LOCALES).find(langKey => {
return langKey === browserLanguage || langKey.startsWith(browserLanguage + '-')
})
return 'en'
return language || DEFAULT_LANGUAGE
}
export const saveLanguage = (lang: string) => {
export function saveLanguage(lang: SupportedLocale) {
localStorage.setItem('language', lang)
setLanguage()
}
export const setLanguage = () => {
loadLanguageAsync(getCurrentLanguage())
}
export function setLanguage() {
return loadLanguageAsync(getCurrentLanguage())
}

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "{type} byl úspěšně přidán."
},
"right": {
"title": "Právo",
"title": "Oprávnění",
"read": "Pouze pro čtení",
"readWrite": "Čtení a zápis",
"admin": "Administrátor"
@ -285,8 +285,8 @@
"default": "Výchozí",
"month": "Měsíc",
"day": "Den",
"from": "Od",
"to": "Do",
"hour": "Hodina",
"range": "Časové období",
"noDates": "Tento úkol nemá nastaveno žádné datum."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "Člen týmu byl úspěšně přidán.",
"madeMember": "Člen týmu se úspěšně stal členem.",
"madeAdmin": "Člen týmu byl úspěšně jmenován správcem.",
"mustSelectUser": "Vyberte prosím uživatele.",
"delete": {
"header": "Smazat tým",
"text1": "Jste si jisti, že chcete smazat tento tým a všechny jeho členy?",

View File

@ -268,7 +268,7 @@
"updatedSuccess": "{type} wurde erfolgreich hinzugefügt."
},
"right": {
"title": "Berechtigungen",
"title": "Berechtigung",
"read": "Nur Leserechte",
"readWrite": "Lesen & Schreiben",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Standard",
"month": "Monat",
"day": "Tag",
"from": "Von",
"to": "Bis",
"hour": "Stunde",
"range": "Zeitraum",
"noDates": "Diese Aufgabe hat keine Daten definiert."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "Das Teammitglied wurde erfolgreich hinzugefügt.",
"madeMember": "Das Teammitglied ist nun ein normales Mitglied.",
"madeAdmin": "Das Teammitglied ist nun ein Admin.",
"mustSelectUser": "Bitte wähle eine:n Benutzer:in.",
"delete": {
"header": "Team löschen",
"text1": "Bist du sicher, dass du dieses Team und alle seine Mitglieder löschen willst?",

View File

@ -268,7 +268,7 @@
"updatedSuccess": "De {type} isch erfolgriich hinzuegfüegt wore."
},
"right": {
"title": "Rechts",
"title": "Berechtigung",
"read": "Nur Lese",
"readWrite": "Lese und Schriibe",
"admin": "Chef"
@ -285,8 +285,8 @@
"default": "Standard",
"month": "Monet",
"day": "Taag",
"from": "Vo",
"to": "Bis",
"hour": "Stunde",
"range": "Zeitraum",
"noDates": "Die Uufgab het no kei Datum gsetzt."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "Teammitglied hinzugefüegt.",
"madeMember": "Das Teammitglied ist ez es normals Mitglied.",
"madeAdmin": "Teammitglied isch ez en Chef.",
"mustSelectUser": "Bitte wähle eine:n Benutzer:in.",
"delete": {
"header": "Das Team chüble",
"text1": "Bischder sicher, dasst wetsch da Team mit allne Mitglieder lösche?",

View File

@ -261,7 +261,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -286,8 +286,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -846,6 +846,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -861,7 +862,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "{type} ajouté."
},
"right": {
"title": "Droit",
"title": "Permission",
"read": "Lecture seule",
"readWrite": "Lecture et écriture",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Par défaut",
"month": "Mois",
"day": "Jour",
"from": "De",
"to": ",
"hour": "Hour",
"range": "Date Range",
"noDates": "Aucune date na été fixée pour cette tâche."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "Membre de léquipe ajouté.",
"madeMember": "Le membre de léquipe est devenu membre.",
"madeAdmin": "Membre de léquipe nommé admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Supprimer léquipe",
"text1": "Supprimer cette équipe et tous ses membres ?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -1,9 +1,9 @@
{
"home": {
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"welcomeNight": "Buonanotte {username}!",
"welcomeMorning": "Buongiorno {username}!",
"welcomeDay": "Ciao {username}!",
"welcomeEvening": "Buonasera {username}!",
"lastViewed": "Ultima visualizzazione",
"list": {
"newText": "È possibile creare una nuova lista per le nuove attività:",
@ -285,8 +285,8 @@
"default": "Predefinito",
"month": "Mese",
"day": "Giorno",
"from": "Da",
"to": "A",
"hour": "Hour",
"range": "Date Range",
"noDates": "Questa attività non ha date impostate."
},
"table": {
@ -672,23 +672,23 @@
"updated": "Aggiornato"
},
"subscription": {
"subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribedListThroughParentNamespace": "Non puoi annullare l'iscrizione perché sei iscritto al namespace di questa lista.",
"subscribedTaskThroughParentNamespace": "Non puoi annullare l'iscrizione perché sei iscritto al namespace di questa attività.",
"subscribedTaskThroughParentList": "Non puoi annullare l'iscrizione perché sei iscritto alla lista di questa attività.",
"subscribedNamespace": "Sei iscritto a questo namespace e verrai notificato delle modifiche.",
"notSubscribedNamespace": "Non sei iscritto a questo namespace e non verrai notificato delle modifiche.",
"subscribedList": "Sei iscritto a questa lista e verrai notificato delle modifiche.",
"notSubscribedList": "Non sei iscritto a questa lista e non verrai notificato delle modifiche.",
"subscribedTask": "Sei iscritto a questa attività e verrai notificato delle modifiche.",
"notSubscribedTask": "Non sei iscritto a questa attività e non verrai notificato delle modifiche.",
"subscribe": "Iscriviti",
"unsubscribe": "Disiscriviti",
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
"subscribeSuccessNamespace": "Sei iscritto a questo namespace",
"unsubscribeSuccessNamespace": "Non sei più iscritto a questo namespace",
"subscribeSuccessList": "Sei iscritto a questa lista",
"unsubscribeSuccessList": "Non sei più iscritto a questa lista",
"subscribeSuccessTask": "Sei iscritto a questa attività",
"unsubscribeSuccessTask": "Non sei più iscritto a questa attività"
},
"attachment": {
"title": "Allegati",
@ -701,10 +701,10 @@
"deleteText1": "Sei sicuro di voler eliminare l'allegato {filename}?",
"copyUrl": "Copia URL",
"copyUrlTooltip": "Copia l'URL di questo allegato per usarlo nel testo",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
"setAsCover": "Crea copertina",
"unsetAsCover": "Rimuovi copertina",
"successfullyChangedCoverImage": "L'immagine di copertina è stata cambiata con successo.",
"usedAsCover": "Immagine di copertina"
},
"comment": {
"title": "Commenti",
@ -842,6 +842,7 @@
"userAddedSuccess": "Membro del gruppo aggiunto.",
"madeMember": "Membro del gruppo reso membro.",
"madeAdmin": "Membro del gruppo reso amministratore.",
"mustSelectUser": "Seleziona un utente.",
"delete": {
"header": "Elimina il gruppo",
"text1": "Sei sicuro di voler eliminare questo gruppo e tutti i suoi membri?",
@ -855,10 +856,10 @@
"success": "Utente rimosso dal gruppo."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
"title": "Abbandona il gruppo",
"text1": "Sei sicuro di voler abbandonare questo gruppo?",
"text2": "Perderai l'accesso a tutte le liste e namespace a cui questo gruppo ha accesso. Se cambi idea, dovrai farti aggiungere di nuovo da un amministratore del gruppo.",
"success": "Hai abbandonato il gruppo."
}
},
"attributes": {

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Rechten",
"title": "Permission",
"read": "Alleen lezen",
"readWrite": "Lezen & schrijven",
"admin": "Beheerder"
@ -285,8 +285,8 @@
"default": "Standaard",
"month": "Maand",
"day": "Dag",
"from": "Van",
"to": "Aan",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "Het teamlid is succesvol toegevoegd.",
"madeMember": "Het teamlid is succesvol tot lid gemaakt.",
"madeAdmin": "Het teamlid is succesvol beheerder gemaakt.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Verwijder het team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "{type} został pomyślnie dodany."
},
"right": {
"title": "Uprawnienia",
"title": "Permission",
"read": "Tylko do odczytu",
"readWrite": "Odczyt i zapis",
"admin": "Administrator"
@ -285,8 +285,8 @@
"default": "Domyślny",
"month": "Miesiąc",
"day": "Dzień",
"from": "Od",
"to": "Do",
"hour": "Hour",
"range": "Date Range",
"noDates": "To zadanie nie ma ustawionych dat."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "Członek zespołu został pomyślnie dodany.",
"madeMember": "Użytkownik został pomyślnie mianowany członkiem zespołu.",
"madeAdmin": "Członek zespołu został pomyślnie mianowany administratorem.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Usuń zespół",
"text1": "Czy na pewno chcesz usunąć ten zespół i wszystkich jego członków?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -1,26 +1,26 @@
{
"home": {
"welcomeNight": "Good Night {username}!",
"welcomeMorning": "Good Morning {username}!",
"welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed",
"welcomeNight": "Boa noite, {username}!",
"welcomeMorning": "Bom dia, {username}!",
"welcomeDay": "Olá, {username}!",
"welcomeEvening": "Boa noite, {username}!",
"lastViewed": "Visto por último",
"list": {
"newText": "You can create a new list for your new tasks:",
"new": "New list",
"newText": "Você pode criar uma nova lista para suas novas tarefas:",
"new": "Nova lista",
"importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
}
},
"404": {
"title": "Not found",
"title": "Não encontrado",
"text": "The page you requested does not exist."
},
"ready": {
"loading": "Vikunja is loading…",
"errorOccured": "An error occurred:",
"errorOccured": "Ocorreu um erro:",
"checkApiUrl": "Please check if the api url is correct.",
"noApiUrlConfigured": "No API url was configured. Please set one below:"
"noApiUrlConfigured": "Nenhuma URL de API foi configurada. Por favor, defina uma abaixo:"
},
"offline": {
"title": "You are offline.",
@ -28,10 +28,10 @@
},
"user": {
"auth": {
"username": "Username",
"usernameEmail": "Username Or Email Address",
"username": "Nome de usuário",
"usernameEmail": "Nome de usuário ou endereço de e-mail",
"usernamePlaceholder": "e.g. frederick",
"email": "Email address",
"email": "Endereço de e-mail",
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
"password": "Password",
"passwordPlaceholder": "e.g. •••••••••••",
@ -44,20 +44,20 @@
"totpTitle": "Two Factor Authentication Code",
"totpPlaceholder": "e.g. 123456",
"login": "Login",
"createAccount": "Create account",
"createAccount": "Criar conta",
"loginWith": "Log in with {provider}",
"authenticating": "Authenticating…",
"openIdStateError": "State does not match, refusing to continue!",
"openIdGeneralError": "An error occured while authenticating against the third party.",
"logout": "Logout",
"emailInvalid": "Please enter a valid email address.",
"usernameRequired": "Please provide a username.",
"passwordRequired": "Please provide a password.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
"alreadyHaveAnAccount": "Already have an account?",
"remember": "Stay logged in"
"emailInvalid": "Por favor, insira um endereço de e-mail válido.",
"usernameRequired": "Por favor, insira um nome de usuário.",
"passwordRequired": "Por favor, insira uma senha.",
"showPassword": "Exibir senha",
"hidePassword": "Ocultar senha",
"noAccountYet": "Não possui uma conta ainda?",
"alreadyHaveAnAccount": "Já possui uma conta?",
"remember": "Permanecer conectado"
},
"settings": {
"title": "Settings",
@ -68,7 +68,7 @@
"currentPasswordPlaceholder": "Your current password",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
"updateEmailTitle": "Update Your Email Address",
"updateEmailTitle": "Atualizar seu endereço de e-mail",
"updateEmailNew": "New Email Address",
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
@ -86,34 +86,34 @@
"weekStartMonday": "Monday",
"language": "Language",
"defaultList": "Default List",
"timezone": "Time Zone",
"timezone": "Fuso horário",
"overdueTasksRemindersTime": "Overdue tasks reminder email time"
},
"totp": {
"title": "Two Factor Authentication",
"enroll": "Enroll",
"enroll": "Inscrever-se",
"finishSetupPart1": "To finish your setup, use this secret in your totp app (Google Authenticator or similar):",
"finishSetupPart2": "After that, enter a code from your app below.",
"scanQR": "Alternatively you can scan this QR code:",
"passcode": "Passcode",
"passcodePlaceholder": "A code generated by your totp application",
"setupSuccess": "You've successfully set up two factor authentication!",
"setupSuccess": "Você configurou a autenticação de dois fatores com sucesso!",
"enterPassword": "Please Enter Your Password",
"disable": "Disable two factor authentication",
"confirmSuccess": "You've successfully confirmed your totp setup and can use it from now on!",
"disableSuccess": "Two factor authentication was successfully disabled."
"disableSuccess": "A autenticação de dois fatores foi desativada com êxito."
},
"caldav": {
"title": "CalDAV",
"howTo": "You can connect Vikunja to CalDAV clients to view and manage all tasks from different clients. Enter this url into your client:",
"more": "More information about CalDAV in Vikunja",
"howTo": "Você pode conectar o Vikunja aos clientes de CalDAV para visualizar e gerenciar todas as tarefas de diferentes clientes. Digite esta url em seu cliente:",
"more": "Mais informações sobre CalDAV em Vikunja",
"tokens": "CalDAV Tokens",
"tokensHowTo": "You can use a CalDAV token to use instead of a password to log in the above endpoint.",
"createToken": "Create a token",
"tokenCreated": "Here is your token: {token}",
"wontSeeItAgain": "Write it down, you won't be able to see it again.",
"mustUseToken": "You need to create a CalDAV token if you want to use CalDAV with a third party client. Use the token as the password.",
"usernameIs": "Your username is: {0}"
"tokensHowTo": "Você pode usar um token CalDAV em vez de uma senha para fazer o login no endpoint acima.",
"createToken": "Criar um token",
"tokenCreated": "Aqui está seu token: {token}",
"wontSeeItAgain": "Anote isso, você não poderá vê-lo novamente.",
"mustUseToken": "Você precisa criar um token CalDAV se quiser usar CalDAV com um cliente de terceiros. Use o token como a senha.",
"usernameIs": "Seu usuário é: {0}"
},
"avatar": {
"title": "Avatar",
@ -173,16 +173,16 @@
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
"empty": "Esta lista está atualmente vazia.",
"newTaskCta": "Criar uma nova tarefa.",
"editTask": "Editar Tarefa"
},
"search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists",
"noDescriptionAvailable": "No list description is available.",
"create": {
"header": "New list",
"header": "Nova lista",
"titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.",
@ -212,8 +212,8 @@
"text1": "Are you sure you want to delete this list and all of its contents?",
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The list was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"noTasksToDelete": "This list does not contain any tasks, it should be safe to delete."
"tasksToDelete": "Isto irá remover irrevogavelmente aprox. {count} tarefas.",
"noTasksToDelete": "Esta lista não contém tarefas, deve ser segura para excluir."
},
"duplicate": {
"title": "Duplicate this list",
@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permissão",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -280,261 +280,261 @@
},
"gantt": {
"title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set",
"size": "Size",
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"noDates": "This task has no dates set."
"showTasksWithoutDates": "Mostrar tarefas que não possuem datas definidas",
"size": "Tamanho",
"default": "Padrão",
"month": "Mês",
"day": "Dia",
"hour": "Hour",
"range": "Date Range",
"noDates": "Esta tarefa não tem datas definidas."
},
"table": {
"title": "Table",
"columns": "Columns"
"title": "Tabela",
"columns": "Colunas"
},
"kanban": {
"title": "Kanban",
"limit": "Limit: {limit}",
"noLimit": "Not Set",
"doneBucket": "Done bucket",
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.",
"deleteLast": "You cannot remove the last bucket.",
"addTaskPlaceholder": "Enter the new task title…",
"addTask": "Add a task",
"addAnotherTask": "Add another task",
"addBucket": "Create a new bucket",
"addBucketPlaceholder": "Enter the new bucket title…",
"deleteHeaderBucket": "Delete the bucket",
"deleteBucketText1": "Are you sure you want to delete this bucket?",
"deleteBucketText2": "This will not delete any tasks but move them into the default bucket.",
"deleteBucketSuccess": "The bucket has been deleted successfully.",
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
"collapse": "Collapse this bucket"
"limit": "Limite: {limit}",
"noLimit": "Não definido",
"doneBucket": "Bucket concluído",
"doneBucketHint": "Todas as tarefas movidas para este bucket serão marcadas automaticamente como concluídas.",
"doneBucketHintExtended": "Todas as tarefas movidas para o bucket concluído serão automaticamente concluídas também. Todas as tarefas concluídas de outro lugar serão movidas também.",
"doneBucketSavedSuccess": "O bucket foi marcado como concluído com sucesso.",
"deleteLast": "Você não pode remover o último bucket.",
"addTaskPlaceholder": "Digite o novo título da tarefa…",
"addTask": "Adicionar uma tarefa",
"addAnotherTask": "Adicionar outra tarefa",
"addBucket": "Criar um novo bucket",
"addBucketPlaceholder": "Digite o novo título do bucket…",
"deleteHeaderBucket": "Excluir o bucket",
"deleteBucketText1": "Tem certeza que deseja excluir este bucket?",
"deleteBucketText2": "Isto não vai apagar nenhuma tarefa, mas as moverá para o bucket padrão.",
"deleteBucketSuccess": "O bucket foi excluído com sucesso.",
"bucketTitleSavedSuccess": "O título do bucket foi salvo com sucesso.",
"bucketLimitSavedSuccess": "O limite do bucket foi salvo com sucesso.",
"collapse": "Recolher este bucket"
},
"pseudo": {
"favorites": {
"title": "Favorites"
"title": "Favoritos"
}
}
},
"namespace": {
"title": "Namespaces & Lists",
"title": "Listas & Namespaces",
"namespace": "Namespace",
"showArchived": "Show Archived",
"noneAvailable": "You don't have any namespaces right now.",
"unarchive": "Un-Archive",
"archived": "Archived",
"noLists": "This namespace does not contain any lists.",
"createList": "Create a new list in this namespace.",
"noneAvailable": "Você não tem nenhum namespace no momento.",
"unarchive": "Desarquivar",
"archived": "Arquivado",
"noLists": "Este namespace não contém nenhuma lista.",
"createList": "Criar uma nova lista neste namespace.",
"namespaces": "Namespaces",
"search": "Type to search for a namespace…",
"search": "Digite para procurar por um namespace…",
"create": {
"title": "New namespace",
"titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namespace.",
"tooltip": "What's a namespace?",
"success": "The namespace was successfully created."
"title": "Novo namespace",
"titleRequired": "Por favor, especifique um título.",
"explanation": "Um namespace é uma coleção de listas que você pode compartilhar e usar para organizar suas listas. Na verdade, todas as listas pertencem a um namespace.",
"tooltip": "O que é um namespace?",
"success": "O namespace foi criado com sucesso."
},
"archive": {
"titleArchive": "Archive \"{namespace}\"",
"titleUnarchive": "Un-Archive \"{namespace}\"",
"titleArchive": "Arquivar \"{namespace}\"",
"titleUnarchive": "Desarquivar \"{namespace}\"",
"archiveText": "You won't be able to edit this namespace or create new lists until you un-archive it. This will also archive all lists in this namespace.",
"unarchiveText": "You will be able to create new lists or edit it.",
"success": "The namespace was successfully archived.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"unarchiveText": "Você será capaz de criar novas listas ou editá-las.",
"success": "O namespace foi arquivado com sucesso.",
"unarchiveSuccess": "O namespace foi desarquivado com sucesso.",
"description": "If a namespace is archived, you cannot create new lists or edit it."
},
"delete": {
"title": "Delete \"{namespace}\"",
"title": "Excluir \"{namespace}\"",
"text1": "Are you sure you want to delete this namespace and all of its contents?",
"text2": "This includes all lists and tasks and CANNOT BE UNDONE!",
"success": "The namespace was successfully deleted."
"success": "O namespace foi excluído com sucesso."
},
"edit": {
"title": "Edit \"{namespace}\"",
"success": "The namespace was successfully updated."
"title": "Editar \"{namespace}\"",
"success": "O namespace foi atualizado com sucesso."
},
"share": {
"title": "Share \"{namespace}\""
"title": "Compartilhar \"{namespace}\""
},
"attributes": {
"title": "Namespace Title",
"titlePlaceholder": "The namespace title goes here…",
"description": "Description",
"descriptionPlaceholder": "The namespaces description goes here…",
"color": "Color",
"archived": "Is Archived",
"isArchived": "This namespace is archived"
"title": "Título do Namespace",
"titlePlaceholder": "O título do namespace fica aqui…",
"description": "Descrição",
"descriptionPlaceholder": "A descrição do namespace fica aqui…",
"color": "Cor",
"archived": "Está arquivado",
"isArchived": "Este namespace está arquivado"
},
"pseudo": {
"sharedLists": {
"title": "Shared Lists"
"title": "Listas Compartilhadas"
},
"favorites": {
"title": "Favorites"
"title": "Favoritos"
},
"savedFilters": {
"title": "Filters"
"title": "Filtros"
}
}
},
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"title": "Filtros",
"clear": "Limpar Filtros",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
"description": "Description",
"descriptionPlaceholder": "The description goes here…",
"includeNulls": "Include Tasks which don't have a value set",
"requireAll": "Require all filters to be true for a task to show up",
"showDoneTasks": "Show Done Tasks",
"sortAlphabetically": "Sort Alphabetically",
"enablePriority": "Enable Filter By Priority",
"enablePercentDone": "Enable Filter By Progress",
"title": "Título",
"titlePlaceholder": "O título do filtro salvo fica aqui…",
"description": "Descrição",
"descriptionPlaceholder": "A descrição fica aqui…",
"includeNulls": "Incluir tarefas que não possuem um conjunto de valores",
"requireAll": "Exigir que todos os filtros sejam verdadeiros para uma tarefa ser exibida",
"showDoneTasks": "Mostrar tarefas concluídas",
"sortAlphabetically": "Ordernar alfabeticamente",
"enablePriority": "Ativar Filtro por Prioridade",
"enablePercentDone": "Ativar Filtro por Progresso",
"dueDateRange": "Due Date Range",
"startDateRange": "Start Date Range",
"startDateRange": "Data de início",
"endDateRange": "End Date Range",
"reminderRange": "Reminder Date Range"
},
"create": {
"title": "New Saved Filter",
"title": "Novo filtro salvo",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter"
},
"delete": {
"header": "Delete this saved filter",
"text": "Are you sure you want to delete this saved filter?",
"success": "The filter was deleted successfully."
"success": "O filtro foi excluído com sucesso."
},
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
"success": "O filtro foi salvo com sucesso."
}
},
"migrate": {
"title": "Migrate from other services to Vikunja",
"titleService": "Import your data from {name} into Vikunja",
"import": "Import your data into Vikunja",
"import": "Importe seus dados para o Vikunja",
"description": "Click on the logo of one of the third-party services below to get started.",
"descriptionDo": "Vikunja will import all lists, tasks, notes, reminders and files you have access to.",
"authorize": "To authorize Vikunja to access your {name} Account, click the button below.",
"getStarted": "Get Started",
"inProgress": "Importing in progress…",
"inProgress": "Importação em andamento…",
"alreadyMigrated1": "It looks like you've already imported your stuff from {name} at {date}.",
"alreadyMigrated2": "Importing again is possible, but might create duplicates. Are you sure?",
"confirm": "I am sure, please start migrating now!",
"confirm": "Tenho certeza, comece a migrar agora, por favor!",
"importUpload": "To import data from {name} into Vikunja, click the button below to select a file.",
"upload": "Upload file"
"upload": "Enviar arquivo"
},
"label": {
"title": "Labels",
"manage": "Manage labels",
"title": "Etiquetas",
"manage": "Editar etiquetas",
"description": "Click on a label to edit it. You can edit all labels you created, you can use all labels which are associated with a task to whose list you have access.",
"newCTA": "You currently do not have any labels.",
"search": "Type to search for a label…",
"newCTA": "Você não tem nenhuma etiqueta atualmente.",
"search": "Digite para procurar por uma etiqueta…",
"create": {
"header": "New label",
"title": "Create a new label",
"titleRequired": "Please specify a title.",
"success": "The label was successfully created."
"header": "Nova etiqueta",
"title": "Criar uma nova etiqueta",
"titleRequired": "Por favor, especifique um título.",
"success": "A etiqueta foi criada com sucesso."
},
"edit": {
"header": "Edit Label",
"header": "Editar etiqueta",
"forbidden": "You are not allowed to edit this label because you dont own it.",
"success": "The label was successfully updated."
"success": "A etiqueta foi atualizada com sucesso."
},
"deleteSuccess": "The label was successfully deleted.",
"deleteSuccess": "A etiqueta foi excluída com sucesso.",
"attributes": {
"title": "Title",
"titlePlaceholder": "The label title goes here…",
"description": "Description",
"descriptionPlaceholder": "Label description",
"color": "Color"
"title": "Título",
"titlePlaceholder": "O título da etiqueta fica aqui…",
"description": "Descrição",
"descriptionPlaceholder": "Descrição da etiqueta",
"color": "Cor"
}
},
"sharing": {
"authenticating": "Authenticating…",
"passwordRequired": "This shared list requires a password. Please enter it below:",
"error": "An error occured.",
"invalidPassword": "The password is invalid."
"authenticating": "Autenticando…",
"passwordRequired": "Esta lista compartilhada requer uma senha. Por favor, digite-a abaixo:",
"error": "Ocorreu um erro.",
"invalidPassword": "A senha é inválida."
},
"navigation": {
"overview": "Overview",
"overview": "Visão geral",
"upcoming": "Upcoming",
"settings": "Settings",
"imprint": "Imprint",
"privacy": "Privacy Policy"
"settings": "Configurações",
"imprint": "Imprimir",
"privacy": "Política de Privacidade"
},
"misc": {
"loading": "Loading…",
"save": "Save",
"delete": "Delete",
"confirm": "Confirm",
"cancel": "Cancel",
"refresh": "Refresh",
"disable": "Disable",
"copy": "Copy to clipboard",
"copyError": "Copy to clipboard failed",
"search": "Search",
"searchPlaceholder": "Type to search…",
"previous": "Previous",
"next": "Next",
"poweredBy": "Powered by Vikunja",
"loading": "Carregando…",
"save": "Salvar",
"delete": "Excluir",
"confirm": "Confirmar",
"cancel": "Cancelar",
"refresh": "Atualizar",
"disable": "Desativar",
"copy": "Copiar para área de transferência",
"copyError": "Falha ao copiar para área de transferência",
"search": "Pesquisar",
"searchPlaceholder": "Digite para pesquisar…",
"previous": "Voltar",
"next": "Avançar",
"poweredBy": "Desenvolvido por Vikunja",
"info": "Info",
"create": "Create",
"create": "Criar",
"doit": "Do it!",
"saving": "Saving…",
"saved": "Saved!",
"default": "Default",
"close": "Close",
"download": "Download",
"showMenu": "Show the menu",
"hideMenu": "Hide the menu",
"forExample": "For example:",
"welcomeBack": "Welcome Back!",
"saving": "Salvando…",
"saved": "Salvo!",
"default": "Padrão",
"close": "Fechar",
"download": "Baixar",
"showMenu": "Mostrar o menu",
"hideMenu": "Esconder o menu",
"forExample": "Por exemplo:",
"welcomeBack": "Bem-vindo de volta!",
"custom": "Custom",
"id": "ID",
"created": "Created at",
"actions": "Actions",
"created": "Criado em",
"actions": "Ações",
"cannotBeUndone": "This cannot be undone!"
},
"input": {
"resetColor": "Reset Color",
"resetColor": "Restaurar Cor",
"datepicker": {
"today": "Today",
"tomorrow": "Tomorrow",
"nextMonday": "Next Monday",
"thisWeekend": "This Weekend",
"today": "Hoje",
"tomorrow": "Amanhã",
"nextMonday": "Próxima segunda-feira",
"thisWeekend": "Este fim de semana",
"laterThisWeek": "Later This Week",
"nextWeek": "Next Week",
"chooseDate": "Choose a date"
"nextWeek": "Próxima semana",
"chooseDate": "Escolha uma data"
},
"editor": {
"edit": "Edit",
"done": "Done",
"heading1": "Heading 1",
"heading2": "Heading 2",
"heading3": "Heading 3",
"edit": "Editar",
"done": "Concluído",
"heading1": "Título 1",
"heading2": "Título 2",
"heading3": "Título 3",
"headingSmaller": "Heading Smaller",
"headingBigger": "Heading Bigger",
"bold": "Bold",
"italic": "Italic",
"strikethrough": "Strikethrough",
"code": "Code",
"quote": "Quote",
"unorderedList": "Unordered List",
"orderedList": "Ordered List",
"bold": "Negrito",
"italic": "Itálico",
"strikethrough": "Riscado",
"code": "Código",
"quote": "Citação",
"unorderedList": "Lista não ordenada",
"orderedList": "Lista ordenada",
"cleanBlock": "Clean Block",
"link": "Link",
"image": "Image",
"table": "Table",
"horizontalRule": "Horizontal Rule",
"sideBySide": "Side By Side",
"image": "Imagem",
"table": "Tabela",
"horizontalRule": "Linha horizontal",
"sideBySide": "Lado a Lado",
"guide": "Guide"
},
"multiselect": {
@ -589,80 +589,80 @@
"beginningOfThisWeek": "The beginning of this week at 00:00",
"endOfThisWeek": "The end of this week",
"in30Days": "In 30 days",
"datePlusMonth": "{0} plus one month at 00:00 of that day"
"datePlusMonth": "{0} mais um mês às 00:00 desse dia"
}
}
},
"task": {
"task": "Task",
"new": "Create a new task",
"delete": "Delete this task",
"createSuccess": "The task was successfully created.",
"addReminder": "Add a new reminder…",
"doneSuccess": "The task was successfully marked as done.",
"undoneSuccess": "The task was successfully un-marked as done.",
"openDetail": "Open task detail view",
"checklistTotal": "{checked} of {total} tasks",
"checklistAllDone": "{total} tasks",
"task": "Tarefa",
"new": "Criar uma nova tarefa",
"delete": "Excluir esta tarefa",
"createSuccess": "A tarefa foi criada com sucesso.",
"addReminder": "Adicionar um novo lembrete…",
"doneSuccess": "A tarefa foi marcada como feita com sucesso.",
"undoneSuccess": "A tarefa foi desmarcada como feita com sucesso.",
"openDetail": "Abrir detalhes da tarefa",
"checklistTotal": "{checked} de {total} tarefas",
"checklistAllDone": "{total} tarefas",
"show": {
"titleCurrent": "Current Tasks",
"titleDates": "Tasks from {from} until {to}",
"noDates": "Show tasks without dates",
"overdue": "Show overdue tasks",
"titleCurrent": "Tarefas atuais",
"titleDates": "Tarefas de {from} até {to}",
"noDates": "Mostrar tarefas sem datas",
"overdue": "Mostrar tarefas atrasadas",
"fromuntil": "Tasks from {from} until {until}",
"select": "Select a date range",
"noTasks": "Nothing to do — Have a nice day!"
"select": "Selecione um intervalo de datas",
"noTasks": "Nada a fazer — Tenha um ótimo dia!"
},
"detail": {
"chooseDueDate": "Click here to set a due date",
"chooseStartDate": "Click here to set a start date",
"chooseEndDate": "Click here to set an end date",
"move": "Move task to a different list",
"done": "Mark task done!",
"undone": "Mark as undone",
"created": "Created {0} by {1}",
"updated": "Updated {0}",
"doneAt": "Done {0}",
"updateSuccess": "The task was saved successfully.",
"deleteSuccess": "The task has been deleted successfully.",
"done": "Marcar tarefa como concluída!",
"undone": "Marcar como não concluído",
"created": "Criado {0} por {1}",
"updated": "Atualizado {0}",
"doneAt": "Concluído {0}",
"updateSuccess": "A tarefa foi salva com sucesso.",
"deleteSuccess": "A tarefa foi excluída com sucesso.",
"belongsToList": "This task belongs to list '{list}'",
"due": "Due {at}",
"closePopup": "Close popup",
"closePopup": "Fechar janela",
"delete": {
"header": "Delete this task",
"text1": "Are you sure you want to remove this task?",
"header": "Excluir esta tarefa",
"text1": "Tem certeza que deseja remover esta tarefa?",
"text2": "This will also remove all attachments, reminders and relations associated with this task and cannot be undone!"
},
"actions": {
"assign": "Assign to User",
"label": "Add Labels",
"priority": "Set Priority",
"dueDate": "Set Due Date",
"startDate": "Set Start Date",
"endDate": "Set End Date",
"reminders": "Set Reminders",
"assign": "Atribuir usuário",
"label": "Adicionar etiquetas",
"priority": "Definir prioridade",
"dueDate": "Definir prazo",
"startDate": "Definir data de início",
"endDate": "Definir data de término",
"reminders": "Definir lembretes",
"repeatAfter": "Set Repeating Interval",
"percentDone": "Set Progress",
"attachments": "Add Attachments",
"percentDone": "Definir progresso",
"attachments": "Adicionar anexos",
"relatedTasks": "Add Relation",
"moveList": "Move",
"color": "Set Color",
"delete": "Delete",
"favorite": "Add to Favorites",
"unfavorite": "Remove from Favorites"
"moveList": "Mover",
"color": "Definir cor",
"delete": "Excluir",
"favorite": "Adicionar aos favoritos",
"unfavorite": "Remover dos favoritos"
}
},
"attributes": {
"assignees": "Assignees",
"color": "Color",
"created": "Created",
"createdBy": "Created By",
"description": "Description",
"done": "Done",
"dueDate": "Due Date",
"endDate": "End Date",
"labels": "Labels",
"percentDone": "Progress",
"color": "Cor",
"created": "Criado",
"createdBy": "Criador por",
"description": "Descrição",
"done": "Concluído",
"dueDate": "Data de vencimento",
"endDate": "Data de término",
"labels": "Etiquetas",
"percentDone": "Progresso",
"priority": "Priority",
"relatedTasks": "Related Tasks",
"reminders": "Reminders",
@ -711,12 +711,12 @@
"loading": "Loading comments…",
"edited": "edited {date}",
"creating": "Creating comment…",
"placeholder": "Add your comment…",
"comment": "Comment",
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"placeholder": "Adicione seu comentário…",
"comment": "Comentário",
"delete": "Apagar este comentário",
"deleteText1": "Tem certeza que deseja apagar este comentário?",
"deleteSuccess": "O comentário foi apagado com sucesso.",
"addedSuccess": "O comentário foi adicionado com sucesso."
},
"deferDueDate": {
"title": "Defer due date",
@ -825,55 +825,56 @@
}
},
"team": {
"title": "Teams",
"title": "Equipes",
"noTeams": "You are currently not part of any teams.",
"create": {
"title": "Create a new team",
"success": "The team was successfully created."
"title": "Criar uma equipe",
"success": "A equipe foi criada com sucesso."
},
"edit": {
"title": "Edit Team \"{team}\"",
"members": "Team Members",
"search": "Type to search a user…",
"addUser": "Add to team",
"addUser": "Adicionar à equipe",
"makeMember": "Make Member",
"makeAdmin": "Make Admin",
"success": "The team was successfully updated.",
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"makeAdmin": "Tornar administrador",
"success": "A equipe foi atualizada com sucesso.",
"userAddedSuccess": "O membro da equipe foi adicionado com sucesso.",
"madeMember": "O integrante da equipe foi promovido a membro com sucesso.",
"madeAdmin": "O membro da equipe foi promovido a administrador com sucesso.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"header": "Excluir a equipe",
"text1": "Are you sure you want to delete this team and all of its members?",
"text2": "All team members will lose access to lists and namespaces shared with this team. This CANNOT BE UNDONE!",
"success": "The team was successfully deleted."
"success": "A equipe foi excluída com sucesso."
},
"deleteUser": {
"header": "Remove a user from the team",
"text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team."
"success": "O usuário foi removido da equipe com sucesso."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},
"attributes": {
"name": "Team Name",
"namePlaceholder": "The team's name goes here…",
"nameRequired": "Please specify a name.",
"nameRequired": "Por favor, especifique um nome.",
"description": "Description",
"descriptionPlaceholder": "The teams description goes here…",
"admin": "Admin",
"member": "Member"
"admin": "Administrador",
"member": "Membro"
}
},
"keyboardShortcuts": {
"title": "Keyboard Shortcuts",
"general": "General",
"title": "Atalhos de teclado",
"general": "Geral",
"allPages": "These shortcuts work on all pages.",
"currentPageOnly": "These shortcuts work only on the current page.",
"somePagesOnly": "These shortcuts work only on some pages.",
@ -885,7 +886,7 @@
"done": "Mark task done / undone",
"assign": "Assign this task to a user",
"labels": "Add labels to this task",
"dueDate": "Change the due date of this task",
"dueDate": "Alterar a data de vencimento desta tarefa",
"attachment": "Add an attachment to this task",
"related": "Modify related tasks of this task",
"color": "Change the color of this task",
@ -914,10 +915,10 @@
"do": "Update Now"
},
"menu": {
"edit": "Edit",
"archive": "Archive",
"duplicate": "Duplicate",
"delete": "Delete",
"edit": "Editar",
"archive": "Arquivar",
"duplicate": "Duplicar",
"delete": "Excluir",
"unarchive": "Un-Archive",
"setBackground": "Set background",
"share": "Share",
@ -929,60 +930,60 @@
"change": "change",
"use": "Using Vikunja installation at {0}",
"error": "Could not find or use Vikunja installation at \"{domain}\". Please try a different url.",
"success": "Using Vikunja installation at \"{domain}\".",
"urlRequired": "A url is required."
"success": "Usando a instalação Vikunja em \"{domain}\".",
"urlRequired": "Uma url é necessária."
},
"loadingError": {
"failed": "Loading failed, please {0}. If the error persists, please {1}.",
"tryAgain": "try again",
"contact": "contact us"
"tryAgain": "tente novamente",
"contact": "contate-nos"
},
"notification": {
"title": "Notifications",
"title": "Notificações",
"none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions on namespaces, lists or tasks you subscribed to happen."
},
"quickActions": {
"commands": "Commands",
"placeholder": "Type a command or search…",
"commands": "Comandos",
"placeholder": "Digite um comando ou pesquise…",
"hint": "You can use {list} to limit the search to a list. Combine {list} or {label} (labels) with a search query to search for a task with these labels or on that list. Use {assignee} to only search for teams.",
"tasks": "Tasks",
"lists": "Lists",
"teams": "Teams",
"newList": "Enter the title of the new list…",
"tasks": "Tarefas",
"lists": "Listas",
"teams": "Equipes",
"newList": "Digite o título da nova lista…",
"newTask": "Enter the title of the new task…",
"newNamespace": "Enter the title of the new namespace…",
"newTeam": "Enter the name of the new team…",
"createTask": "Create a task in the current list ({title})",
"createList": "Create a list in the current namespace ({title})",
"cmds": {
"newTask": "New task",
"newList": "New list",
"newNamespace": "New namespace",
"newTeam": "New team"
"newTask": "Nova tarefa",
"newList": "Nova lista",
"newNamespace": "Novo namespace",
"newTeam": "Nova equipe"
}
},
"date": {
"locale": "en",
"locale": "pt-br",
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"error": {
"error": "Error",
"success": "Success",
"error": "Erro",
"success": "Sucesso",
"0001": "You're not allowed to do that.",
"1001": "A user with this username already exists.",
"1002": "A user with this email address already exists.",
"1004": "No username and password specified.",
"1005": "The user does not exist.",
"1006": "Could not get the user id.",
"1005": "O usuário não existe.",
"1006": "Não foi possível obter o ID do usuário.",
"1008": "No password reset token provided.",
"1009": "Invalid password reset token.",
"1010": "Invalid email confirm token.",
"1011": "Wrong username or password.",
"1012": "Email address of the user not confirmed.",
"1013": "New password is empty.",
"1014": "Old password is empty.",
"1011": "Usuário ou senha incorretos.",
"1012": "Endereço de e-mail do usuário não confirmado.",
"1013": "A senha nova está vazia.",
"1014": "A senha antiga está vazia.",
"1015": "Totp is already enabled for this user.",
"1016": "Totp is not enabled for this user.",
"1017": "The totp passcode is invalid.",

View File

@ -268,7 +268,7 @@
"updatedSuccess": "{type} foi adicionado com sucesso."
},
"right": {
"title": "Permissões",
"title": "Permissão",
"read": "Apenas de leitura",
"readWrite": "Leitura e escrita",
"admin": "Administração"
@ -285,8 +285,8 @@
"default": "Padrão",
"month": "Mês",
"day": "Dia",
"from": "De",
"to": "Até",
"hour": "Hora",
"range": "Intervalo de Datas",
"noDates": "Esta tarefa não tem datas definidas."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "O membro da equipa foi adicionado com sucesso.",
"madeMember": "O membro da equipa foi tornado membro com sucesso.",
"madeAdmin": "O membro da equipa foi tornado admin com sucesso.",
"mustSelectUser": "Por favor, seleciona um utilizador.",
"delete": {
"header": "Eliminar equipa",
"text1": "Tens a certeza que pretendes eliminar esta equipa e todos os seus membros?",
@ -857,7 +858,7 @@
"leave": {
"title": "Sair da equipa",
"text1": "Tens a certeza de que queres sair desta equipa?",
"text2": "Vais perder acesso a todas as listas e espaços a que esta equipa tem acesso. Se mudares de ideias, vais necessitar que um administrador da equipa te adicione novamente.",
"text2": "Vais perder o acesso a todas as listas e espaços a que esta equipa tem acesso. Se mudares de ideias, vais necessitar que um administrador da equipa te adicione novamente.",
"success": "Saíste da equipa com sucesso."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "{type} успешно добавлен."
},
"right": {
"title": "Права",
"title": "Permission",
"read": "Только чтение",
"readWrite": "Чтение и запись",
"admin": "Админ"
@ -285,8 +285,8 @@
"default": "По умолчанию",
"month": "Месяц",
"day": "День",
"from": "С",
"to": "По",
"hour": "Hour",
"range": "Date Range",
"noDates": "В этой задаче нет установленной даты."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "Участник добавлен.",
"madeMember": "Участник команды теперь участник.",
"madeAdmin": "Участник команды теперь администратор.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Удалить команду",
"text1": "Удалить эту команду вместе с участниками?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "{type} đã được thêm thành công."
},
"right": {
"title": "Quyền hạn",
"title": "Permission",
"read": "Chỉ đọc",
"readWrite": "Đọc & ghi",
"admin": "Quản trị viên"
@ -285,8 +285,8 @@
"default": "Mặc định",
"month": "Tháng",
"day": "Ngày",
"from": "Từ",
"to": "Đến",
"hour": "Hour",
"range": "Date Range",
"noDates": "Công việc này không thiết lập ngày."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "Thành viên Team đã được thêm.",
"madeMember": "Vai trò thanh viên đã được cập nhật.",
"madeAdmin": "Vai trò thành viên đã được cập nhật.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Giải tán Team",
"text1": "Bạn có chắc giải tán Team này không?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -268,7 +268,7 @@
"updatedSuccess": "The {type} was successfully added."
},
"right": {
"title": "Right",
"title": "Permission",
"read": "Read only",
"readWrite": "Read & write",
"admin": "Admin"
@ -285,8 +285,8 @@
"default": "Default",
"month": "Month",
"day": "Day",
"from": "From",
"to": "To",
"hour": "Hour",
"range": "Date Range",
"noDates": "This task has no dates set."
},
"table": {
@ -842,6 +842,7 @@
"userAddedSuccess": "The team member was successfully added.",
"madeMember": "The team member was successfully made member.",
"madeAdmin": "The team member was successfully made admin.",
"mustSelectUser": "Please select a user.",
"delete": {
"header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?",
@ -857,7 +858,7 @@
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"text2": "You will lose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
}
},

View File

@ -0,0 +1,60 @@
import {computed, ref, watch} from 'vue'
import type dayjs from 'dayjs'
import type ILocale from 'dayjs/locale/*'
import {i18n, type SupportedLocale, type ISOLanguage} from '@/i18n'
export const DAYJS_LOCALE_MAPPING = {
'de-de': 'de',
'de-swiss': 'de-at',
'ru-ru': 'ru',
'fr-fr': 'fr',
'vi-vn': 'vi',
'it-it': 'it',
'cs-cz': 'cs',
'pl-pl': 'pl',
'nl-nl': 'nl',
'pt-pt': 'pt',
'zh-cn': 'zh-cn',
} as Record<SupportedLocale, ISOLanguage>
export const DAYJS_LANGUAGE_IMPORTS = {
'de-de': () => import('dayjs/locale/de'),
'de-swiss': () => import('dayjs/locale/de-at'),
'ru-ru': () => import('dayjs/locale/ru'),
'fr-fr': () => import('dayjs/locale/fr'),
'vi-vn': () => import('dayjs/locale/vi'),
'it-it': () => import('dayjs/locale/it'),
'cs-cz': () => import('dayjs/locale/cs'),
'pl-pl': () => import('dayjs/locale/pl'),
'nl-nl': () => import('dayjs/locale/nl'),
'pt-pt': () => import('dayjs/locale/pt'),
'zh-cn': () => import('dayjs/locale/zh-cn'),
} as Record<SupportedLocale, () => Promise<ILocale>>
export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) {
const dayjsLanguageLoaded = ref(false)
watch(
() => i18n.global.locale,
async (currentLanguage: string) => {
if (!dayjsGlobal) {
return
}
const dayjsLanguageCode = DAYJS_LOCALE_MAPPING[currentLanguage.toLowerCase()] || currentLanguage.toLowerCase()
dayjsLanguageLoaded.value = dayjsGlobal.locale() === dayjsLanguageCode
if (dayjsLanguageLoaded.value) {
return
}
await DAYJS_LANGUAGE_IMPORTS[currentLanguage.toLowerCase()]()
dayjsGlobal.locale(dayjsLanguageCode)
dayjsLanguageLoaded.value = true
},
{immediate: true},
)
// we export the loading state since that's easier to work with
const isLoading = computed(() => !dayjsLanguageLoaded.value)
return isLoading
}

View File

@ -1,5 +1,5 @@
import {i18n} from '@/i18n'
import { notify } from '@kyvg/vue3-notification'
import {notify} from '@kyvg/vue3-notification'
export const getErrorText = (r) => {

View File

@ -1,6 +1,28 @@
import {SECONDS_A_HOUR} from '@/constants/date'
import { REPEAT_TYPES, type IRepeatAfter } from '@/types/IRepeatAfter'
import { nativeEnum, number, object, preprocess } from 'zod'
/**
* Parses `repeatAfterSeconds` into a usable js object.
*/
export function parseRepeatAfter(repeatAfterSeconds: number): IRepeatAfter {
let repeatAfter: IRepeatAfter = {type: 'hours', amount: repeatAfterSeconds / SECONDS_A_HOUR}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterSeconds % SECONDS_A_DAY === 0) {
if (repeatAfterSeconds % SECONDS_A_WEEK === 0) {
repeatAfter = {type: 'weeks', amount: repeatAfterSeconds / SECONDS_A_WEEK}
} else if (repeatAfterSeconds % SECONDS_A_MONTH === 0) {
repeatAfter = {type:'months', amount: repeatAfterSeconds / SECONDS_A_MONTH}
} else if (repeatAfterSeconds % SECONDS_A_YEAR === 0) {
repeatAfter = {type: 'years', amount: repeatAfterSeconds / SECONDS_A_YEAR}
} else {
repeatAfter = {type: 'days', amount: repeatAfterSeconds / SECONDS_A_DAY}
}
}
return repeatAfter
}
export const RepeatsSchema = preprocess(
(repeats: unknown) => {
// Parses the "repeat after x seconds" from the task into a usable js object inside the task.
@ -9,32 +31,7 @@ export const RepeatsSchema = preprocess(
return repeats
}
const repeatAfterHours = (repeats / 60) / 60
const repeatAfter : IRepeatAfter = {
type: 'hours',
amount: repeatAfterHours,
}
// if its dividable by 24, its something with days, otherwise hours
if (repeatAfterHours % 24 === 0) {
const repeatAfterDays = repeatAfterHours / 24
if (repeatAfterDays % 7 === 0) {
repeatAfter.type = 'weeks'
repeatAfter.amount = repeatAfterDays / 7
} else if (repeatAfterDays % 30 === 0) {
repeatAfter.type = 'months'
repeatAfter.amount = repeatAfterDays / 30
} else if (repeatAfterDays % 365 === 0) {
repeatAfter.type = 'years'
repeatAfter.amount = repeatAfterDays / 365
} else {
repeatAfter.type = 'days'
repeatAfter.amount = repeatAfterDays
}
}
return repeatAfter
return parseRepeatAfter(repeats)
},
object({
type: nativeEnum(REPEAT_TYPES),

View File

@ -12,6 +12,8 @@ import type {IRelationKind} from '@/types/IRelationKind'
import type {IRepeatAfter} from '@/types/IRepeatAfter'
import type {IRepeatMode} from '@/types/IRepeatMode'
import type {PartialWithId} from '@/types/PartialWithId'
export interface ITask extends IAbstract {
id: number
title: string
@ -34,7 +36,7 @@ export interface ITask extends IAbstract {
percentDone: number
relatedTasks: Partial<Record<IRelationKind, ITask[]>>
attachments: IAttachment[]
coverImageAttachmentId: IAttachment['id']
coverImageAttachmentId: IAttachment['id'] | null
identifier: string
index: number
isFavorite: boolean
@ -49,4 +51,6 @@ export interface ITask extends IAbstract {
listId: IList['id'] // Meta, only used when creating a new task
bucketId: IBucket['id']
}
}
export type ITaskPartialWithId = PartialWithId<ITask>

View File

@ -20,4 +20,7 @@ export interface IUser extends IAbstract {
created: Date
updated: Date
settings: IUserSettings
isLocalUser: boolean
deletionScheduledAt: string | Date | null
}

View File

@ -8,6 +8,7 @@ export interface IUserSettings extends IAbstract {
discoverableByName: boolean
discoverableByEmail: boolean
overdueTasksRemindersEnabled: boolean
overdueTasksRemindersTime: any
defaultListId: undefined | IList['id']
weekStart: 0 | 1 | 2 | 3 | 4 | 5 | 6
timezone: string

View File

@ -6,7 +6,7 @@ export default class EmailUpdateModel extends AbstractModel<IEmailUpdate> implem
newEmail = ''
password = ''
constructor(data : Partial<IEmailUpdate>) {
constructor(data : Partial<IEmailUpdate> = {}) {
super()
this.assignData(data)
}

View File

@ -6,7 +6,7 @@ export default class PasswordUpdateModel extends AbstractModel<IPasswordUpdate>
newPassword = ''
oldPassword = ''
constructor(data: Partial<IPasswordUpdate>) {
constructor(data: Partial<IPasswordUpdate> = {}) {
super()
this.assignData(data)
}

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