Compare commits

..

456 commits

Author SHA1 Message Date
renovate
90ecea74c7 chore(deps): update dependency vue-tsc to v1.0.5 (#2527)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2527
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-12 06:20:50 +00:00
renovate
9bde803ef9 chore(deps): update dependency vue-tsc to v1.0.4 (#2526)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2526
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-11 21:44:21 +00:00
renovate
82ac8aeb6c chore(deps): update dependency cypress to v10.10.0 (#2525)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2525
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-11 19:36:04 +00:00
renovate
b73d285c88 chore(deps): update dependency @cypress/vite-dev-server to v3.3.1 (#2523)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2523
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-11 14:14:50 +00:00
renovate
dd252273d7 chore(deps): update dependency rollup to v3 (#2524)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2524
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-11 14:14:41 +00:00
renovate
47cc400417 fix(deps): update dependency minimist to v1.2.7 (#2521)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2521
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-11 05:27:16 +00:00
renovate
5826e2ebcc chore(deps): update dependency @types/node to v16.11.65 (#2520)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2520
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-11 05:26:35 +00:00
drone
e5b60b7cd3 [skip ci] Updated translations via Crowdin 2022-10-11 00:24:33 +00:00
renovate
a47167197d chore(deps): update typescript-eslint monorepo to v5.40.0 (#2519)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2519
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 20:06:50 +00:00
renovate
e811695cb0 chore(deps): update pnpm to v7.13.4 (#2518)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2518
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 20:05:57 +00:00
renovate
87ad58b4b7 chore(deps): update dependency vitest to v0.24.1 (#2517)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2517
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 20:05:13 +00:00
renovate
3e8e5bb554 fix(deps): update sentry-javascript monorepo to v7.15.0 (#2516)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2516
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 14:42:13 +00:00
renovate
322724c2a6 chore(deps): update dependency vite to v3.1.7 (#2515)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2515
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 09:08:16 +00:00
renovate
37956b5933 chore(deps): update dependency caniuse-lite to v1.0.30001418 (#2513)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2513
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 06:55:32 +00:00
renovate
be64831035 chore(deps): update dependency netlify-cli to v12.0.7 (#2514)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2514
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-10 06:54:45 +00:00
renovate
8458e8f687 chore(deps): update dependency vue-tsc to v1.0.3 (#2512)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2512
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-09 20:49:08 +00:00
kolaente
c6d6da3171
fix: lint 2022-10-09 22:46:18 +02:00
kolaente
1af4f7811a
feat: add TickTick migrator support 2022-10-09 22:42:40 +02:00
renovate
35155034e0 chore(deps): update dependency vue-tsc to v1.0.2 (#2510)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2510
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-09 15:14:27 +00:00
kolaente
feeaca2c02
feat: allow users to leave a team they're in 2022-10-09 16:49:26 +02:00
renovate
4784e3a22f chore(deps): update pnpm to v7.13.3 (#2511)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2511
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-09 14:15:41 +00:00
renovate
ba28617464 chore(deps): update dependency express to v4.18.2 2022-10-09 11:57:00 +00:00
renovate
766f2b7461 fix(deps): update dependency pinia to v2.0.23 (#2509)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2509
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-09 11:06:58 +00:00
drone
6c1cd9f911 [skip ci] Updated translations via Crowdin 2022-10-09 00:24:25 +00:00
renovate
48cb3defc4 chore(deps): update dependency vue-tsc to v1.0.1 (#2507)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2507
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-08 19:38:34 +00:00
renovate
59cb0c82f9 chore(deps): update dependency eslint to v8.25.0 2022-10-07 23:03:56 +00:00
kolaente
6d587fad6e
fix(filters): page freezing when entering a date as a result of an endless loop
Resolves https://kolaente.dev/vikunja/frontend/issues/2384
2022-10-07 19:49:57 +02:00
kolaente
458df80443
chore: update happy-dom less frequently 2022-10-07 18:17:20 +02:00
renovate
96a8308d16 chore(deps): update dependency happy-dom to v7.4.0 (#2505)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2505
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-07 16:16:03 +00:00
kolaente
0a29197715
fix: make sure subscriptions are properly inherited between namespaces and lists 2022-10-07 18:15:05 +02:00
kolaente
172d353df7
fix: make sure subscription strings work consistently across languages 2022-10-07 18:15:05 +02:00
kolaente
a895bde661
fix: make sure subscriptions are properly inherited between lists and namespaces 2022-10-07 18:15:05 +02:00
renovate
4ebe17f4f3 chore(deps): update dependency vue-tsc to v1 (#2504)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2504
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-07 14:15:32 +00:00
renovate
59b0f12424 chore(deps): update dependency vitest to v0.24.0 (#2503)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2503
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-07 13:11:04 +00:00
renovate
44549bb441 chore(deps): update dependency happy-dom to v7.3.0 (#2502)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2502
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-07 11:13:29 +00:00
drone
c8aa3db205 [skip ci] Updated translations via Crowdin 2022-10-07 00:24:16 +00:00
kolaente
74a9b9ab1b
feat: show done tasks as strikethrough when searching for new tasks to relate 2022-10-06 22:41:53 +02:00
renovate
5b733ffa8a chore(deps): update dependency happy-dom to v7.0.6 (#2500)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2500
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-06 20:31:27 +00:00
renovate
b62ec79939 chore(deps): update dependency @cypress/vite-dev-server to v3.3.0 (#2501)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2501
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-06 20:31:16 +00:00
renovate
17b31e0b95 chore(deps): update dependency happy-dom to v7.0.4 (#2499)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2499
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-06 16:30:23 +00:00
kolaente
bc1e366750
fix(tasks): don't allow adding the same assignee multiple times
See https://community.vikunja.io/t/task-can-be-assigned-twice-or-more-to-the-same-user/883
2022-10-06 18:07:43 +02:00
renovate
d4c179c862 chore(deps): update dependency vite to v3.1.6 2022-10-06 14:03:21 +00:00
renovate
d77addd266 fix(deps): update dependency vue-flatpickr-component to v9.0.8 (#2494)
This PR contains the following updates:

| Package | Type | Update | Change |
|---|---|---|---|
| [vue-flatpickr-component](https://github.com/ankurk91/vue-flatpickr-component) | dependencies | patch | [`9.0.6` -> `9.0.8`](https://renovatebot.com/diffs/npm/vue-flatpickr-component/9.0.6/9.0.8) |

---

### Release Notes

<details>
<summary>ankurk91/vue-flatpickr-component</summary>

### [`v9.0.8`](https://github.com/ankurk91/vue-flatpickr-component/blob/HEAD/CHANGELOG.md#&#8203;908-httpsgithubcomankurk91vue-flatpickr-componentcompare906908)

[Compare Source](13c93d0e16...9.0.8)

-   Add type definition
-   Allow to run with `@vue/compat`

### [`v9.0.7`](https://github.com/ankurk91/vue-flatpickr-component/compare/9.0.6...13c93d0e16884cf9b788a48a4af2d6783f242304)

[Compare Source](https://github.com/ankurk91/vue-flatpickr-component/compare/9.0.6...13c93d0e16884cf9b788a48a4af2d6783f242304)

</details>

---

### Configuration

📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined).

🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied.

♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

🔕 **Ignore**: Close this PR and you won't be reminded about this update again.

---

 - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, click this checkbox.

---

This PR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate).
<!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiIzMi4xNTMuNCIsInVwZGF0ZWRJblZlciI6IjMyLjE1My40In0=-->

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2494
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-06 13:10:33 +00:00
renovate
5be75f3c54 chore(deps): update pnpm to v7.13.2 2022-10-06 12:49:54 +00:00
renovate
8e905de41b fix(deps): update sentry-javascript monorepo to v7.14.2 2022-10-06 12:03:21 +00:00
renovate
07d38a4aa3 chore(deps): update dependency happy-dom to v7.0.2 2022-10-06 10:29:07 +00:00
renovate
ab75e3ab50 chore(deps): update dependency vite to v3.1.5 2022-10-06 10:03:41 +00:00
renovate
b806a01e95 chore(deps): update dependency happy-dom to v7 (#2492)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2492
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-05 16:14:36 +00:00
kolaente
820db3e96d
feat(openid): show error message from query after being redirected from third party
Resolves https://kolaente.dev/vikunja/frontend/issues/2111
2022-10-05 18:02:03 +02:00
kolaente
f405b2105b
fix: lint 2022-10-05 17:57:55 +02:00
kolaente
3af20b6220
fix: don't try to render auth routes when the user is not authenticated
Resolves #2419
2022-10-05 16:51:35 +02:00
kolaente
38fc157f24
feat(tests): add tests for task attachments 2022-10-05 16:40:42 +02:00
kolaente
01f648c20c
fix(task): setting a label would not show up on the kanban board after setting it 2022-10-05 16:27:12 +02:00
kolaente
1be516a905
fix(task): setting progress was not properly saved 2022-10-05 16:06:41 +02:00
kolaente
fd71de4b5d
fix(task): setting a priority was not properly saved 2022-10-05 16:02:44 +02:00
konrad
31e39aa6c8 feat(task): cover image for tasks (#2460)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2460
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-05 13:28:09 +00:00
kolaente
f01107fd73
feat: add indicator if an attachment is task cover 2022-10-05 15:04:03 +02:00
kolaente
a7731370a0
chore: better wording for cover set button 2022-10-05 14:56:11 +02:00
kolaente
84a1abf347
fix: lint 2022-10-05 14:56:11 +02:00
kolaente
ee3965eae9
chore(task): move cover image setter to store 2022-10-05 14:56:11 +02:00
kolaente
fad72e091b
chore(i18n): use global scope 2022-10-05 14:56:11 +02:00
kolaente
eb80bfa00d
chore: add line-wrap 2022-10-05 14:56:11 +02:00
kolaente
43258ab74e
fix: lint 2022-10-05 14:56:11 +02:00
kolaente
877e425055
feat: promote an attachment to task cover image 2022-10-05 14:56:11 +02:00
kolaente
054d70cbe5
fix: unset cover image when the task does not have one 2022-10-05 14:56:10 +02:00
kolaente
3d88fdaadd
feat: add display of kanban card attachment image 2022-10-05 14:56:10 +02:00
Dominik Pschenitschni
eae7cc5a6b fix: initial modal scroll lock (#2489)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2489
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-04 19:41:41 +00:00
renovate
e2f02cfc61 chore(deps): pin dependency @types/lodash.debounce to 4.0.7 (#2488)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2488
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-04 18:25:43 +00:00
Dominik Pschenitschni
00e0a23d48 fix: add lodash.debounce types (#2487)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2487
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-04 17:54:55 +00:00
renovate
114724faaa chore(deps): pin dependency @rushstack/eslint-patch to 1.2.0 (#2486)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2486
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-04 12:23:55 +00:00
Dominik Pschenitschni
c206fc6f34 feat: move composables in separate files (#2485)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2485
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-04 12:10:12 +00:00
Dominik Pschenitschni
6f2dedcb48 feature/update-eslint-config (#2484)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2484
Reviewed-by: konrad <k@knt.li>
2022-10-04 11:55:27 +00:00
Dominik Pschenitschni
4655e1ce34
feat: update eslint config
support async component, see: https://eslint.vuejs.org/user-guide/#how-to-use-a-custom-parser
2022-10-04 13:45:16 +02:00
Dominik Pschenitschni
f360ebfe98 feat: use floating-ui (#2482)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2482
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-04 11:28:33 +00:00
renovate
812f519de9 fix(deps): update dependency vue-advanced-cropper to v2.8.6 (#2483)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2483
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-04 11:16:44 +00:00
renovate
104fe7433b chore(deps): update pnpm to v7.13.1 2022-10-04 10:03:39 +00:00
Dominik Pschenitschni
db627ed28a
feat: editor script setup 2022-10-04 09:41:59 +02:00
renovate
cbec1f24aa chore(deps): update dependency eslint-plugin-vue to v9.6.0 (#2480)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2480
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-04 07:03:09 +00:00
renovate
bb8ed57b7c chore(deps): update dependency @types/node to v16.11.64 (#2479)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2479
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-04 06:05:41 +00:00
Dominik Pschenitschni
3248dcd663 chore: remove IE edge fallback (#2477)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2477
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-03 22:16:19 +00:00
Dominik Pschenitschni
06c1a54886 feat: fancycheckbox script setup (#2462)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2462
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-03 20:19:35 +00:00
renovate
9022954257 chore(deps): update typescript-eslint monorepo to v5.39.0 2022-10-03 18:03:24 +00:00
kolaente
12215c043d
feat: add nix flake for dev shell 2022-10-03 16:10:53 +02:00
konrad
3e21a8ed6e feat: migrate kanban card to script setup (#2459)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2459
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-03 13:17:02 +00:00
Dominik Pschenitschni
f1852f1f33 fix: attachment deletion (#2472)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2472
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-03 13:11:07 +00:00
Dominik Pschenitschni
c2321703a7 chore: remove unneeded this from PasswordReset.vue (#2473)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2473
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-03 13:10:09 +00:00
renovate
3060d09287 fix(deps): update sentry-javascript monorepo to v7.14.1 (#2471)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2471
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-03 13:09:05 +00:00
Dominik Pschenitschni
af7f8400e9
chore: small review adjustments 2022-10-03 12:35:15 +02:00
Dominik Pschenitschni
367ad1e5a5
fix: don't add class method to interface 2022-10-03 12:34:51 +02:00
renovate
9126d22822 fix(deps): update dependency blurhash to v2.0.3 (#2468)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2468
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-03 10:29:17 +00:00
renovate
2923504bf4 fix(deps): update dependency vue-advanced-cropper to v2.8.5 (#2469)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2469
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-03 10:28:40 +00:00
renovate
2cda229b69 chore(deps): update dependency netlify-cli to v12 (#2466)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2466
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-03 07:27:03 +00:00
renovate
44c0d55fba chore(deps): update pnpm to v7.13.0 (#2467)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2467
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-03 07:26:38 +00:00
renovate
80226df0de chore(deps): update dependency caniuse-lite to v1.0.30001414 (#2465)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2465
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-03 07:26:17 +00:00
renovate
ed47aa4119 chore(deps): update dependency @types/node to v16.11.63 (#2464)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2464
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-02 20:40:39 +00:00
renovate
4c0ce26f2d chore(deps): update dependency @vitejs/plugin-vue to v3.1.2 (#2461)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2461
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-10-02 17:33:16 +00:00
kolaente
a5925baff0
feat: migrate kanban card to script setup 2022-10-02 12:28:57 +02:00
Dominik Pschenitschni
b08dd58552 feat: colorPicker script setup (#2457)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2457
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-02 10:00:19 +00:00
Dominik Pschenitschni
0620b8f0b3 feat: multiselect script setup (#2458)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2458
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-02 09:58:51 +00:00
Dominik Pschenitschni
ff1968aa36 feat: datepicker script setup (#2456)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2456
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-01 22:13:50 +00:00
Dominik Pschenitschni
63fb8a1962 feat: Login script setup (#2417)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2417
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-01 15:58:39 +00:00
kolaente
940063784b
feat: add github issue template 2022-10-01 17:56:40 +02:00
Dominik Pschenitschni
78a6d38641 fix: type of config stores maxFileSize (#2455)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2455
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-10-01 15:03:10 +00:00
Dominik Pschenitschni
2dc36c032b feat: TaskDetail as script setup (#1792)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1792
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-01 15:02:35 +00:00
Dominik Pschenitschni
1d7f857070
feat: rework loading state of stores 2022-10-01 16:22:01 +02:00
Dominik Pschenitschni
7f281fc5e9
feat: port base store to pinia 2022-10-01 15:36:44 +02:00
Dominik Pschenitschni
df74f9d80c
feat: move base store to stores 2022-10-01 15:22:31 +02:00
konrad
d1d7cd535e feat: migrate kanban store to pina (#2411)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2411
2022-10-01 12:15:22 +00:00
renovate
e2d5c6e12f fix(deps): update dependency marked to v4.1.1 2022-10-01 09:03:52 +00:00
kolaente
0ada383395
fix(tests): fake current time in gantt tests to make them more reliable 2022-10-01 10:13:23 +02:00
kolaente
68219ee224
fix(lint): unnecessary catch clause 2022-09-30 22:36:50 +02:00
kolaente
2f2fb357c1
fix(task): new tasks with quick add magic not showing up in task list 2022-09-30 21:31:20 +02:00
kolaente
5585966584
fix(task): cancel loading state when creating a new task does not work 2022-09-30 21:25:05 +02:00
kolaente
e999b38d3b
fix(task): cancel loading state when creating a new task does not work 2022-09-30 21:24:08 +02:00
kolaente
f8450f58a4
fix(task): make sure users can be assigned via quick add magic via their real name as well
See https://kolaente.dev/vikunja/frontend/issues/2196#issuecomment-36206
2022-09-30 21:20:26 +02:00
kolaente
72b731d620
fix(list): automatically close task edit pane when switching between lists
Resolves https://kolaente.dev/vikunja/frontend/issues/2442
2022-09-30 21:13:13 +02:00
kolaente
266f877455
fix(filters): make sure all checkboxes are aligned properly 2022-09-30 21:09:24 +02:00
kolaente
13157e3bba
fix(filters): changing filter checkbox values not being emitted to parent components
See https://community.vikunja.io/t/saved-filters-option-include-tasks-which-dont-have-a-value-set-is-still-set-after-saving-the-filter-despite-the-option-was-unselected/858
2022-09-30 21:06:26 +02:00
kolaente
fd3c15d064
fix: update top header list title when saving a filter
See https://community.vikunja.io/t/changes-to-a-saved-filter-name-is-not-updated-in-the-page-title
2022-09-30 20:55:07 +02:00
kolaente
5b606936c3
fix: docker build 2022-09-30 19:13:06 +02:00
renovate
d120e8c82d chore(deps): update dependency postcss to v8.4.17 (#2449)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2449
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-30 12:58:39 +00:00
konrad
ec227a6872 feat: automatically create subtask relations based on indention (#2443)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2443
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-30 11:47:20 +00:00
kolaente
5bd7c77b68
chore: use jsDoc to explain param 2022-09-30 13:36:57 +02:00
kolaente
d58f8b4ba1
chore: break earlier if index === 0 2022-09-30 13:35:13 +02:00
kolaente
8ce242bb65
chore: use better variable names 2022-09-30 13:35:12 +02:00
kolaente
5f5ed410df
fix: don't emit a possible null task 2022-09-30 13:35:11 +02:00
kolaente
3970d0fd31
chore: spread title 2022-09-30 13:35:02 +02:00
kolaente
cc378b83fe
feat: automatically create subtask relations based on indention 2022-09-30 13:34:51 +02:00
konrad
8c394d8024 feat: port tasks store to pina (#2409)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2409
2022-09-30 11:17:19 +00:00
renovate
b84da722ca chore(deps): update dependency @cypress/vite-dev-server to v3.2.0 (#2448)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2448
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-29 21:53:19 +00:00
Dominik Pschenitschni
c35810f28f
feat: port kanban store to pinia 2022-09-29 21:42:39 +02:00
Dominik Pschenitschni
9f26ae1ee6
feat: move kanban to stores 2022-09-29 21:42:39 +02:00
Dominik Pschenitschni
34ffd1d572
feat: port tasks store to pinia 2022-09-29 21:40:15 +02:00
renovate
64cf1c8ccf chore(deps): update dependency esbuild to v0.15.10 2022-09-29 17:03:26 +00:00
Dominik Pschenitschni
1fdda07f65
feat: move tasks to stores 2022-09-29 14:01:39 +02:00
konrad
9856fab38f feat: migrate auth store to pina (#2398)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2398
Reviewed-by: konrad <k@knt.li>
2022-09-29 11:20:22 +00:00
Dominik Pschenitschni
8e3f54ae42
feat: convert model methods to named functions 2022-09-29 13:11:06 +02:00
Dominik Pschenitschni
8f25f5d353 feat: improve api-config (#2444)
remove obsolet `success` (not used)

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2444
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-29 10:47:45 +00:00
Dominik Pschenitschni
94d6f38e89 fix missed conversion to ref (#2445)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2445
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-29 09:44:58 +00:00
renovate
c421701fb7 chore(deps): update dependency @vue/test-utils to v2.1.0 2022-09-29 06:03:19 +00:00
Dominik Pschenitschni
176ad565cc
feat: auth store type improvements 2022-09-28 23:43:15 +02:00
Dominik Pschenitschni
7b53e684aa
feat: port auth store to pinia 2022-09-28 23:43:14 +02:00
Dominik Pschenitschni
f30c964c06
feat: move auth to stores 2022-09-28 23:38:15 +02:00
Dominik Pschenitschni
bbf4ef4697 feat: ListList script setup (#2441)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2441
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-09-28 16:08:23 +00:00
Dominik Pschenitschni
63f2e6ba6f feat NewNamespace script setup (#2415)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2415
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-09-28 13:31:06 +00:00
Dominik Pschenitschni
e9cf562969 fix: vueI18n global scope fallback warnings (#2437)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2437
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-28 13:29:24 +00:00
Dominik Pschenitschni
878c6ea9e1 chore: make const out of export download file name (#2436)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2436
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-28 13:28:45 +00:00
Dominik Pschenitschni
ca899d3b51 chore: optimise loading order (#2435)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2435
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-28 13:28:03 +00:00
renovate
8b63df6f41 chore(deps): update dependency vite to v3.1.4 (#2439)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2439
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-28 13:15:31 +00:00
renovate
ebfbb5ca9b fix(deps): update sentry-javascript monorepo to v7.14.0 (#2440)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2440
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-28 13:14:45 +00:00
renovate
fd13b453f4 fix(deps): update dependency blurhash to v2.0.2 2022-09-28 10:03:22 +00:00
renovate
2cd442780d fix(deps): update dependency vue to v3.2.40 2022-09-28 04:03:18 +00:00
renovate
c342756efd chore(deps): update dependency typescript to v4.8.4 2022-09-27 20:03:08 +00:00
renovate
cf745726bc chore(deps): update dependency @types/node to v16.11.62 (#2430)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2430
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-27 18:36:44 +00:00
renovate
c9546d52c0 chore(deps): update dependency cypress to v10.9.0 (#2429)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2429
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-27 17:17:19 +00:00
Dominik Pschenitschni
6a828078a3
fix: add types for node 2022-09-27 18:51:15 +02:00
konrad
bc833091f2 feat: possible fix for pnpm ci errors (#2413)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2413
Reviewed-by: konrad <k@knt.li>
2022-09-27 15:37:25 +00:00
kolaente
f4a5f59400
chore(ci): sign drone config 2022-09-27 17:18:38 +02:00
Dominik Pschenitschni
7f581cbe27 feat: NewLabel script setup (#2414)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2414
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-09-27 15:12:21 +00:00
Dominik Pschenitschni
a199fc7a8e
fix: don't use corepack prepare at all 2022-09-27 11:24:51 +02:00
Dominik Pschenitschni
e8f0b56651
feat: possible fix for pnpm ci errors 2022-09-27 11:24:50 +02:00
renovate
ecd0df21c1 fix(deps): update dependency blurhash to v2.0.1 2022-09-27 09:03:21 +00:00
renovate
069d03932d chore(deps): update typescript-eslint monorepo to v5.38.1 (#2426)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2426
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-26 18:12:43 +00:00
Dominik Pschenitschni
9f3936544d fix: use https for api url (#2425)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2425
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-09-26 16:17:48 +00:00
Dominik Pschenitschni
89e428b4d2 feat: ListLabels script setup (#2416)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2416
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-09-26 16:09:24 +00:00
Dominik Pschenitschni
ba2605af1b feat: filter-popup script setup (#2418)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2418
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-26 15:58:54 +00:00
renovate
1eae7ece9c chore(deps): update dependency rollup-plugin-visualizer to v5.8.2 (#2420)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2420
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-26 11:16:21 +00:00
renovate
5f6cfa8e09 fix(deps): update vueuse to v9.3.0 (#2423)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2423
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-26 10:58:34 +00:00
renovate
b1f2229d22 chore(deps): update dependency eslint to v8.24.0 (#2410)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2410
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-26 10:57:32 +00:00
renovate
b44c71e6cc chore(deps): update dependency netlify-cli to v11.8.3 (#2422)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2422
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-26 10:45:27 +00:00
renovate
4e9c592090 chore(deps): update dependency caniuse-lite to v1.0.30001412 (#2421)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2421
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-26 10:39:10 +00:00
Dominik Pschenitschni
a737fc5bc2
feat: port config store to pinia 2022-09-23 13:30:09 +02:00
Dominik Pschenitschni
9e8c429864
feat: move config to stores 2022-09-23 13:29:43 +02:00
Dominik Pschenitschni
95ad245b59
fix: missed porting these getters and commits 2022-09-23 13:29:10 +02:00
kolaente
62ed7c5964
fix: color list titles so that they are visible on cards with a background
Resolves #2372
2022-09-23 12:10:58 +02:00
kolaente
2bf9771e28
fix: lint 2022-09-23 11:50:30 +02:00
kolaente
3c9c5eff12
fix: make add task button 100% height 2022-09-23 11:13:35 +02:00
kolaente
aa64e9835c
fix: loading state when creating a new task from list view 2022-09-23 11:11:11 +02:00
kolaente
ede5cdd8cf
fix: only pass date to flatpickr if it's a valid date
Resolves #2384
2022-09-23 10:36:21 +02:00
renovate
5ffb13a3a8 chore(deps): update pnpm to v7.12.2 (#2408)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2408
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-23 06:41:16 +00:00
Dominik Pschenitschni
20e9420638 feat: port attachments store to pinia 2022-09-22 21:44:41 +00:00
Dominik Pschenitschni
c2ba1b2828 feat: move attachments store to stores 2022-09-22 21:44:41 +00:00
renovate
2fd9f0ee47 chore(deps): update dependency esbuild to v0.15.9 2022-09-22 20:03:25 +00:00
kolaente
6e5501a5f1
fix(labels): unset loading state after loading all labels 2022-09-22 19:02:12 +02:00
konrad
937fd36f72 feat: convert namespaces store to pina (#2393)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2393
Reviewed-by: konrad <k@knt.li>
2022-09-22 15:34:32 +00:00
Dominik Pschenitschni
4dfcd8e70f feat: feat-attachments-script-setup (#2358)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2358
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-09-22 15:31:13 +00:00
renovate
13bc25ff5d chore(deps): update dependency sass to v1.55.0 2022-09-22 12:03:29 +00:00
renovate
0e072b582b chore(deps): update pnpm to v7.12.1 2022-09-22 11:47:36 +00:00
renovate
b9335a7362 fix(deps): update dependency codemirror to v5.65.9 2022-09-22 11:23:20 +00:00
renovate
479fc7e433 chore(deps): update dependency rollup to v2.79.1 2022-09-22 11:22:27 +00:00
renovate
02e73fa377 chore(deps): update dependency vite-plugin-pwa to v0.13.1 2022-09-22 11:03:35 +00:00
renovate
ad694ff8bc chore(deps): update dependency @vue/eslint-config-typescript to v11.0.2 2022-09-22 07:03:29 +00:00
Dominik Pschenitschni
093ab766d4
feat: port namespace store to pinia 2022-09-21 23:32:21 +02:00
Dominik Pschenitschni
9474240cb9
feat: move namespaces store to stores 2022-09-21 23:32:21 +02:00
Dominik Pschenitschni
1c58fccd92
feat: add hot reloading support 2022-09-21 23:32:21 +02:00
konrad
f7ca064127 feat: use pnpm (#1789)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1789
2022-09-21 20:49:13 +00:00
kolaente
ef6454483a
chore(ci): sign drone config 2022-09-21 20:56:14 +02:00
kolaente
dfb3561310
chore: use node alpine image
This reverts commit 6624db1d49.
2022-09-21 20:52:30 +02:00
kolaente
62e227c767
fix: explicitly install cypress 2022-09-21 20:42:49 +02:00
kolaente
6624db1d49
chore: don't use node alpine image 2022-09-21 20:34:19 +02:00
kolaente
b542221dac
chore: don't cache node_modules 2022-09-21 20:29:29 +02:00
konrad
d57e27b4a6 feat: task relatedTasks script setup (#1939)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1939
Reviewed-by: konrad <k@knt.li>
2022-09-21 18:22:30 +00:00
Dominik Pschenitschni
0814890cac feat: deleteNamespace script setup (#2387)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2387
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-09-21 18:21:47 +00:00
Dominik Pschenitschni
43e2d036d7
fix: remove console.log 2022-09-21 18:36:38 +02:00
kolaente
ce0f58c783
feat: allow marking a related task done directly from the list 2022-09-21 18:36:38 +02:00
Dominik Pschenitschni
943d5f7975
feat: task relatedTasks script setup 2022-09-21 18:35:46 +02:00
Dominik Pschenitschni
a38075f376 feat: move list store to pina (#2392)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2392
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-09-21 16:21:25 +00:00
Dominik Pschenitschni
e5d04c98da
fix: test pnpm cache 2022-09-21 17:29:37 +02:00
Dominik Pschenitschni
d76b526916
feat: use pnpm 2022-09-21 17:29:36 +02:00
renovate
f85a08afb4 fix(deps): update dependency pinia to v2.0.22 (#2400)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2400
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-21 15:16:23 +00:00
Dominik Pschenitschni
d67e5e386d feat: port label store to pinia | pinia 1/9 (#2391)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2391
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-09-21 14:23:57 +00:00
Dominik Pschenitschni
e91b5fde02 feat: NewTeam script setup (#2388)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2388
Co-authored-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-committed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-21 14:13:37 +00:00
Dominik Pschenitschni
ff5d1fc8c1 feat: ListNamespaces script setup (#2389)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2389
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-09-21 14:12:36 +00:00
renovate
ddabd7f63a chore(deps): update dependency autoprefixer to v10.4.12 2022-09-21 00:03:32 +00:00
renovate
4c9a018c13 fix(deps): update dependency easymde to v2.18.0 (#2386)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2386
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-20 15:38:56 +00:00
renovate
fadaac4ef1 chore(deps): update dependency vite-plugin-pwa to v0.13.0 (#2385)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2385
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-20 15:12:32 +00:00
renovate
b06e3cac3f chore(deps): update typescript-eslint monorepo to v5.38.0 (#2383)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2383
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-19 21:18:25 +00:00
renovate
1ecb186b7c chore(deps): update dependency vite to v3.1.3 (#2382)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2382
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-19 15:01:12 +00:00
renovate
070ef7e5aa chore(deps): update dependency @vitejs/plugin-legacy to v2.2.0 (#2381)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2381
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-19 15:00:19 +00:00
renovate
e8613b48fc chore(deps): update dependency netlify-cli to v11.8.0 (#2380)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2380
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-19 08:08:09 +00:00
renovate
de7617fbaf chore(deps): update caniuse-and-related to v4.21.4 (#2379)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2379
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-19 08:04:45 +00:00
renovate
0360cd0867 chore(deps): update dependency vite-plugin-pwa to v0.12.8 (#2375)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2375
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-18 19:41:31 +00:00
renovate
54e9513c14 chore(deps): update dependency esbuild to v0.15.8 2022-09-18 19:03:53 +00:00
renovate
fc0cf3fc87 chore(deps): update dependency vitest to v0.23.4 2022-09-18 16:37:39 +00:00
renovate
0fccb3d5a9 fix(deps): update dependency @types/sortablejs to v1.15.0 2022-09-18 08:03:50 +00:00
renovate
9134269ead chore(deps): update dependency vite to v3.1.2 2022-09-17 07:03:27 +00:00
drone
ba4913e2cb [skip ci] Updated translations via Crowdin 2022-09-17 00:31:23 +00:00
renovate
2eff239f9e chore(deps): update dependency eslint-plugin-vue to v9.5.1 (#2373)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2373
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-16 20:07:49 +00:00
renovate
fa77b20c13 chore(deps): update dependency eslint-plugin-vue to v9.5.0 (#2371)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2371
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-16 15:51:37 +00:00
Dominik Pschenitschni
3b713dede2 [skip ci] Updated translations via Crowdin 2022-09-16 00:30:52 +00:00
Dominik Pschenitschni
ad6b335d41 feat: namespace settings archive script setup 2022-09-15 20:46:26 +00:00
renovate
221edb2086 fix(deps): update sentry-javascript monorepo to v7.13.0 2022-09-15 14:53:22 +00:00
renovate
565765537d chore(deps): pin dependency @types/dompurify to 2.3.4 2022-09-15 13:02:49 +00:00
konrad
f70b1d2902 feat: color the task color button when the task has a color set (#2331)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2331
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-15 12:46:12 +00:00
kolaente
4fce71f729
fix: remove margin from the color bubble component itself 2022-09-15 14:37:07 +02:00
konrad
84260841be feat(list): add info dialoge to show list description (#2368)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2368
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-15 12:36:43 +00:00
kolaente
6d9c4a7aa0
chore: improve types 2022-09-15 14:32:29 +02:00
konrad
b24d5f2dce fix(quick add magic): time parsing for certain conditions (#2367)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2367
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-15 11:59:29 +00:00
kolaente
2683fec0a6
feat: show the task color bubble everywhere 2022-09-15 13:56:14 +02:00
kolaente
2df2bd38e2
fix: only try to save user settings when a user is authenticated 2022-09-15 12:36:19 +02:00
kolaente
91976e23f9
fix: redirect to login when the jwt token expires 2022-09-15 12:35:53 +02:00
Dominik Pschenitschni
602ab8379e fix: vue-i18n global scope (#2366)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2366
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-09-15 09:54:01 +00:00
kolaente
92f24e59a7
fix: don't parse dates in urls
Resolves #2353
2022-09-15 10:23:37 +02:00
kolaente
49217889b5
fix: bucket title edit success message appearing twice 2022-09-15 10:08:17 +02:00
renovate
9c367877f4 chore(deps): update dependency vite to v3.1.1 (#2365)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2365
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-15 08:03:08 +00:00
renovate
064001e259 chore(deps): update dependency postcss-preset-env to v7.8.2 2022-09-15 06:03:05 +00:00
renovate
0316656d81 chore(deps): update dependency autoprefixer to v10.4.11 (#2363)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2363
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-14 19:18:06 +00:00
kolaente
a18c6ab8d8
feat: move the update available dialoge always to the bottom 2022-09-14 19:06:34 +02:00
kolaente
bdf992c9bf
feat: color the color button icon instead of the button itself 2022-09-14 18:56:51 +02:00
kolaente
51c806c12b
feat: color the task color button when the task has a color set 2022-09-14 18:43:56 +02:00
konrad
a6e9b36bd6 feat(link shares): allows switching the initial view by passing a query parameter (#2335)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2335
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-14 16:37:54 +00:00
renovate
854068fff9 chore(deps): update dependency cypress to v10.8.0 (#2359)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2359
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-14 14:46:03 +00:00
kolaente
6576b6148c
feat: move the url link to the bottom of the items 2022-09-14 16:31:05 +02:00
kolaente
224cea33ce
feat: make share link name italic 2022-09-14 16:27:57 +02:00
kolaente
7e7fa807fd
chore: set more expressive variable names for available views dropdowns 2022-09-14 16:17:31 +02:00
Dominik Pschenitschni
f083f181e2 fix: only warn once if triggeredNotifications are not supported (#2344)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2344
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-09-14 12:45:21 +00:00
kolaente
2b82df5dbd
Merge branch 'main' into feature/redirect-to-specific-view
# Conflicts:
#	src/components/sharing/linkSharing.vue
2022-09-13 22:07:50 +02:00
kolaente
e67fc7fb7e
fix: use proper computed for available views list 2022-09-13 22:04:24 +02:00
Dominik Pschenitschni
db8b8812af feat: use v-model more consequent (#2356)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2356
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-09-13 15:30:33 +00:00
renovate
2013924949 chore(deps): update dependency autoprefixer to v10.4.10 (#2355)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2355
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-13 15:17:25 +00:00
Dominik Pschenitschni
1a11b43ca8 feat: improve models 2022-09-13 14:59:02 +00:00
renovate
61427987c2 fix(deps): update dependency date-fns to v2.29.3 (#2354)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2354
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-13 13:26:48 +00:00
Dominik Pschenitschni
7b398f73f6 feat: add fallback for useCopyToClipboard (#2343)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2343
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-09-13 12:56:29 +00:00
renovate
64726a6421 fix(deps): update dependency blurhash to v2 (#2351)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2351
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-13 12:54:46 +00:00
renovate
53858e0c31 chore(deps): update typescript-eslint monorepo to v5.37.0 2022-09-12 18:02:50 +00:00
renovate
2f0f648d28 chore(deps): update dependency eslint to v8.23.1 2022-09-12 06:14:59 +00:00
renovate
6e026cc7cc chore(deps): update dependency netlify-cli to v11.7.1 2022-09-12 06:13:48 +00:00
renovate
d0fefd3c08 chore(deps): update dependency caniuse-lite to v1.0.30001397 2022-09-12 06:12:28 +00:00
renovate
4dd397e3d2 chore(deps): update dependency autoprefixer to v10.4.9 2022-09-12 00:03:06 +00:00
drone
2a41ccb980 [skip ci] Updated translations via Crowdin 2022-09-11 00:31:16 +00:00
renovate
38d72b59df chore(deps): update dependency vitest to v0.23.2 2022-09-10 08:02:50 +00:00
drone
add080d214 [skip ci] Updated translations via Crowdin 2022-09-10 00:31:03 +00:00
renovate
65f9def438 chore(deps): update dependency typescript to v4.8.3 (#2341)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2341
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-09 06:57:05 +00:00
drone
f1a726550e [skip ci] Updated translations via Crowdin 2022-09-09 00:31:14 +00:00
kolaente
26d02d5593
feat: programmatically generate list of available views 2022-09-08 16:29:29 +02:00
kolaente
5f678e2449
chore: remove unused id 2022-09-08 16:26:51 +02:00
kolaente
23598dd2ee
fix: introduce a ListView type to properly type all available list views 2022-09-08 14:11:19 +02:00
kolaente
d91d1fecf1
chore: remove &nbsp; 2022-09-08 13:58:49 +02:00
kolaente
7a457eb161
feat(link shares): cleanup link share table 2022-09-08 13:56:52 +02:00
kolaente
d3171b59be
feat(link shares): allows switching the initial view by passing a query parameter 2022-09-08 13:56:50 +02:00
kolaente
63f5f446fd feat(link shares): hide the logo if a query parameter was passed 2022-09-08 09:56:09 +00:00
kolaente
b8d77a617b
chore: rearrange non-dev dependencies 2022-09-08 11:34:48 +02:00
kolaente
d822709991
chore: automerge renovate dev dependency updates 2022-09-08 11:31:51 +02:00
renovate
86a04da470 fix(deps): update dependency vue to v3.2.39 2022-09-08 08:25:34 +00:00
renovate
e6fbf1cb50 chore(deps): update dependency vue-tsc to v0.40.13 2022-09-08 04:03:13 +00:00
renovate
4fe6186ee6 chore(deps): update dependency sass to v1.54.9 (#2336)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2336
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 22:09:57 +00:00
kolaente
6bf5f6efd4
fix: dragging a list on mobile Safari 2022-09-07 23:11:44 +02:00
renovate
03f448457a chore(deps): update dependency vue-tsc to v0.40.11 (#2333)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2333
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 18:23:17 +00:00
konrad
7f6f8963e7 feat: add keyboard shortcut to toggle task description edit (#2332)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2332
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
2022-09-07 17:55:59 +00:00
kolaente
65fd2f14a0
feat: show user display name when searching for assignees on a list 2022-09-07 17:05:44 +02:00
renovate
2e84c27d1e chore(deps): update dependency vite-svg-loader to v3.6.0 (#2327)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2327
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 14:29:59 +00:00
renovate
ee79a1d604 chore(deps): update dependency postcss-preset-env to v7.8.1 (#2328)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2328
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 13:23:59 +00:00
renovate
d17428d4d0 chore(deps): update dependency vue-tsc to v0.40.10 (#2326)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2326
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 13:23:30 +00:00
renovate
bca53ec8ae chore(deps): update dependency vite-plugin-pwa to v0.12.7 (#2325)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2325
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-07 13:22:35 +00:00
renovate
fef1af6ce7 chore(deps): update dependency @vue/eslint-config-typescript to v11.0.1 (#2324)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2324
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-06 15:06:58 +00:00
renovate
3209206260 fix(deps): pin dependency @types/lodash.clonedeep to 4.5.7 (#2323)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2323
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-06 12:20:22 +00:00
kolaente
d819b9b0ba
fix: don't encode attachment upload file blob as json 2022-09-06 13:02:49 +02:00
kolaente
e95904351f
feat: add sponsor logo to readme (relm) 2022-09-06 12:02:11 +02:00
renovate
a541afdf9d chore(deps): update dependency vue-tsc to v0.40.9 (#2322)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2322
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-06 09:27:57 +00:00
renovate
5d189035f2 chore(deps): update typescript-eslint monorepo to v5.36.2 (#2321)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2321
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-06 09:27:30 +00:00
konrad
dbea1f7a51 feat: convert services and models to ts (#1798)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1798
Reviewed-by: konrad <k@knt.li>
2022-09-06 09:26:48 +00:00
Dominik Pschenitschni
54de368642
fix: use typed useStore 2022-09-05 20:10:32 +02:00
Dominik Pschenitschni
878b5bf236
fix: defaultListId 2022-09-05 19:01:42 +02:00
Dominik Pschenitschni
b4cba6f7d9
fix: mutation error in TaskDetailView 2022-09-05 19:01:42 +02:00
kolaente
c06b781837
fix: don't try to set the bucket of a task when it was moved to a new list 2022-09-05 19:01:42 +02:00
kolaente
9616badc33
fix: don't push a select event when nothing was selected 2022-09-05 19:01:42 +02:00
Dominik Pschenitschni
49f3b928cb
chore: minor fixes 2022-09-05 19:01:42 +02:00
Dominik Pschenitschni
7d4ba6249e
feat: add modelTypes 2022-09-05 17:57:21 +02:00
Dominik Pschenitschni
8416b1f448
fix: use new assignData method for default data 2022-09-05 17:57:21 +02:00
Dominik Pschenitschni
8be1f81848
fix: use IAbstract to extend model interface 2022-09-05 17:57:20 +02:00
Dominik Pschenitschni
d36577c04e
fix: mark abstractModel and abstractService abstract 2022-09-05 17:57:19 +02:00
Dominik Pschenitschni
2445f0eec8
chore: align docker cypress image version with drone 2022-09-05 17:43:56 +02:00
Dominik Pschenitschni
96f5f00c07
fix: use definite assignment assertion operator
We used declare which is the wrong use-case for this. See: https://www.typescriptlang.org/docs/handbook/2/classes.html#--strictpropertyinitialization
2022-09-05 17:43:56 +02:00
Dominik Pschenitschni
041a1a4cc0
move constants 2022-09-05 17:43:55 +02:00
Dominik Pschenitschni
4a50e6aae2
fix: improve some types 2022-09-05 17:43:55 +02:00
Dominik Pschenitschni
f9b51306c3
fix: createNewTask typing 2022-09-05 17:43:24 +02:00
Dominik Pschenitschni
106abfc842
fix: merge duplicate types 2022-09-05 17:43:24 +02:00
Dominik Pschenitschni
3ba423ed23
fix: use correct model for generics 2022-09-05 17:43:23 +02:00
Dominik Pschenitschni
244478400a
feat: improve store typing 2022-09-05 17:43:23 +02:00
Dominik Pschenitschni
a6b96f857d
feat: extend mode interface from class instead from interface 2022-09-05 17:43:22 +02:00
Dominik Pschenitschni
80eaf38090
fix: add lodash.clonedeep types 2022-09-05 17:43:22 +02:00
Dominik Pschenitschni
79e7e4a8ae
feat: use lib ESNext setting for typescript 2022-09-05 17:43:22 +02:00
Dominik Pschenitschni
3766b5e51b
feat: improve store and model typing 2022-09-05 17:43:22 +02:00
Dominik Pschenitschni
c9e85cb52b
feat: improve types 2022-09-05 17:38:37 +02:00
Dominik Pschenitschni
42e72d14a4
chore: better variable typing 2022-09-05 17:38:37 +02:00
Dominik Pschenitschni
6f93d6343c
chore: remove unnecessary defineComponent 2022-09-05 17:38:37 +02:00
Dominik Pschenitschni
4a247b2a7d
chore: remove global mixing 2022-09-05 17:38:11 +02:00
renovate
e93f95f8c2 fix(deps): update vueuse to v9.2.0 (#2320)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2320
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-05 15:30:31 +00:00
Dominik Pschenitschni
b0ee316a26
chore: remove date mixins 2022-09-05 16:37:37 +02:00
Dominik Pschenitschni
332acf012c
feat: function attribute typing 2022-09-05 16:37:37 +02:00
Dominik Pschenitschni
8fb00653e4
feat: constants 2022-09-05 16:37:37 +02:00
Dominik Pschenitschni
af630d3b8c
chore: improve type imports 2022-09-05 16:37:36 +02:00
Dominik Pschenitschni
797de0c543
feat: add properties to models 2022-09-05 16:37:36 +02:00
Dominik Pschenitschni
74ad6e65e8
feat: convert abstractService to ts 2022-09-05 16:37:35 +02:00
renovate
02ddf90cbb chore(deps): update dependency vite to v3.1.0 (#2319)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2319
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-05 11:16:42 +00:00
renovate
59801d797b chore(deps): update dependency @vitejs/plugin-vue to v3.1.0 (#2318)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2318
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-05 11:16:00 +00:00
renovate
d7b7328a0e chore(deps): update dependency @vitejs/plugin-legacy to v2.1.0 (#2317)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2317
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-05 10:53:58 +00:00
renovate
377201dda9 chore(deps): update dependency vitest to v0.23.1 (#2316)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2316
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-05 09:30:13 +00:00
renovate
5b8ebba4c0 chore(deps): update dependency vue-tsc to v0.40.7 (#2315)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2315
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-05 07:49:34 +00:00
renovate
9657b6b2d3 chore(deps): update dependency caniuse-lite to v1.0.30001390 (#2314)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2314
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-05 07:48:57 +00:00
Dominik Pschenitschni
6e4a3ff199 fix: authenticate per request (#2258)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2258
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-09-04 14:30:11 +00:00
renovate
3e770e11f1 chore(deps): update dependency esbuild to v0.15.7 (#2313)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2313
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-04 13:13:09 +00:00
renovate
48af80dcf9 chore(deps): update dependency vitest to v0.23.0 (#2312)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2312
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-04 12:12:18 +00:00
renovate
b3300f5552 chore(deps): update dependency rollup-plugin-visualizer to v5.8.1 (#2311)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2311
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-03 21:05:30 +00:00
renovate
9bc04a3ffc chore(deps): update dependency vue-tsc to v0.40.6 (#2310)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2310
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-03 21:04:51 +00:00
renovate
b77b4105a0 chore(deps): update dependency vite-plugin-pwa to v0.12.6 (#2309)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2309
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-03 10:33:01 +00:00
Dominik Pschenitschni
f6437c81da feat: list settings edit script setup (#1988)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/1988
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-09-02 15:13:32 +00:00
renovate
deef7106e6 fix(deps): update sentry-javascript monorepo to v7.12.1 (#2308)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2308
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 10:27:28 +00:00
renovate
2521102712 fix(deps): update dependency @kyvg/vue3-notification to v2.4.1 (#2305)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2305
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 07:38:40 +00:00
renovate
ed02c6c81c chore(deps): update font awesome to v6.2.0 (#2303)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2303
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 06:49:20 +00:00
renovate
5c5f3d88af chore(deps): update dependency typescript to v4.8.2 (#2301)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2301
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 06:48:43 +00:00
renovate
1409750a30 chore(deps): update dependency vite-svg-loader to v3.5.1 (#2302)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2302
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 06:48:07 +00:00
renovate
5923bd8225 chore(deps): update typescript-eslint monorepo to v5.36.1 (#2304)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2304
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 06:47:13 +00:00
renovate
a5629bd556 fix(deps): update dependency dompurify to v2.4.0 (#2306)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2306
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 06:46:25 +00:00
renovate
2e2eebf206 fix(deps): update sentry-javascript monorepo to v7.12.0 (#2307)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2307
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 06:45:28 +00:00
renovate
e8572fe13b chore(deps): update dependency eslint-plugin-vue to v9.4.0 (#2300)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2300
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-02 06:44:49 +00:00
renovate
308971ad05 chore(deps): update dependency cypress to v10.7.0 (#2298)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2298
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 16:19:19 +00:00
renovate
67cee63cf0 chore(deps): update dependency eslint to v8.23.0 (#2299)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2299
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 16:19:16 +00:00
renovate
5a92228b96 chore(deps): update dependency @faker-js/faker to v7.5.0 (#2297)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2297
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 16:17:24 +00:00
Dominik Pschenitschni
ff655808b3 feat: settings background script setup (#2104)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2104
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-09-01 16:09:50 +00:00
renovate
a8d4892a0f chore(deps): update dependency @cypress/vue to v4.2.0 (#2296)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2296
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 15:13:11 +00:00
renovate
c936b92654 fix(deps): update vueuse to v9.1.1 (#2295)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2295
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 14:26:05 +00:00
renovate
d323ed5b4b fix(deps): update dependency vue-router to v4.1.5 (#2294)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2294
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 14:16:43 +00:00
renovate
91ad503a11 fix(deps): update dependency vue to v3.2.38 (#2293)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2293
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 13:44:28 +00:00
renovate
35081befae chore(deps): update dependency vue-tsc to v0.40.5 (#2292)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2292
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 13:36:46 +00:00
renovate
17607df6ff fix(deps): update dependency easymde to v2.17.0 (#2283)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2283
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 12:57:42 +00:00
renovate
149696b7de chore(deps): update dependency rollup-plugin-visualizer to v5.8.0 (#2282)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2282
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 12:53:19 +00:00
renovate
4582e98795 chore(deps): update dependency vite-plugin-pwa to v0.12.4 (#2291)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2291
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 12:46:37 +00:00
renovate
d6f82955a6 chore(deps): update dependency esbuild to v0.15.6 (#2290)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2290
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 12:37:11 +00:00
renovate
270432afe4 fix(deps): update dependency date-fns to v2.29.2 (#2277)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2277
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 11:24:52 +00:00
renovate
4d3b7c8759 chore(deps): update dependency vite to v3.0.9 (#2279)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2279
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 11:24:28 +00:00
renovate
c4f3e09cc7 chore(deps): update dependency netlify-cli to v11 (#2287)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2287
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 11:22:52 +00:00
renovate
bdd5ea27ac fix(deps): update dependency marked to v4.1.0 (#2284)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2284
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 11:22:19 +00:00
renovate
65b694d8e6 chore(deps): update dependency rollup to v2.79.0 (#2278)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2278
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 11:19:32 +00:00
renovate
78abe20f7d chore(deps): update dependency caniuse-lite to v1.0.30001387 (#2285)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2285
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 11:19:02 +00:00
renovate
7163dc351b chore(deps): update dependency sass to v1.54.8 (#2281)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2281
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-09-01 11:18:24 +00:00
renovate
7098b6315f chore(deps): update dependency vitest to v0.22.1 (#2276)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2276
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-18 08:24:55 +00:00
renovate
dc70df54ba fix(deps): update sentry-javascript monorepo to v7.11.1 (#2275)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2275
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-17 20:29:27 +00:00
renovate
24dcbaba27 fix(deps): update sentry-javascript monorepo to v7.11.0 (#2274)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2274
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-17 11:11:14 +00:00
kolaente
eedbba5657
chore: release preparations 2022-08-17 10:17:18 +02:00
renovate
1223d8b679 chore(deps): update dependency esbuild to v0.15.5 (#2272)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2272
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-17 06:17:47 +00:00
konrad
c6e7390f13 fix: search for assignees by username (#2264)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2264
2022-08-16 21:26:54 +00:00
kolaente
a84fb8b5df
fix(lists): moving a list into another namespace on the first position
Resolves https://kolaente.dev/vikunja/frontend/issues/2240
2022-08-16 23:21:34 +02:00
renovate
7bbd452f3b chore(deps): update dependency cypress to v10.6.0 (#2271)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2271
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-16 21:11:39 +00:00
renovate
01c29578fb chore(deps): update dependency esbuild to v0.15.4 (#2270)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2270
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-16 19:35:39 +00:00
renovate
5d58c4baa3 chore(deps): update dependency vite to v3.0.8 (#2269)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2269
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-16 09:12:10 +00:00
renovate
b1493e2be6 chore(deps): update dependency postcss-preset-env to v7.8.0 (#2268)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2268
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-16 07:50:58 +00:00
renovate
50f19829d5 chore(deps): update dependency @cypress/vite-dev-server to v3.1.1 (#2267)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2267
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-16 07:50:25 +00:00
renovate
99cba7fc02 chore(deps): update dependency cypress to v10.5.0 (#2266)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2266
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-15 22:38:15 +00:00
renovate
89c67c9726 chore(deps): update dependency vitest to v0.22.0 (#2265)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2265
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-15 21:13:00 +00:00
kolaente
3ab58a015c
fix(dark mode): make a focused text only button actually readable
Resolves https://github.com/go-vikunja/api/issues/41
2022-08-15 23:08:18 +02:00
kolaente
45b7434bda
fix(dark mode): code background color 2022-08-15 22:57:28 +02:00
renovate
ae47298b3c chore(deps): update typescript-eslint monorepo to v5.33.1 (#2263)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2263
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-15 19:00:10 +00:00
renovate
9f36085505 chore(deps): update dependency caniuse-lite to v1.0.30001376 (#2261)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2261
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-15 08:13:16 +00:00
renovate
cf6eea97b1 chore(deps): update dependency netlify-cli to v10.17.4 (#2262)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2262
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-15 08:12:46 +00:00
renovate
739b26ae9f chore(deps): update dependency esbuild to v0.15.3 2022-08-14 15:02:47 +00:00
renovate
f9e9c9d6e3 chore(deps): update dependency rollup to v2.78.0 (#2257)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2257
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-14 06:30:14 +00:00
renovate
2cb482dc93 chore(deps): update dependency eslint to v8.22.0 (#2256)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2256
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-14 06:29:48 +00:00
renovate
196b2f0c6c chore(deps): update dependency @vitejs/plugin-vue to v3.0.3 (#2253)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2253
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-13 06:28:56 +00:00
renovate
52e94df048 chore(deps): update dependency vite to v3.0.7 (#2254)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2254
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-13 06:28:32 +00:00
renovate
2454b0862d chore(deps): update dependency esbuild to v0.15.2 (#2255)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2255
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-13 06:28:04 +00:00
renovate
63bbc44ed6 chore(deps): update dependency @cypress/vite-dev-server to v3.1.0 (#2248)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2248
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-12 12:22:04 +00:00
renovate
05e8986916 chore(deps): update dependency @vitejs/plugin-vue to v3.0.2 (#2251)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2251
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-12 12:21:30 +00:00
renovate
600808beb0 chore(deps): update dependency @cypress/vue to v4.1.0 (#2249)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2249
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-12 11:25:52 +00:00
renovate
30bc74a5aa chore(deps): update dependency @vitejs/plugin-legacy to v2.0.1 (#2250)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2250
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-12 11:20:42 +00:00
renovate
d44d9f44bc chore(deps): update dependency vite to v3.0.6 (#2252)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2252
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-12 11:19:09 +00:00
renovate
ee89358be6 chore(deps): update dependency vue-tsc to v0.40.1 (#2243)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2243
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-11 10:25:34 +00:00
renovate
7e5e3f81b3 chore(deps): update dependency esbuild to v0.15.1 (#2244)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2244
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-11 10:25:05 +00:00
renovate
1bf41edb96 chore(deps): update dependency rollup to v2.77.3 (#2245)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2245
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-11 10:24:34 +00:00
renovate
67b1b0b118 fix(deps): update sentry-javascript monorepo to v7.10.0 (#2242)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2242
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-10 18:38:33 +00:00
renovate
8d76bec525 chore(deps): update dependency vitest to v0.21.1 (#2236)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2236
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-10 12:11:53 +00:00
renovate
0da9262cce chore(deps): update dependency vue-tsc to v0.40.0 (#2241)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2241
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-10 11:15:14 +00:00
renovate
1149d7adfd chore(deps): update dependency esbuild to v0.15.0 (#2239)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2239
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-10 06:39:00 +00:00
renovate
8e472211ff chore(deps): update dependency sass to v1.54.4 (#2238)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2238
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-10 06:37:04 +00:00
renovate
2ca0bc6dad chore(deps): update dependency vite to v3.0.5 (#2237)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2237
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-09 11:16:18 +00:00
kolaente
51ffe93048
fix: clear all localstorage when logging out 2022-08-09 11:55:19 +02:00
renovate
3440d71e74 chore(deps): update dependency @faker-js/faker to v7.4.0 (#2234)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2234
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-08 18:26:38 +00:00
renovate
067173f08e chore(deps): update typescript-eslint monorepo to v5.33.0 (#2235)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2235
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-08 18:25:02 +00:00
renovate
40fa551f28 chore(deps): update dependency esbuild to v0.14.54 (#2233)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2233
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-08 15:36:08 +00:00
renovate
a804df342e chore(deps): update dependency netlify-cli to v10.15.0 (#2232)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2232
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-08 10:32:09 +00:00
renovate
896196f08f chore(deps): update dependency caniuse-lite to v1.0.30001374 (#2231)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2231
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-08 10:31:38 +00:00
renovate
245b134c92 chore(deps): update dependency vue-tsc to v0.39.5 2022-08-07 09:28:02 +00:00
renovate
667afa3c05 chore(deps): update dependency postcss to v8.4.16 (#2230)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2230
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-07 09:27:16 +00:00
renovate
42e830eb73 fix(deps): update dependency vue-i18n to v9.2.2 (#2228)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2228
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-05 18:28:31 +00:00
renovate
8718efbc1c chore(deps): update dependency vitest to v0.21.0 2022-08-05 17:39:27 +00:00
renovate
8c8a07f1de fix(deps): update dependency vue-i18n to v9.2.1 2022-08-05 17:03:15 +00:00
renovate
985f379845 fix(deps): update sentry-javascript monorepo to v7.9.0 (#2224)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2224
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-05 14:13:49 +00:00
Dominik Pschenitschni
5b8d142abb fix: i18n scope 2022-08-04 21:33:24 +00:00
renovate
76cb94c488 chore(deps): update dependency sass to v1.54.3 (#2223)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2223
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-04 21:31:55 +00:00
renovate
4994db4f77 fix(deps): update vueuse to v9.1.0 (#2220)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2220
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-04 07:18:47 +00:00
renovate
35d3cd6e83 chore(deps): update dependency sass to v1.54.2 (#2219)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2219
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-04 07:15:52 +00:00
kolaente
7a46843a28
chore: release preparations 2022-08-03 20:17:37 +02:00
kolaente
bafef06e90
chore: add git-cliff config 2022-08-03 20:10:39 +02:00
renovate
d5445e0298 chore(deps): update dependency esbuild to v0.14.53 (#2217)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2217
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-03 17:14:28 +00:00
renovate
9bbaa67eec chore(deps): update dependency sass to v1.54.1 (#2218)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2218
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-03 17:14:23 +00:00
renovate
1f14e7bee8 chore(deps): update dependency cypress to v10.4.0 (#2216)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2216
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-02 17:49:29 +00:00
renovate
e05b729c21 chore(deps): update dependency vitest to v0.20.3 (#2215)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2215
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-02 16:19:17 +00:00
kolaente
ad7ed86d36
fix: don't replace the last edited task with the one currently editing 2022-08-02 15:27:24 +02:00
kolaente
e82a83c8cf
fix: properly parse dates or null
Resolves https://kolaente.dev/vikunja/frontend/issues/2214
2022-08-02 15:19:58 +02:00
kolaente
31480eae72
fix: default label color in dark mode
Resolves https://kolaente.dev/vikunja/frontend/issues/2200
2022-08-02 15:05:33 +02:00
renovate
c8162728b7 chore(deps): update workbox monorepo to v6.5.4 (#2204)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2204
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-02 11:40:56 +00:00
kolaente
8b3072672a
fix: progress bar color in dark mode
Resolves https://kolaente.dev/vikunja/frontend/issues/2194
2022-08-02 13:02:19 +02:00
renovate
0e09f9fded chore(deps): update typescript-eslint monorepo to v5.32.0 (#2213)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2213
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-01 18:49:08 +00:00
renovate
ab406f00d9 fix(deps): update sentry-javascript monorepo to v7.8.1 (#2212)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2212
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-01 15:21:05 +00:00
renovate
2507f661e8 chore(deps): update dependency eslint to v8.21.0 (#2211)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2211
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-01 13:41:42 +00:00
renovate
cf27131e48 fix(deps): update dependency vue-i18n to v9.2.0 (#2210)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2210
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-08-01 13:40:14 +00:00
drone
54d8e341ab [skip ci] Updated translations via Crowdin 2022-08-01 00:13:27 +00:00
renovate
7b17ccbf1f fix(deps): update sentry-javascript monorepo to v7.8.0 (#2208)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2208
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-31 18:45:49 +00:00
renovate
ce7563ea4c fix(deps): update vueuse to v9 (major) (#2209)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2209
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-31 17:39:53 +00:00
renovate
d9f3555d8d fix(deps): update dependency vue-router to v4.1.3 (#2206)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2206
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-31 17:14:05 +00:00
renovate
b2dd63630c chore(deps): update typescript-eslint monorepo to v5.31.0 (#2207)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2207
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-31 17:12:34 +00:00
renovate
db1a41f845 fix(deps): update dependency @kyvg/vue3-notification to v2.3.6 (#2205)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2205
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-31 17:01:26 +00:00
renovate
08ae0046de chore(deps): update dependency vue-tsc to v0.39.4 (#2187)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2187
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-31 16:21:19 +00:00
renovate
c173542b23 chore(deps): update dependency rollup to v2.77.2 (#2203)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2203
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-31 16:19:32 +00:00
kolaente
518417c0de
feat: add more testcases for parsing weekdays
Related to https://kolaente.dev/vikunja/api/issues/1223
2022-07-31 18:16:31 +02:00
renovate
c2e58a2320 chore(deps): update dependency autoprefixer to v10.4.8 (#2202)
Reviewed-on: https://kolaente.dev/vikunja/frontend/pulls/2202
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-07-31 16:09:59 +00:00
347 changed files with 25539 additions and 19868 deletions

View file

@ -29,26 +29,91 @@ steps:
# AWS_SECRET_ACCESS_KEY: # AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key # from_secret: cache_aws_secret_access_key
# settings: # settings:
# debug: true
# restore: true # restore: true
# bucket: kolaente.dev-drone-dependency-cache # bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud # endpoint: https://s3.fr-par.scw.cloud
# region: fr-par # region: fr-par
# path_style: true # path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}' # cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
# mount: # mount:
# - '.cache' # - .cache
- name: dependencies - name: dependencies
image: node:18 image: node:18-alpine
pull: true pull: true
environment: environment:
YARN_CACHE_FOLDER: .cache/yarn/ PNPM_CACHE_FOLDER: .cache/pnpm
CYPRESS_CACHE_FOLDER: .cache/cypress/ CYPRESS_CACHE_FOLDER: .cache/cypress
commands: commands:
- yarn --frozen-lockfile --network-timeout 100000 - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000
# depends_on: # depends_on:
# - restore-cache # - restore-cache
- name: lint
image: node:18-alpine
pull: true
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run lint
depends_on:
- dependencies
- name: build-prod
image: node:18-alpine
pull: true
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run build
depends_on:
- dependencies
- name: test-unit
image: node:18-alpine
pull: true
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run test:unit
depends_on:
- dependencies
- name: typecheck
failure: ignore
image: node:18-alpine
pull: true
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
commands:
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run typecheck
depends_on:
- dependencies
- name: test-frontend
image: cypress/browsers:node16.14.0-chrome99-ff97
pull: true
environment:
CYPRESS_API_URL: http://api:3456/api/v1
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
PNPM_CACHE_FOLDER: .cache/pnpm
CYPRESS_CACHE_FOLDER: .cache/cypress
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
CYPRESS_RECORD_KEY:
from_secret: cypress_project_key
commands:
- sed -i 's/localhost/api/g' dist/index.html
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm cypress install
- pnpm run serve:dist & npx wait-on http://localhost:4173
- pnpm run test:frontend --browser chrome --record
depends_on:
- build-prod
# - name: rebuild-cache # - name: rebuild-cache
# image: meltwater/drone-cache:dev # image: meltwater/drone-cache:dev
# pull: true # pull: true
@ -63,70 +128,14 @@ steps:
# endpoint: https://s3.fr-par.scw.cloud # endpoint: https://s3.fr-par.scw.cloud
# region: fr-par # region: fr-par
# path_style: true # path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}' # cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
# mount: # mount:
# - '.cache' # - .cache
# depends_on: # depends_on:
# - dependencies # - dependencies
- name: lint
image: node:18
pull: true
environment:
YARN_CACHE_FOLDER: .cache/yarn/
CYPRESS_CACHE_FOLDER: .cache/cypress/
commands:
- yarn run lint
depends_on:
- dependencies
- name: build-prod
image: node:18
pull: true
environment:
YARN_CACHE_FOLDER: .cache/yarn/
commands:
- yarn build
depends_on:
- dependencies
- name: test-unit
image: node:18
pull: true
commands:
- yarn test:unit
depends_on:
- dependencies
- name: typecheck
failure: ignore
image: node:18
pull: true
commands:
- yarn typecheck
depends_on:
- dependencies
- name: test-frontend
image: cypress/browsers:node16.5.0-chrome94-ff93
pull: true
environment:
CYPRESS_API_URL: http://api:3456/api/v1
CYPRESS_TEST_SECRET: averyLongSecretToSe33dtheDB
YARN_CACHE_FOLDER: .cache/yarn/
CYPRESS_CACHE_FOLDER: .cache/cypress/
CYPRESS_DEFAULT_COMMAND_TIMEOUT: 60000
CYPRESS_RECORD_KEY:
from_secret: cypress_project_key
commands:
- sed -i 's/localhost/api/g' dist/index.html
- yarn serve:dist & npx wait-on http://localhost:4173
- yarn test:frontend --browser chrome --record
depends_on:
- build-prod
- name: deploy-preview - name: deploy-preview
image: node:18 image: node:18-alpine
pull: true pull: true
environment: environment:
NETLIFY_AUTH_TOKEN: NETLIFY_AUTH_TOKEN:
@ -138,7 +147,8 @@ steps:
commands: commands:
- cp -r dist dist-preview - cp -r dist dist-preview
# Override the default api url used for preview # Override the default api url used for preview
- sed -i 's|localhost:3456|try.vikunja.io|g' dist-preview/index.html - sed -i 's|http://localhost:3456|https://try.vikunja.io|g' dist-preview/index.html
- apk add --no-cache perl-utils
- shasum -a 384 -c ./scripts/deploy-preview-netlify.js.sha384 - shasum -a 384 -c ./scripts/deploy-preview-netlify.js.sha384
- node ./scripts/deploy-preview-netlify.js - node ./scripts/deploy-preview-netlify.js
depends_on: depends_on:
@ -181,21 +191,22 @@ steps:
# endpoint: https://s3.fr-par.scw.cloud # endpoint: https://s3.fr-par.scw.cloud
# region: fr-par # region: fr-par
# path_style: true # path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}' # cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
# mount: # mount:
# - '.cache' # - .cache
- name: build - name: build
image: node:18 image: node:18-alpine
pull: true pull: true
group: build-static group: build-static
environment: environment:
YARN_CACHE_FOLDER: .cache/yarn/ PNPM_CACHE_FOLDER: .cache/pnpm
commands: commands:
- yarn --frozen-lockfile --network-timeout 100000 - corepack enable && pnpm config set store-dir .cache/.pnp
- yarn run lint - pnpm install --fetch-timeout 100000
- pnpm run lint
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json" - "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- yarn run build - pnpm run build
- sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing - sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing
# depends_on: # depends_on:
# - restore-cache # - restore-cache
@ -256,21 +267,22 @@ steps:
# endpoint: https://s3.fr-par.scw.cloud # endpoint: https://s3.fr-par.scw.cloud
# region: fr-par # region: fr-par
# path_style: true # path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "yarn.lock" }}_{{ arch }}_{{ os }}' # cache_key: '{{ .Repo.Name }}_{{ checksum "pnpm-lock.yaml" }}_{{ arch }}_{{ os }}'
# mount: # mount:
# - '.cache' # - .cache
- name: build - name: build
image: node:18 image: node:18-alpine
pull: true pull: true
group: build-static group: build-static
environment: environment:
YARN_CACHE_FOLDER: .cache/yarn/ PNPM_CACHE_FOLDER: .cache/pnpm
commands: commands:
- yarn --frozen-lockfile --network-timeout 100000 - corepack enable && pnpm config set store-dir .cache/pnpm
- yarn run lint - pnpm install --fetch-timeout 100000
- pnpm run lint
- "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json" - "echo '{\"VERSION\": \"'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'\"}' > src/version.json"
- yarn run build - pnpm run build
- sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing - sed -i 's/http\:\\/\\/localhost\\:3456\\/api\\/v1/\\/api\\/v1/g' dist/index.html # Override the default api url used for developing
# depends_on: # depends_on:
# - restore-cache # - restore-cache
@ -647,6 +659,6 @@ steps:
from_secret: crowdin_key from_secret: crowdin_key
--- ---
kind: signature kind: signature
hmac: 997e1badebe484ac29557c4af356e63db4d3d57f3d32e92d482f117f8cec64da hmac: c885a0e50db729842402494aa645dd3ac662828b691108550f6bf302158295ba
... ...

1
.envrc Normal file
View file

@ -0,0 +1 @@
use flake

View file

@ -1,3 +1,6 @@
/* eslint-env node */
require("@rushstack/eslint-patch/modern-module-resolution")
module.exports = { module.exports = {
'root': true, 'root': true,
'env': { 'env': {
@ -9,7 +12,7 @@ module.exports = {
'extends': [ 'extends': [
'eslint:recommended', 'eslint:recommended',
'plugin:vue/vue3-essential', 'plugin:vue/vue3-essential',
'@vue/typescript', '@vue/eslint-config-typescript/recommended',
], ],
'rules': { 'rules': {
'vue/html-quotes': [ 'vue/html-quotes': [
@ -28,7 +31,6 @@ module.exports = {
'error', 'error',
'never', 'never',
], ],
'vue/script-setup-uses-vars': 'error',
// see https://segmentfault.com/q/1010000040813116/a-1020000041134455 (original in chinese) // see https://segmentfault.com/q/1010000040813116/a-1020000041134455 (original in chinese)
'no-unused-vars': 'off', 'no-unused-vars': 'off',
@ -40,6 +42,7 @@ module.exports = {
'parserOptions': { 'parserOptions': {
'parser': '@typescript-eslint/parser', 'parser': '@typescript-eslint/parser',
'ecmaVersion': 2022, 'ecmaVersion': 2022,
'sourceType': 'module',
}, },
'ignorePatterns': [ 'ignorePatterns': [
'*.test.*', '*.test.*',

58
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View file

@ -0,0 +1,58 @@
name: Bug Report
description: Found something you weren't expecting? Report it here!
labels: kind/bug
body:
- type: markdown
attributes:
value: |
NOTE: If your issue is a security concern, please send an email to security@vikunja.io instead of opening a public issue.
- type: markdown
attributes:
value: |
Please fill out this issue template to report a bug.
1. If you want to propose a new feature, please open a discussion thread in the forum: https://community.vikunja.io
2. Please ask questions or configuration/deploy problems on our [Matrix Room](https://matrix.to/#/#vikunja:matrix.org) or forum (https://community.vikunja.io).
3. Make sure you are using the latest release and
take a moment to check that your issue hasn't been reported before.
4. Please give all relevant information below for bug reports, because
incomplete details will be handled as an invalid report and closed.
- type: textarea
id: description
attributes:
label: Description
description: |
Please provide a description of your issue here, with a URL if you were able to reproduce the issue (see below).
- type: input
id: frontend-version
attributes:
label: Vikunja Frontend Version
description: Vikunja frontend version (or commit reference) of your instance
validations:
required: true
- type: input
id: api-version
attributes:
label: Vikunja API Version
description: Vikunja API version (or commit reference) of your instance
validations:
required: true
- type: input
id: browser-version
attributes:
label: Browser and version
description: If your issue is related to a frontend problem, please provide the browser and version you used to reproduce it.
- type: dropdown
id: can-reproduce
attributes:
label: Can you reproduce the bug on the Vikunja demo site?
options:
- "Yes"
- "No"
validations:
required: true
- type: textarea
id: screenshots
attributes:
label: Screenshots
description: If this issue involves the Web Interface, please provide one or more screenshots

17
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View file

@ -0,0 +1,17 @@
blank_issues_enabled: false
contact_links:
- name: API issues
url: https://code.vikunja.io/api/issues
about: This is the frontend repo. Please open api-related bug reports and discussions in the api 0repo. Not sure if your issue is frontend or api? Ask in Matrix or the forum first.
- name: Forum
url: https://community.vikunja.io/
about: Feature Requests, Questions, configuration or deployment problems should be discussed in the forum.
- name: Security-related issues
url: https://vikunja.io/contact/#security
about: For security concerns, please send a mail to security@vikunja.io instead of opening a public issue.
- name: Chat on Matrix
url: https://matrix.to/#/#vikunja:matrix.org
about: Please ask any quick questions here.
- name: Translations
url: https://crowdin.com/project/vikunja
about: Any problems or requests for new languages about translations should be handled in crowdin.

5
.gitignore vendored
View file

@ -2,16 +2,21 @@
node_modules node_modules
/dist* /dist*
*.zip *.zip
.direnv/
# local env files # local env files
.env.local .env.local
.env.*.local .env.*.local
# Log files # Log files
logs
*.log
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
stats.html stats.html
pnpm-debug.log*
lerna-debug.log*
# Editor directories and files # Editor directories and files
.idea .idea

2
.npmrc Normal file
View file

@ -0,0 +1,2 @@
auto-install-peers=true
fetch-timeout=100000

View file

@ -1,5 +1,5 @@
{ {
"eslint.packageManager": "yarn", "eslint.packageManager": "pnpm",
"editor.formatOnSave": false, "editor.formatOnSave": false,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": true "source.fixAll": true

File diff suppressed because it is too large Load diff

View file

@ -1,28 +1,32 @@
# Stage 1: Build application # Stage 1: Build application
FROM node:18 AS compile-image FROM node:18-alpine AS compile-image
WORKDIR /build WORKDIR /build
ARG USE_RELEASE=false ARG USE_RELEASE=false
ARG RELEASE_VERSION=main ARG RELEASE_VERSION=main
ENV YARN_CACHE_FOLDER .cache/yarn/ ENV PNPM_CACHE_FOLDER .cache/pnpm/
COPY . ./ ADD . ./
RUN \ RUN \
if [ $USE_RELEASE = true ]; then \ if [ $USE_RELEASE = true ]; then \
rm -rf dist/ && \
wget https://dl.vikunja.io/frontend/vikunja-frontend-$RELEASE_VERSION.zip -O frontend-release.zip && \ wget https://dl.vikunja.io/frontend/vikunja-frontend-$RELEASE_VERSION.zip -O frontend-release.zip && \
unzip frontend-release.zip -d dist/ && \ unzip frontend-release.zip -d dist/ && \
exit 0; \ exit 0; \
fi && \ fi && \
# https://pnpm.io/installation#using-corepack
corepack enable && \
# we don't use corepack prepare here by intend since
# we have renovate to keep our dependencies up to date
# Build the frontend # Build the frontend
yarn install --frozen-lockfile --network-timeout 100000 && \ pnpm install && \
apk add --no-cache git && \
echo '{"VERSION": "'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'"}' > src/version.json && \ echo '{"VERSION": "'$(git describe --tags --always --abbrev=10 | sed 's/-/+/' | sed 's/^v//' | sed 's/-g/-/')'"}' > src/version.json && \
yarn run build pnpm run build
# Stage 2: copy # Stage 2: copy
FROM nginx FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf COPY nginx.conf /etc/nginx/nginx.conf
COPY run.sh /run.sh COPY run.sh /run.sh
@ -36,4 +40,10 @@ ENV PGID 1000
LABEL maintainer="maintainers@vikunja.io" LABEL maintainer="maintainers@vikunja.io"
RUN apk add --no-cache \
# for sh file
bash \
# installs usermod and groupmod
shadow
CMD "/run.sh" CMD "/run.sh"

View file

@ -4,7 +4,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/frontend/status.svg)](https://drone.kolaente.de/vikunja/frontend) [![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) [![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Download](https://img.shields.io/badge/download-v0.18.2-brightgreen.svg)](https://dl.vikunja.io) [![Download](https://img.shields.io/badge/download-v0.19.1-brightgreen.svg)](https://dl.vikunja.io)
[![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja) [![Translation](https://badges.crowdin.net/vikunja/localized.svg)](https://crowdin.com/project/vikunja)
This is the web frontend for Vikunja, written in Vue.js. This is the web frontend for Vikunja, written in Vue.js.
@ -22,23 +22,27 @@ There is a [docker image available](https://hub.docker.com/r/vikunja/api) with s
## Project setup ## Project setup
```shell ```shell
yarn install pnpm install
``` ```
### Compiles and hot-reloads for development ### Compiles and hot-reloads for development
```shell ```shell
yarn run serve pnpm run serve
``` ```
### Compiles and minifies for production ### Compiles and minifies for production
```shell ```shell
yarn run build pnpm run build
``` ```
### Lints and fixes files ### Lints and fixes files
```shell ```shell
yarn run lint pnpm run lint
``` ```
## Sponsors
[![Relm](https://vikunja.io/images/sponsors/relm.png)](https://relm.us)

59
cliff.toml Normal file
View file

@ -0,0 +1,59 @@
[changelog]
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | upper_first }}
{% for commit in commits
| filter(attribute="scope")
| sort(attribute="scope") %}
* *({{commit.scope}})* {{ commit.message | upper_first }}
{%- if commit.breaking %}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{%- endif -%}
{%- endfor -%}
{%- for commit in commits %}
{%- if commit.scope -%}
{% else -%}
* {{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))
{% if commit.breaking -%}
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
{% endif -%}
{% endif -%}
{% endfor -%}
{% raw %}\n{% endraw %}\
{% endfor %}\n
"""
#{% for group, commits in commits | group_by(attribute="group") %}
# ### {{ group | upper_first }}
# {% for commit in commits %}\
# - {% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message | upper_first }} ([{{ commit.id | truncate(length=7, end="") }}]({{ commit.id }}))
# {% endfor %}\
#{% endfor %}\n
# remove the leading and trailing whitespace from the template
trim = true
[git]
conventional_commits = true
filter_unconventional = false
commit_parsers = [
{ message = ".*(deps).*", group = "Dependencies"},
{ message = "^feat", group = "Features"},
{ message = "^fix", group = "Bug Fixes"},
{ message = "^doc", group = "Documentation"},
{ message = "^perf", group = "Performance"},
{ message = "^refactor", group = "Refactor"},
{ message = "^style", group = "Styling"},
{ message = "^test", group = "Testing"},
{ message = "^chore\\(release\\): prepare for", skip = true},
{ message = "^chore", group = "Miscellaneous Tasks"},
{ body = ".*security", group = "Security"},
{ message = ".*", group = "Other", default_scope = "other"}, # Everything that's not a conventional commit goes into the "Other" category
]

View file

@ -36,7 +36,7 @@ to get a shell inside the cypress container.
In that shell you can then execute the tests with In that shell you can then execute the tests with
```shell ```shell
yarn test:frontend pnpm run test:frontend
``` ```
### Using The Cypress Dashboard ### Using The Cypress Dashboard
@ -44,5 +44,5 @@ yarn test:frontend
To open the Cypress Dashboard and run tests from there, run To open the Cypress Dashboard and run tests from there, run
```shell ```shell
yarn cypress:open pnpm run cypress:open
``` ```

View file

@ -9,7 +9,7 @@ services:
ports: ports:
- 3456:3456 - 3456:3456
cypress: cypress:
image: cypress/browsers:node12.18.3-chrome87-ff82 image: cypress/browsers:node16.14.0-chrome99-ff97
volumes: volumes:
- ..:/project - ..:/project
- $HOME/.cache:/home/node/.cache/ - $HOME/.cache:/home/node/.cache/

View file

@ -16,10 +16,12 @@ describe('List View Gantt', () => {
}) })
it('Shows tasks from the current and next month', () => { it('Shows tasks from the current and next month', () => {
const now = new Date() const now = Date.UTC(2022, 8, 25)
const nextMonth = now cy.clock(now, ['Date'])
const nextMonth = new Date(now)
nextMonth.setDate(1) nextMonth.setDate(1)
nextMonth.setMonth(now.getMonth() + 1) nextMonth.setMonth(9)
cy.visit('/lists/1/gantt') cy.visit('/lists/1/gantt')
@ -32,7 +34,7 @@ describe('List View Gantt', () => {
const now = new Date() const now = new Date()
const tasks = TaskFactory.create(1, { const tasks = TaskFactory.create(1, {
start_date: formatISO(now), start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)) end_date: formatISO(now.setDate(now.getDate() + 4)),
}) })
cy.visit('/lists/1/gantt') cy.visit('/lists/1/gantt')
@ -63,7 +65,7 @@ describe('List View Gantt', () => {
const now = new Date() const now = new Date()
TaskFactory.create(1, { TaskFactory.create(1, {
start_date: formatISO(now), start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)) end_date: formatISO(now.setDate(now.getDate() + 4)),
}) })
cy.visit('/lists/1/gantt') cy.visit('/lists/1/gantt')

View file

@ -59,7 +59,7 @@ describe('Lists', () => {
.click() .click()
cy.get('#title') cy.get('#title')
.type(`{selectall}${newListName}`) .type(`{selectall}${newListName}`)
cy.get('footer.modal-card-foot .button') cy.get('footer.card-footer .button')
.contains('Save') .contains('Save')
.click() .click()

View file

@ -63,7 +63,7 @@ describe('Namepaces', () => {
.should('equal', newNamespaces[0].title) // wait until the namespace data is loaded .should('equal', newNamespaces[0].title) // wait until the namespace data is loaded
cy.get('#namespacetext') cy.get('#namespacetext')
.type(`{selectall}${newNamespaceName}`) .type(`{selectall}${newNamespaceName}`)
cy.get('footer.modal-card-foot .button') cy.get('footer.card-footer .button')
.contains('Save') .contains('Save')
.click() .click()

View file

@ -3,14 +3,19 @@ import {UserFactory} from '../../factories/user'
import {NamespaceFactory} from '../../factories/namespace' import {NamespaceFactory} from '../../factories/namespace'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
export function prepareLists(setLists = () => {}) { export function createLists() {
beforeEach(() => {
UserFactory.create(1) UserFactory.create(1)
NamespaceFactory.create(1) NamespaceFactory.create(1)
const lists = ListFactory.create(1, { const lists = ListFactory.create(1, {
title: 'First List' title: 'First List'
}) })
setLists(lists)
TaskFactory.truncate() TaskFactory.truncate()
return lists
}
export function prepareLists(setLists = () => {}) {
beforeEach(() => {
const lists = createLists()
setLists(lists)
}) })
} }

View file

@ -12,15 +12,51 @@ import {LabelTaskFactory} from '../../factories/label_task'
import {BucketFactory} from '../../factories/bucket' import {BucketFactory} from '../../factories/bucket'
import '../../support/authenticateUser' import '../../support/authenticateUser'
import {TaskAttachmentFactory} from '../../factories/task_attachments'
function addLabelToTaskAndVerify(labelTitle: string) {
cy.get('.task-view .action-buttons .button')
.contains('Add Labels')
.click()
cy.get('.task-view .details.labels-list .multiselect input')
.type(labelTitle)
cy.get('.task-view .details.labels-list .multiselect .search-results')
.children()
.first()
.click()
cy.get('.global-notification', { timeout: 4000 })
.should('contain', 'Success')
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag')
.should('exist')
.should('contain', labelTitle)
}
function uploadAttachmentAndVerify(taskId: number) {
cy.intercept(`${Cypress.env('API_URL')}/tasks/${taskId}/attachments`).as('uploadAttachment')
cy.get('.task-view .action-buttons .button')
.contains('Add Attachments')
.click()
cy.get('input[type=file]', {timeout: 1000})
.selectFile('cypress/fixtures/image.jpg', {force: true}) // The input is not visible, but on purpose
cy.wait('@uploadAttachment')
cy.get('.attachments .attachments .files a.attachment')
.should('exist')
}
describe('Task', () => { describe('Task', () => {
let namespaces let namespaces
let lists let lists
let buckets
beforeEach(() => { beforeEach(() => {
UserFactory.create(1) UserFactory.create(1)
namespaces = NamespaceFactory.create(1) namespaces = NamespaceFactory.create(1)
lists = ListFactory.create(1) lists = ListFactory.create(1)
buckets = BucketFactory.create(1, {
list_id: lists[0].id,
})
TaskFactory.truncate() TaskFactory.truncate()
UserListFactory.truncate() UserListFactory.truncate()
}) })
@ -80,6 +116,7 @@ describe('Task', () => {
describe('Task Detail View', () => { describe('Task Detail View', () => {
beforeEach(() => { beforeEach(() => {
TaskCommentFactory.truncate() TaskCommentFactory.truncate()
LabelTaskFactory.truncate()
}) })
it('Shows all task details', () => { it('Shows all task details', () => {
@ -344,21 +381,31 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`) cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button') addLabelToTaskAndVerify(labels[0].title)
.contains('Add Labels') })
.click()
cy.get('.task-view .details.labels-list .multiselect input') it('Can add a label to a task and it shows up on the kanban board afterwards', () => {
.type(labels[0].title) const tasks = TaskFactory.create(1, {
cy.get('.task-view .details.labels-list .multiselect .search-results') id: 1,
.children() list_id: lists[0].id,
.first() bucket_id: buckets[0].id,
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
cy.visit(`/lists/${lists[0].id}/kanban`)
cy.get('.bucket .task')
.contains(tasks[0].title)
.click() .click()
cy.get('.global-notification', { timeout: 4000 }) addLabelToTaskAndVerify(labels[0].title)
.should('contain', 'Success')
cy.get('.task-view .details.labels-list .multiselect .input-wrapper span.tag') cy.get('.modal-content .close')
.should('exist') .click()
.should('contain', labels[0].title)
cy.get('.bucket .task')
.should('contain.text', labels[0].title)
}) })
it('Can remove a label from a task', () => { it('Can remove a label from a task', () => {
@ -417,5 +464,87 @@ describe('Task', () => {
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')
}) })
it('Can set a priority for a task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button')
.contains('Set Priority')
.click()
cy.get('.task-view .columns.details .column')
.contains('Priority')
.get('.select select')
.select('Urgent')
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.task-view .columns.details .column')
.contains('Priority')
.get('.select select')
.should('have.value', '4')
})
it('Can set the progress for a task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
})
cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button')
.contains('Set Progress')
.click()
cy.get('.task-view .columns.details .column')
.contains('Progress')
.get('.select select')
.select('50%')
cy.get('.global-notification')
.should('contain', 'Success')
cy.wait(200)
cy.get('.task-view .columns.details .column')
.contains('Progress')
.get('.select select')
.should('be.visible')
.should('have.value', '0.5')
})
it('Can add an attachment to a task', () => {
TaskAttachmentFactory.truncate()
const tasks = TaskFactory.create(1, {
id: 1,
})
cy.visit(`/tasks/${tasks[0].id}`)
uploadAttachmentAndVerify(tasks[0].id)
})
it('Can add an attachment to a task and see it appearing on kanban', () => {
TaskAttachmentFactory.truncate()
const tasks = TaskFactory.create(1, {
id: 1,
list_id: lists[0].id,
bucket_id: buckets[0].id,
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
cy.visit(`/lists/${lists[0].id}/kanban`)
cy.get('.bucket .task')
.contains(tasks[0].title)
.click()
uploadAttachmentAndVerify(tasks[0].id)
cy.get('.modal-content .close')
.click()
cy.get('.bucket .task .footer .icon svg.fa-paperclip')
.should('exist')
})
}) })
}) })

View file

@ -1,16 +1,44 @@
import '../../support/authenticateUser' import '../../support/authenticateUser'
import {createLists} from '../list/prepareLists'
describe('Log out', () => { function logout() {
it('Logs the user out', () => {
cy.visit('/')
cy.get('.navbar .user .username') cy.get('.navbar .user .username')
.click() .click()
cy.get('.navbar .user .dropdown-menu .dropdown-item') cy.get('.navbar .user .dropdown-menu .dropdown-item')
.contains('Logout') .contains('Logout')
.click() .click()
}
describe('Log out', () => {
it('Logs the user out', () => {
cy.visit('/')
expect(localStorage.getItem('token')).to.not.eq(null)
logout()
cy.url() cy.url()
.should('contain', '/login') .should('contain', '/login')
.then(() => {
expect(localStorage.getItem('token')).to.eq(null)
})
})
it.skip('Should clear the list history after logging the user out', () => {
const lists = createLists()
cy.visit(`/lists/${lists[0].id}`)
.then(() => {
expect(localStorage.getItem('listHistory')).to.not.eq(null)
})
logout()
cy.wait(1000) // This makes re-loading of the list and associated entities (and the resulting error) visible
cy.url()
.should('contain', '/login')
.then(() => {
expect(localStorage.getItem('listHistory')).to.eq(null)
})
}) })
}) })

View file

@ -0,0 +1,17 @@
import {Factory} from '../support/factory'
import {formatISO} from 'date-fns'
export class TaskAttachmentFactory extends Factory {
static table = 'task_attachments'
static factory() {
const now = new Date()
return {
id: '{increment}',
task_id: 1,
file_id: 1,
created: formatISO(now),
}
}
}

25
flake.lock Normal file
View file

@ -0,0 +1,25 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1664753041,
"narHash": "sha256-0ogaD8PaGHluARFeupofvk1Nq9gpVeZdlFM0Kcwguys=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "a62844b302507c7531ad68a86cb7aa54704c9cb4",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

10
flake.nix Normal file
View file

@ -0,0 +1,10 @@
{
description = "Vikunja frontend dev environment";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
defaultPackage.x86_64-linux =
pkgs.mkShell { buildInputs = [ pkgs.nodePackages.pnpm pkgs.cypress ]; };
};
}

View file

@ -1,10 +1,9 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Vikunja</title>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0"> <meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Vikunja</title>
<meta name="description" content="Vikunja (/vɪˈkuːnjə/) - The to-do app to organize your life."> <meta name="description" content="Vikunja (/vɪˈkuːnjə/) - The to-do app to organize your life.">
<meta name="theme-color" content="#1973ff"/> <meta name="theme-color" content="#1973ff"/>

View file

@ -1,5 +1,5 @@
[build] [build]
command = "yarn build" command = "pnpm run build"
publish = "dist-preview" publish = "dist-preview"
[[redirects]] [[redirects]]

View file

@ -11,88 +11,96 @@
"build:dev": "vite build -m development --outDir dist-dev/", "build:dev": "vite build -m development --outDir dist-dev/",
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts", "lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"test:unit": "vitest", "test:unit": "vitest --run",
"test:unit-watch": "vitest watch", "test:unit-watch": "vitest watch",
"test:frontend": "cypress run", "test:frontend": "cypress run",
"typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false", "typecheck": "vue-tsc --noEmit && vue-tsc --noEmit -p tsconfig.vitest.json --composite false",
"browserslist:update": "npx browserslist@latest --update-db" "browserslist:update": "npx browserslist@latest --update-db"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "6.2.0",
"@fortawesome/free-regular-svg-icons": "6.2.0",
"@fortawesome/free-solid-svg-icons": "6.2.0",
"@fortawesome/vue-fontawesome": "3.0.1",
"@github/hotkey": "2.0.1", "@github/hotkey": "2.0.1",
"@kyvg/vue3-notification": "2.3.5", "@kyvg/vue3-notification": "2.4.1",
"@sentry/tracing": "7.7.0", "@sentry/tracing": "7.15.0",
"@sentry/vue": "7.7.0", "@sentry/vue": "7.15.0",
"@types/is-touch-device": "1.0.0", "@types/is-touch-device": "1.0.0",
"@types/sortablejs": "1.13.0", "@types/lodash.clonedeep": "4.5.7",
"@vueuse/core": "8.9.4", "@types/sortablejs": "1.15.0",
"@vueuse/router": "8.9.4", "@vueuse/core": "9.3.0",
"blurhash": "1.1.5", "@vueuse/router": "9.3.0",
"axios": "0.27.2",
"blurhash": "2.0.3",
"bulma-css-variables": "0.9.33", "bulma-css-variables": "0.9.33",
"camel-case": "4.1.2", "camel-case": "4.1.2",
"date-fns": "2.29.1", "codemirror": "5.65.9",
"dompurify": "2.3.10", "date-fns": "2.29.3",
"easymde": "2.16.1", "dompurify": "2.4.0",
"easymde": "2.18.0",
"flatpickr": "4.6.13", "flatpickr": "4.6.13",
"flexsearch": "0.7.21", "flexsearch": "0.7.21",
"floating-vue": "2.0.0-beta.20",
"highlight.js": "11.6.0", "highlight.js": "11.6.0",
"is-touch-device": "1.0.1", "is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0", "lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"marked": "4.0.18", "marked": "4.1.1",
"minimist": "1.2.6", "minimist": "1.2.7",
"pinia": "2.0.23",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
"snake-case": "3.0.4", "snake-case": "3.0.4",
"sortablejs": "1.15.0",
"ufo": "0.8.5", "ufo": "0.8.5",
"v-tooltip": "4.0.0-beta.17", "vue": "3.2.40",
"vue": "3.2.37", "vue-advanced-cropper": "2.8.6",
"vue-advanced-cropper": "2.8.3",
"vue-drag-resize": "2.0.3", "vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.6", "vue-flatpickr-component": "9.0.8",
"vue-i18n": "9.2.0-beta.40", "vue-i18n": "9.2.2",
"vue-router": "4.1.2", "vue-router": "4.1.5",
"vuex": "4.0.2",
"workbox-precaching": "6.5.4", "workbox-precaching": "6.5.4",
"zhyswan-vuedraggable": "4.1.3" "zhyswan-vuedraggable": "4.1.3"
}, },
"devDependencies": { "devDependencies": {
"@4tw/cypress-drag-drop": "2.2.1", "@4tw/cypress-drag-drop": "2.2.1",
"@cypress/vite-dev-server": "3.0.0", "@cypress/vite-dev-server": "3.3.1",
"@cypress/vue": "4.0.0", "@cypress/vue": "4.2.0",
"@faker-js/faker": "7.3.0", "@faker-js/faker": "7.5.0",
"@fortawesome/fontawesome-svg-core": "6.1.2", "@rushstack/eslint-patch": "1.2.0",
"@fortawesome/free-regular-svg-icons": "6.1.2", "@types/dompurify": "2.3.4",
"@fortawesome/free-solid-svg-icons": "6.1.2",
"@fortawesome/vue-fontawesome": "3.0.1",
"@types/flexsearch": "0.7.3", "@types/flexsearch": "0.7.3",
"@typescript-eslint/eslint-plugin": "5.30.7", "@types/lodash.debounce": "4.0.7",
"@typescript-eslint/parser": "5.30.7", "@types/marked": "4.0.7",
"@vitejs/plugin-legacy": "2.0.0", "@types/node": "16.11.65",
"@vitejs/plugin-vue": "3.0.1", "@typescript-eslint/eslint-plugin": "5.40.0",
"@vue/eslint-config-typescript": "11.0.0", "@typescript-eslint/parser": "5.40.0",
"@vue/test-utils": "2.0.2", "@vitejs/plugin-legacy": "2.2.0",
"@vitejs/plugin-vue": "3.1.2",
"@vue/eslint-config-typescript": "11.0.2",
"@vue/test-utils": "2.1.0",
"@vue/tsconfig": "0.1.3", "@vue/tsconfig": "0.1.3",
"autoprefixer": "10.4.7", "autoprefixer": "10.4.12",
"axios": "0.27.2", "browserslist": "4.21.4",
"browserslist": "4.21.3", "caniuse-lite": "1.0.30001418",
"caniuse-lite": "1.0.30001373", "cypress": "10.10.0",
"cypress": "10.3.1", "esbuild": "0.15.10",
"esbuild": "0.14.51", "eslint": "8.25.0",
"eslint": "8.20.0", "eslint-plugin-vue": "9.6.0",
"eslint-plugin-vue": "9.3.0", "express": "4.18.2",
"express": "4.18.1", "happy-dom": "7.4.0",
"happy-dom": "6.0.4", "netlify-cli": "12.0.7",
"netlify-cli": "10.13.0", "postcss": "8.4.17",
"postcss": "8.4.14", "postcss-preset-env": "7.8.2",
"postcss-preset-env": "7.7.2", "rollup": "3.0.0",
"rollup": "2.77.0", "rollup-plugin-visualizer": "5.8.2",
"rollup-plugin-visualizer": "5.7.1", "sass": "1.55.0",
"sass": "1.54.0", "typescript": "4.8.4",
"typescript": "4.7.4", "vite": "3.1.7",
"vite": "3.0.4", "vite-plugin-pwa": "0.13.1",
"vite-plugin-pwa": "0.12.3", "vite-svg-loader": "3.6.0",
"vite-svg-loader": "3.4.0", "vitest": "0.24.1",
"vitest": "0.20.2", "vue-tsc": "1.0.5",
"vue-tsc": "0.38.9",
"wait-on": "6.0.1", "wait-on": "6.0.1",
"workbox-cli": "6.5.4" "workbox-cli": "6.5.4"
}, },
@ -102,5 +110,5 @@
} }
}, },
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"packageManager": "yarn@1.22.19" "packageManager": "pnpm@7.13.4"
} }

13245
pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,7 @@
], ],
"packageRules": [ "packageRules": [
{ {
"matchPackageNames": ["netlify-cli"], "matchPackageNames": ["netlify-cli", "happy-dom"],
"extends": ["schedule:weekly"] "extends": ["schedule:weekly"]
}, },
{ {
@ -19,6 +19,12 @@
"matchPackagePrefixes": [ "matchPackagePrefixes": [
"@vueuse/" "@vueuse/"
] ]
},
{
"matchDepTypes": ["devDependencies"],
"automerge": true,
"automergeStrategy": "squash",
"automergeType": "pr"
} }
] ]
} }

View file

@ -15,10 +15,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, watch, Ref} from 'vue' import {computed, watch, type Ref} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import {useRouteQuery} from '@vueuse/router' import {useRouteQuery} from '@vueuse/router'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import isTouchDevice from 'is-touch-device' import isTouchDevice from 'is-touch-device'
import {success} from '@/message' import {success} from '@/message'
@ -34,17 +33,20 @@ import Ready from '@/components/misc/ready.vue'
import {setLanguage} from './i18n' import {setLanguage} from './i18n'
import AccountDeleteService from '@/services/accountDelete' import AccountDeleteService from '@/services/accountDelete'
import {useBaseStore} from '@/stores/base'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {useBodyClass} from '@/composables/useBodyClass' import {useBodyClass} from '@/composables/useBodyClass'
import {useAuthStore} from './stores/auth'
const store = useStore() const baseStore = useBaseStore()
const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
useBodyClass('is-touch', isTouchDevice()) useBodyClass('is-touch', isTouchDevice())
const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive) const keyboardShortcutsActive = computed(() => baseStore.keyboardShortcutsActive)
const authUser = computed(() => store.getters['auth/authUser']) const authUser = computed(() => authStore.authUser)
const authLinkShare = computed(() => store.getters['auth/authLinkShare']) const authLinkShare = computed(() => authStore.authLinkShare)
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -58,7 +60,7 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
const accountDeletionService = new AccountDeleteService() const accountDeletionService = new AccountDeleteService()
await accountDeletionService.confirm(accountDeletionConfirm) await accountDeletionService.confirm(accountDeletionConfirm)
success({message: t('user.deletion.confirmSuccess')}) success({message: t('user.deletion.confirmSuccess')})
store.dispatch('auth/refreshUserInfo') authStore.refreshUserInfo()
}, { immediate: true }) }, { immediate: true })
// setup password reset redirect // setup password reset redirect

View file

@ -7,17 +7,12 @@
:disabled="disabled || undefined" :disabled="disabled || undefined"
ref="button" ref="button"
> >
<slot /> <slot/>
</component> </component>
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default { inheritAttrs: false }
// see https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script
export default defineComponent({
inheritAttrs: false,
})
</script> </script>
<script lang="ts" setup> <script lang="ts" setup>
@ -30,7 +25,7 @@ export default defineComponent({
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead! // NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
import { ref, watchEffect, computed, useAttrs, PropType } from 'vue' import { ref, watchEffect, computed, useAttrs, type PropType } from 'vue'
const BASE_BUTTON_TYPES_MAP = Object.freeze({ const BASE_BUTTON_TYPES_MAP = Object.freeze({
button: 'button', button: 'button',
@ -52,6 +47,7 @@ const props = defineProps({
const componentNodeName = ref<Node['nodeName']>('button') const componentNodeName = ref<Node['nodeName']>('button')
interface ElementBindings { interface ElementBindings {
type?: string; type?: string;
rel?: string; rel?: string;
@ -92,6 +88,7 @@ watchEffect(() => {
const isButton = computed(() => componentNodeName.value === 'button') const isButton = computed(() => componentNodeName.value === 'button')
const button = ref() const button = ref()
function focus() { function focus() {
button.value.focus() button.value.focus()
} }
@ -123,7 +120,7 @@ defineExpose({
user-select: none; user-select: none;
pointer-events: auto; // disable possible resets pointer-events: auto; // disable possible resets
&:focus { &:focus, &.is-focused {
outline: transparent; outline: transparent;
} }

View file

@ -6,13 +6,13 @@
{{ $t('input.datemathHelp.intro') }} {{ $t('input.datemathHelp.intro') }}
</p> </p>
<p> <p>
<i18n-t keypath="input.datemathHelp.expression"> <i18n-t keypath="input.datemathHelp.expression" scope="global">
<code>now</code> <code>now</code>
<code>||</code> <code>||</code>
</i18n-t> </i18n-t>
</p> </p>
<p> <p>
<i18n-t keypath="input.datemathHelp.similar"> <i18n-t keypath="input.datemathHelp.similar" scope="global">
<BaseButton <BaseButton
href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/" href="https://grafana.com/docs/grafana/latest/dashboards/time-range-controls/"
target="_blank"> target="_blank">
@ -99,7 +99,7 @@
<tr> <tr>
<td><code>{{ exampleDate }}||+1M/d</code></td> <td><code>{{ exampleDate }}||+1M/d</code></td>
<td> <td>
<i18n-t keypath="input.datemathHelp.examples.datePlusMonth"> <i18n-t keypath="input.datemathHelp.examples.datePlusMonth" scope="global">
<code>{{ exampleDate }}</code> <code>{{ exampleDate }}</code>
</i18n-t> </i18n-t>
</td> </td>
@ -110,10 +110,10 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {format} from 'date-fns' import { formatDate } from '@/helpers/time/formatDate'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
const exampleDate = format(new Date(), 'yyyy-MM-dd') const exampleDate = formatDate(new Date(), 'yyyy-MM-dd')
</script> </script>
<style scoped> <style scoped>

View file

@ -71,7 +71,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed, ref, watch} from 'vue' import {computed, ref, watch} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component' import flatPickr from 'vue-flatpickr-component'
@ -81,11 +80,12 @@ import Popup from '@/components/misc/popup.vue'
import {DATE_RANGES} from '@/components/date/dateRanges' import {DATE_RANGES} from '@/components/date/dateRanges'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import DatemathHelp from '@/components/date/datemathHelp.vue' import DatemathHelp from '@/components/date/datemathHelp.vue'
import {useAuthStore} from '@/stores/auth'
const store = useStore() const authStore = useAuthStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const emit = defineEmits(['dateChanged', 'update:modelValue']) const emit = defineEmits(['update:modelValue'])
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
required: false, required: false,
@ -93,7 +93,7 @@ const props = defineProps({
}) })
// FIXME: This seems to always contain the default value - that breaks the picker // FIXME: This seems to always contain the default value - that breaks the picker
const weekStart = computed<number>(() => store.state.auth.settings.weekStart ?? 0) const weekStart = computed(() => authStore.settings.weekStart ?? 0)
const flatPickerConfig = computed(() => ({ const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'), altFormat: t('date.altFormatLong'),
altInput: true, altInput: true,
@ -118,7 +118,13 @@ watch(
newValue => { newValue => {
from.value = newValue.dateFrom from.value = newValue.dateFrom
to.value = newValue.dateTo to.value = newValue.dateTo
// Only set the date back to flatpickr when it's an actual date.
// Otherwise flatpickr runs in an endless loop and slows down the browser.
const dateFrom = new Date(from.value)
const dateTo = new Date(to.value)
if (dateTo.getTime() === dateTo.getTime() && dateFrom.getTime() === dateFrom.getTime()) {
flatpickrRange.value = `${from.value} to ${to.value}` flatpickrRange.value = `${from.value} to ${to.value}`
}
}, },
) )
@ -127,7 +133,6 @@ function emitChanged() {
dateFrom: from.value === '' ? null : from.value, dateFrom: from.value === '' ? null : from.value,
dateTo: to.value === '' ? null : to.value, dateTo: to.value === '' ? null : to.value,
} }
emit('dateChanged', args)
emit('update:modelValue', args) emit('update:modelValue', args)
} }

View file

@ -1,8 +1,8 @@
<template> <template>
<BaseButton <BaseButton
class="menu-show-button" class="menu-show-button"
@click="$store.commit('toggleMenu')" @click="baseStore.toggleMenu()"
@shortkey="() => $store.commit('toggleMenu')" @shortkey="() => baseStore.toggleMenu()"
v-shortcut="'Control+e'" v-shortcut="'Control+e'"
:title="$t('keyboardShortcuts.toggleMenu')" :title="$t('keyboardShortcuts.toggleMenu')"
:aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')" :aria-label="menuActive ? $t('misc.hideMenu') : $t('misc.showMenu')"
@ -11,12 +11,12 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from 'vuex' import {useBaseStore} from '@/stores/base'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
const store = useStore() const baseStore = useBaseStore()
const menuActive = computed(() => store.state.menuActive) const menuActive = computed(() => baseStore.menuActive)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -16,6 +16,10 @@
{{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }} {{ currentList.title === '' ? $t('misc.loading') : getListTitle(currentList) }}
</h1> </h1>
<BaseButton :to="{name: 'list.info', params: {listId: currentList.id}}" class="info-button">
<icon icon="circle-info"/>
</BaseButton>
<list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/> <list-settings-dropdown v-if="canWriteCurrentList && currentList.id !== -1" :list="currentList"/>
</template> </template>
</div> </div>
@ -66,7 +70,7 @@
{{ $t('navigation.privacy') }} {{ $t('navigation.privacy') }}
</dropdown-item> </dropdown-item>
<dropdown-item <dropdown-item
@click="$store.commit('keyboardShortcutsActive', true)" @click="baseStore.setKeyboardShortcutsActive(true)"
> >
{{ $t('keyboardShortcuts.title') }} {{ $t('keyboardShortcuts.title') }}
</dropdown-item> </dropdown-item>
@ -88,11 +92,8 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, onMounted, nextTick} from 'vue' import {ref, computed, onMounted, nextTick} from 'vue'
import {useStore} from 'vuex'
import {useRouter} from 'vue-router'
import {QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types' import {RIGHTS as Rights} from '@/constants/rights'
import Rights from '@/models/constants/rights.json'
import Update from '@/components/home/update.vue' import Update from '@/components/home/update.vue'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue' import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
@ -103,16 +104,25 @@ import Logo from '@/components/home/Logo.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import MenuButton from '@/components/home/MenuButton.vue' import MenuButton from '@/components/home/MenuButton.vue'
const store = useStore() import {getListTitle} from '@/helpers/getListTitle'
const userInfo = computed(() => store.state.auth.info) import {useBaseStore} from '@/stores/base'
const userAvatar = computed(() => store.state.auth.avatarUrl) import {useConfigStore} from '@/stores/config'
const currentList = computed(() => store.state.currentList) import {useAuthStore} from '@/stores/auth'
const background = computed(() => store.state.background)
const imprintUrl = computed(() => store.state.config.legal.imprintUrl) const baseStore = useBaseStore()
const privacyPolicyUrl = computed(() => store.state.config.legal.privacyPolicyUrl) const currentList = computed(() => baseStore.currentList)
const canWriteCurrentList = computed(() => store.state.currentList.maxRight > Rights.READ) const background = computed(() => baseStore.background)
const menuActive = computed(() => store.state.menuActive) const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ)
const menuActive = computed(() => baseStore.menuActive)
const authStore = useAuthStore()
const userInfo = computed(() => authStore.info)
const userAvatar = computed(() => authStore.avatarUrl)
const configStore = useConfigStore()
const imprintUrl = computed(() => configStore.legal.imprintUrl)
const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl)
const usernameDropdown = ref() const usernameDropdown = ref()
const listTitle = ref() const listTitle = ref()
@ -126,15 +136,12 @@ onMounted(async () => {
listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`) listTitle.value.style.setProperty('--nav-username-width', `${usernameWidth}px`)
}) })
const router = useRouter()
function logout() { function logout() {
store.dispatch('auth/logout') authStore.logout()
router.push({name: 'user.login'})
} }
function openQuickActions() { function openQuickActions() {
store.commit(QUICK_ACTIONS_ACTIVE, true) baseStore.setQuickActionsActive(true)
} }
</script> </script>
@ -282,10 +289,21 @@ $hamburger-menu-icon-width: 28px;
:deep(.dropdown-trigger) { :deep(.dropdown-trigger) {
color: var(--grey-400); color: var(--grey-400);
margin-left: 1rem; margin-left: .5rem;
height: 1rem; height: 1rem;
width: 1rem; width: 1rem;
cursor: pointer; cursor: pointer;
} }
} }
.info-button {
text-align: center;
height: 1.25rem;
line-height: 1.25rem;
width: 2rem;
margin-top: .25rem;
padding: 0 .5rem;
color: var(--grey-400);
margin-left: .5rem;
}
</style> </style>

View file

@ -2,7 +2,7 @@
<div class="content-auth"> <div class="content-auth">
<BaseButton <BaseButton
v-if="menuActive" v-if="menuActive"
@click="$store.commit('menuActive', false)" @click="baseStore.setMenuActive(false)"
class="menu-hide-button d-print-none" class="menu-hide-button d-print-none"
> >
<icon icon="times"/> <icon icon="times"/>
@ -26,7 +26,7 @@
> >
<BaseButton <BaseButton
v-if="menuActive" v-if="menuActive"
@click="$store.commit('menuActive', false)" @click="baseStore.setMenuActive(false)"
class="mobile-overlay d-print-none" class="mobile-overlay d-print-none"
/> />
@ -60,81 +60,34 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {watch, computed, shallowRef, watchEffect, VNode, h} from 'vue' import {watch, computed} from 'vue'
import {useStore} from 'vuex' import {useRoute} from 'vue-router'
import {useRoute, useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
import {CURRENT_LIST, KEYBOARD_SHORTCUTS_ACTIVE, MENU_ACTIVE} from '@/store/mutation-types'
import Navigation from '@/components/home/navigation.vue' import Navigation from '@/components/home/navigation.vue'
import QuickActions from '@/components/quick-actions/quick-actions.vue' import QuickActions from '@/components/quick-actions/quick-actions.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
function useRouteWithModal() { import {useBaseStore} from '@/stores/base'
const router = useRouter() import {useLabelStore} from '@/stores/labels'
const route = useRoute()
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
const routeWithModal = computed(() => { import {useRouteWithModal} from '@/composables/useRouteWithModal'
return backdropView.value import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
? router.resolve(backdropView.value)
: route
})
const currentModal = shallowRef<VNode>()
watchEffect(() => {
if (!backdropView.value) {
currentModal.value = undefined
return
}
// logic 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
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
currentModal.value = h(
route.matched[0]?.components.default,
routeProps,
)
})
function closeModal() {
const historyState = computed(() => route.fullPath && window.history.state)
if (historyState.value) {
router.back()
} else {
const backdropRoute = historyState.value?.backdropView && router.resolve(historyState.value.backdropView)
router.push(backdropRoute)
}
}
return {routeWithModal, currentModal, closeModal}
}
const {routeWithModal, currentModal, closeModal} = useRouteWithModal() const {routeWithModal, currentModal, closeModal} = useRouteWithModal()
const store = useStore() const baseStore = useBaseStore()
const background = computed(() => baseStore.background)
const background = computed(() => store.state.background) const blurHash = computed(() => baseStore.blurHash)
const blurHash = computed(() => store.state.blurHash) const menuActive = computed(() => baseStore.menuActive)
const menuActive = computed(() => store.state.menuActive)
function showKeyboardShortcuts() { function showKeyboardShortcuts() {
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, true) baseStore.setKeyboardShortcutsActive(true)
} }
const route = useRoute() const route = useRoute()
// hide menu on mobile // hide menu on mobile
watch(() => route.fullPath, () => window.innerWidth < 769 && store.commit(MENU_ACTIVE, false)) watch(() => route.fullPath, () => window.innerWidth < 769 && baseStore.setMenuActive(false))
// FIXME: this is really error prone // FIXME: this is really error prone
// Reset the current list highlight in menu if the current route is not list related. // Reset the current list highlight in menu if the current route is not list related.
@ -156,48 +109,16 @@ watch(() => route.name as string, (routeName) => {
routeName.startsWith('user.settings') routeName.startsWith('user.settings')
) )
) { ) {
store.dispatch(CURRENT_LIST, {list: null}) baseStore.handleSetCurrentList({list: null})
} }
}) })
// TODO: Reset the title if the page component does not set one itself // TODO: Reset the title if the page component does not set one itself
function useRenewTokenOnFocus() {
const router = useRouter()
const userInfo = computed(() => store.state.auth.info)
const authenticated = computed(() => store.state.auth.authenticated)
// Try renewing the token every time vikunja is loaded initially
// (When opening the browser the focus event is not fired)
store.dispatch('auth/renewToken')
// Check if the token is still valid if the window gets focus again to maybe renew it
useEventListener('focus', () => {
if (!authenticated.value) {
return
}
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - +new Date() / 1000
// If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page
if (expiresIn < 0) {
store.dispatch('auth/checkAuth')
router.push({name: 'user.login'})
return
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
store.dispatch('auth/renewToken')
console.debug('renewed token')
}
})
}
useRenewTokenOnFocus() useRenewTokenOnFocus()
store.dispatch('labels/loadAllLabels')
const labelStore = useLabelStore()
labelStore.loadAllLabels()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -6,8 +6,9 @@
> >
<div class="container has-text-centered link-share-view"> <div class="container has-text-centered link-share-view">
<div class="column is-10 is-offset-1"> <div class="column is-10 is-offset-1">
<Logo class="logo"/> <Logo class="logo" v-if="logoVisible"/>
<h1 <h1
:class="{'m-0': !logoVisible}"
:style="{ 'opacity': currentList.title === '' ? '0': '1' }" :style="{ 'opacity': currentList.title === '' ? '0': '1' }"
class="title"> class="title">
{{ currentList.title === '' ? $t('misc.loading') : currentList.title }} {{ currentList.title === '' ? $t('misc.loading') : currentList.title }}
@ -23,14 +24,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from 'vuex'
import {useBaseStore} from '@/stores/base'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue' import PoweredByLink from './PoweredByLink.vue'
const store = useStore() const baseStore = useBaseStore()
const currentList = computed(() => store.state.currentList) const currentList = computed(() => baseStore.currentList)
const background = computed(() => store.state.background) const background = computed(() => baseStore.background)
const logoVisible = computed(() => baseStore.logoVisible)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -56,10 +56,10 @@
class="menu-label" class="menu-label"
v-tooltip="namespaceTitles[nk]" v-tooltip="namespaceTitles[nk]"
> >
<span <ColorBubble
v-if="n.hexColor !== ''" v-if="n.hexColor !== ''"
:style="{ backgroundColor: n.hexColor }" :color="n.hexColor"
class="color-bubble" class="mr-1"
/> />
<span class="name">{{ namespaceTitles[nk] }}</span> <span class="name">{{ namespaceTitles[nk] }}</span>
<div <div
@ -114,17 +114,17 @@
<span class="icon handle"> <span class="icon handle">
<icon icon="grip-lines"/> <icon icon="grip-lines"/>
</span> </span>
<span <ColorBubble
:style="{ backgroundColor: l.hexColor }" v-if="l.hexColor !== ''"
class="color-bubble" :color="l.hexColor"
v-if="l.hexColor !== ''"> class="mr-1"
</span> />
<span class="list-menu-title">{{ getListTitle(l) }}</span> <span class="list-menu-title">{{ getListTitle(l) }}</span>
</BaseButton> </BaseButton>
<BaseButton <BaseButton
class="favorite" class="favorite"
:class="{'is-favorite': l.isFavorite}" :class="{'is-favorite': l.isFavorite}"
@click="toggleFavoriteList(l)" @click="listStore.toggleListFavorite(l)"
> >
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/> <icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton> </BaseButton>
@ -141,9 +141,8 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, onMounted, onBeforeMount} from 'vue' import {ref, computed, onMounted, onBeforeMount} from 'vue'
import {useStore} from 'vuex'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import {SortableEvent} from 'sortablejs' import type {SortableEvent} from 'sortablejs'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue' import ListSettingsDropdown from '@/components/list/list-settings-dropdown.vue'
@ -151,12 +150,17 @@ import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings
import PoweredByLink from '@/components/home/PoweredByLink.vue' import PoweredByLink from '@/components/home/PoweredByLink.vue'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import {MENU_ACTIVE} from '@/store/mutation-types'
import {calculateItemPosition} from '@/helpers/calculateItemPosition' import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle' import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getListTitle} from '@/helpers/getListTitle'
import {useEventListener} from '@vueuse/core' import {useEventListener} from '@vueuse/core'
import NamespaceModel from '@/models/namespace' import type {IList} from '@/modelTypes/IList'
import ListModel from '@/models/list' import type {INamespace} from '@/modelTypes/INamespace'
import ColorBubble from '@/components/misc/colorBubble.vue'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
const drag = ref(false) const drag = ref(false)
const dragOptions = { const dragOptions = {
@ -164,14 +168,15 @@ const dragOptions = {
ghostClass: 'ghost', ghostClass: 'ghost',
} }
const store = useStore() const baseStore = useBaseStore()
const currentList = computed(() => store.state.currentList) const namespaceStore = useNamespaceStore()
const menuActive = computed(() => store.state.menuActive) const currentList = computed(() => baseStore.currentList)
const loading = computed(() => store.state.loading && store.state.loadingModule === 'namespaces') const menuActive = computed(() => baseStore.menuActive)
const loading = computed(() => namespaceStore.isLoading)
const namespaces = computed(() => { const namespaces = computed(() => {
return (store.state.namespaces.namespaces as NamespaceModel[]).filter(n => !n.isArchived) return namespaceStore.namespaces.filter(n => !n.isArchived)
}) })
const activeLists = computed(() => { const activeLists = computed(() => {
return namespaces.value.map(({lists}) => { return namespaces.value.map(({lists}) => {
@ -193,29 +198,21 @@ const namespaceListsCount = computed(() => {
useEventListener('resize', resize) useEventListener('resize', resize)
onMounted(() => resize()) onMounted(() => resize())
const listStore = useListStore()
function toggleFavoriteList(list: ListModel) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
store.dispatch('lists/toggleListFavorite', list)
}
function resize() { function resize() {
// Hide the menu by default on mobile // Hide the menu by default on mobile
store.commit(MENU_ACTIVE, window.innerWidth >= 770) baseStore.setMenuActive(window.innerWidth >= 770)
} }
function toggleLists(namespaceId: number) { function toggleLists(namespaceId: INamespace['id']) {
listsVisible.value[namespaceId] = !listsVisible.value[namespaceId] listsVisible.value[namespaceId] = !listsVisible.value[namespaceId]
} }
const listsVisible = ref<{ [id: NamespaceModel['id']]: boolean }>({}) const listsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
// FIXME: async action will be unfinished when component mounts // FIXME: async action will be unfinished when component mounts
onBeforeMount(async () => { onBeforeMount(async () => {
const namespaces = await store.dispatch('namespaces/loadNamespaces') as NamespaceModel[] const namespaces = await namespaceStore.loadNamespaces()
namespaces.forEach(n => { namespaces.forEach(n => {
if (typeof listsVisible.value[n.id] === 'undefined') { if (typeof listsVisible.value[n.id] === 'undefined') {
listsVisible.value[n.id] = true listsVisible.value[n.id] = true
@ -223,7 +220,7 @@ onBeforeMount(async () => {
}) })
}) })
function updateActiveLists(namespace: NamespaceModel, activeLists: ListModel[]) { function updateActiveLists(namespace: INamespace, activeLists: IList[]) {
// This is a bit hacky: since we do have to filter out the archived items from the list // This is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it. // for vue draggable updating it is not as simple as replacing it.
// To work around this, we merge the active lists with the archived ones. Doing so breaks the order // To work around this, we merge the active lists with the archived ones. Doing so breaks the order
@ -234,24 +231,29 @@ function updateActiveLists(namespace: NamespaceModel, activeLists: ListModel[])
...namespace.lists.filter(l => l.isArchived), ...namespace.lists.filter(l => l.isArchived),
] ]
store.commit('namespaces/setNamespaceById', { namespaceStore.setNamespaceById({
...namespace, ...namespace,
lists, lists,
}) })
} }
const listUpdating = ref<{ [id: NamespaceModel['id']]: boolean }>({}) const listUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
async function saveListPosition(e: SortableEvent) { async function saveListPosition(e: SortableEvent) {
if (!e.newIndex) return if (!e.newIndex && e.newIndex !== 0) return
const namespaceId = parseInt(e.to.dataset.namespaceId as string) const namespaceId = parseInt(e.to.dataset.namespaceId as string)
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string) const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
const listsActive = activeLists.value[newNamespaceIndex] const listsActive = activeLists.value[newNamespaceIndex]
const list = listsActive[e.newIndex] // If the list was dragged to the last position, Safari will report e.newIndex as the size of the listsActive
const listBefore = listsActive[e.newIndex - 1] ?? null // array instead of using the position. Because the index is wrong in that case, dragging the list will fail.
const listAfter = listsActive[e.newIndex + 1] ?? null // To work around that we're explicitly checking that case here and decrease the index.
const newIndex = e.newIndex === listsActive.length ? e.newIndex - 1 : e.newIndex
const list = listsActive[newIndex]
const listBefore = listsActive[newIndex - 1] ?? null
const listAfter = listsActive[newIndex + 1] ?? null
listUpdating.value[list.id] = true listUpdating.value[list.id] = true
const position = calculateItemPosition( const position = calculateItemPosition(
@ -260,8 +262,8 @@ async function saveListPosition(e: SortableEvent) {
) )
try { try {
// create a copy of the list in order to not violate vuex mutations // create a copy of the list in order to not violate pinia manipulation
await store.dispatch('lists/updateList', { await listStore.updateList({
...list, ...list,
position, position,
namespaceId, namespaceId,

View file

@ -1,7 +1,7 @@
<template> <template>
<div class="update-notification" v-if="updateAvailable"> <div class="update-notification" v-if="updateAvailable">
<p>{{ $t('update.available') }}</p> <p>{{ $t('update.available') }}</p>
<x-button @click="refreshApp()" :shadow="false"> <x-button @click="refreshApp()" :shadow="false" class="has-no-text-wrap">
{{ $t('update.do') }} {{ $t('update.do') }}
</x-button> </x-button>
</div> </div>
@ -43,24 +43,19 @@ function refreshApp() {
<style lang="scss" scoped> <style lang="scss" scoped>
.update-notification { .update-notification {
margin: .5rem;
display: flex; display: flex;
align-items: center; align-items: center;
background: $warning; background: $warning;
padding: 0 .25rem; padding: .5rem;
border-radius: $radius; border-radius: $radius;
font-size: .9rem; font-size: .9rem;
color: var(--grey-900); color: var(--grey-900);
justify-content: space-between; justify-content: space-between;
@media screen and (max-width: $desktop) {
position: fixed; position: fixed;
bottom: 1rem; bottom: 1rem;
margin: 0;
width: 450px; width: 450px;
left: calc(50vw - 225px); left: calc(50vw - 225px);
padding: .5rem;
}
@media screen and (max-width: $tablet) { @media screen and (max-width: $tablet) {
position: fixed; position: fixed;

View file

@ -9,24 +9,27 @@
} }
]" ]"
> >
<icon :icon="icon" v-if="showIconOnly"/> <icon
v-if="showIconOnly"
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : false}"
/>
<span class="icon is-small" v-else-if="icon !== ''"> <span class="icon is-small" v-else-if="icon !== ''">
<icon :icon="icon"/> <icon
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : false}"
/>
</span> </span>
<slot /> <slot />
</BaseButton> </BaseButton>
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default { name: 'x-button' }
export default defineComponent({
name: 'x-button',
})
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import {computed, useSlots, PropType} from 'vue' import {computed, useSlots, type PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
const BUTTON_TYPES_MAP = Object.freeze({ const BUTTON_TYPES_MAP = Object.freeze({
@ -46,6 +49,10 @@ const props = defineProps({
type: [String, Array], type: [String, Array],
default: '', default: '',
}, },
iconColor: {
type: String,
default: '',
},
loading: { loading: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -69,9 +76,11 @@ const showIconOnly = computed(() => props.icon !== '' && typeof slots.default ==
text-transform: uppercase; text-transform: uppercase;
font-size: 0.85rem; font-size: 0.85rem;
font-weight: bold; font-weight: bold;
height: auto;
min-height: $button-height; min-height: $button-height;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
display: inline-flex; display: inline-flex;
white-space: break-spaces;
&:hover { &:hover {
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);

View file

@ -34,9 +34,8 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {computed, ref, toRef, watch} from 'vue'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
const DEFAULT_COLORS = [ const DEFAULT_COLORS = [
@ -48,17 +47,12 @@ const DEFAULT_COLORS = [
'#00db60', '#00db60',
] ]
export default defineComponent({ const color = ref('')
name: 'colorPicker', const lastChangeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
data() { const defaultColors = ref(DEFAULT_COLORS)
return { const colorListID = ref(createRandomID())
color: '',
lastChangeTimeout: null, const props = defineProps({
defaultColors: DEFAULT_COLORS,
colorListID: createRandomID(),
}
},
props: {
modelValue: { modelValue: {
type: String, type: String,
required: true, required: true,
@ -67,48 +61,43 @@ export default defineComponent({
type: String, type: String,
default: 'top', default: 'top',
}, },
}, })
emits: ['update:modelValue', 'change'],
watch: {
modelValue: {
handler(modelValue) {
this.color = modelValue
},
immediate: true,
},
color() {
this.update()
},
},
computed: {
isEmpty() {
return this.color === '#000000' || this.color === ''
},
},
methods: {
update(force = false) {
if(this.isEmpty && !force) { const emit = defineEmits(['update:modelValue'])
const modelValue = toRef(props, 'modelValue')
watch(
modelValue,
(newValue) => {
color.value = newValue
},
{immediate: true},
)
watch(color, () => update())
const isEmpty = computed(() => color.value === '#000000' || color.value === '')
function update(force = false) {
if(isEmpty.value && !force) {
return return
} }
if (this.lastChangeTimeout !== null) { if (lastChangeTimeout.value !== null) {
clearTimeout(this.lastChangeTimeout) clearTimeout(lastChangeTimeout.value)
} }
this.lastChangeTimeout = setTimeout(() => { lastChangeTimeout.value = setTimeout(() => {
this.$emit('update:modelValue', this.color) emit('update:modelValue', color.value)
this.$emit('change')
}, 500) }, 500)
}, }
reset() {
function reset() {
// FIXME: I havn't found a way to make it clear to the user the color war reset. // FIXME: I havn't found a way to make it clear to the user the color war reset.
// Not sure if verte is capable of this - it does not show the change when setting this.color = '' // Not sure if verte is capable of this - it does not show the change when setting this.color = ''
this.color = '' color.value = ''
this.update(true) update(true)
}, }
},
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -88,155 +88,152 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {ref, onMounted, onBeforeUnmount, toRef, watch, computed, type PropType} from 'vue'
import flatPickr from 'vue-flatpickr-component' import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css' import 'flatpickr/dist/flatpickr.css'
import {i18n} from '@/i18n'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {format} from 'date-fns' import {formatDate, formatDateShort} from '@/helpers/time/formatDate'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval' import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours' import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {createDateFromString} from '@/helpers/time/createDateFromString' import {createDateFromString} from '@/helpers/time/createDateFromString'
import {useAuthStore} from '@/stores/auth'
import {useI18n} from 'vue-i18n'
export default defineComponent({ const props = defineProps({
name: 'datepicker',
data() {
return {
date: null,
show: false,
changed: false,
}
},
components: {
flatPickr,
BaseButton,
},
props: {
modelValue: { modelValue: {
type: [Date, null, String] as PropType<Date | null | string>,
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string', validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
default: null,
}, },
chooseDateLabel: { chooseDateLabel: {
type: String, type: String,
default() { default() {
return i18n.global.t('input.datepicker.chooseDate') const {t} = useI18n({useScope: 'global'})
return t('input.datepicker.chooseDate')
}, },
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
}, })
emits: ['update:modelValue', 'change', 'close', 'close-on-change'],
mounted() { const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
document.addEventListener('click', this.hideDatePopup)
}, const {t} = useI18n({useScope: 'global'})
beforeUnmount() {
document.removeEventListener('click', this.hideDatePopup) const date = ref<Date | null>()
}, const show = ref(false)
watch: { const changed = ref(false)
modelValue: {
handler: 'setDateValue', onMounted(() => document.addEventListener('click', hideDatePopup))
immediate: true, onBeforeUnmount(() =>document.removeEventListener('click', hideDatePopup))
},
}, const modelValue = toRef(props, 'modelValue')
computed: { watch(
flatPickerConfig() { modelValue,
return { setDateValue,
altFormat: this.$t('date.altFormatLong'), {immediate: true},
)
const authStore = useAuthStore()
const weekStart = computed(() => authStore.settings.weekStart)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true, altInput: true,
dateFormat: 'Y-m-d H:i', dateFormat: 'Y-m-d H:i',
enableTime: true, enableTime: true,
time_24hr: true, time_24hr: true,
inline: true, inline: true,
locale: { locale: {
firstDayOfWeek: this.$store.state.auth.settings.weekStart, firstDayOfWeek: weekStart.value,
}, },
} }))
},
// Since flatpickr dates are strings, we need to convert them to native date objects. // Since flatpickr dates are strings, we need to convert them to native date objects.
// To make that work, we need a separate variable since flatpickr does not have a change event. // To make that work, we need a separate variable since flatpickr does not have a change event.
flatPickrDate: { const flatPickrDate = computed({
set(newValue) { set(newValue: string | Date) {
this.date = createDateFromString(newValue) date.value = createDateFromString(newValue)
this.updateData() updateData()
}, },
get() { get() {
if (!this.date) { if (!date.value) {
return '' return ''
} }
return format(this.date, 'yyy-LL-dd H:mm') return formatDate(date.value, 'yyy-LL-dd H:mm')
}, },
}, })
},
methods: {
setDateValue(newVal) { function setDateValue(dateString: string | Date | null) {
if (newVal === null) { if (dateString === null) {
this.date = null date.value = null
return return
} }
this.date = createDateFromString(newVal) date.value = createDateFromString(dateString)
}, }
updateData() {
this.changed = true function updateData() {
this.$emit('update:modelValue', this.date) changed.value = true
this.$emit('change', this.date) emit('update:modelValue', date.value)
}, }
toggleDatePopup() {
if (this.disabled) { function toggleDatePopup() {
if (props.disabled) {
return return
} }
this.show = !this.show show.value = !show.value
}, }
hideDatePopup(e) {
if (this.show) { const datepickerPopup = ref<HTMLElement | null>(null)
closeWhenClickedOutside(e, this.$refs.datepickerPopup, this.close) function hideDatePopup(e) {
if (show.value) {
closeWhenClickedOutside(e, datepickerPopup.value, close)
} }
}, }
close() {
function close() {
// Kind of dirty, but the timeout allows us to enter a time and click on "confirm" without // Kind of dirty, but the timeout allows us to enter a time and click on "confirm" without
// having to click on another input field before it is actually used. // having to click on another input field before it is actually used.
setTimeout(() => { setTimeout(() => {
this.show = false show.value = false
this.$emit('close', this.changed) emit('close', changed.value)
if (this.changed) { if (changed.value) {
this.changed = false changed.value = false
this.$emit('close-on-change', this.changed) emit('close-on-change', changed.value)
} }
}, 200) }, 200)
}, }
setDate(date) {
if (this.date === null) { function setDate(dateString: string) {
this.date = new Date() if (date.value === null) {
date.value = new Date()
} }
const interval = calculateDayInterval(date) const interval = calculateDayInterval(dateString)
const newDate = new Date() const newDate = new Date()
newDate.setDate(newDate.getDate() + interval) newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate)) newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0) newDate.setMinutes(0)
newDate.setSeconds(0) newDate.setSeconds(0)
this.date = newDate date.value = newDate
this.flatPickrDate = newDate flatPickrDate.value = newDate
this.updateData() updateData()
}, }
getDayIntervalFromString(date) {
return calculateDayInterval(date) function getWeekdayFromStringInterval(dateString: string) {
}, const interval = calculateDayInterval(dateString)
getWeekdayFromStringInterval(date) {
const interval = calculateDayInterval(date)
const newDate = new Date() const newDate = new Date()
newDate.setDate(newDate.getDate() + interval) newDate.setDate(newDate.getDate() + interval)
return format(newDate, 'E') return formatDate(newDate, 'E')
}, }
},
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -4,7 +4,7 @@
<vue-easymde <vue-easymde
:configs="config" :configs="config"
@change="bubble" @change="() => bubble()"
@update:modelValue="handleInput" @update:modelValue="handleInput"
class="content" class="content"
v-if="isEditActive" v-if="isEditActive"
@ -16,14 +16,29 @@
<p class="has-text-centered has-text-grey is-italic my-5" v-if="showPreviewText"> <p class="has-text-centered has-text-grey is-italic my-5" v-if="showPreviewText">
{{ emptyText }} {{ emptyText }}
<template v-if="isEditEnabled"> <template v-if="isEditEnabled">
<ButtonLink @click="toggleEdit" class="d-print-none">{{ $t('input.editor.edit') }}</ButtonLink>. <ButtonLink
@click="toggleEdit"
v-shortcut="editShortcut"
class="d-print-none">
{{ $t('input.editor.edit') }}
</ButtonLink>.
</template> </template>
</p> </p>
<ul class="actions d-print-none" v-if="bottomActions.length > 0"> <ul class="actions d-print-none" v-if="bottomActions.length > 0">
<li v-if="isEditEnabled && !showPreviewText && showSave"> <li v-if="isEditEnabled && !showPreviewText && showSave">
<BaseButton v-if="showEditButton" @click="toggleEdit">{{ $t('input.editor.edit') }}</BaseButton> <BaseButton
<BaseButton v-else-if="isEditActive" @click="toggleEdit" class="done-edit">{{ $t('misc.save') }}</BaseButton> v-if="showEditButton"
@click="toggleEdit"
v-shortcut="editShortcut">
{{ $t('input.editor.edit') }}
</BaseButton>
<BaseButton
v-else-if="isEditActive"
@click="toggleEdit"
class="done-edit">
{{ $t('misc.save') }}
</BaseButton>
</li> </li>
<li v-for="(action, k) in bottomActions" :key="k"> <li v-for="(action, k) in bottomActions" :key="k">
<BaseButton @click="action.action">{{ action.title }}</BaseButton> <BaseButton @click="action.action">{{ action.title }}</BaseButton>
@ -32,7 +47,11 @@
<template v-else-if="isEditEnabled && showSave"> <template v-else-if="isEditEnabled && showSave">
<ul v-if="showEditButton" class="actions d-print-none"> <ul v-if="showEditButton" class="actions d-print-none">
<li> <li>
<BaseButton @click="toggleEdit">{{ $t('input.editor.edit') }}</BaseButton> <BaseButton
@click="toggleEdit"
v-shortcut="editShortcut">
{{ $t('input.editor.edit') }}
</BaseButton>
</li> </li>
</ul> </ul>
<x-button <x-button
@ -47,32 +66,28 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {computed, nextTick, onMounted, ref, toRefs, watch} from 'vue'
import VueEasymde from './vue-easymde.vue' import VueEasymde from './vue-easymde.vue'
import {marked} from 'marked' import {marked} from 'marked'
import DOMPurify from 'dompurify' import DOMPurify from 'dompurify'
import hljs from 'highlight.js/lib/common'
import {createEasyMDEConfig} from './editorConfig' import {createEasyMDEConfig} from './editorConfig'
import AttachmentModel from '../../models/attachment' import AttachmentModel from '@/models/attachment'
import AttachmentService from '../../services/attachment' import AttachmentService from '@/services/attachment'
import {findCheckboxesInText} from '../../helpers/checklistFromText'
import {setupMarkdownRenderer} from '@/helpers/markdownRenderer'
import {findCheckboxesInText} from '@/helpers/checklistFromText'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue' import ButtonLink from '@/components/misc/ButtonLink.vue'
import type { IAttachment } from '@/modelTypes/IAttachment'
import type { ITask } from '@/modelTypes/ITask'
export default defineComponent({ const props = defineProps({
name: 'editor',
components: {
VueEasymde,
BaseButton,
ButtonLink,
},
props: {
modelValue: { modelValue: {
type: String, type: String,
default: '', default: '',
@ -110,133 +125,114 @@ export default defineComponent({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
// If a key is passed the editor will go in "edit" mode when the key is pressed.
// Disabled if an empty string is passed.
editShortcut: {
type: String,
default: '',
}, },
emits: ['update:modelValue', 'change'], })
computed: {
showPreviewText() {
return this.isPreviewActive && this.text === '' && this.emptyText !== ''
},
showEditButton() {
return !this.isEditActive && this.text !== ''
},
},
data() {
return {
text: '',
changeTimeout: null,
isEditActive: false,
isPreviewActive: true,
preview: '', const emit = defineEmits(['update:modelValue'])
attachmentService: null,
loadedAttachments: {}, const text = ref('')
config: createEasyMDEConfig({ const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
placeholder: this.placeholder, const isEditActive = ref(false)
uploadImage: this.uploadEnabled, const isPreviewActive = ref(true)
imageUploadFunction: this.uploadCallback,
}), const showPreviewText = computed(() => isPreviewActive.value && text.value === '' && props.emptyText !== '')
checkboxId: createRandomID(), const showEditButton = computed(() => !isEditActive.value && text.value !== '')
}
const preview = ref('')
const attachmentService = new AttachmentService()
type CacheKey = `${ITask['id']}-${IAttachment['id']}`
const loadedAttachments = ref<{[key: CacheKey]: string}>({})
const config = ref(createEasyMDEConfig({
placeholder: props.placeholder,
uploadImage: props.uploadEnabled,
imageUploadFunction: props.uploadCallback,
}))
const checkboxId = ref(createRandomID())
const {modelValue} = toRefs(props)
watch(
modelValue,
async (value) => {
text.value = value
await nextTick()
renderPreview()
}, },
watch: { )
modelValue(modelValue) {
this.text = modelValue watch(
this.$nextTick(this.renderPreview) text,
}, (newVal, oldVal) => {
text(newVal, oldVal) {
// Only bubble the new value if it actually changed, but not if the component just got mounted and the text changed from the outside. // Only bubble the new value if it actually changed, but not if the component just got mounted and the text changed from the outside.
if (oldVal === '' && this.text === this.modelValue) { if (oldVal === '' && text.value === modelValue.value) {
return return
} }
this.bubble() bubble()
}, },
}, )
mounted() {
if (this.modelValue !== '') {
this.text = this.modelValue onMounted(() => {
if (modelValue.value !== '') {
text.value = modelValue.value
} }
if (this.previewIsDefault && this.hasPreview) { if (props.previewIsDefault && props.hasPreview) {
this.$nextTick(this.renderPreview) nextTick(() => renderPreview())
return return
} }
this.isPreviewActive = false isPreviewActive.value = false
this.isEditActive = true isEditActive.value = true
}, })
methods: {
// This gets triggered when only pasting content into the editor.
// A change event would not get generated by that, an input event does. // This gets triggered when only pasting content into the editor.
// Therefore, we're using this handler to catch paste events. // A change event would not get generated by that, an input event does.
// But because this also gets triggered when typing into the editor, we give // Therefore, we're using this handler to catch paste events.
// it a higher timeout to make the timouts cancel each other in that case so // But because this also gets triggered when typing into the editor, we give
// that in the end, only one change event is triggered to the outside per change. // it a higher timeout to make the timouts cancel each other in that case so
handleInput(val) { // that in the end, only one change event is triggered to the outside per change.
function handleInput(val: string) {
// Don't bubble if the text is up to date // Don't bubble if the text is up to date
if (val === this.text) { if (val === text.value) {
return return
} }
this.text = val text.value = val
this.bubble(1000) bubble(1000)
}, }
bubble(timeout = 500) {
if (this.changeTimeout !== null) { function bubble(timeout = 500) {
clearTimeout(this.changeTimeout) if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
} }
this.changeTimeout = setTimeout(() => { changeTimeout.value = setTimeout(() => {
this.$emit('update:modelValue', this.text) emit('update:modelValue', text.value)
this.$emit('change', this.text)
}, timeout) }, timeout)
}, }
replaceAt(str, index, replacement) {
return str.substr(0, index) + replacement + str.substr(index + replacement.length) function replaceAt(str: string, index: number, replacement: string) {
}, return str.slice(0, index) + replacement + str.slice(index + replacement.length)
findNthIndex(str, n) { }
function findNthIndex(str: string, n: number) {
const checkboxes = findCheckboxesInText(str) const checkboxes = findCheckboxesInText(str)
return checkboxes[n] return checkboxes[n]
}, }
renderPreview() {
const renderer = new marked.Renderer()
const linkRenderer = renderer.link
let checkboxNum = -1 function renderPreview() {
marked.use({ setupMarkdownRenderer(checkboxId.value)
renderer: {
image: (src, title, text) => {
title = title ? ` title="${title}` : '' preview.value = DOMPurify.sanitize(marked(text.value), {ADD_ATTR: ['target']})
// If the url starts with the api url, the image is likely an attachment and
// we'll need to download and parse it properly.
if (src.substr(0, window.API_URL.length + 7) === `${window.API_URL}/tasks/`) {
return `<img data-src="${src}" alt="${text}" ${title} class="attachment-image"/>`
}
return `<img src="${src}" alt="${text}" ${title}/>`
},
checkbox: (checked) => {
if (checked) {
checked = ' checked="checked"'
}
checkboxNum++
return `<input type="checkbox" data-checkbox-num="${checkboxNum}" ${checked} class="text-checkbox-${this.checkboxId}"/>`
},
link: (href, title, text) => {
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
const html = linkRenderer.call(renderer, href, title, text)
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
},
},
highlight: function (code, language) {
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
return hljs.highlight(code, {language: validLanguage}).value
},
})
this.preview = DOMPurify.sanitize(marked(this.text), {ADD_ATTR: ['target']})
// Since the render function is synchronous, we can't do async http requests in it. // Since the render function is synchronous, we can't do async http requests in it.
// Therefore, we can't resolve the blob url at (markdown) compile time. // Therefore, we can't resolve the blob url at (markdown) compile time.
@ -245,78 +241,70 @@ export default defineComponent({
// dom tree. If we're calling this right after setting this.preview it could be the images were // dom tree. If we're calling this right after setting this.preview it could be the images were
// not already made available. // not already made available.
// Some docs at https://stackoverflow.com/q/62865160/10924593 // Some docs at https://stackoverflow.com/q/62865160/10924593
this.$nextTick(async () => { nextTick().then(async () => {
const attachmentImage = document.getElementsByClassName('attachment-image') const attachmentImage = document.querySelectorAll<HTMLImageElement>('.attachment-image')
if (attachmentImage) { if (attachmentImage) {
for (const img of attachmentImage) { Array.from(attachmentImage).forEach(async (img) => {
// The url is something like /tasks/<id>/attachments/<id> // The url is something like /tasks/<id>/attachments/<id>
const parts = img.dataset.src.substr(window.API_URL.length + 1).split('/') const parts = img.dataset.src?.slice(window.API_URL.length + 1).split('/')
const taskId = parseInt(parts[1]) const taskId = Number(parts[1])
const attachmentId = parseInt(parts[3]) const attachmentId = Number(parts[3])
const cacheKey = `${taskId}-${attachmentId}` const cacheKey: CacheKey = `${taskId}-${attachmentId}`
if (typeof this.loadedAttachments[cacheKey] !== 'undefined') { if (typeof loadedAttachments.value[cacheKey] !== 'undefined') {
img.src = this.loadedAttachments[cacheKey] img.src = loadedAttachments.value[cacheKey]
continue return
} }
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId}) const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
if (this.attachmentService === null) { const url = await attachmentService.getBlobUrl(attachment)
this.attachmentService = new AttachmentService()
}
const url = await this.attachmentService.getBlobUrl(attachment)
img.src = url img.src = url
this.loadedAttachments[cacheKey] = url loadedAttachments.value[cacheKey] = url
} })
} }
const textCheckbox = document.getElementsByClassName(`text-checkbox-${this.checkboxId}`) const textCheckbox = document.querySelectorAll<HTMLInputElement>(`.text-checkbox-${checkboxId.value}`)
if (textCheckbox) { if (textCheckbox) {
for (const check of textCheckbox) { Array.from(textCheckbox).forEach(check => {
check.removeEventListener('change', this.handleCheckboxClick) check.removeEventListener('change', handleCheckboxClick)
check.addEventListener('change', this.handleCheckboxClick) check.addEventListener('change', handleCheckboxClick)
check.parentElement.classList.add('has-checkbox') check.parentElement?.classList.add('has-checkbox')
} })
} }
}) })
}, }
handleCheckboxClick(e) {
// Find the original markdown checkbox this is targeting
const checked = e.target.checked
const numMarkdownCheck = parseInt(e.target.dataset.checkboxNum)
const index = this.findNthIndex(this.text, numMarkdownCheck) function handleCheckboxClick(e: Event) {
// Find the original markdown checkbox this is targeting
const checked = (e.target as HTMLInputElement).checked
const numMarkdownCheck = Number((e.target as HTMLInputElement).dataset.checkboxNum)
const index = findNthIndex(text.value, numMarkdownCheck)
if (index < 0 || typeof index === 'undefined') { if (index < 0 || typeof index === 'undefined') {
console.debug('no index found') console.debug('no index found')
return return
} }
console.debug(index, this.text.substr(index, 9)) console.debug(index, text.value.slice(index, 9))
const listPrefix = this.text.substr(index, 1) const listPrefix = text.value.slice(index, 1)
if (checked) { text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `)
this.text = this.replaceAt(this.text, index, `${listPrefix} [x] `) bubble()
renderPreview()
}
function toggleEdit() {
if (isEditActive.value) {
isPreviewActive.value = true
isEditActive.value = false
renderPreview()
bubble(0) // save instantly
} else { } else {
this.text = this.replaceAt(this.text, index, `${listPrefix} [ ] `) isPreviewActive.value = false
isEditActive.value = true
} }
this.bubble() }
this.renderPreview()
},
toggleEdit() {
if (this.isEditActive) {
this.isPreviewActive = true
this.isEditActive = false
this.renderPreview()
this.bubble(0) // save instantly
} else {
this.isPreviewActive = false
this.isEditActive = true
}
},
},
})
</script> </script>
<style lang="scss"> <style lang="scss">

View file

@ -4,8 +4,9 @@
:checked="checked" :checked="checked"
:disabled="disabled || undefined" :disabled="disabled || undefined"
:id="checkBoxId" :id="checkBoxId"
@change="(event) => updateData(event.target.checked)" @change="(event: Event) => updateData((event.target as HTMLInputElement).checked)"
type="checkbox"/> type="checkbox"
/>
<label :for="checkBoxId" class="check"> <label :for="checkBoxId" class="check">
<svg height="18px" viewBox="0 0 18 18" width="18px"> <svg height="18px" viewBox="0 0 18 18" width="18px">
<path <path
@ -19,21 +20,17 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {ref, toRef, watch} from 'vue'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
export default defineComponent({ const checked = ref(false)
name: 'fancycheckbox', const checkBoxId = `fancycheckbox_${createRandomID()}`
data() {
return { const props = defineProps({
checked: false,
checkBoxId: `fancycheckbox_${createRandomID()}`,
}
},
props: {
modelValue: { modelValue: {
type: Boolean,
required: false, required: false,
}, },
disabled: { disabled: {
@ -41,25 +38,24 @@ export default defineComponent({
required: false, required: false,
default: false, default: false,
}, },
},
emits: ['update:modelValue', 'change'],
watch: {
modelValue: {
handler(modelValue) {
this.checked = modelValue
},
immediate: true,
},
},
methods: {
updateData(checked) {
this.checked = checked
this.$emit('update:modelValue', checked)
this.$emit('change', checked)
},
},
}) })
const emit = defineEmits(['update:modelValue', 'change'])
const modelValue = toRef(props, 'modelValue')
watch(
modelValue,
newValue => {
checked.value = newValue
},
{immediate: true},
)
function updateData(newChecked: boolean) {
checked.value = newChecked
emit('update:modelValue', newChecked)
emit('change', newChecked)
}
</script> </script>

View file

@ -39,11 +39,11 @@
<div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible"> <div class="search-results" :class="{'search-results-inline': inline}" v-if="searchResultsVisible">
<BaseButton <BaseButton
class="is-fullwidth" class="is-fullwidth"
v-for="(data, key) in filteredSearchResults" v-for="(data, index) in filteredSearchResults"
:key="key" :key="index"
:ref="`result-${key}`" :ref="(el) => setResult(el, index)"
@keydown.up.prevent="() => preSelect(key - 1)" @keydown.up.prevent="() => preSelect(index - 1)"
@keydown.down.prevent="() => preSelect(key + 1)" @keydown.down.prevent="() => preSelect(index + 1)"
@click.prevent.stop="() => select(data)" @click.prevent.stop="() => select(data)"
> >
<span> <span>
@ -59,7 +59,7 @@
<BaseButton <BaseButton
v-if="creatableAvailable" v-if="creatableAvailable"
class="is-fullwidth" class="is-fullwidth"
:ref="`result-${filteredSearchResults.length}`" :ref="(el) => setResult(el, filteredSearchResults.length)"
@keydown.up.prevent="() => preSelect(filteredSearchResults.length - 1)" @keydown.up.prevent="() => preSelect(filteredSearchResults.length - 1)"
@keydown.down.prevent="() => preSelect(filteredSearchResults.length + 1)" @keydown.down.prevent="() => preSelect(filteredSearchResults.length + 1)"
@keyup.enter.prevent="create" @keyup.enter.prevent="create"
@ -82,9 +82,10 @@
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance, type PropType} from 'vue'
import {i18n} from '@/i18n' import {useI18n} from 'vue-i18n'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -98,55 +99,31 @@ function elementInResults(elem: string | any, label: string, query: string): boo
return elem === query return elem === query
} }
export default defineComponent({ const props = defineProps({
name: 'multiselect',
components: {
BaseButton,
},
data() {
return {
query: '',
searchTimeout: null,
localLoading: false,
showSearchResults: false,
internalValue: null,
}
},
props: {
// When true, shows a loading spinner // When true, shows a loading spinner
loading: { loading: {
type: Boolean, type: Boolean,
default() { default: false,
return false
},
}, },
// The placeholder of the search input // The placeholder of the search input
placeholder: { placeholder: {
type: String, type: String,
default() { default: '',
return ''
},
}, },
// 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: { searchResults: {
type: Array, type: Array as PropType<{[id: string]: any}>,
default() { default: () => [],
return []
},
}, },
// The name of the property of the searched object to show the user. // 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. // If empty the component will show all raw data of an entry.
label: { label: {
type: String, type: String,
default() { default: '',
return ''
},
}, },
// 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: { modelValue: {
default() { default: null,
return 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: { creatable: {
@ -157,14 +134,16 @@ export default defineComponent({
createPlaceholder: { createPlaceholder: {
type: String, type: String,
default() { default() {
return i18n.global.t('input.multiselect.createPlaceholder') const {t} = useI18n({useScope: 'global'})
return t('input.multiselect.createPlaceholder')
}, },
}, },
// The text shown next to an option. // The text shown next to an option.
selectPlaceholder: { selectPlaceholder: {
type: String, type: String,
default() { default() {
return i18n.global.t('input.multiselect.selectPlaceholder') const {t} = useI18n({useScope: 'global'})
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.
@ -185,136 +164,151 @@ export default defineComponent({
// 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: { searchDelay: {
type: Number, type: Number,
default() { default: 200,
return 200
},
}, },
closeAfterSelect: { closeAfterSelect: {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
}, })
/** const emit = defineEmits<{
* Available events: (e: 'update:modelValue', value: null): void
* @search: Triggered every time the search query input changes // @search: Triggered every time the search query input changes
* @select: Triggered every time an option from the search results is selected. Also triggers a change in v-model. (e: 'search', query: string): 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. // @select: Triggered every time an option from the search results is selected. Also triggers a change in v-model.
* @remove: If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items. (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.
emits: ['update:modelValue', 'search', 'select', 'create', 'remove'], (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.
(e: 'remove', value: null): void
}>()
mounted() { const query = ref('')
document.addEventListener('click', this.hideSearchResultsHandler) const searchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
}, const localLoading = ref(false)
beforeUnmount() { const showSearchResults = ref(false)
document.removeEventListener('click', this.hideSearchResultsHandler) const internalValue = ref<string | {[key: string]: any} | any[] | null>(null)
},
watch: { onMounted(() => document.addEventListener('click', hideSearchResultsHandler))
modelValue: { onBeforeUnmount(() => document.removeEventListener('click', hideSearchResultsHandler))
handler(value) {
this.setSelectedObject(value) const {modelValue, searchResults} = toRefs(props)
},
watch(
modelValue,
(value) => setSelectedObject(value),
{
immediate: true, immediate: true,
deep: true, deep: true,
}, },
}, )
computed: {
searchResultsVisible() { const searchResultsVisible = computed(() => {
if (this.query === '' && !this.showEmpty) { if (query.value === '' && !props.showEmpty) {
return false return false
} }
return this.showSearchResults && ( return showSearchResults.value && (
(this.filteredSearchResults.length > 0) || (filteredSearchResults.value.length > 0) ||
(this.creatable && this.query !== '') (props.creatable && query.value !== '')
) )
}, })
creatableAvailable() {
const hasResult = this.filteredSearchResults.some(elem => elementInResults(elem, this.label, this.query))
const hasQueryAlreadyAdded = Array.isArray(this.internalValue) && this.internalValue.some(elem => elementInResults(elem, this.label, this.query))
return this.creatable const creatableAvailable = computed(() => {
&& this.query !== '' const hasResult = filteredSearchResults.value.some((elem: any) => elementInResults(elem, props.label, query.value))
const hasQueryAlreadyAdded = Array.isArray(internalValue.value) && internalValue.value.some(elem => elementInResults(elem, props.label, query.value))
return props.creatable
&& query.value !== ''
&& !(hasResult || hasQueryAlreadyAdded) && !(hasResult || hasQueryAlreadyAdded)
}, })
filteredSearchResults() {
if (this.multiple && this.internalValue !== null && Array.isArray(this.internalValue)) { const filteredSearchResults = computed(() => {
return this.searchResults.filter(item => !this.internalValue.some(e => e === item)) const currentInternal = internalValue.value
if (props.multiple && currentInternal !== null && Array.isArray(currentInternal)) {
return searchResults.value.filter((item: any) => !currentInternal.some(e => e === item))
} }
return this.searchResults return searchResults.value
}, })
hasMultiple() {
return this.multiple && Array.isArray(this.internalValue) && this.internalValue.length > 0 const hasMultiple = computed(() => {
}, return props.multiple && Array.isArray(internalValue.value) && internalValue.value.length > 0
}, })
methods: {
// Searching will be triggered with a 200ms delay to avoid searching on every keyup event. const searchInput = ref<HTMLInputElement | null>(null)
search() { // Searching will be triggered with a 200ms delay to avoid searching on every keyup event.
function search() {
// Updating the query with a binding does not work on mobile for some reason, // Updating the query with a binding does not work on mobile for some reason,
// getting the value manual does. // getting the value manual does.
this.query = this.$refs.searchInput.value query.value = searchInput.value?.value || ''
if (this.searchTimeout !== null) { if (searchTimeout.value !== null) {
clearTimeout(this.searchTimeout) clearTimeout(searchTimeout.value)
this.searchTimeout = null searchTimeout.value = null
} }
this.localLoading = true localLoading.value = true
this.searchTimeout = setTimeout(() => { searchTimeout.value = setTimeout(() => {
this.$emit('search', this.query) emit('search', query.value)
setTimeout(() => { setTimeout(() => {
this.localLoading = false localLoading.value = false
}, 100) // The duration of the loading timeout of the services }, 100) // The duration of the loading timeout of the services
this.showSearchResults = true showSearchResults.value = true
}, this.searchDelay) }, props.searchDelay)
}, }
hideSearchResultsHandler(e) {
closeWhenClickedOutside(e, this.$refs.multiselectRoot, this.closeSearchResults) const multiselectRoot = ref<HTMLElement | null>(null)
}, function hideSearchResultsHandler(e: MouseEvent) {
closeSearchResults() { closeWhenClickedOutside(e, multiselectRoot.value, closeSearchResults)
this.showSearchResults = false }
},
handleFocus() { function closeSearchResults() {
showSearchResults.value = false
}
function handleFocus() {
// We need the timeout to avoid the hideSearchResultsHandler hiding the search results right after the input // We need the timeout to avoid the hideSearchResultsHandler hiding the search results right after the input
// is focused. That would lead to flickering pre-loaded search results and hiding them right after showing. // is focused. That would lead to flickering pre-loaded search results and hiding them right after showing.
setTimeout(() => { setTimeout(() => {
this.showSearchResults = true showSearchResults.value = true
}, 10) }, 10)
}, }
select(object) {
if (this.multiple) { function select(object: {[key: string]: any}) {
if (this.internalValue === null) { if (props.multiple) {
this.internalValue = [] if (internalValue.value === null) {
internalValue.value = []
} }
this.internalValue.push(object) (internalValue.value as any[]).push(object)
} else { } else {
this.internalValue = object internalValue.value = object
} }
this.$emit('update:modelValue', this.internalValue) emit('update:modelValue', internalValue.value)
this.$emit('select', object) emit('select', object)
this.setSelectedObject(object) setSelectedObject(object)
if (this.closeAfterSelect && this.filteredSearchResults.length > 0 && !this.creatableAvailable) { if (props.closeAfterSelect && filteredSearchResults.value.length > 0 && !creatableAvailable.value) {
this.closeSearchResults() closeSearchResults()
} }
}, }
setSelectedObject(object, resetOnly = false) {
this.internalValue = object function setSelectedObject(object: string | {[id: string]: any} | null, resetOnly = false) {
internalValue.value = object
// We assume we're getting an array when multiple is enabled and can therefore leave the query // We assume we're getting an array when multiple is enabled and can therefore leave the query
// value etc as it is // value etc as it is
if (this.multiple) { if (props.multiple) {
this.query = '' query.value = ''
return return
} }
if (object === null) { if (object === null) {
this.query = '' query.value = ''
return return
} }
@ -322,15 +316,25 @@ export default defineComponent({
return return
} }
this.query = this.label !== '' ? object[this.label] : object query.value = props.label !== '' ? object[props.label] : object
}, }
preSelect(index) {
const results = ref<(Element | ComponentPublicInstance)[]>([])
function setResult(el: Element | ComponentPublicInstance | null, index: number) {
if (el === null) {
delete results.value[index]
} else {
results.value[index] = el
}
}
function preSelect(index: number) {
if (index < 0) { if (index < 0) {
this.$refs.searchInput.focus() searchInput.value?.focus()
return return
} }
const elems = this.$refs[`result-${index}`] const elems = results.value[index]
if (typeof elems === 'undefined' || elems.length === 0) { if (typeof elems === 'undefined' || elems.length === 0) {
return return
} }
@ -341,51 +345,52 @@ export default defineComponent({
} }
elems.focus() elems.focus()
}, }
create() {
if (this.query === '') { function create() {
if (query.value === '') {
return return
} }
this.$emit('create', this.query) emit('create', query.value)
this.setSelectedObject(this.query, true) setSelectedObject(query.value, true)
this.closeSearchResults() closeSearchResults()
}, }
createOrSelectOnEnter() {
if (!this.creatableAvailable && this.searchResults.length === 1) { function createOrSelectOnEnter() {
this.select(this.searchResults[0]) if (!creatableAvailable.value && searchResults.value.length === 1) {
select(searchResults.value[0])
return return
} }
if (!this.creatableAvailable) { if (!creatableAvailable.value) {
// Check if there's an exact match for our search term // Check if there's an exact match for our search term
const exactMatch = this.filteredSearchResults.find(elem => elementInResults(elem, this.label, this.query)) const exactMatch = filteredSearchResults.value.find((elem: any) => elementInResults(elem, props.label, query.value))
if(exactMatch) { if(exactMatch) {
this.select(exactMatch) select(exactMatch)
} }
return return
} }
this.create() create()
}, }
remove(item) {
for (const ind in this.internalValue) { function remove(item: any) {
if (this.internalValue[ind] === item) { for (const ind in internalValue.value) {
this.internalValue.splice(ind, 1) if (internalValue.value[ind] === item) {
internalValue.value.splice(ind, 1)
break break
} }
} }
this.$emit('update:modelValue', this.internalValue) emit('update:modelValue', internalValue.value)
this.$emit('remove', item) emit('remove', item)
}, }
focus() {
this.$refs.searchInput.focus() function focus() {
}, searchInput.value?.focus()
}, }
})
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -1,6 +1,6 @@
<template> <template>
<dropdown> <dropdown>
<template v-if="isSavedFilter"> <template v-if="isSavedFilter(list)">
<dropdown-item <dropdown-item
:to="{ name: 'filter.settings.edit', params: { listId: list.id } }" :to="{ name: 'filter.settings.edit', params: { listId: list.id } }"
icon="pen" icon="pen"
@ -55,13 +55,13 @@
> >
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
</dropdown-item> </dropdown-item>
<task-subscription <Subscription
class="has-no-shadow" class="has-no-shadow"
:is-button="false" :is-button="false"
entity="list" entity="list"
:entity-id="list.id" :entity-id="list.id"
:subscription="list.subscription" :model-value="list.subscription"
@change="sub => subscription = sub" @update:model-value="setSubscriptionInStore"
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
@ -76,29 +76,42 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, watchEffect} from 'vue' import {ref, computed, watchEffect, type PropType} from 'vue'
import {useStore} from 'vuex'
import {getSavedFilterIdFromListId} from '@/helpers/savedFilter' import {isSavedFilter} from '@/helpers/savedFilter'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue' import Subscription from '@/components/misc/subscription.vue'
import ListModel from '@/models/list' import type {IList} from '@/modelTypes/IList'
import SubscriptionModel from '@/models/subscription' import type {ISubscription} from '@/modelTypes/ISubscription'
import {useConfigStore} from '@/stores/config'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
list: { list: {
type: ListModel, type: Object as PropType<IList>,
required: true, required: true,
}, },
}) })
const subscription = ref<SubscriptionModel | null>(null) const listStore = useListStore()
const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null)
watchEffect(() => { watchEffect(() => {
subscription.value = props.list.subscription ?? null subscription.value = props.list.subscription ?? null
}) })
const store = useStore() const configStore = useConfigStore()
const backgroundsEnabled = computed(() => store.state.config.enabledBackgroundProviders?.length > 0) const backgroundsEnabled = computed(() => configStore.enabledBackgroundProviders?.length > 0)
const isSavedFilter = computed(() => getSavedFilterIdFromListId(props.list.id) > 0)
function setSubscriptionInStore(sub: ISubscription) {
subscription.value = sub
const updatedList = {
...props.list,
subscription: sub,
}
listStore.setList(updatedList)
namespaceStore.setListInNamespaceById(updatedList)
}
</script> </script>

View file

@ -29,37 +29,41 @@
</modal> </modal>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent, ref} from 'vue' import {computed, ref, watch} from 'vue'
import Filters from '@/components/list/partials/filters.vue' import Filters from '@/components/list/partials/filters.vue'
import {getDefaultParams} from '@/composables/taskList' import {getDefaultParams} from '@/composables/taskList'
export default defineComponent({ const props = defineProps({
name: 'filter-popup',
components: {
Filters,
},
props: {
modelValue: { modelValue: {
required: true, required: true,
}, },
}, })
emits: ['update:modelValue'], const emit = defineEmits(['update:modelValue'])
computed: {
value: { const value = computed({
get() { get() {
return this.modelValue return props.modelValue
}, },
set(value) { set(value) {
this.$emit('update:modelValue', value) emit('update:modelValue', value)
}, },
})
watch(
() => props.modelValue,
(modelValue) => {
value.value = modelValue
}, },
hasFilters() { {immediate: true},
)
const hasFilters = computed(() => {
// this.value also contains the page parameter which we don't want to include in filters // this.value also contains the page parameter which we don't want to include in filters
// eslint-disable-next-line no-unused-vars // eslint-disable-next-line no-unused-vars
const {filter_by, filter_value, filter_comparator, filter_concat, s} = this.value const {filter_by, filter_value, filter_comparator, filter_concat, s} = value.value
const def = {...getDefaultParams()} const def = {...getDefaultParams()}
const params = {filter_by, filter_value, filter_comparator, filter_concat, s} const params = {filter_by, filter_value, filter_comparator, filter_concat, s}
@ -72,29 +76,13 @@ export default defineComponent({
} }
return JSON.stringify(params) !== JSON.stringify(defaultParams) return JSON.stringify(params) !== JSON.stringify(defaultParams)
},
},
watch: {
modelValue: {
handler(value) {
this.value = value
},
immediate: true,
},
},
setup() {
const modalOpen = ref(false)
return {
modalOpen,
}
},
methods: {
clearFilters() {
this.value = {...getDefaultParams()}
},
},
}) })
const modalOpen = ref(false)
function clearFilters() {
value.value = {...getDefaultParams()}
}
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">

View file

@ -1,21 +1,25 @@
<template> <template>
<card class="filters has-overflow" :title="hasTitle ? $t('filters.title') : ''"> <card class="filters has-overflow" :title="hasTitle ? $t('filters.title') : ''">
<div class="field"> <div class="field is-flex is-flex-direction-column">
<fancycheckbox v-model="params.filter_include_nulls"> <fancycheckbox
v-model="params.filter_include_nulls"
@update:model-value="change()"
>
{{ $t('filters.attributes.includeNulls') }} {{ $t('filters.attributes.includeNulls') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox <fancycheckbox
v-model="filters.requireAllFilters" v-model="filters.requireAllFilters"
@change="setFilterConcat()" @update:model-value="setFilterConcat()"
> >
{{ $t('filters.attributes.requireAll') }} {{ $t('filters.attributes.requireAll') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox @change="setDoneFilter" v-model="filters.done"> <fancycheckbox v-model="filters.done" @update:model-value="setDoneFilter">
{{ $t('filters.attributes.showDoneTasks') }} {{ $t('filters.attributes.showDoneTasks') }}
</fancycheckbox> </fancycheckbox>
<fancycheckbox <fancycheckbox
v-if="!$route.name.includes('list.kanban') || !$route.name.includes('list.table')" v-if="!$route.name.includes('list.kanban') || !$route.name.includes('list.table')"
v-model="sortAlphabetically" v-model="sortAlphabetically"
@update:model-value="change()"
> >
{{ $t('filters.attributes.sortAlphabetically') }} {{ $t('filters.attributes.sortAlphabetically') }}
</fancycheckbox> </fancycheckbox>
@ -38,11 +42,11 @@
<priority-select <priority-select
:disabled="!filters.usePriority || undefined" :disabled="!filters.usePriority || undefined"
v-model.number="filters.priority" v-model.number="filters.priority"
@change="setPriority" @update:model-value="setPriority"
/> />
<fancycheckbox <fancycheckbox
v-model="filters.usePriority" v-model="filters.usePriority"
@change="setPriority" @update:model-value="setPriority"
> >
{{ $t('filters.attributes.enablePriority') }} {{ $t('filters.attributes.enablePriority') }}
</fancycheckbox> </fancycheckbox>
@ -53,12 +57,12 @@
<div class="control single-value-control"> <div class="control single-value-control">
<percent-done-select <percent-done-select
v-model.number="filters.percentDone" v-model.number="filters.percentDone"
@change="setPercentDoneFilter" @update:model-value="setPercentDoneFilter"
:disabled="!filters.usePercentDone || undefined" :disabled="!filters.usePercentDone || undefined"
/> />
<fancycheckbox <fancycheckbox
v-model="filters.usePercentDone" v-model="filters.usePercentDone"
@change="setPercentDoneFilter" @update:model-value="setPercentDoneFilter"
> >
{{ $t('filters.attributes.enablePercentDone') }} {{ $t('filters.attributes.enablePercentDone') }}
</fancycheckbox> </fancycheckbox>
@ -68,8 +72,9 @@
<label class="label">{{ $t('task.attributes.dueDate') }}</label> <label class="label">{{ $t('task.attributes.dueDate') }}</label>
<div class="control"> <div class="control">
<datepicker-with-range <datepicker-with-range
@dateChanged="values => setDateFilter('due_date', values)" v-model="filters.dueDate"
v-model="filters.dueDate"> @update:model-value="values => setDateFilter('due_date', values)"
>
<template #trigger="{toggle, buttonText}"> <template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2"> <x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
{{ buttonText }} {{ buttonText }}
@ -82,8 +87,9 @@
<label class="label">{{ $t('task.attributes.startDate') }}</label> <label class="label">{{ $t('task.attributes.startDate') }}</label>
<div class="control"> <div class="control">
<datepicker-with-range <datepicker-with-range
@dateChanged="values => setDateFilter('start_date', values)" v-model="filters.startDate"
v-model="filters.startDate"> @update:model-value="values => setDateFilter('start_date', values)"
>
<template #trigger="{toggle, buttonText}"> <template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2"> <x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
{{ buttonText }} {{ buttonText }}
@ -96,8 +102,9 @@
<label class="label">{{ $t('task.attributes.endDate') }}</label> <label class="label">{{ $t('task.attributes.endDate') }}</label>
<div class="control"> <div class="control">
<datepicker-with-range <datepicker-with-range
@dateChanged="values => setDateFilter('end_date', values)" v-model="filters.endDate"
v-model="filters.endDate"> @update:model-value="values => setDateFilter('end_date', values)"
>
<template #trigger="{toggle, buttonText}"> <template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2"> <x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
{{ buttonText }} {{ buttonText }}
@ -110,8 +117,9 @@
<label class="label">{{ $t('task.attributes.reminders') }}</label> <label class="label">{{ $t('task.attributes.reminders') }}</label>
<div class="control"> <div class="control">
<datepicker-with-range <datepicker-with-range
@dateChanged="values => setDateFilter('reminders', values)" v-model="filters.reminders"
v-model="filters.reminders"> @update:model-value="values => setDateFilter('reminders', values)"
>
<template #trigger="{toggle, buttonText}"> <template #trigger="{toggle, buttonText}">
<x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2"> <x-button @click.prevent.stop="toggle()" variant="secondary" :shadow="false" class="mb-2">
{{ buttonText }} {{ buttonText }}
@ -141,7 +149,7 @@
<div class="field"> <div class="field">
<label class="label">{{ $t('task.attributes.labels') }}</label> <label class="label">{{ $t('task.attributes.labels') }}</label>
<div class="control labels-list"> <div class="control labels-list">
<edit-labels v-model="labels" @change="changeLabelFilter"/> <edit-labels v-model="labels" @update:model-value="changeLabelFilter"/>
</div> </div>
</div> </div>
@ -186,8 +194,10 @@
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import DatepickerWithRange from '@/components/date/datepickerWithRange' import {useLabelStore} from '@/stores/labels'
import Fancycheckbox from '../../input/fancycheckbox'
import DatepickerWithRange from '@/components/date/datepickerWithRange.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {includesById} from '@/helpers/utils' import {includesById} from '@/helpers/utils'
import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue' import PrioritySelect from '@/components/tasks/partials/prioritySelect.vue'
@ -200,6 +210,7 @@ import ListService from '@/services/list'
import NamespaceService from '@/services/namespace' import NamespaceService from '@/services/namespace'
import EditLabels from '@/components/tasks/partials/editLabels.vue' import EditLabels from '@/components/tasks/partials/editLabels.vue'
import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
import {objectToSnakeCase} from '@/helpers/case' import {objectToSnakeCase} from '@/helpers/case'
import {getDefaultParams} from '@/composables/taskList' import {getDefaultParams} from '@/composables/taskList'
import {camelCase} from 'camel-case' import {camelCase} from 'camel-case'
@ -278,7 +289,7 @@ export default defineComponent({
default: false, default: false,
}, },
}, },
emits: ['update:modelValue', 'change'], emits: ['update:modelValue'],
watch: { watch: {
modelValue: { modelValue: {
handler(value) { handler(value) {
@ -303,8 +314,10 @@ export default defineComponent({
this.change() this.change()
}, },
}, },
foundLabels() { foundLabels() {
return this.$store.getters['labels/filterLabelsByQuery'](this.labels, this.query) const labelStore = useLabelStore()
return labelStore.filterLabelsByQuery(this.labels, this.labelQuery)
}, },
}, },
methods: { methods: {
@ -312,7 +325,6 @@ export default defineComponent({
const params = {...this.params} const params = {...this.params}
params.filter_value = params.filter_value.map(v => v instanceof Date ? v.toISOString() : v) params.filter_value = params.filter_value.map(v => v instanceof Date ? v.toISOString() : v)
this.$emit('update:modelValue', params) this.$emit('update:modelValue', params)
this.$emit('change', params)
}, },
prepareFilters() { prepareFilters() {
this.prepareDone() this.prepareDone()
@ -333,7 +345,8 @@ export default defineComponent({
: '' : ''
const labelIds = labels.split(',').map(i => parseInt(i)) const labelIds = labels.split(',').map(i => parseInt(i))
this.labels = this.$store.getters['labels/getLabelsByIds'](labelIds) const labelStore = useLabelStore()
this.labels = labelStore.getLabelsByIds(labelIds)
}, },
removePropertyFromFilter(propertyName) { removePropertyFromFilter(propertyName) {
// Because of the way arrays work, we can only ever remove one element at once. // Because of the way arrays work, we can only ever remove one element at once.
@ -379,7 +392,14 @@ export default defineComponent({
this.params.filter_value.push(dateTo) this.params.filter_value.push(dateTo)
} }
this.filters[camelCase(filterName)] = {dateFrom, dateTo} this.filters[camelCase(filterName)] = {
// Passing the dates as string values avoids an endless loop between values changing
// in the datepicker (bubbling up to here) and changing here and bubbling down to the
// datepicker (because there's a new date instance every time this function gets called).
// See https://kolaente.dev/vikunja/frontend/issues/2384
dateFrom: dateIsValid(dateFrom) ? formatISO(dateFrom) : dateFrom,
dateTo: dateIsValid(dateTo) ? formatISO(dateTo) : dateTo,
}
this.change() this.change()
return return
} }
@ -500,6 +520,14 @@ export default defineComponent({
return return
} }
// Don't load things if we already have something loaded.
// This is not the most ideal solution because it prevents a re-population when filters are changed
// from the outside. It is still fine because we're not changing them from the outside, other than
// loading them initially.
if (this[kind].length > 0) {
return
}
this[kind] = await this[`${servicePrefix}Service`].getAll({}, {s: this.filters[filterName]}) this[kind] = await this[`${servicePrefix}Service`].getAll({}, {s: this.filters[filterName]})
}, },
setDoneFilter() { setDoneFilter() {
@ -518,6 +546,7 @@ export default defineComponent({
} else { } else {
this.params.filter_concat = 'or' this.params.filter_concat = 'or'
} }
this.change()
}, },
setPriority() { setPriority() {
this.setSingleValueFilter('priority', 'priority', 'usePriority') this.setSingleValueFilter('priority', 'priority', 'usePriority')
@ -532,6 +561,7 @@ export default defineComponent({
if (query === '') { if (query === '') {
this.clear(kind) this.clear(kind)
return
} }
const response = await this[`${kind}Service`].getAll({}, {s: query}) const response = await this[`${kind}Service`].getAll({}, {s: query})
@ -556,9 +586,9 @@ export default defineComponent({
return return
} }
let ids = [] const ids = []
this[kind].forEach(u => { this[kind].forEach(u => {
ids.push(u.id) ids.push(kind === 'users' ? u.username : u.id)
}) })
this.filters[filterName] = ids.join(',') this.filters[filterName] = ids.join(',')
@ -591,7 +621,7 @@ export default defineComponent({
return return
} }
let labelIDs = [] const labelIDs = []
this.labels.forEach(u => { this.labels.forEach(u => {
labelIDs.push(u.id) labelIDs.push(u.id)
}) })

View file

@ -1,8 +1,8 @@
<template> <template>
<router-link <router-link
:class="{ :class="{
'has-light-text': !colorIsDark(list.hexColor), 'has-light-text': !colorIsDark(list.hexColor) || background !== null,
'has-background': blurHashUrl !== '' 'has-background': blurHashUrl !== '' || background !== null,
}" }"
:style="{ :style="{
'background-color': list.hexColor, 'background-color': list.hexColor,
@ -24,7 +24,7 @@
<BaseButton <BaseButton
v-else v-else
:class="{'is-favorite': list.isFavorite}" :class="{'is-favorite': list.isFavorite}"
@click.stop="toggleFavoriteList(list)" @click.stop="listStore.toggleListFavorite(list)"
class="favorite" class="favorite"
> >
<icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/> <icon :icon="list.isFavorite ? 'star' : ['far', 'star']"/>
@ -36,16 +36,16 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {PropType, ref, watch} from 'vue' import {type PropType, ref, watch} from 'vue'
import {useStore} from 'vuex'
import ListService from '@/services/list' import ListService from '@/services/list'
import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash' import {getBlobFromBlurHash} from '@/helpers/getBlobFromBlurHash'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
import ListModel from '@/models/list'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import type {IList} from '@/modelTypes/IList'
import {useListStore} from '@/stores/lists'
const background = ref<string | null>(null) const background = ref<string | null>(null)
const backgroundLoading = ref(false) const backgroundLoading = ref(false)
@ -53,7 +53,7 @@ const blurHashUrl = ref('')
const props = defineProps({ const props = defineProps({
list: { list: {
type: Object as PropType<ListModel>, type: Object as PropType<IList>,
required: true, required: true,
}, },
showArchived: { showArchived: {
@ -84,16 +84,7 @@ async function loadBackground() {
} }
} }
const store = useStore() const listStore = useListStore()
function toggleFavoriteList(list: ListModel) {
// The favorites pseudo list is always favorite
// Archived lists cannot be marked favorite
if (list.id === -1 || list.isArchived) {
return
}
store.dispatch('lists/toggleListFavorite', list)
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -110,7 +101,7 @@ function toggleFavoriteList(list: ListModel) {
overflow: hidden; overflow: hidden;
&.has-light-text .title { &.has-light-text .title {
color: var(--grey-100); color: var(--grey-100) !important;
} }
&.has-background, &.has-background,

View file

@ -9,7 +9,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {PropType} from 'vue' import type {PropType} from 'vue'
type Variants = 'default' | 'small' type Variants = 'default' | 'small'
defineProps({ defineProps({

View file

@ -11,16 +11,17 @@ import {
faCheckDouble, faCheckDouble,
faChessKnight, faChessKnight,
faChevronDown, faChevronDown,
faCircleInfo,
faCloudDownloadAlt, faCloudDownloadAlt,
faCloudUploadAlt, faCloudUploadAlt,
faCocktail, faCocktail,
faCoffee, faCoffee,
faCog, faCog,
faEye,
faEyeSlash,
faEllipsisH, faEllipsisH,
faEllipsisV, faEllipsisV,
faExclamation, faExclamation,
faEye,
faEyeSlash,
faFillDrip, faFillDrip,
faFilter, faFilter,
faForward, faForward,
@ -82,6 +83,7 @@ library.add(faCheck)
library.add(faCheckDouble) library.add(faCheckDouble)
library.add(faChessKnight) library.add(faChessKnight)
library.add(faChevronDown) library.add(faChevronDown)
library.add(faCircleInfo)
library.add(faClock) library.add(faClock)
library.add(faCloudDownloadAlt) library.add(faCloudDownloadAlt)
library.add(faCloudUploadAlt) library.add(faCloudUploadAlt)
@ -89,11 +91,11 @@ library.add(faCocktail)
library.add(faCoffee) library.add(faCoffee)
library.add(faCog) library.add(faCog)
library.add(faComments) library.add(faComments)
library.add(faEye)
library.add(faEyeSlash)
library.add(faEllipsisH) library.add(faEllipsisH)
library.add(faEllipsisV) library.add(faEllipsisV)
library.add(faExclamation) library.add(faExclamation)
library.add(faEye)
library.add(faEyeSlash)
library.add(faFillDrip) library.add(faFillDrip)
library.add(faFilter) library.add(faFilter)
library.add(faForward) library.add(faForward)

View file

@ -23,17 +23,14 @@
</div> </div>
</div> </div>
<div class="api-url-info" v-else> <div class="api-url-info" v-else>
<i18n-t keypath="apiConfig.use"> <i18n-t keypath="apiConfig.use" scope="global">
<span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span> <span class="url" v-tooltip="apiUrl"> {{ apiDomain }} </span>
</i18n-t> </i18n-t>
<br/> <br/>
<ButtonLink class="api-config__change-button" @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</ButtonLink> <ButtonLink class="api-config__change-button" @click="() => (configureApi = true)">{{ $t('apiConfig.change') }}</ButtonLink>
</div> </div>
<message variant="success" v-if="successMsg !== '' && errorMsg === ''" class="mt-2"> <message variant="danger" v-if="errorMsg !== ''" class="mt-2">
{{ successMsg }}
</message>
<message variant="danger" v-if="errorMsg !== '' && successMsg === ''" class="mt-2">
{{ errorMsg }} {{ errorMsg }}
</message> </message>
</div> </div>
@ -74,7 +71,6 @@ watch(() => props.configureOpen, (value) => {
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const errorMsg = ref('') const errorMsg = ref('')
const successMsg = ref('')
async function setApiUrl() { async function setApiUrl() {
if (apiUrl.value === '') { if (apiUrl.value === '') {
@ -99,7 +95,6 @@ async function setApiUrl() {
emit('foundApi', apiUrl.value) emit('foundApi', apiUrl.value)
} catch (e) { } catch (e) {
// Still not found, url is still invalid // Still not found, url is still invalid
successMsg.value = ''
errorMsg.value = t('apiConfig.error', {domain: apiDomain.value}) errorMsg.value = t('apiConfig.error', {domain: apiDomain.value})
} }
} }

View file

@ -16,11 +16,21 @@
</span> </span>
</BaseButton> </BaseButton>
</header> </header>
<div class="card-content loader-container" :class="{'p-0': !padding, 'is-loading': loading}"> <div
class="card-content loader-container"
:class="{
'p-0': !padding,
'is-loading': loading
}"
>
<div :class="{'content': hasContent}"> <div :class="{'content': hasContent}">
<slot></slot> <slot />
</div> </div>
</div> </div>
<footer v-if="$slots.footer" class="card-footer">
<slot name="footer" />
</footer>
</div> </div>
</template> </template>
@ -76,9 +86,11 @@ defineEmits(['close'])
border-radius: $radius $radius 0 0; border-radius: $radius $radius 0 0;
} }
// FIXME: should maybe be merged somehow with modal .card-footer {
:deep(.modal-card-foot) {
background-color: var(--grey-50); background-color: var(--grey-50);
border-top: 0; border-top: 0;
padding: var(--modal-card-head-padding);
display: flex;
justify-content: flex-end;
} }
</style> </style>

View file

@ -0,0 +1,24 @@
<template>
<span
:style="{backgroundColor: color }"
class="color-bubble"
></span>
</template>
<script lang="ts" setup>
import type { Color } from 'csstype'
defineProps< {
color: Color,
}>()
</script>
<style scoped>
.color-bubble {
display: inline-block;
border-radius: 100%;
height: 10px;
width: 10px;
flex-shrink: 0;
}
</style>

View file

@ -4,15 +4,17 @@
:title="title" :title="title"
:shadow="false" :shadow="false"
:padding="false" :padding="false"
class="has-text-left has-overflow" class="has-text-left"
:has-close="true" :has-close="true"
@close="$router.back()" @close="$router.back()"
:loading="loading" :loading="loading"
> >
<div class="p-4"> <div class="p-4">
<slot></slot> <slot />
</div> </div>
<footer class="modal-card-foot is-flex is-justify-content-flex-end">
<template #footer>
<slot name="footer">
<x-button <x-button
v-if="tertiary !== ''" v-if="tertiary !== ''"
:shadow="false" :shadow="false"
@ -31,11 +33,12 @@
variant="primary" variant="primary"
@click.prevent.stop="primary()" @click.prevent.stop="primary()"
:icon="primaryIcon" :icon="primaryIcon"
:disabled="primaryDisabled" :disabled="primaryDisabled || loading"
> >
{{ primaryLabel || $t('misc.create') }} {{ primaryLabel || $t('misc.create') }}
</x-button> </x-button>
</footer> </slot>
</template>
</card> </card>
</modal> </modal>
</template> </template>

View file

@ -1,6 +1,6 @@
<template> <template>
<message variant="danger"> <message variant="danger">
<i18n-t keypath="loadingError.failed"> <i18n-t keypath="loadingError.failed" scope="global">
<ButtonLink @click="reload">{{ $t('loadingError.tryAgain') }}</ButtonLink> <ButtonLink @click="reload">{{ $t('loadingError.tryAgain') }}</ButtonLink>
<ButtonLink href="https://vikunja.io/contact/">{{ $t('loadingError.contact') }}</ButtonLink> <ButtonLink href="https://vikunja.io/contact/">{{ $t('loadingError.contact') }}</ButtonLink>
</i18n-t> </i18n-t>

View file

@ -33,18 +33,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {useStore} from 'vuex' import {useBaseStore} from '@/stores/base'
import Shortcut from '@/components/misc/shortcut.vue' import Shortcut from '@/components/misc/shortcut.vue'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import {KEYBOARD_SHORTCUTS_ACTIVE} from '@/store/mutation-types'
import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts' import {KEYBOARD_SHORTCUTS as shortcuts} from './shortcuts'
const store = useStore()
function close() { function close() {
store.commit(KEYBOARD_SHORTCUTS_ACTIVE, false) useBaseStore().setKeyboardShortcutsActive(false)
} }
</script> </script>

View file

@ -136,6 +136,10 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
title: 'keyboardShortcuts.task.reminder', title: 'keyboardShortcuts.task.reminder',
keys: ['alt', 'r'], keys: ['alt', 'r'],
}, },
{
title: 'keyboardShortcuts.task.description',
keys: ['e'],
},
], ],
}, },
] ]

View file

@ -8,14 +8,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from 'vuex'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {useConfigStore} from '@/stores/config'
const store = useStore() const configStore = useConfigStore()
const imprintUrl = computed(() => store.state.config.legal.imprintUrl) const imprintUrl = computed(() => configStore.legal.imprintUrl)
const privacyPolicyUrl = computed(() => store.state.config.legal.privacyPolicyUrl) const privacyPolicyUrl = computed(() => configStore.legal.privacyPolicyUrl)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -7,7 +7,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, PropType} from 'vue' import {computed, type PropType} from 'vue'
const TEXT_ALIGN_MAP = Object.freeze({ const TEXT_ALIGN_MAP = Object.freeze({
left: '', left: '',

View file

@ -71,10 +71,9 @@ export default {
<script lang="ts" setup> <script lang="ts" setup>
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {onUnmounted, ref, useAttrs, watch} from 'vue' import {ref, useAttrs, watchEffect} from 'vue'
import {useScrollLock} from '@vueuse/core' import {useScrollLock} from '@vueuse/core'
const props = withDefaults(defineProps<{ const props = withDefaults(defineProps<{
enabled?: boolean, enabled?: boolean,
overflow?: boolean, overflow?: boolean,
@ -94,14 +93,9 @@ const attrs = useAttrs()
const modal = ref<HTMLElement | null>(null) const modal = ref<HTMLElement | null>(null)
const scrollLock = useScrollLock(modal) const scrollLock = useScrollLock(modal)
watch( watchEffect(() => {
() => props.enabled, scrollLock.value = props.enabled
enabled => { })
scrollLock.value = enabled
},
)
onUnmounted(() => scrollLock.value = false)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -26,22 +26,23 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed} from 'vue'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import Legal from '@/components/misc/legal.vue' import Legal from '@/components/misc/legal.vue'
import ApiConfig from '@/components/misc/api-config.vue' import ApiConfig from '@/components/misc/api-config.vue'
import {useStore} from 'vuex'
import {computed} from 'vue'
import {useRoute} from 'vue-router'
import {useI18n} from 'vue-i18n'
import {useTitle} from '@/composables/useTitle' import {useTitle} from '@/composables/useTitle'
import {useConfigStore} from '@/stores/config'
const configStore = useConfigStore()
const motd = computed(() => configStore.motd)
const route = useRoute() const route = useRoute()
const store = useStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const motd = computed(() => store.state.config.motd)
const title = computed(() => t(route.meta?.title as string || '')) const title = computed(() => t(route.meta?.title as string || ''))
useTitle(() => title.value) useTitle(() => title.value)
</script> </script>

View file

@ -42,7 +42,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref, computed} from 'vue' import {ref, computed} from 'vue'
import {useStore} from 'vuex' import {useRouter, useRoute} from 'vue-router'
import Logo from '@/assets/logo.svg?component' import Logo from '@/assets/logo.svg?component'
import ApiConfig from '@/components/misc/api-config.vue' import ApiConfig from '@/components/misc/api-config.vue'
@ -52,13 +52,14 @@ import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue'
import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl' import {ERROR_NO_API_URL} from '@/helpers/checkAndSetApiUrl'
import {useOnline} from '@/composables/useOnline' import {useOnline} from '@/composables/useOnline'
import {useRouter, useRoute} from 'vue-router'
import {getAuthForRoute} from '@/router' import {getAuthForRoute} from '@/router'
import {useBaseStore} from '@/stores/base'
const router = useRouter() const router = useRouter()
const route = useRoute() const route = useRoute()
const store = useStore() const baseStore = useBaseStore()
const ready = ref(false) const ready = ref(false)
const online = useOnline() const online = useOnline()
@ -68,14 +69,14 @@ const showLoading = computed(() => !ready.value && error.value === '')
async function load() { async function load() {
try { try {
await store.dispatch('loadApp') await baseStore.loadApp()
const redirectTo = getAuthForRoute(route) const redirectTo = getAuthForRoute(route)
if (typeof redirectTo !== 'undefined') { if (typeof redirectTo !== 'undefined') {
await router.push(redirectTo) await router.push(redirectTo)
} }
ready.value = true ready.value = true
} catch (e: any) { } catch (e: unknown) {
error.value = e error.value = String(e)
} }
} }

View file

@ -5,7 +5,7 @@
:icon="iconName" :icon="iconName"
v-tooltip="tooltipText" v-tooltip="tooltipText"
@click="changeSubscription" @click="changeSubscription"
:disabled="disabled || undefined" :disabled="disabled"
> >
{{ buttonText }} {{ buttonText }}
</x-button> </x-button>
@ -23,6 +23,7 @@
v-tooltip="tooltipText" v-tooltip="tooltipText"
@click="changeSubscription" @click="changeSubscription"
:class="{'is-disabled': disabled}" :class="{'is-disabled': disabled}"
:disabled="disabled"
> >
<span class="icon"> <span class="icon">
<icon :icon="iconName"/> <icon :icon="iconName"/>
@ -32,7 +33,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, shallowRef} from 'vue' import {computed, shallowRef, type PropType} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -40,57 +41,78 @@ import DropdownItem from '@/components/misc/dropdown-item.vue'
import SubscriptionService from '@/services/subscription' import SubscriptionService from '@/services/subscription'
import SubscriptionModel from '@/models/subscription' import SubscriptionModel from '@/models/subscription'
import type {ISubscription} from '@/modelTypes/ISubscription'
import {success} from '@/message' import {success} from '@/message'
interface Props { const props = defineProps({
entity: string entity: String,
entityId: number entityId: Number,
subscription: SubscriptionModel | null isButton: {
type?: 'button' | 'dropdown' | null type: Boolean,
} default: true,
},
const props = withDefaults(defineProps<Props>(), { modelValue: {
subscription: null, type: Object as PropType<ISubscription>,
type: 'button', default: null,
},
type: {
type: String as PropType<'button' | 'dropdown' | 'null'>,
default: 'button',
},
}) })
const subscriptionEntity = computed<string | null>(() => props.subscription?.entity ?? null) const subscriptionEntity = computed<string | null>(() => props.modelValue?.entity ?? null)
const emit = defineEmits(['change']) const emit = defineEmits(['update:modelValue'])
const subscriptionService = shallowRef(new SubscriptionService()) const subscriptionService = shallowRef(new SubscriptionService())
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const tooltipText = computed(() => { const tooltipText = computed(() => {
if (disabled.value) { if (disabled.value) {
return t('task.subscription.subscribedThroughParent', { if (props.entity === 'list' && subscriptionEntity.value === 'namespace') {
entity: props.entity, return t('task.subscription.subscribedListThroughParentNamespace')
parent: subscriptionEntity.value, }
}) if (props.entity === 'task' && subscriptionEntity.value === 'namespace') {
return t('task.subscription.subscribedTaskThroughParentNamespace')
}
if (props.entity === 'task' && subscriptionEntity.value === 'list') {
return t('task.subscription.subscribedTaskThroughParentList')
} }
return props.subscription !== null ? return ''
t('task.subscription.subscribed', {entity: props.entity}) :
t('task.subscription.notSubscribed', {entity: props.entity})
})
const buttonText = computed(() => props.subscription !== null ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const iconName = computed(() => props.subscription !== null ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => {
if (props.subscription === null) {
return false
} }
return subscriptionEntity.value !== props.entity switch (props.entity) {
case 'namespace':
return props.modelValue !== null ?
t('task.subscription.subscribedNamespace') :
t('task.subscription.notSubscribedNamespace')
case 'list':
return props.modelValue !== null ?
t('task.subscription.subscribedList') :
t('task.subscription.notSubscribedList')
case 'task':
return props.modelValue !== null ?
t('task.subscription.subscribedTask') :
t('task.subscription.notSubscribedTask')
}
return ''
}) })
const buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
const iconName = computed(() => props.modelValue ? ['far', 'bell-slash'] : 'bell')
const disabled = computed(() => props.modelValue && subscriptionEntity.value !== props.entity)
function changeSubscription() { function changeSubscription() {
if (disabled.value) { if (disabled.value) {
return return
} }
if (props.subscription === null) { if (props.modelValue === null) {
subscribe() subscribe()
} else { } else {
unsubscribe() unsubscribe()
@ -103,8 +125,21 @@ async function subscribe() {
entityId: props.entityId, entityId: props.entityId,
}) })
await subscriptionService.value.create(subscription) await subscriptionService.value.create(subscription)
emit('change', subscription) emit('update:modelValue', subscription)
success({message: t('task.subscription.subscribeSuccess', {entity: props.entity})})
let message = ''
switch (props.entity) {
case 'namespace':
message = t('task.subscription.subscribeSuccessNamespace')
break
case 'list':
message = t('task.subscription.subscribeSuccessList')
break
case 'task':
message = t('task.subscription.subscribeSuccessTask')
break
}
success({message})
} }
async function unsubscribe() { async function unsubscribe() {
@ -113,7 +148,20 @@ async function unsubscribe() {
entityId: props.entityId, entityId: props.entityId,
}) })
await subscriptionService.value.delete(subscription) await subscriptionService.value.delete(subscription)
emit('change', null) emit('update:modelValue', null)
success({message: t('task.subscription.unsubscribeSuccess', {entity: props.entity})})
let message = ''
switch (props.entity) {
case 'namespace':
message = t('task.subscription.unsubscribeSuccessNamespace')
break
case 'list':
message = t('task.subscription.unsubscribeSuccessList')
break
case 'task':
message = t('task.subscription.unsubscribeSuccessTask')
break
}
success({message})
} }
</script> </script>

View file

@ -2,34 +2,39 @@
<div :class="{'is-inline': isInline}" class="user"> <div :class="{'is-inline': isInline}" class="user">
<img <img
:height="avatarSize" :height="avatarSize"
:src="user.getAvatarUrl(avatarSize)" :src="getAvatarUrl(user, avatarSize)"
:width="avatarSize" :width="avatarSize"
alt="" alt=""
class="avatar" class="avatar"
v-tooltip="user.getDisplayName()"/> v-tooltip="getDisplayName(user)"/>
<span class="username" v-if="showUsername">{{ user.getDisplayName() }}</span> <span class="username" v-if="showUsername">{{ getDisplayName(user) }}</span>
</div> </div>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import type {PropType} from 'vue'
import {getAvatarUrl, getDisplayName} from '@/models/user'
import type {IUser} from '@/modelTypes/IUser'
defineProps({ defineProps({
user: { user: {
type: Object as PropType<IUser>,
required: true, required: true,
type: Object,
}, },
showUsername: { showUsername: {
required: false,
type: Boolean, type: Boolean,
required: false,
default: true, default: true,
}, },
avatarSize: { avatarSize: {
required: false,
type: Number, type: Number,
required: false,
default: 50, default: 50,
}, },
isInline: { isInline: {
required: false,
type: Boolean, type: Boolean,
required: false,
default: false, default: false,
}, },
}) })

View file

@ -33,13 +33,13 @@
> >
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
</dropdown-item> </dropdown-item>
<task-subscription <Subscription
class="has-no-shadow" class="has-no-shadow"
:is-button="false" :is-button="false"
entity="namespace" entity="namespace"
:entity-id="namespace.id" :entity-id="namespace.id"
:subscription="subscription" :model-value="subscription"
@change="sub => subscription = sub" @update:model-value="setSubscriptionInStore"
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
@ -54,21 +54,36 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, onMounted} from 'vue' import {ref, onMounted, type PropType} from 'vue'
import Dropdown from '@/components/misc/dropdown.vue' import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue' import DropdownItem from '@/components/misc/dropdown-item.vue'
import TaskSubscription from '@/components/misc/subscription.vue' import Subscription from '@/components/misc/subscription.vue'
import type {INamespace} from '@/modelTypes/INamespace'
import type {ISubscription} from '@/modelTypes/ISubscription'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
namespace: { namespace: {
type: Object, // NamespaceModel type: Object as PropType<INamespace>,
required: true, required: true,
}, },
}) })
const subscription = ref(null) const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null)
onMounted(() => { onMounted(() => {
subscription.value = props.namespace.subscription subscription.value = props.namespace.subscription
}) })
function setSubscriptionInStore(sub: ISubscription) {
subscription.value = sub
namespaceStore.setNamespaces([
{
...props.namespace,
subscription: sub,
},
])
}
</script> </script>

View file

@ -24,13 +24,13 @@
<div class="detail"> <div class="detail">
<div> <div>
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer"> <span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
{{ n.notification.doer.getDisplayName() }} {{ getDisplayName(n.notification.doer) }}
</span> </span>
<BaseButton @click="() => to(n, index)()"> <BaseButton @click="() => to(n, index)()">
{{ n.toText(userInfo) }} {{ n.toText(userInfo) }}
</BaseButton> </BaseButton>
</div> </div>
<span class="created" v-tooltip="formatDate(n.created)"> <span class="created" v-tooltip="formatDateLong(n.created)">
{{ formatDateSince(n.created) }} {{ formatDateSince(n.created) }}
</span> </span>
</div> </div>
@ -48,21 +48,23 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed, onMounted, onUnmounted, ref} from 'vue' import {computed, onMounted, onUnmounted, ref} from 'vue'
import {useRouter} from 'vue-router'
import NotificationService from '@/services/notification' import NotificationService from '@/services/notification'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
import names from '@/models/constants/notificationNames.json' import { NOTIFICATION_NAMES as names, type INotification} from '@/modelTypes/INotification'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {useStore} from 'vuex' import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import {useRouter} from 'vue-router' import {getDisplayName} from '@/models/user'
import {useAuthStore} from '@/stores/auth'
const LOAD_NOTIFICATIONS_INTERVAL = 10000 const LOAD_NOTIFICATIONS_INTERVAL = 10000
const store = useStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
const allNotifications = ref([]) const allNotifications = ref<INotification[]>([])
const showNotifications = ref(false) const showNotifications = ref(false)
const popup = ref(null) const popup = ref(null)
@ -72,7 +74,7 @@ const unreadNotifications = computed(() => {
const notifications = computed(() => { const notifications = computed(() => {
return allNotifications.value ? allNotifications.value.filter(n => n.name !== '') : [] return allNotifications.value ? allNotifications.value.filter(n => n.name !== '') : []
}) })
const userInfo = computed(() => store.state.auth.info) const userInfo = computed(() => authStore.info)
let interval: number let interval: number

View file

@ -61,7 +61,6 @@ import TeamService from '@/services/team'
import NamespaceModel from '@/models/namespace' import NamespaceModel from '@/models/namespace'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
import {CURRENT_LIST, LOADING, LOADING_MODULE, QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
@ -71,6 +70,12 @@ import {parseTaskText, PrefixMode} from '@/modules/parseTaskText'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {PREFIXES} from '@/modules/parseTaskText' import {PREFIXES} from '@/modules/parseTaskText'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks'
const TYPE_LIST = 'list' const TYPE_LIST = 'list'
const TYPE_TASK = 'task' const TYPE_TASK = 'task'
const TYPE_CMD = 'cmd' const TYPE_CMD = 'cmd'
@ -108,14 +113,18 @@ export default defineComponent({
}, },
computed: { computed: {
active() { active() {
const active = this.$store.state[QUICK_ACTIONS_ACTIVE] const active = useBaseStore().quickActionsActive
if (!active) { if (!active) {
// FIXME: computeds shouldn't have side effects.
// create a watcher instead
this.reset() this.reset()
} }
return active return active
}, },
results() { results() {
let lists = [] let lists = []
const listStore = useListStore()
if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) { if (this.searchMode === SEARCH_MODE_ALL || this.searchMode === SEARCH_MODE_LISTS) {
const {list} = this.parsedQuery const {list} = this.parsedQuery
@ -126,10 +135,8 @@ export default defineComponent({
const history = getHistory() const history = getHistory()
// Puts recently visited lists at the top // Puts recently visited lists at the top
const allLists = [...new Set([ const allLists = [...new Set([
...history.map(l => { ...history.map(l => listStore.getListById(l.id)),
return this.$store.getters['lists/getListById'](l.id) ...listStore.searchList(list),
}),
...this.$store.getters['lists/searchList'](list),
])] ])]
lists = allLists.filter(l => { lists = allLists.filter(l => {
@ -138,7 +145,7 @@ export default defineComponent({
} }
if (typeof ncache[l.namespaceId] === 'undefined') { if (typeof ncache[l.namespaceId] === 'undefined') {
ncache[l.namespaceId] = this.$store.getters['namespaces/getNamespaceById'](l.namespaceId) ncache[l.namespaceId] = useNamespaceStore().getNamespaceById(l.namespaceId)
} }
return !ncache[l.namespaceId].isArchived return !ncache[l.namespaceId].isArchived
@ -177,8 +184,7 @@ export default defineComponent({
}, },
loading() { loading() {
return this.taskService.loading || return this.taskService.loading ||
(this.$store.state[LOADING] && this.$store.state[LOADING_MODULE] === 'namespaces') || useNamespaceStore().isLoading || useListStore().isLoading ||
(this.$store.state[LOADING] && this.$store.state[LOADING_MODULE] === 'lists') ||
this.teamService.loading this.teamService.loading
}, },
placeholder() { placeholder() {
@ -205,7 +211,7 @@ export default defineComponent({
case CMD_NEW_TASK: case CMD_NEW_TASK:
return this.$t('quickActions.createTask', {title: this.currentList.title}) return this.$t('quickActions.createTask', {title: this.currentList.title})
case CMD_NEW_LIST: case CMD_NEW_LIST:
namespace = this.$store.getters['namespaces/getNamespaceById'](this.currentList.namespaceId) namespace = useNamespaceStore().getNamespaceById(this.currentList.namespaceId)
return this.$t('quickActions.createList', {title: namespace.title}) return this.$t('quickActions.createList', {title: namespace.title})
} }
} }
@ -215,7 +221,8 @@ export default defineComponent({
return this.$t('quickActions.hint', prefixes) return this.$t('quickActions.hint', prefixes)
}, },
currentList() { currentList() {
return Object.keys(this.$store.state[CURRENT_LIST]).length === 0 ? null : this.$store.state[CURRENT_LIST] const currentList = useBaseStore().currentList
return Object.keys(currentList).length === 0 ? null : currentList
}, },
availableCmds() { availableCmds() {
const cmds = [] const cmds = []
@ -296,8 +303,10 @@ export default defineComponent({
filter_comparator: [], filter_comparator: [],
} }
const listStore = useListStore()
if (list !== null) { if (list !== null) {
const l = this.$store.getters['lists/findListByExactname'](list) const l = listStore.findListByExactname(list)
if (l !== null) { if (l !== null) {
params.filter_by.push('list_id') params.filter_by.push('list_id')
params.filter_value.push(l.id) params.filter_value.push(l.id)
@ -306,7 +315,7 @@ export default defineComponent({
} }
if (labels.length > 0) { if (labels.length > 0) {
const labelIds = this.$store.getters['labels/getLabelsByExactTitles'](labels).map(l => l.id) const labelIds = useLabelStore().getLabelsByExactTitles(labels).map(l => l.id)
if (labelIds.length > 0) { if (labelIds.length > 0) {
params.filter_by.push('labels') params.filter_by.push('labels')
params.filter_value.push(labelIds.join()) params.filter_value.push(labelIds.join())
@ -318,7 +327,7 @@ export default defineComponent({
const r = await this.taskService.getAll({}, params) const r = await this.taskService.getAll({}, params)
this.foundTasks = r.map(t => { this.foundTasks = r.map(t => {
t.type = TYPE_TASK t.type = TYPE_TASK
const list = this.$store.getters['lists/getListById'](t.listId) const list = listStore.getListById(t.listId)
if (list !== null) { if (list !== null) {
t.title = `${t.title} (${list.title})` t.title = `${t.title} (${list.title})`
} }
@ -354,7 +363,7 @@ export default defineComponent({
}, 150) }, 150)
}, },
closeQuickActions() { closeQuickActions() {
this.$store.commit(QUICK_ACTIONS_ACTIVE, false) useBaseStore().setQuickActionsActive(false)
}, },
doAction(type, item) { doAction(type, item) {
switch (type) { switch (type) {
@ -407,7 +416,8 @@ export default defineComponent({
return return
} }
const task = await this.$store.dispatch('tasks/createNewTask', { const taskStore = useTaskStore()
const task = await taskStore.createNewTask({
title: this.query, title: this.query,
listId: this.currentList.id, listId: this.currentList.id,
}) })
@ -424,7 +434,8 @@ export default defineComponent({
title: this.query, title: this.query,
namespaceId: this.currentList.namespaceId, namespaceId: this.currentList.namespaceId,
}) })
const list = await this.$store.dispatch('lists/createList', newList) const listStore = useListStore()
const list = await listStore.createList(newList)
this.$message.success({message: this.$t('list.create.createdSuccess')}) this.$message.success({message: this.$t('list.create.createdSuccess')})
this.$router.push({name: 'list.index', params: {listId: list.id}}) this.$router.push({name: 'list.index', params: {listId: list.id}})
this.closeQuickActions() this.closeQuickActions()
@ -432,7 +443,7 @@ export default defineComponent({
async newNamespace() { async newNamespace() {
const newNamespace = new NamespaceModel({title: this.query}) const newNamespace = new NamespaceModel({title: this.query})
await this.$store.dispatch('namespaces/createNamespace', newNamespace) await useNamespaceStore().createNamespace(newNamespace)
this.$message.success({message: this.$t('namespace.create.success')}) this.$message.success({message: this.$t('namespace.create.success')})
this.closeQuickActions() this.closeQuickActions()
}, },

View file

@ -79,48 +79,25 @@
> >
<thead> <thead>
<tr> <tr>
<th>{{ $t('list.share.attributes.link') }}</th> <th></th>
<th>{{ $t('list.share.attributes.name') }}</th> <th>{{ $t('list.share.links.view') }}</th>
<th>{{ $t('list.share.attributes.sharedBy') }}</th>
<th>{{ $t('list.share.attributes.right') }}</th>
<th>{{ $t('list.share.attributes.delete') }}</th> <th>{{ $t('list.share.attributes.delete') }}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr :key="s.id" v-for="s in linkShares"> <tr :key="s.id" v-for="s in linkShares">
<td> <td>
<div class="field has-addons no-input-mobile"> <p class="mb-2 is-italic" v-if="s.name !== ''">
<div class="control">
<input
:value="getShareLink(s.hash)"
class="input"
readonly
type="text"
/>
</div>
<div class="control">
<x-button
@click="copy(getShareLink(s.hash))"
:shadow="false"
v-tooltip="$t('misc.copy')"
>
<span class="icon">
<icon icon="paste"/>
</span>
</x-button>
</div>
</div>
</td>
<td>
<template v-if="s.name !== ''">
{{ s.name }} {{ s.name }}
</template> </p>
<i v-else>{{ $t('list.share.links.noName') }}</i>
</td> <p class="mb-2">
<td> <i18n-t keypath="list.share.links.sharedBy" scope="global">
{{ s.sharedBy.getDisplayName() }} <strong>{{ getDisplayName(s.sharedBy) }}</strong>
</td> </i18n-t>
<td class="type"> </p>
<p class="mb-2">
<template v-if="s.right === RIGHTS.ADMIN"> <template v-if="s.right === RIGHTS.ADMIN">
<span class="icon is-small"> <span class="icon is-small">
<icon icon="lock"/> <icon icon="lock"/>
@ -139,6 +116,41 @@
</span>&nbsp; </span>&nbsp;
{{ $t('list.share.right.read') }} {{ $t('list.share.right.read') }}
</template> </template>
</p>
<div class="field has-addons no-input-mobile">
<div class="control">
<input
:value="getShareLink(s.hash, selectedView[s.id])"
class="input"
readonly
type="text"
/>
</div>
<div class="control">
<x-button
@click="copy(getShareLink(s.hash, selectedView[s.id]))"
:shadow="false"
v-tooltip="$t('misc.copy')"
>
<span class="icon">
<icon icon="paste"/>
</span>
</x-button>
</div>
</div>
</td>
<td>
<div class="select">
<select v-model="selectedView[s.id]">
<option
v-for="(title, key) in availableViews"
:value="key"
:key="key">
{{ title }}
</option>
</select>
</div>
</td> </td>
<td class="actions"> <td class="actions">
<x-button <x-button
@ -177,16 +189,22 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, watch, computed, shallowReactive} from 'vue' import {ref, watch, computed, shallowReactive} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import RIGHTS from '@/models/constants/rights.json' import {RIGHTS} from '@/constants/rights'
import LinkShareModel from '@/models/linkShare' import LinkShareModel from '@/models/linkShare'
import type {ILinkShare} from '@/modelTypes/ILinkShare'
import type {IList} from '@/modelTypes/IList'
import LinkShareService from '@/services/linkShare' import LinkShareService from '@/services/linkShare'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard' import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message' import {success} from '@/message'
import {getDisplayName} from '@/models/user'
import type {ListView} from '@/types/ListView'
import {LIST_VIEWS} from '@/types/ListView'
import {useConfigStore} from '@/stores/config'
const props = defineProps({ const props = defineProps({
listId: { listId: {
@ -197,7 +215,7 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const linkShares = ref([]) const linkShares = ref<ILinkShare[]>([])
const linkShareService = shallowReactive(new LinkShareService()) const linkShareService = shallowReactive(new LinkShareService())
const selectedRight = ref(RIGHTS.READ) const selectedRight = ref(RIGHTS.READ)
const name = ref('') const name = ref('')
@ -206,6 +224,17 @@ const showDeleteModal = ref(false)
const linkIdToDelete = ref(0) const linkIdToDelete = ref(0)
const showNewForm = ref(false) const showNewForm = ref(false)
type SelectedViewMapper = Record<IList['id'], ListView>
const selectedView = ref<SelectedViewMapper>({})
const availableViews = computed<Record<ListView, string>>(() => ({
list: t('list.list.title'),
gantt: t('list.gantt.title'),
table: t('list.table.title'),
kanban: t('list.kanban.title'),
}))
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
watch( watch(
() => props.listId, () => props.listId,
@ -213,19 +242,23 @@ watch(
{immediate: true}, {immediate: true},
) )
const store = useStore() const configStore = useConfigStore()
const frontendUrl = computed(() => store.state.config.frontendUrl) const frontendUrl = computed(() => configStore.frontendUrl)
async function load(listId) { async function load(listId: IList['id']) {
// If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here // If listId == 0 the list on the calling component wasn't already loaded, so we just bail out here
if (listId === 0) { if (listId === 0) {
return return
} }
linkShares.value = await linkShareService.getAll({listId}) const links = await linkShareService.getAll({listId})
links.forEach((l: ILinkShare) => {
selectedView.value[l.id] = 'list'
})
linkShares.value = links
} }
async function add(listId) { async function add(listId: IList['id']) {
const newLinkShare = new LinkShareModel({ const newLinkShare = new LinkShareModel({
right: selectedRight.value, right: selectedRight.value,
listId, listId,
@ -241,7 +274,7 @@ async function add(listId) {
await load(listId) await load(listId)
} }
async function remove(listId) { async function remove(listId: IList['id']) {
try { try {
await linkShareService.delete(new LinkShareModel({ await linkShareService.delete(new LinkShareModel({
id: linkIdToDelete.value, id: linkIdToDelete.value,
@ -254,8 +287,8 @@ async function remove(listId) {
} }
} }
function getShareLink(hash: string) { function getShareLink(hash: string, view: ListView = LIST_VIEWS.LIST) {
return frontendUrl.value + 'share/' + hash + '/auth' return frontendUrl.value + 'share/' + hash + '/auth?view=' + view
} }
</script> </script>

View file

@ -28,7 +28,7 @@
<tbody> <tbody>
<tr :key="s.id" v-for="s in sharables"> <tr :key="s.id" v-for="s in sharables">
<template v-if="shareType === 'user'"> <template v-if="shareType === 'user'">
<td>{{ s.getDisplayName() }}</td> <td>{{ getDisplayName(s) }}</td>
<td> <td>
<template v-if="s.id === userInfo.id"> <template v-if="s.id === userInfo.id">
<b class="is-success">{{ $t('list.share.userTeam.you') }}</b> <b class="is-success">{{ $t('list.share.userTeam.you') }}</b>
@ -133,35 +133,44 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' export default {name: 'userTeamShare'}
export default defineComponent({name: 'userTeamShare'})
</script> </script>
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, computed, shallowReactive, ShallowReactive, Ref} from 'vue' import {ref, reactive, computed, shallowReactive, type Ref} from 'vue'
import type {PropType} from 'vue' import type {PropType} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import UserNamespaceService from '@/services/userNamespace' import UserNamespaceService from '@/services/userNamespace'
import UserNamespaceModel from '@/models/userNamespace' import UserNamespaceModel from '@/models/userNamespace'
import UserListModel from '@/models/userList' import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
import UserListService from '@/services/userList' import UserListService from '@/services/userList'
import UserListModel from '@/models/userList'
import type {IUserList} from '@/modelTypes/IUserList'
import UserService from '@/services/user' import UserService from '@/services/user'
import UserModel from '@/models/user' import UserModel, { getDisplayName } from '@/models/user'
import type {IUser} from '@/modelTypes/IUser'
import TeamNamespaceService from '@/services/teamNamespace' import TeamNamespaceService from '@/services/teamNamespace'
import TeamNamespaceModel from '@/models/teamNamespace' import TeamNamespaceModel from '@/models/teamNamespace'
import TeamListModel from '@/models/teamList' import type { ITeamNamespace } from '@/modelTypes/ITeamNamespace'
import TeamListService from '@/services/teamList' import TeamListService from '@/services/teamList'
import TeamListModel from '@/models/teamList'
import type { ITeamList } from '@/modelTypes/ITeamList'
import TeamService from '@/services/team' import TeamService from '@/services/team'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
import type {ITeam} from '@/modelTypes/ITeam'
import RIGHTS from '@/models/constants/rights.json'
import {RIGHTS} from '@/constants/rights'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import Nothing from '@/components/misc/nothing.vue' import Nothing from '@/components/misc/nothing.vue'
import {success} from '@/message' import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
const props = defineProps({ const props = defineProps({
type: { type: {
@ -185,10 +194,10 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
// This user service is either a userNamespaceService or a userListService, depending on the type we are using // This user service is either a userNamespaceService or a userListService, depending on the type we are using
let stuffService: ShallowReactive<UserNamespaceService | UserListService | TeamListService | TeamNamespaceService> let stuffService: UserNamespaceService | UserListService | TeamListService | TeamNamespaceService
let stuffModel: UserNamespaceModel | UserListModel | TeamListModel | TeamNamespaceModel let stuffModel: IUserNamespace | IUserList | ITeamList | ITeamNamespace
let searchService: ShallowReactive<UserService | TeamService> let searchService: UserService | TeamService
let sharable: Ref<UserModel | TeamModel> let sharable: Ref<IUser | ITeam>
const searchLabel = ref('') const searchLabel = ref('')
const selectedRight = ref({}) const selectedRight = ref({})
@ -199,8 +208,8 @@ const sharables = ref([])
const showDeleteModal = ref(false) const showDeleteModal = ref(false)
const store = useStore() const authStore = useAuthStore()
const userInfo = computed(() => store.state.auth.info) const userInfo = computed(() => authStore.info)
function createShareTypeNameComputed(count: number) { function createShareTypeNameComputed(count: number) {
return computed(() => { return computed(() => {
@ -233,6 +242,7 @@ const sharableName = computed(() => {
if (props.shareType === 'user') { if (props.shareType === 'user') {
searchService = shallowReactive(new UserService()) searchService = shallowReactive(new UserService())
// eslint-disable-next-line vue/no-ref-as-operand
sharable = ref(new UserModel()) sharable = ref(new UserModel())
searchLabel.value = 'username' searchLabel.value = 'username'
@ -249,6 +259,7 @@ if (props.shareType === 'user') {
} }
} else if (props.shareType === 'team') { } else if (props.shareType === 'team') {
searchService = new TeamService() searchService = new TeamService()
// eslint-disable-next-line vue/no-ref-as-operand
sharable = ref(new TeamModel()) sharable = ref(new TeamModel())
searchLabel.value = 'name' searchLabel.value = 'name'
@ -354,8 +365,8 @@ async function toggleType(sharable) {
const found = ref([]) const found = ref([])
const currentUserId = computed(() => store.state.auth.info.id) const currentUserId = computed(() => authStore.info.id)
async function find(query) { async function find(query: string) {
if (query === '') { if (query === '') {
found.value = [] found.value = []
return return

View file

@ -3,7 +3,7 @@
<div class="field is-grouped"> <div class="field is-grouped">
<p class="control has-icons-left is-expanded"> <p class="control has-icons-left is-expanded">
<textarea <textarea
:disabled="taskService.loading || undefined" :disabled="loading || undefined"
class="add-task-textarea input" class="add-task-textarea input"
:class="{'textarea-empty': newTaskTitle === ''}" :class="{'textarea-empty': newTaskTitle === ''}"
:placeholder="$t('list.list.addPlaceholder')" :placeholder="$t('list.list.addPlaceholder')"
@ -21,10 +21,10 @@
<p class="control"> <p class="control">
<x-button <x-button
class="add-task-button" class="add-task-button"
:disabled="newTaskTitle === '' || taskService.loading || undefined" :disabled="newTaskTitle === '' || loading || undefined"
@click="addTask()" @click="addTask()"
icon="plus" icon="plus"
:loading="taskService.loading" :loading="loading"
:aria-label="$t('list.list.add')" :aria-label="$t('list.list.add')"
> >
<span class="button-text"> <span class="button-text">
@ -41,17 +41,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, watch, unref, shallowReactive} from 'vue' import {computed, ref, unref, watch} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useStore} from 'vuex' import {debouncedWatch, type MaybeRef, tryOnMounted, useWindowSize} from '@vueuse/core'
import {tryOnMounted, debouncedWatch, useWindowSize, MaybeRef} from '@vueuse/core'
import TaskService from '@/services/task'
import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue' import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import type {ITask} from '@/modelTypes/ITask'
function cleanupTitle(title: string) { import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention'
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '') import TaskRelationService from '@/services/taskRelation'
} import TaskRelationModel from '@/models/taskRelation'
import {RELATION_KIND} from '@/types/IRelationKind'
import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
function useAutoHeightTextarea(value: MaybeRef<string>) { function useAutoHeightTextarea(value: MaybeRef<string>) {
const textarea = ref<HTMLInputElement>() const textarea = ref<HTMLInputElement>()
@ -134,9 +135,9 @@ const newTaskTitle = ref('')
const newTaskInput = useAutoHeightTextarea(newTaskTitle) const newTaskInput = useAutoHeightTextarea(newTaskTitle)
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore() const authStore = useAuthStore()
const taskStore = useTaskStore()
const taskService = shallowReactive(new TaskService())
const errorMessage = ref('') const errorMessage = ref('')
function resetEmptyTitleError(e) { function resetEmptyTitleError(e) {
@ -148,6 +149,7 @@ function resetEmptyTitleError(e) {
} }
} }
const loading = computed(() => taskStore.isLoading)
async function addTask() { async function addTask() {
if (newTaskTitle.value === '') { if (newTaskTitle.value === '') {
errorMessage.value = t('list.create.addTitleRequired') errorMessage.value = t('list.create.addTitleRequired')
@ -155,30 +157,64 @@ async function addTask() {
} }
errorMessage.value = '' errorMessage.value = ''
if (taskService.loading) { if (loading.value) {
return return
} }
const taskTitleBackup = newTaskTitle.value const taskTitleBackup = newTaskTitle.value
const newTasks = newTaskTitle.value.split(/[\r\n]+/).map(async uncleanedTitle => { // This allows us to find the tasks with the title they had before being parsed
const title = cleanupTitle(uncleanedTitle) // by quick add magic.
const createdTasks: { [key: ITask['title']]: ITask } = {}
const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value)
const newTasks = tasksToCreate.map(async ({title}) => {
if (title === '') { if (title === '') {
return return
} }
const task = await store.dispatch('tasks/createNewTask', { const task = await taskStore.createNewTask({
title, title,
listId: store.state.auth.settings.defaultListId, listId: authStore.settings.defaultListId,
position: props.defaultPosition, position: props.defaultPosition,
}) })
emit('taskAdded', task) createdTasks[title] = task
return task return task
}) })
try { try {
newTaskTitle.value = '' newTaskTitle.value = ''
await Promise.all(newTasks) await Promise.all(newTasks)
} catch (e: any) {
const taskRelationService = new TaskRelationService()
const relations = tasksToCreate.map(async t => {
const createdTask = createdTasks[t.title]
if (typeof createdTask === 'undefined') {
return
}
if (t.parent === null) {
emit('taskAdded', createdTask)
return
}
const createdParentTask = createdTasks[t.parent]
if (typeof createdTask === 'undefined' || typeof createdParentTask === 'undefined') {
return
}
const rel = await taskRelationService.create(new TaskRelationModel({
taskId: createdTask.id,
otherTaskId: createdParentTask.id,
relationKind: RELATION_KIND.PARENTTASK,
}))
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [createdParentTask]
// we're only emitting here so that the relation shows up in the task list
emit('taskAdded', createdTask)
return rel
})
await Promise.all(relations)
} catch (e: { message?: string }) {
newTaskTitle.value = taskTitleBackup newTaskTitle.value = taskTitleBackup
if (e?.message === 'NO_LIST') { if (e?.message === 'NO_LIST') {
errorMessage.value = t('list.create.addListRequired') errorMessage.value = t('list.create.addListRequired')
@ -200,7 +236,7 @@ function handleEnter(e: KeyboardEvent) {
} }
function focusTaskInput() { function focusTaskInput() {
newTaskInput.value.focus() newTaskInput.value?.focus()
} }
defineExpose({ defineExpose({
@ -214,7 +250,7 @@ defineExpose({
} }
.add-task-button { .add-task-button {
height: 2.5rem; height: 100% !important;
@media screen and (max-width: $mobile) { @media screen and (max-width: $mobile) {
.button-text { .button-text {

View file

@ -36,8 +36,8 @@
<strong>{{ $t('task.attributes.reminders') }}</strong> <strong>{{ $t('task.attributes.reminders') }}</strong>
<reminders <reminders
@change="editTaskSubmit()"
v-model="taskEditTask.reminderDates" v-model="taskEditTask.reminderDates"
@update:model-value="editTaskSubmit()"
/> />
<div class="field"> <div class="field">
@ -76,7 +76,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, computed, shallowReactive, watch, nextTick} from 'vue' import {ref, reactive, computed, shallowReactive, watch, nextTick, type PropType} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
@ -84,6 +84,7 @@ import Editor from '@/components/input/AsyncEditor'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TaskModel from '@/models/task' import TaskModel from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import EditLabels from './partials/editLabels.vue' import EditLabels from './partials/editLabels.vue'
import Reminders from './partials/reminders.vue' import Reminders from './partials/reminders.vue'
import ColorPicker from '../input/colorPicker.vue' import ColorPicker from '../input/colorPicker.vue'
@ -95,15 +96,14 @@ const router = useRouter()
const props = defineProps({ const props = defineProps({
task: { task: {
type: TaskModel, type: Object as PropType<ITask | null>,
required: true,
}, },
}) })
const taskService = shallowReactive(new TaskService()) const taskService = shallowReactive(new TaskService())
const editorActive = ref(false) const editorActive = ref(false)
let taskEditTask: TaskModel | undefined let taskEditTask: ITask | undefined
// FIXME: this initialization should not be necessary here // FIXME: this initialization should not be necessary here

View file

@ -173,21 +173,24 @@
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' import {defineComponent} from 'vue'
import {mapState} from 'pinia'
import VueDragResize from 'vue-drag-resize' import VueDragResize from 'vue-drag-resize'
import EditTask from './edit-task' import EditTask from './edit-task.vue'
import TaskService from '../../services/task' import TaskService from '../../services/task'
import TaskModel from '../../models/task' import TaskModel from '../../models/task'
import priorities from '../../models/constants/priorities' import {PRIORITIES as priorities} from '@/constants/priorities'
import PriorityLabel from './partials/priorityLabel' import PriorityLabel from './partials/priorityLabel.vue'
import TaskCollectionService from '../../services/taskCollection' import TaskCollectionService from '../../services/taskCollection'
import {mapState} from 'vuex' import {RIGHTS as Rights} from '@/constants/rights'
import Rights from '../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue' import FilterPopup from '@/components/list/partials/filter-popup.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
import {formatDate} from '@/helpers/time/formatDate'
import {useBaseStore} from '@/stores/base'
export default defineComponent({ export default defineComponent({
name: 'GanttChart', name: 'GanttChart',
@ -255,7 +258,7 @@ export default defineComponent({
mounted() { mounted() {
this.buildTheGanttChart() this.buildTheGanttChart()
}, },
computed: mapState({ computed: mapState(useBaseStore, {
canWrite: (state) => state.currentList.maxRight > Rights.READ, canWrite: (state) => state.currentList.maxRight > Rights.READ,
}), }),
methods: { methods: {
@ -275,13 +278,13 @@ export default defineComponent({
prepareGanttDays() { prepareGanttDays() {
console.debug('prepareGanttDays; start date: ', this.startDate, 'end date:', this.endDate) console.debug('prepareGanttDays; start date: ', this.startDate, 'end date:', this.endDate)
// Layout: years => [months => [days]] // Layout: years => [months => [days]]
let years = {} const years = {}
for ( for (
let d = this.startDate; let d = this.startDate;
d <= this.endDate; d <= this.endDate;
d.setDate(d.getDate() + 1) d.setDate(d.getDate() + 1)
) { ) {
let date = new Date(d) const date = new Date(d)
if (years[date.getFullYear() + ''] === undefined) { if (years[date.getFullYear() + ''] === undefined) {
years[date.getFullYear() + ''] = {} years[date.getFullYear() + ''] = {}
} }
@ -350,7 +353,7 @@ export default defineComponent({
const didntHaveDates = newTask.startDate === null ? true : false const didntHaveDates = newTask.startDate === null ? true : false
let startDate = new Date(this.startDate) const startDate = new Date(this.startDate)
startDate.setDate( startDate.setDate(
startDate.getDate() + newRect.left / this.dayWidth, startDate.getDate() + newRect.left / this.dayWidth,
) )
@ -359,7 +362,7 @@ export default defineComponent({
startDate.setUTCSeconds(0) startDate.setUTCSeconds(0)
startDate.setUTCMilliseconds(0) startDate.setUTCMilliseconds(0)
newTask.startDate = startDate newTask.startDate = startDate
let endDate = new Date(startDate) const endDate = new Date(startDate)
endDate.setDate( endDate.setDate(
startDate.getDate() + newRect.width / this.dayWidth, startDate.getDate() + newRect.width / this.dayWidth,
) )
@ -427,7 +430,7 @@ export default defineComponent({
if (!this.newTaskFieldActive) { if (!this.newTaskFieldActive) {
return return
} }
let task = new TaskModel({ const task = new TaskModel({
title: this.newTaskTitle, title: this.newTaskTitle,
listId: this.listId, listId: this.listId,
}) })
@ -439,7 +442,7 @@ export default defineComponent({
formatMonthAndYear(year, month) { formatMonthAndYear(year, month) {
month = month < 10 ? '0' + month : month month = month < 10 ? '0' + month : month
const date = new Date(`${year}-${month}-01`) const date = new Date(`${year}-${month}-01`)
return this.format(date, 'MMMM, yyyy') return formatDate(date, 'MMMM, yyyy')
}, },
}, },
}) })

View file

@ -8,19 +8,19 @@
</h3> </h3>
<input <input
:disabled="attachmentService.loading || undefined" v-if="editEnabled"
:disabled="loading || undefined"
@change="uploadNewAttachment()" @change="uploadNewAttachment()"
id="files" id="files"
multiple multiple
ref="files" ref="filesRef"
type="file" type="file"
v-if="editEnabled"
/> />
<progress <progress
v-if="attachmentService.uploadProgress > 0"
:value="attachmentService.uploadProgress" :value="attachmentService.uploadProgress"
class="progress is-primary" class="progress is-primary"
max="100" max="100"
v-if="attachmentService.uploadProgress > 0"
> >
{{ attachmentService.uploadProgress }}% {{ attachmentService.uploadProgress }}%
</progress> </progress>
@ -35,21 +35,29 @@
:key="a.id" :key="a.id"
@click="viewOrDownload(a)" @click="viewOrDownload(a)"
> >
<div class="filename">{{ a.file.name }}</div> <div class="filename">
{{ a.file.name }}
<span
v-if="task.coverImageAttachmentId === a.id"
class="is-task-cover"
>
{{ $t('task.attachment.usedAsCover') }}
</span>
</div>
<div class="info"> <div class="info">
<p class="attachment-info-meta"> <p class="attachment-info-meta">
<i18n-t keypath="task.attachment.createdBy"> <i18n-t keypath="task.attachment.createdBy" scope="global">
<span v-tooltip="formatDate(a.created)"> <span v-tooltip="formatDateLong(a.created)">
{{ formatDateSince(a.created) }} {{ formatDateSince(a.created) }}
</span> </span>
<user <User
:avatar-size="24" :avatar-size="24"
:user="a.createdBy" :user="a.createdBy"
:is-inline="true" :is-inline="true"
/> />
</i18n-t> </i18n-t>
<span> <span>
{{ a.file.getHumanSize() }} {{ getHumanSize(a.file.size) }}
</span> </span>
<span v-if="a.file.mime"> <span v-if="a.file.mime">
{{ a.file.mime }} {{ a.file.mime }}
@ -73,11 +81,22 @@
<BaseButton <BaseButton
v-if="editEnabled" v-if="editEnabled"
class="attachment-info-meta-button" class="attachment-info-meta-button"
@click.prevent.stop="() => {attachmentToDelete = a; showDeleteModal = true}" @click.prevent.stop="setAttachmentToDelete(a)"
v-tooltip="$t('task.attachment.deleteTooltip')" v-tooltip="$t('task.attachment.deleteTooltip')"
> >
{{ $t('misc.delete') }} {{ $t('misc.delete') }}
</BaseButton> </BaseButton>
<BaseButton
v-if="editEnabled"
class="attachment-info-meta-button"
@click.prevent.stop="setCoverImage(task.coverImageAttachmentId === a.id ? null : a)"
>
{{
task.coverImageAttachmentId === a.id
? $t('task.attachment.unsetAsCover')
: $t('task.attachment.setAsCover')
}}
</BaseButton>
</p> </p>
</div> </div>
</a> </a>
@ -85,8 +104,8 @@
<x-button <x-button
v-if="editEnabled" v-if="editEnabled"
:disabled="attachmentService.loading" :disabled="loading"
@click="$refs.files.click()" @click="filesRef?.click()"
class="mb-4" class="mb-4"
icon="cloud-upload-alt" icon="cloud-upload-alt"
variant="secondary" variant="secondary"
@ -97,7 +116,7 @@
<!-- Dropzone --> <!-- Dropzone -->
<div <div
:class="{ hidden: !showDropzone }" :class="{ hidden: !isOverDropZone }"
class="dropzone" class="dropzone"
v-if="editEnabled" v-if="editEnabled"
> >
@ -110,13 +129,14 @@
</div> </div>
<!-- Delete modal --> <!-- Delete modal -->
<transition name="modal">
<modal <modal
@close="showDeleteModal = false" v-if="attachmentToDelete !== null"
v-if="showDeleteModal" @close="setAttachmentToDelete(null)"
@submit="deleteAttachment()" @submit="deleteAttachment()"
> >
<template #header><span>{{ $t('task.attachment.delete') }}</span></template> <template #header>
<span>{{ $t('task.attachment.delete') }}</span>
</template>
<template #text> <template #text>
<p> <p>
@ -125,150 +145,130 @@
</p> </p>
</template> </template>
</modal> </modal>
</transition>
<transition name="modal"> <!-- Attachment image modal -->
<modal <modal
@close=" v-if="attachmentImageBlobUrl !== null"
() => { @close="attachmentImageBlobUrl = null"
showImageModal = false
attachmentImageBlobUrl = null
}
"
v-if="showImageModal"
> >
<img :src="attachmentImageBlobUrl" alt=""/> <img :src="attachmentImageBlobUrl" alt=""/>
</modal> </modal>
</transition>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {ref, shallowReactive, computed} from 'vue'
import {useDropZone} from '@vueuse/core'
import AttachmentService from '../../../services/attachment' import User from '@/components/misc/user.vue'
import AttachmentModel from '../../../models/attachment' import BaseButton from '@/components/base/BaseButton.vue'
import User from '../../misc/user'
import {mapState} from 'vuex'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard' import AttachmentService from '@/services/attachment'
import { uploadFiles, generateAttachmentUrl } from '@/helpers/attachments' 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'
import BaseButton from '@/components/base/BaseButton' import {useAttachmentStore} from '@/stores/attachments'
import {formatDateSince, formatDateLong} from '@/helpers/time/formatDate'
import {uploadFiles, generateAttachmentUrl} from '@/helpers/attachments'
import {getHumanSize} from '@/helpers/getHumanSize'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import {useI18n} from 'vue-i18n'
export default defineComponent({ const taskStore = useTaskStore()
name: 'attachments', const {t} = useI18n({useScope: 'global'})
components: {
BaseButton,
User,
},
data() {
return {
attachmentService: new AttachmentService(),
showDropzone: false,
showDeleteModal: false, const props = withDefaults(defineProps<{
attachmentToDelete: AttachmentModel, task: ITask,
initialAttachments?: IAttachment[],
editEnabled: boolean,
}>(), {
editEnabled: true,
})
showImageModal: false, // FIXME: this should go through the store
attachmentImageBlobUrl: null, const emit = defineEmits(['task-changed'])
const attachmentService = shallowReactive(new AttachmentService())
const attachmentStore = useAttachmentStore()
const attachments = computed(() => attachmentStore.attachments)
const loading = computed(() => attachmentService.loading || taskStore.isLoading)
function onDrop(files: File[] | null) {
if (files && files.length !== 0) {
uploadFilesToTask(files)
} }
}, }
props: {
taskId: {
required: true,
type: Number,
},
initialAttachments: {
type: Array,
},
editEnabled: {
default: true,
},
},
setup(props) { const {isOverDropZone} = useDropZone(document, onDrop)
const copy = useCopyToClipboard()
function copyUrl(attachment: AttachmentModel) { function downloadAttachment(attachment: IAttachment) {
copy(generateAttachmentUrl(props.taskId, attachment.id)) attachmentService.download(attachment)
} }
return { copyUrl } const filesRef = ref<HTMLInputElement | null>(null)
},
computed: mapState({ function uploadNewAttachment() {
attachments: (state) => state.attachments.attachments, const files = filesRef.value?.files
}),
mounted() {
document.addEventListener('dragenter', (e) => {
e.stopPropagation()
e.preventDefault()
this.showDropzone = true
})
window.addEventListener('dragleave', (e) => { if (!files || files.length === 0) {
e.stopPropagation() return
e.preventDefault() }
this.showDropzone = false
}) uploadFilesToTask(files)
}
document.addEventListener('dragover', (e) => {
e.stopPropagation() function uploadFilesToTask(files: File[] | FileList) {
e.preventDefault() uploadFiles(attachmentService, props.task.id, files)
this.showDropzone = true }
})
const attachmentToDelete = ref<AttachmentModel | null>(null)
document.addEventListener('drop', (e) => {
e.stopPropagation() function setAttachmentToDelete(attachment: AttachmentModel | null) {
e.preventDefault() attachmentToDelete.value = attachment
}
let files = e.dataTransfer.files
this.uploadFiles(files) async function deleteAttachment() {
this.showDropzone = false if (attachmentToDelete.value === null) {
})
},
methods: {
downloadAttachment(attachment) {
this.attachmentService.download(attachment)
},
uploadNewAttachment() {
if (this.$refs.files.files.length === 0) {
return return
} }
this.uploadFiles(this.$refs.files.files)
},
uploadFiles(files) {
uploadFiles(this.attachmentService, this.taskId, files)
},
async deleteAttachment() {
try { try {
const r = await this.attachmentService.delete(this.attachmentToDelete) const r = await attachmentService.delete(attachmentToDelete.value)
this.$store.commit( attachmentStore.removeById(attachmentToDelete.value.id)
'attachments/removeById', success(r)
this.attachmentToDelete.id, setAttachmentToDelete(null)
) } catch (e) {
this.$message.success(r) error(e)
} finally{
this.showDeleteModal = false
} }
}, }
async viewOrDownload(attachment) {
if ( const attachmentImageBlobUrl = ref<string | null>(null)
attachment.file.name.endsWith('.jpg') ||
attachment.file.name.endsWith('.png') || async function viewOrDownload(attachment: AttachmentModel) {
attachment.file.name.endsWith('.bmp') || if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
attachment.file.name.endsWith('.gif') attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
) {
this.showImageModal = true
this.attachmentImageBlobUrl = await this.attachmentService.getBlobUrl(attachment)
} else { } else {
this.downloadAttachment(attachment) downloadAttachment(attachment)
} }
}, }
},
}) const copy = useCopyToClipboard()
function copyUrl(attachment: IAttachment) {
copy(generateAttachmentUrl(props.task.id, attachment.id))
}
async function setCoverImage(attachment: IAttachment | null) {
const task = await taskStore.setCoverImage(props.task, attachment)
emit('task-changed', task)
success({message: t('task.attachment.successfullyChangedCoverImage')})
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -277,10 +277,18 @@ export default defineComponent({
display: none; display: none;
} }
.files { @media screen and (max-width: $tablet) {
margin-bottom: 1rem; .button {
width: 100%;
}
}
}
.attachment { .files {
margin-bottom: 1rem;
}
.attachment {
margin-bottom: .5rem; margin-bottom: .5rem;
display: block; display: block;
transition: background-color $transition; transition: background-color $transition;
@ -290,14 +298,15 @@ export default defineComponent({
&:hover { &:hover {
background-color: var(--grey-200); background-color: var(--grey-200);
} }
}
.filename { .filename {
font-weight: bold; font-weight: bold;
margin-bottom: .25rem; margin-bottom: .25rem;
color: var(--text); color: var(--text);
} }
.info { .info {
color: var(--grey-500); color: var(--grey-500);
font-size: .9rem; font-size: .9rem;
@ -311,17 +320,9 @@ export default defineComponent({
padding: 0 .25rem; padding: 0 .25rem;
} }
} }
} }
}
}
@media screen and (max-width: $tablet) { .dropzone {
.button {
width: 100%;
}
}
.dropzone {
position: fixed; position: fixed;
background: rgba(250, 250, 250, 0.8); background: rgba(250, 250, 250, 0.8);
top: 0; top: 0;
@ -364,7 +365,6 @@ export default defineComponent({
max-width: 300px; max-width: 300px;
} }
} }
}
} }
.attachment-info-meta { .attachment-info-meta {
@ -426,5 +426,13 @@ export default defineComponent({
} }
} }
.is-task-cover {
background: var(--primary);
color: var(--white);
padding: .25rem .35rem;
border-radius: 4px;
font-size: .75rem;
}
@include modal-transition(); @include modal-transition();
</style> </style>

View file

@ -10,15 +10,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {computed} from 'vue' import {computed, type PropType} from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import {getChecklistStatistics} from '@/helpers/checklistFromText' import {getChecklistStatistics} from '@/helpers/checklistFromText'
import TaskModel from '@/models/task' import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({ const props = defineProps({
task: { task: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
}) })

View file

@ -17,7 +17,7 @@
<div :key="c.id" class="media comment" v-for="c in comments"> <div :key="c.id" class="media comment" v-for="c in comments">
<figure class="media-left is-hidden-mobile"> <figure class="media-left is-hidden-mobile">
<img <img
:src="c.author.getAvatarUrl(48)" :src="getAvatarUrl(c.author, 48)"
alt="" alt=""
class="image is-avatar" class="image is-avatar"
height="48" height="48"
@ -27,19 +27,19 @@
<div class="media-content"> <div class="media-content">
<div class="comment-info"> <div class="comment-info">
<img <img
:src="c.author.getAvatarUrl(20)" :src="getAvatarUrl(c.author, 20)"
alt="" alt=""
class="image is-avatar d-print-none" class="image is-avatar d-print-none"
height="20" height="20"
width="20" width="20"
/> />
<strong>{{ c.author.getDisplayName() }}</strong>&nbsp; <strong>{{ getDisplayName(c.author) }}</strong>&nbsp;
<span v-tooltip="formatDate(c.created)" class="has-text-grey"> <span v-tooltip="formatDateLong(c.created)" class="has-text-grey">
{{ formatDateSince(c.created) }} {{ formatDateSince(c.created) }}
</span> </span>
<span <span
v-if="+new Date(c.created) !== +new Date(c.updated)" v-if="+new Date(c.created) !== +new Date(c.updated)"
v-tooltip="formatDate(c.updated)" v-tooltip="formatDateLong(c.updated)"
> >
· {{ $t('task.comment.edited', {date: formatDateSince(c.updated)}) }} · {{ $t('task.comment.edited', {date: formatDateSince(c.updated)}) }}
</span> </span>
@ -70,13 +70,13 @@
:is-edit-enabled="canWrite && c.author.id === currentUserId" :is-edit-enabled="canWrite && c.author.id === currentUserId"
:upload-callback="attachmentUpload" :upload-callback="attachmentUpload"
:upload-enabled="true" :upload-enabled="true"
@change=" v-model="c.comment"
@update:model-value="
() => { () => {
toggleEdit(c) toggleEdit(c)
editComment() editComment()
} }
" "
v-model="c.comment"
:bottom-actions="actions[c.id]" :bottom-actions="actions[c.id]"
:show-save="true" :show-save="true"
/> />
@ -153,15 +153,22 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, computed, shallowReactive, watch, nextTick} from 'vue' import {ref, reactive, computed, shallowReactive, watch, nextTick} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import TaskCommentService from '@/services/taskComment' import TaskCommentService from '@/services/taskComment'
import TaskCommentModel from '@/models/taskComment' import TaskCommentModel from '@/models/taskComment'
import type {ITaskComment} from '@/modelTypes/ITaskComment'
import type {ITask} from '@/modelTypes/ITask'
import {uploadFile} from '@/helpers/attachments' import {uploadFile} from '@/helpers/attachments'
import {success} from '@/message' import {success} from '@/message'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import {getAvatarUrl, getDisplayName} from '@/models/user'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -174,9 +181,10 @@ const props = defineProps({
}) })
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore() const configStore = useConfigStore()
const authStore = useAuthStore()
const comments = ref<TaskCommentModel[]>([]) const comments = ref<ITaskComment[]>([])
const showDeleteModal = ref(false) const showDeleteModal = ref(false)
const commentToDelete = reactive(new TaskCommentModel()) const commentToDelete = reactive(new TaskCommentModel())
@ -186,12 +194,12 @@ const commentEdit = reactive(new TaskCommentModel())
const newComment = reactive(new TaskCommentModel()) const newComment = reactive(new TaskCommentModel())
const saved = ref(null) const saved = ref<ITask['id'] | null>(null)
const saving = ref(null) const saving = ref<ITask['id'] | null>(null)
const userAvatar = computed(() => store.state.auth.info.getAvatarUrl(48)) const userAvatar = computed(() => getAvatarUrl(authStore.info, 48))
const currentUserId = computed(() => store.state.auth.info.id) const currentUserId = computed(() => authStore.info.id)
const enabled = computed(() => store.state.config.taskCommentsEnabled) const enabled = computed(() => configStore.taskCommentsEnabled)
const actions = computed(() => { const actions = computed(() => {
if (!props.canWrite) { if (!props.canWrite) {
return {} return {}
@ -207,13 +215,18 @@ const actions = computed(() => {
]))) ])))
}) })
function attachmentUpload(...args) { function attachmentUpload(
return uploadFile(props.taskId, ...args) file: File,
onSuccess: (url: string) => void,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
onError: (error: string) => void,
) {
return uploadFile(props.taskId, file, onSuccess)
} }
const taskCommentService = shallowReactive(new TaskCommentService()) const taskCommentService = shallowReactive(new TaskCommentService())
async function loadComments(taskId) { async function loadComments(taskId: ITask['id']) {
if (!enabled.value) { if (!enabled.value) {
return return
} }
@ -257,12 +270,12 @@ async function addComment() {
} }
} }
function toggleEdit(comment: TaskCommentModel) { function toggleEdit(comment: ITaskComment) {
isCommentEdit.value = !isCommentEdit.value isCommentEdit.value = !isCommentEdit.value
Object.assign(commentEdit, comment) Object.assign(commentEdit, comment)
} }
function toggleDelete(commentId) { function toggleDelete(commentId: ITaskComment['id']) {
showDeleteModal.value = !showDeleteModal.value showDeleteModal.value = !showDeleteModal.value
commentToDelete.id = commentId commentToDelete.id = commentId
} }
@ -292,7 +305,7 @@ async function editComment() {
} }
} }
async function deleteComment(commentToDelete: TaskCommentModel) { async function deleteComment(commentToDelete: ITaskComment) {
try { try {
await taskCommentService.delete(commentToDelete) await taskCommentService.delete(commentToDelete)
const index = comments.value.findIndex(({id}) => id === commentToDelete.id) const index = comments.value.findIndex(({id}) => id === commentToDelete.id)

View file

@ -1,16 +1,16 @@
<template> <template>
<p class="created"> <p class="created">
<time :datetime="formatISO(task.created)" v-tooltip="formatDate(task.created)"> <time :datetime="formatISO(task.created)" v-tooltip="formatDateLong(task.created)">
<i18n-t keypath="task.detail.created"> <i18n-t keypath="task.detail.created" scope="global">
<span>{{ formatDateSince(task.created) }}</span> <span>{{ formatDateSince(task.created) }}</span>
{{ task.createdBy.getDisplayName() }} {{ getDisplayName(task.createdBy) }}
</i18n-t> </i18n-t>
</time> </time>
<template v-if="+new Date(task.created) !== +new Date(task.updated)"> <template v-if="+new Date(task.created) !== +new Date(task.updated)">
<br/> <br/>
<!-- Computed properties to show the actual date every time it gets updated --> <!-- Computed properties to show the actual date every time it gets updated -->
<time :datetime="formatISO(task.updated)" v-tooltip="updatedFormatted"> <time :datetime="formatISO(task.updated)" v-tooltip="updatedFormatted">
<i18n-t keypath="task.detail.updated"> <i18n-t keypath="task.detail.updated" scope="global">
<span>{{ updatedSince }}</span> <span>{{ updatedSince }}</span>
</i18n-t> </i18n-t>
</time> </time>
@ -18,7 +18,7 @@
<template v-if="task.done"> <template v-if="task.done">
<br/> <br/>
<time :datetime="formatISO(task.doneAt)" v-tooltip="doneFormatted"> <time :datetime="formatISO(task.doneAt)" v-tooltip="doneFormatted">
<i18n-t keypath="task.detail.doneAt"> <i18n-t keypath="task.detail.doneAt" scope="global">
<span>{{ doneSince }}</span> <span>{{ doneSince }}</span>
</i18n-t> </i18n-t>
</time> </time>
@ -27,13 +27,14 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, toRefs} from 'vue' import {computed, toRefs, type PropType} from 'vue'
import TaskModel from '@/models/task' import type {ITask} from '@/modelTypes/ITask'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate' import {formatISO, formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import {getDisplayName} from '@/models/user'
const props = defineProps({ const props = defineProps({
task: { task: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
}) })

View file

@ -1,12 +1,14 @@
<template> <template>
<td v-tooltip="+date === 0 ? '' : formatDate(date)"> <td v-tooltip="+date === 0 ? '' : formatDateLong(date)">
<time :datetime="date ? formatISO(date) : null"> <time :datetime="date ? formatISO(date) : undefined">
{{ +date === 0 ? '-' : formatDateSince(date) }} {{ +date === 0 ? '-' : formatDateSince(date) }}
</time> </time>
</td> </td>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {formatISO, formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
defineProps({ defineProps({
date: { date: {
type: Date, type: Date,

View file

@ -38,37 +38,37 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount} from 'vue' import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount, toRef, type PropType} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component' import flatPickr from 'vue-flatpickr-component'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TaskModel from '@/models/task' import type {ITask} from '@/modelTypes/ITask'
import {useAuthStore} from '@/stores/auth'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const store = useStore() const authStore = useAuthStore()
const taskService = shallowReactive(new TaskService()) const taskService = shallowReactive(new TaskService())
const task = ref<TaskModel>() const task = ref<ITask>()
// We're saving the due date seperately to prevent null errors in very short periods where the task is null. // 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 dueDate = ref<Date>()
const lastValue = ref<Date>() const lastValue = ref<Date>()
const changeInterval = ref<number>() const changeInterval = ref<ReturnType<typeof setInterval>>()
watch( watch(
() => props.modelValue, toRef(props, 'modelValue'),
(value) => { (value) => {
task.value = value task.value = { ...value }
dueDate.value = value.dueDate dueDate.value = value.dueDate
lastValue.value = value.dueDate lastValue.value = value.dueDate
}, },
@ -103,7 +103,7 @@ const flatPickerConfig = computed(() => ({
time_24hr: true, time_24hr: true,
inline: true, inline: true,
locale: { locale: {
firstDayOfWeek: store.state.auth.settings.weekStart, firstDayOfWeek: authStore.settings.weekStart,
}, },
})) }))
@ -123,9 +123,10 @@ async function updateDueDate() {
return return
} }
// FIXME: direct prop manipulation const newTask = await taskService.update({
task.value.dueDate = new Date(dueDate.value) ...task.value,
const newTask = await taskService.update(task.value) dueDate: new Date(dueDate.value),
})
lastValue.value = newTask.dueDate lastValue.value = newTask.dueDate
task.value = newTask task.value = newTask
emit('update:modelValue', newTask) emit('update:modelValue', newTask)

View file

@ -20,47 +20,50 @@
:is-edit-enabled="canWrite" :is-edit-enabled="canWrite"
:upload-callback="attachmentUpload" :upload-callback="attachmentUpload"
:upload-enabled="true" :upload-enabled="true"
@change="save"
:placeholder="$t('task.description.placeholder')" :placeholder="$t('task.description.placeholder')"
:empty-text="$t('task.description.empty')" :empty-text="$t('task.description.empty')"
:show-save="true" :show-save="true"
edit-shortcut="e"
v-model="task.description" v-model="task.description"
@update:model-value="save"
/> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref,computed, watch} from 'vue' import {ref,computed, watch, type PropType} from 'vue'
import {useStore} from 'vuex'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks'
import TaskModel from '@/models/task' import TaskModel from '@/models/task'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
attachmentUpload: { attachmentUpload: {
required: true, required: true,
}, },
canWrite: { canWrite: {
type: Boolean,
required: true, required: true,
}, },
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const task = ref<TaskModel>({description: ''}) const task = ref<ITask>(new TaskModel())
const saved = ref(false) const saved = ref(false)
// Since loading is global state, this variable ensures we're only showing the saving icon when saving the description. // Since loading is global state, this variable ensures we're only showing the saving icon when saving the description.
const saving = ref(false) const saving = ref(false)
const store = useStore() const taskStore = useTaskStore()
const loading = computed(() => store.state.loading) const loading = computed(() => taskStore.isLoading)
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -75,7 +78,7 @@ async function save() {
try { try {
// FIXME: don't update state from internal. // FIXME: don't update state from internal.
task.value = await store.dispatch('tasks/update', task.value) task.value = await taskStore.update(task.value)
emit('update:modelValue', task.value) emit('update:modelValue', task.value)
saved.value = true saved.value = true

View file

@ -10,7 +10,7 @@
@search="findUser" @search="findUser"
:search-results="foundUsers" :search-results="foundUsers"
@select="addAssignee" @select="addAssignee"
label="username" label="name"
:select-placeholder="$t('task.assignee.selectPlaceholder')" :select-placeholder="$t('task.assignee.selectPlaceholder')"
v-model="assignees" v-model="assignees"
ref="multiselect" ref="multiselect"
@ -28,8 +28,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, watch, PropType} from 'vue' import {ref, shallowReactive, watch, nextTick, type PropType} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
@ -37,9 +36,11 @@ import Multiselect from '@/components/input/multiselect.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {includesById} from '@/helpers/utils' import {includesById} from '@/helpers/utils'
import UserModel from '@/models/user'
import ListUserService from '@/services/listUsers' import ListUserService from '@/services/listUsers'
import {success} from '@/message' import {success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import type {IUser} from '@/modelTypes/IUser'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -54,18 +55,19 @@ const props = defineProps({
default: false, default: false,
}, },
modelValue: { modelValue: {
type: Array as PropType<UserModel[]>, type: Array as PropType<IUser[]>,
default: () => [], default: () => [],
}, },
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const store = useStore() const taskStore = useTaskStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const listUserService = shallowReactive(new ListUserService()) const listUserService = shallowReactive(new ListUserService())
const foundUsers = ref([]) const foundUsers = ref([])
const assignees = ref<UserModel[]>([]) const assignees = ref<IUser[]>([])
let isAdding = false
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -78,14 +80,24 @@ watch(
}, },
) )
async function addAssignee(user: UserModel) { async function addAssignee(user: IUser) {
await store.dispatch('tasks/addAssignee', {user: user, taskId: props.taskId}) if (isAdding) {
return
}
try {
nextTick(() => isAdding = true)
await taskStore.addAssignee({user: user, taskId: props.taskId})
emit('update:modelValue', assignees.value) emit('update:modelValue', assignees.value)
success({message: t('task.assignee.assignSuccess')}) success({message: t('task.assignee.assignSuccess')})
} finally {
nextTick(() => isAdding = false)
}
} }
async function removeAssignee(user: UserModel) { async function removeAssignee(user: IUser) {
await store.dispatch('tasks/removeAssignee', {user: user, taskId: props.taskId}) await taskStore.removeAssignee({user: user, taskId: props.taskId})
// Remove the assignee from the list // Remove the assignee from the list
for (const a in assignees.value) { for (const a in assignees.value) {
@ -106,6 +118,11 @@ async function findUser(query: string) {
// Filter the results to not include users who are already assigned // Filter the results to not include users who are already assigned
foundUsers.value = response.filter(({id}) => !includesById(assignees.value, id)) foundUsers.value = response.filter(({id}) => !includesById(assignees.value, id))
.map(u => {
// Users may not have a display name set, so we fall back on the username in that case
u.name = u.name === '' ? u.username : u.name
return u
})
} }
function clearAllFoundUsers() { function clearAllFoundUsers() {
@ -113,6 +130,7 @@ function clearAllFoundUsers() {
} }
const multiselect = ref() const multiselect = ref()
function focus() { function focus() {
multiselect.value.focus() multiselect.value.focus()
} }

View file

@ -39,8 +39,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {PropType, ref, computed, shallowReactive, watch} from 'vue' import {type PropType, ref, computed, shallowReactive, watch} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import LabelModel from '@/models/label' import LabelModel from '@/models/label'
@ -49,10 +48,13 @@ import {success} from '@/message'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import type {ILabel} from '@/modelTypes/ILabel'
import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Array as PropType<LabelModel[]>, type: Array as PropType<ILabel[]>,
default: () => [], default: () => [],
}, },
taskId: { taskId: {
@ -65,13 +67,12 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue'])
const store = useStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const labelTaskService = shallowReactive(new LabelTaskService()) const labelTaskService = shallowReactive(new LabelTaskService())
const labels = ref<LabelModel[]>([]) const labels = ref<ILabel[]>([])
const query = ref('') const query = ref('')
watch( watch(
@ -85,43 +86,40 @@ watch(
}, },
) )
const foundLabels = computed(() => store.getters['labels/filterLabelsByQuery'](labels.value, query.value)) const taskStore = useTaskStore()
const loading = computed(() => labelTaskService.loading || (store.state.loading && store.state.loadingModule === 'labels')) const labelStore = useLabelStore()
const foundLabels = computed(() => labelStore.filterLabelsByQuery(labels.value, query.value))
const loading = computed(() => labelTaskService.loading || labelStore.isLoading)
function findLabel(newQuery: string) { function findLabel(newQuery: string) {
query.value = newQuery query.value = newQuery
} }
async function addLabel(label: LabelModel, showNotification = true) { async function addLabel(label: ILabel, showNotification = true) {
const bubble = () => {
emit('update:modelValue', labels.value)
emit('change', labels.value)
}
if (props.taskId === 0) { if (props.taskId === 0) {
bubble() emit('update:modelValue', labels.value)
return return
} }
await store.dispatch('tasks/addLabel', {label, taskId: props.taskId}) await taskStore.addLabel({label, taskId: props.taskId})
bubble() emit('update:modelValue', labels.value)
if (showNotification) { if (showNotification) {
success({message: t('task.label.addSuccess')}) success({message: t('task.label.addSuccess')})
} }
} }
async function removeLabel(label: LabelModel) { async function removeLabel(label: ILabel) {
if (props.taskId !== 0) { if (props.taskId !== 0) {
await store.dispatch('tasks/removeLabel', {label, taskId: props.taskId}) await taskStore.removeLabel({label, taskId: props.taskId})
} }
for (const l in labels.value) { for (const l in labels.value) {
if (labels.value[l].id === label.id) { if (labels.value[l].id === label.id) {
labels.value.splice(l, 1) labels.value.splice(l, 1) // FIXME: l should be index
} }
} }
emit('update:modelValue', labels.value) emit('update:modelValue', labels.value)
emit('change', labels.value)
success({message: t('task.label.removeSuccess')}) success({message: t('task.label.removeSuccess')})
} }
@ -130,7 +128,8 @@ async function createAndAddLabel(title: string) {
return return
} }
const newLabel = await store.dispatch('labels/createLabel', new LabelModel({title})) const labelStore = useLabelStore()
const newLabel = await labelStore.createLabel(new LabelModel({title}))
addLabel(newLabel, false) addLabel(newLabel, false)
labels.value.push(newLabel) labels.value.push(newLabel)
success({message: t('task.label.addCreateSuccess')}) success({message: t('task.label.addCreateSuccess')})

View file

@ -1,7 +1,12 @@
<template> <template>
<div class="heading"> <div class="heading">
<BaseButton @click="copyUrl"><h1 class="title task-id">{{ textIdentifier }}</h1></BaseButton> <BaseButton @click="copyUrl"><h1 class="title task-id">{{ textIdentifier }}</h1></BaseButton>
<Done class="heading__done" :is-done="task.done" /> <Done class="heading__done" :is-done="task.done"/>
<ColorBubble
v-if="task.hexColor !== ''"
:color="task.getHexColor()"
class="mt-1 ml-2"
/>
<h1 <h1
class="title input" class="title input"
:class="{'disabled': !canWrite}" :class="{'disabled': !canWrite}"
@ -32,18 +37,21 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed} from 'vue' import {ref, computed, type PropType} from 'vue'
import {useStore} from 'vuex' import {useRouter} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ColorBubble from '@/components/misc/colorBubble.vue'
import Done from '@/components/misc/Done.vue' import Done from '@/components/misc/Done.vue'
import TaskModel from '@/models/task'
import { useRouter } from 'vue-router' import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard' import {useTaskStore} from '@/stores/tasks'
import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({ const props = defineProps({
task: { task: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
canWrite: { canWrite: {
@ -56,15 +64,16 @@ const emit = defineEmits(['update:task'])
const router = useRouter() const router = useRouter()
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
async function copyUrl() { async function copyUrl() {
const route = router.resolve({ name: 'task.detail', query: { taskId: props.task.id}}) const route = router.resolve({name: 'task.detail', query: {taskId: props.task.id}})
const absoluteURL = new URL(route.href, window.location.href).href const absoluteURL = new URL(route.href, window.location.href).href
await copy(absoluteURL) await copy(absoluteURL)
} }
const store = useStore() const taskStore = useTaskStore()
const loading = computed(() => store.state.loading) const loading = computed(() => taskStore.isLoading)
const textIdentifier = computed(() => props.task?.getTextIdentifier() || '') const textIdentifier = computed(() => props.task?.getTextIdentifier() || '')
@ -84,7 +93,7 @@ async function save(title: string) {
try { try {
saving.value = true saving.value = true
const newTask = await store.dispatch('tasks/update', { const newTask = await taskStore.update({
...props.task, ...props.task,
title, title,
}) })
@ -93,8 +102,7 @@ async function save(title: string) {
setTimeout(() => { setTimeout(() => {
showSavedMessage.value = false showSavedMessage.value = false
}, 2000) }, 2000)
} } finally {
finally {
saving.value = false saving.value = false
} }
} }
@ -104,4 +112,9 @@ async function save(title: string) {
.heading__done { .heading__done {
margin-left: .5rem; margin-left: .5rem;
} }
.color-bubble {
height: .75rem;
width: .75rem;
}
</style> </style>

View file

@ -6,13 +6,20 @@
'draggable': !(loadingInternal || loading), 'draggable': !(loadingInternal || loading),
'has-light-text': color !== TASK_DEFAULT_COLOR && !colorIsDark(color), 'has-light-text': color !== TASK_DEFAULT_COLOR && !colorIsDark(color),
}" }"
:style="{'background-color': color !== TASK_DEFAULT_COLOR ? color : false}" :style="{'background-color': color !== TASK_DEFAULT_COLOR ? color : undefined}"
@click.exact="openTaskDetail()" @click.exact="openTaskDetail()"
@click.ctrl="() => toggleTaskDone(task)" @click.ctrl="() => toggleTaskDone(task)"
@click.meta="() => toggleTaskDone(task)" @click.meta="() => toggleTaskDone(task)"
> >
<img
v-if="coverImageBlobUrl"
:src="coverImageBlobUrl"
alt=""
class="cover-image"
/>
<div class="p-2">
<span class="task-id"> <span class="task-id">
<Done class="kanban-card__done" :is-done="task.done" variant="small" /> <Done class="kanban-card__done" :is-done="task.done" variant="small"/>
<template v-if="task.identifier === ''"> <template v-if="task.identifier === ''">
#{{ task.index }} #{{ task.index }}
</template> </template>
@ -24,7 +31,7 @@
:class="{'overdue': task.dueDate <= new Date() && !task.done}" :class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="due-date" class="due-date"
v-if="task.dueDate > 0" v-if="task.dueDate > 0"
v-tooltip="formatDate(task.dueDate)"> v-tooltip="formatDateLong(task.dueDate)">
<span class="icon"> <span class="icon">
<icon :icon="['far', 'calendar-alt']"/> <icon :icon="['far', 'calendar-alt']"/>
</span> </span>
@ -44,11 +51,11 @@
<priority-label :priority="task.priority" :done="task.done"/> <priority-label :priority="task.priority" :done="task.done"/>
<div class="assignees" v-if="task.assignees.length > 0"> <div class="assignees" v-if="task.assignees.length > 0">
<user <user
v-for="u in task.assignees"
:avatar-size="24" :avatar-size="24"
:key="task.id + 'assignee' + u.id" :key="task.id + 'assignee' + u.id"
:show-username="false" :show-username="false"
:user="u" :user="u"
v-for="u in task.assignees"
/> />
</div> </div>
<checklist-summary :task="task"/> <checklist-summary :task="task"/>
@ -63,79 +70,83 @@
</span> </span>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import {defineComponent} from 'vue' import {ref, computed, watch} from 'vue'
import {useRouter} from 'vue-router'
import {playPop} from '../../../helpers/playPop' import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import PriorityLabel from '../../../components/tasks/partials/priorityLabel' import User from '@/components/misc/user.vue'
import User from '../../../components/misc/user'
import Done from '@/components/misc/Done.vue' import Done from '@/components/misc/Done.vue'
import Labels from '../../../components/tasks/partials/labels' import Labels from '@/components/tasks/partials/labels.vue'
import ChecklistSummary from './checklist-summary' import ChecklistSummary from './checklist-summary.vue'
import {TASK_DEFAULT_COLOR} from '@/models/task'
import {TASK_DEFAULT_COLOR, getHexColor} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark' import {colorIsDark} from '@/helpers/color/colorIsDark'
import {useTaskStore} from '@/stores/tasks'
export default defineComponent({ const router = useRouter()
name: 'kanban-card',
components: { const loadingInternal = ref(false)
ChecklistSummary,
Done, const props = withDefaults(defineProps<{
PriorityLabel, task: ITask,
User, loading: boolean,
Labels, }>(), {
}, loading: false,
data() {
return {
loadingInternal: false,
TASK_DEFAULT_COLOR,
}
},
props: {
task: {
required: true,
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
color() {
return this.task.getHexColor
? this.task.getHexColor()
: TASK_DEFAULT_COLOR
},
},
methods: {
colorIsDark,
async toggleTaskDone(task) {
this.loadingInternal = true
try {
const done = !task.done
await this.$store.dispatch('tasks/update', {
...task,
done,
})
if (done) {
playPop()
}
} finally {
this.loadingInternal = false
}
},
openTaskDetail() {
this.$router.push({
name: 'task.detail',
params: { id: this.task.id },
state: { backdropView: this.$router.currentRoute.value.fullPath },
})
},
},
}) })
const color = computed(() => getHexColor(props.task.hexColor))
async function toggleTaskDone(task: ITask) {
loadingInternal.value = true
try {
await useTaskStore().update({
...task,
done: !task.done,
})
} finally {
loadingInternal.value = false
}
}
function openTaskDetail() {
router.push({
name: 'task.detail',
params: {id: props.task.id},
state: {backdropView: router.currentRoute.value.fullPath},
})
}
const coverImageBlobUrl = ref<string | null>(null)
async function maybeDownloadCoverImage() {
if (!props.task.coverImageAttachmentId) {
coverImageBlobUrl.value = null
return
}
const attachment = props.task.attachments.find(a => a.id === props.task.coverImageAttachmentId)
if (!attachment || !SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
return
}
const attachmentService = new AttachmentService()
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
}
watch(
() => props.task.coverImageAttachmentId,
maybeDownloadCoverImage,
{immediate: true},
)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -147,12 +158,11 @@ $task-background: var(--white);
cursor: pointer; cursor: pointer;
box-shadow: var(--shadow-xs); box-shadow: var(--shadow-xs);
display: block; display: block;
border: 3px solid transparent;
font-size: .9rem; font-size: .9rem;
padding: .4rem;
border-radius: $radius; border-radius: $radius;
background: $task-background; background: $task-background;
overflow: hidden;
&.loader-container.is-loading::after { &.loader-container.is-loading::after {
width: 1.5rem; width: 1.5rem;

View file

@ -11,8 +11,12 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type {PropType} from 'vue'
import type {ILabel} from '@/modelTypes/ILabel'
defineProps({ defineProps({
labels: { labels: {
type: Array as PropType<ILabel[]>,
required: true, required: true,
}, },
}) })

View file

@ -1,5 +1,5 @@
<template> <template>
<multiselect <Multiselect
class="control is-expanded" class="control is-expanded"
:placeholder="$t('list.search')" :placeholder="$t('list.search')"
@search="findLists" @search="findLists"
@ -13,32 +13,30 @@
<span class="list-namespace-title search-result">{{ namespace(props.option.namespaceId) }} ></span> <span class="list-namespace-title search-result">{{ namespace(props.option.namespaceId) }} ></span>
{{ props.option.title }} {{ props.option.title }}
</template> </template>
</multiselect> </Multiselect>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {reactive, ref, watch} from 'vue' import {reactive, ref, watch} from 'vue'
import type {PropType} from 'vue' import type {PropType} from 'vue'
import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import ListModel from '@/models/list' import ListModel from '@/models/list'
import type {IList} from '@/modelTypes/IList'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object as PropType<ListModel>, type: Object as PropType<IList>,
validator(value) {
return value instanceof ListModel
},
required: false, required: false,
}, },
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const store = useStore()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
const list = reactive<ListModel>(new ListModel()) const list: IList = reactive(new ListModel())
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -49,21 +47,26 @@ watch(
}, },
) )
const foundLists = ref([]) const listStore = useListStore()
const namespaceStore = useNamespaceStore()
const foundLists = ref<IList[]>([])
function findLists(query: string) { function findLists(query: string) {
if (query === '') { if (query === '') {
select(null) select(null)
} }
foundLists.value = store.getters['lists/searchList'](query) foundLists.value = listStore.searchList(query)
} }
function select(l: ListModel | null) { function select(l: IList | null) {
if (l === null) {
return
}
Object.assign(list, l) Object.assign(list, l)
emit('update:modelValue', list) emit('update:modelValue', list)
} }
function namespace(namespaceId: number) { function namespace(namespaceId: number) {
const namespace = store.getters['namespaces/getNamespaceById'](namespaceId) const namespace = namespaceStore.getNamespaceById(namespaceId)
return namespace !== null return namespace !== null
? namespace.title ? namespace.title
: t('list.shared') : t('list.shared')

View file

@ -32,13 +32,12 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue'])
const percentDone = computed({ const percentDone = computed({
get: () => props.modelValue, get: () => props.modelValue,
set(percentDone) { set(percentDone) {
emit('update:modelValue', percentDone) emit('update:modelValue', percentDone)
emit('change')
}, },
}) })
</script> </script>

View file

@ -21,7 +21,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import priorities from '@/models/constants/priorities' import {PRIORITIES as priorities} from '@/constants/priorities'
defineProps({ defineProps({
priority: { priority: {

View file

@ -5,33 +5,33 @@
@change="updateData" @change="updateData"
:disabled="disabled || undefined" :disabled="disabled || undefined"
> >
<option :value="priorities.UNSET">{{ $t('task.priority.unset') }}</option> <option :value="PRIORITIES.UNSET">{{ $t('task.priority.unset') }}</option>
<option :value="priorities.LOW">{{ $t('task.priority.low') }}</option> <option :value="PRIORITIES.LOW">{{ $t('task.priority.low') }}</option>
<option :value="priorities.MEDIUM">{{ $t('task.priority.medium') }}</option> <option :value="PRIORITIES.MEDIUM">{{ $t('task.priority.medium') }}</option>
<option :value="priorities.HIGH">{{ $t('task.priority.high') }}</option> <option :value="PRIORITIES.HIGH">{{ $t('task.priority.high') }}</option>
<option :value="priorities.URGENT">{{ $t('task.priority.urgent') }}</option> <option :value="PRIORITIES.URGENT">{{ $t('task.priority.urgent') }}</option>
<option :value="priorities.DO_NOW">{{ $t('task.priority.doNow') }}</option> <option :value="PRIORITIES.DO_NOW">{{ $t('task.priority.doNow') }}</option>
</select> </select>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, watch} from 'vue' import {ref, watch} from 'vue'
import priorities from '@/models/constants/priorities.json' import {PRIORITIES} from '@/constants/priorities'
const priority = ref(0)
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
default: 0,
type: Number, type: Number,
default: 0,
}, },
disabled: { disabled: {
default: false, default: false,
}, },
}) })
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue'])
const priority = ref(0)
// FIXME: store value outside // FIXME: store value outside
// Set the priority to the :value every time it changes from the outside // Set the priority to the :value every time it changes from the outside
@ -45,6 +45,5 @@ watch(
function updateData() { function updateData() {
emit('update:modelValue', priority.value) emit('update:modelValue', priority.value)
emit('change')
} }
</script> </script>

View file

@ -25,49 +25,52 @@
</transition> </transition>
</label> </label>
<div class="field" key="field-search"> <div class="field" key="field-search">
<multiselect <Multiselect
:placeholder="$t('task.relation.searchPlaceholder')" :placeholder="$t('task.relation.searchPlaceholder')"
@search="findTasks" @search="findTasks"
:loading="taskService.loading" :loading="taskService.loading"
:search-results="mappedFoundTasks" :search-results="mappedFoundTasks"
label="title" label="title"
v-model="newTaskRelationTask" v-model="newTaskRelation.task"
:creatable="true" :creatable="true"
:create-placeholder="$t('task.relation.createPlaceholder')" :create-placeholder="$t('task.relation.createPlaceholder')"
@create="createAndRelateTask" @create="createAndRelateTask"
@select="addTaskRelation"
> >
<template #searchResult="props"> <template #searchResult="{option: task}">
<span v-if="typeof props.option !== 'string'" class="search-result"> <span
v-if="typeof task !== 'string'"
class="search-result"
:class="{'is-strikethrough': task.done}"
>
<span <span
class="different-list" class="different-list"
v-if="props.option.listId !== listId" v-if="task.listId !== listId"
> >
<span <span
v-if="props.option.differentNamespace !== null" v-if="task.differentNamespace !== null"
v-tooltip="$t('task.relation.differentNamespace')"> v-tooltip="$t('task.relation.differentNamespace')">
{{ props.option.differentNamespace }} > {{ task.differentNamespace }} >
</span> </span>
<span <span
v-if="props.option.differentList !== null" v-if="task.differentList !== null"
v-tooltip="$t('task.relation.differentList')"> v-tooltip="$t('task.relation.differentList')">
{{ props.option.differentList }} > {{ task.differentList }} >
</span> </span>
</span> </span>
{{ props.option.title }} {{ task.title }}
</span> </span>
<span class="search-result" v-else> <span class="search-result" v-else>
{{ props.option }} {{ task }}
</span> </span>
</template> </template>
</multiselect> </Multiselect>
</div> </div>
<div class="field has-addons mb-4" key="field-kind"> <div class="field has-addons mb-4" key="field-kind">
<div class="control is-expanded"> <div class="control is-expanded">
<div class="select is-fullwidth has-defaults"> <div class="select is-fullwidth has-defaults">
<select v-model="newTaskRelationKind"> <select v-model="newTaskRelation.kind">
<option value="unset">{{ $t('task.relation.select') }}</option> <option value="unset">{{ $t('task.relation.select') }}</option>
<option :key="rk" :value="rk" v-for="rk in relationKinds"> <option :key="`option_${rk}`" :value="rk" v-for="rk in RELATION_KINDS">
{{ $tc(`task.relation.kinds.${rk}`, 1) }} {{ $tc(`task.relation.kinds.${rk}`, 1) }}
</option> </option>
</select> </select>
@ -84,9 +87,16 @@
<span class="title">{{ rts.title }}</span> <span class="title">{{ rts.title }}</span>
<div class="tasks"> <div class="tasks">
<div :key="t.id" class="task" v-for="t in rts.tasks"> <div :key="t.id" class="task" v-for="t in rts.tasks">
<div class="is-flex is-align-items-center">
<Fancycheckbox
class="task-done-checkbox"
v-model="t.done"
@update:model-value="toggleTaskDone(t)"
/>
<router-link <router-link
:to="{ name: $route.name, params: { id: t.id } }" :to="{ name: route.name as string, params: { id: t.id } }"
:class="{ 'is-strikethrough': t.done}"> :class="{ 'is-strikethrough': t.done}"
>
<span <span
class="different-list" class="different-list"
v-if="t.listId !== listId" v-if="t.listId !== listId"
@ -104,9 +114,13 @@
</span> </span>
{{ t.title }} {{ t.title }}
</router-link> </router-link>
</div>
<BaseButton <BaseButton
v-if="editEnabled" v-if="editEnabled"
@click="() => {showDeleteModal = true; relationToDelete = {relationKind: rts.kind, otherTaskId: t.id}}" @click="setRelationToDelete({
relationKind: rts.kind,
otherTaskId: t.id
})"
class="remove" class="remove"
> >
<icon icon="trash-alt"/> <icon icon="trash-alt"/>
@ -118,12 +132,10 @@
{{ $t('task.relation.noneYet') }} {{ $t('task.relation.noneYet') }}
</p> </p>
<!-- Delete modal -->
<transition name="modal">
<modal <modal
@close="showDeleteModal = false" v-if="relationToDelete !== undefined"
@close="relationToDelete = undefined"
@submit="removeTaskRelation()" @submit="removeTaskRelation()"
v-if="showDeleteModal"
> >
<template #header><span>{{ $t('task.relation.delete') }}</span></template> <template #header><span>{{ $t('task.relation.delete') }}</span></template>
@ -134,53 +146,40 @@
</p> </p>
</template> </template>
</modal> </modal>
</transition>
</div> </div>
</template> </template>
<script lang="ts"> <script setup lang="ts">
import {defineComponent} from 'vue' import {ref, reactive, shallowReactive, watch, computed, type PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRoute} from 'vue-router'
import TaskService from '../../../services/task' import TaskService from '@/services/task'
import TaskModel from '../../../models/task' import TaskModel from '@/models/task'
import TaskRelationService from '../../../services/taskRelation' import type {ITask} from '@/modelTypes/ITask'
import relationKinds from '../../../models/constants/relationKinds' import type {ITaskRelation} from '@/modelTypes/ITaskRelation'
import TaskRelationModel from '../../../models/taskRelation' import {RELATION_KINDS, RELATION_KIND, type IRelationKind} from '@/types/IRelationKind'
import TaskRelationService from '@/services/taskRelation'
import TaskRelationModel from '@/models/taskRelation'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
export default defineComponent({ import {useNamespaceStore} from '@/stores/namespaces'
name: 'relatedTasks',
data() { import {error, success} from '@/message'
return { import {useTaskStore} from '@/stores/tasks'
relatedTasks: {},
taskService: new TaskService(), const props = defineProps({
foundTasks: [],
relationKinds: relationKinds,
newTaskRelationTask: new TaskModel(),
newTaskRelationKind: 'related',
taskRelationService: new TaskRelationService(),
showDeleteModal: false,
relationToDelete: {},
saved: false,
showNewRelationForm: false,
query: '',
}
},
components: {
BaseButton,
Multiselect,
},
props: {
taskId: { taskId: {
type: Number, type: Number,
required: true, required: true,
}, },
initialRelatedTasks: { initialRelatedTasks: {
type: Object, type: Object as PropType<ITask['relatedTasks']>,
default: () => { default: () => ({}),
},
}, },
showNoRelationsNotice: { showNoRelationsNotice: {
type: Boolean, type: Boolean,
@ -193,129 +192,177 @@ export default defineComponent({
editEnabled: { editEnabled: {
default: true, default: true,
}, },
}, })
watch: {
initialRelatedTasks: {
handler(value) {
this.relatedTasks = value
},
immediate: true,
},
},
computed: {
showCreate() {
return Object.keys(this.relatedTasks).length === 0 || this.showNewRelationForm
},
namespace() {
return this.$store.getters['namespaces/getListAndNamespaceById'](this.listId, true)?.namespace
},
mappedRelatedTasks() {
return Object.entries(this.relatedTasks).map(([kind, tasks]) => ({
title: this.$tc(`task.relation.kinds.${kind}`, tasks.length),
tasks: this.mapRelatedTasks(tasks),
kind,
}))
},
mappedFoundTasks() {
return this.mapRelatedTasks(this.foundTasks.filter(t => t.id !== this.taskId))
},
},
methods: {
async findTasks(query) {
this.query = query
this.foundTasks = await this.taskService.getAll({}, {s: query})
},
async addTaskRelation() { const taskStore = useTaskStore()
if (this.newTaskRelationTask.id === 0 && this.query !== '') { const namespaceStore = useNamespaceStore()
return this.createAndRelateTask(this.query) const route = useRoute()
} const {t} = useI18n({useScope: 'global'})
if (this.newTaskRelationTask.id === 0) { type TaskRelation = {kind: IRelationKind, task: ITask}
this.$message.error({message: this.$t('task.relation.taskRequired')})
return
}
const rel = new TaskRelationModel({ const taskService = shallowReactive(new TaskService())
taskId: this.taskId,
otherTaskId: this.newTaskRelationTask.id, const relatedTasks = ref<ITask['relatedTasks']>({})
relationKind: this.newTaskRelationKind,
}) const newTaskRelation: TaskRelation = reactive({
await this.taskRelationService.create(rel) kind: RELATION_KIND.RELATED,
if (!this.relatedTasks[this.newTaskRelationKind]) { task: new TaskModel(),
this.relatedTasks[this.newTaskRelationKind] = [] })
}
this.relatedTasks[this.newTaskRelationKind].push(this.newTaskRelationTask) watch(
this.newTaskRelationTask = null () => props.initialRelatedTasks,
this.saved = true (value) => {
this.showNewRelationForm = false relatedTasks.value = value
setTimeout(() => {
this.saved = false
}, 2000)
}, },
{immediate: true},
)
async removeTaskRelation() { const showNewRelationForm = ref(false)
const rel = new TaskRelationModel({ const showCreate = computed(() => Object.keys(relatedTasks.value).length === 0 || showNewRelationForm.value)
relationKind: this.relationToDelete.relationKind,
taskId: this.taskId,
otherTaskId: this.relationToDelete.otherTaskId,
})
try {
await this.taskRelationService.delete(rel)
const kind = this.relationToDelete.relationKind const query = ref('')
for (const t in this.relatedTasks[kind]) { const foundTasks = ref<ITask[]>([])
if (this.relatedTasks[kind][t].id === this.relationToDelete.otherTaskId) {
this.relatedTasks[kind].splice(t, 1)
break async function findTasks(newQuery: string) {
} query.value = newQuery
} foundTasks.value = await taskService.getAll({}, {s: newQuery})
}
this.saved = true const getListAndNamespaceById = (listId: number) => namespaceStore.getListAndNamespaceById(listId, true)
setTimeout(() => {
this.saved = false
}, 2000)
} finally {
this.showDeleteModal = false
}
},
async createAndRelateTask(title) { const namespace = computed(() => getListAndNamespaceById(props.listId)?.namespace)
const newTask = new TaskModel({title: title, listId: this.listId})
this.newTaskRelationTask = await this.taskService.create(newTask)
await this.addTaskRelation()
},
relationKindTitle(kind, length) { function mapRelatedTasks(tasks: ITask[]) {
return this.$tc(`task.relation.kinds.${kind}`, length) return tasks.map(task => {
},
mapRelatedTasks(tasks) {
return tasks
.map(task => {
// by doing this here once we can save a lot of duplicate calls in the template // by doing this here once we can save a lot of duplicate calls in the template
const listAndNamespace = this.$store.getters['namespaces/getListAndNamespaceById'](task.listId, true)
const { const {
list, list,
namespace, namespace: taskNamespace,
} = listAndNamespace === null ? {list: null, namespace: null} : listAndNamespace } = getListAndNamespaceById(task.listId) || {list: null, namespace: null}
return { return {
...task, ...task,
differentNamespace: differentNamespace:
(namespace !== null && (taskNamespace !== null &&
namespace.id !== this.namespace.id && taskNamespace.id !== namespace.value.id &&
namespace?.title) || null, taskNamespace?.title) || null,
differentList: differentList:
(list !== null && (list !== null &&
task.listId !== this.listId && task.listId !== props.listId &&
list?.title) || null, list?.title) || null,
} }
}) })
}, }
},
}) const mapRelationKindsTitleGetter = computed(() => ({
'subtask': (count: number) => t('task.relation.kinds.subtask', count),
'parenttask': (count: number) => t('task.relation.kinds.parenttask', count),
'related': (count: number) => t('task.relation.kinds.related', count),
'duplicateof': (count: number) => t('task.relation.kinds.duplicateof', count),
'duplicates': (count: number) => t('task.relation.kinds.duplicates', count),
'blocking': (count: number) => t('task.relation.kinds.blocking', count),
'blocked': (count: number) => t('task.relation.kinds.blocked', count),
'precedes': (count: number) => t('task.relation.kinds.precedes', count),
'follows': (count: number) => t('task.relation.kinds.follows', count),
'copiedfrom': (count: number) => t('task.relation.kinds.copiedfrom', count),
'copiedto': (count: number) => t('task.relation.kinds.copiedto', count),
}))
const mappedRelatedTasks = computed(() => Object.entries(relatedTasks.value).map(
([kind, tasks]) => ({
title: mapRelationKindsTitleGetter.value[kind as IRelationKind](tasks.length),
tasks: mapRelatedTasks(tasks),
kind: kind as IRelationKind,
}),
))
const mappedFoundTasks = computed(() => mapRelatedTasks(foundTasks.value.filter(t => t.id !== props.taskId)))
const taskRelationService = shallowReactive(new TaskRelationService())
const saved = ref(false)
async function addTaskRelation() {
if (newTaskRelation.task.id === 0 && query.value !== '') {
return createAndRelateTask(query.value)
}
if (newTaskRelation.task.id === 0) {
error({message: t('task.relation.taskRequired')})
return
}
await taskRelationService.create(new TaskRelationModel({
taskId: props.taskId,
otherTaskId: newTaskRelation.task.id,
relationKind: newTaskRelation.kind,
}))
relatedTasks.value[newTaskRelation.kind] = [
...(relatedTasks.value[newTaskRelation.kind] || []),
newTaskRelation.task,
]
newTaskRelation.task = new TaskModel()
saved.value = true
showNewRelationForm.value = false
setTimeout(() => {
saved.value = false
}, 2000)
}
const relationToDelete = ref<Partial<ITaskRelation>>()
function setRelationToDelete(relation: Partial<ITaskRelation>) {
relationToDelete.value = relation
}
async function removeTaskRelation() {
const relation = relationToDelete.value
if (!relation || !relation.relationKind || !relation.otherTaskId) {
relationToDelete.value = undefined
return
}
try {
const relationKind = relation.relationKind
await taskRelationService.delete(new TaskRelationModel({
relationKind,
taskId: props.taskId,
otherTaskId: relation.otherTaskId,
}))
relatedTasks.value[relationKind] = relatedTasks.value[relationKind]?.filter(
({id}) => id !== relation.otherTaskId,
)
saved.value = true
setTimeout(() => {
saved.value = false
}, 2000)
} finally {
relationToDelete.value = undefined
}
}
async function createAndRelateTask(title: string) {
const newTask = await taskService.create(new TaskModel({title, listId: props.listId}))
newTaskRelation.task = newTask
await addTaskRelation()
}
async function toggleTaskDone(task: ITask) {
await taskStore.update(task)
// Find the task in the list and update it so that it is correctly strike through
Object.entries(relatedTasks.value).some(([kind, tasks]) => {
return (tasks as ITask[]).some((t, key) => {
const found = t.id === task.id
if (found) {
relatedTasks.value[kind as IRelationKind]![key] = task
}
return found
})
})
success({message: t('task.detail.updateSuccess')})
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -366,15 +413,16 @@ export default defineComponent({
} }
} }
.remove { }
.remove {
text-align: center; text-align: center;
color: var(--danger); color: var(--danger);
opacity: 0; opacity: 0;
transition: opacity $transition; transition: opacity $transition;
}
} }
.related-tasks:hover .tasks .task .remove { .task:hover .remove {
opacity: 1; opacity: 1;
} }
@ -387,5 +435,13 @@ export default defineComponent({
padding: 0.5rem; padding: 0.5rem;
} }
// FIXME: The height of the actual checkbox in the <Fancycheckbox/> component is too much resulting in a
// weired positioning of the checkbox. Setting the height here is a workaround until we fix the styling
// of the component.
.task-done-checkbox {
padding: 0;
height: 18px; // The exact height of the checkbox in the container
}
@include modal-transition(); @include modal-transition();
</style> </style>

View file

@ -26,7 +26,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {PropType, ref, onMounted, watch} from 'vue' import {type PropType, ref, onMounted, watch} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import Datepicker from '@/components/input/datepicker.vue' import Datepicker from '@/components/input/datepicker.vue'
@ -45,8 +45,8 @@ const props = defineProps({
return false return false
} }
const isDate = (e: any) => e instanceof Date const isDate = (e: unknown) => e instanceof Date
const isString = (e: any) => typeof e === 'string' const isString = (e: unknown) => typeof e === 'string'
for (const e of prop) { for (const e of prop) {
if (!isDate(e) && !isString(e)) { if (!isDate(e) && !isString(e)) {
@ -63,7 +63,7 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue'])
const reminders = ref<Reminder[]>([]) const reminders = ref<Reminder[]>([])
@ -86,7 +86,6 @@ watch(
function updateData() { function updateData() {
emit('update:modelValue', reminders.value) emit('update:modelValue', reminders.value)
emit('change')
} }
const newReminder = ref(null) const newReminder = ref(null)

View file

@ -18,17 +18,14 @@
<div class="control"> <div class="control">
<div class="select"> <div class="select">
<select @change="updateData" v-model="task.repeatMode" id="repeatMode"> <select @change="updateData" v-model="task.repeatMode" id="repeatMode">
<option :value="repeatModes.REPEAT_MODE_DEFAULT">{{ $t('misc.default') }}</option> <option :value="TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT">{{ $t('misc.default') }}</option>
<option :value="repeatModes.REPEAT_MODE_MONTH">{{ $t('task.repeat.monthly') }}</option> <option :value="TASK_REPEAT_MODES.REPEAT_MODE_MONTH">{{ $t('task.repeat.monthly') }}</option>
<option :value="repeatModes.REPEAT_MODE_FROM_CURRENT_DATE">{{ <option :value="TASK_REPEAT_MODES.REPEAT_MODE_FROM_CURRENT_DATE">{{ $t('task.repeat.fromCurrentDate') }}</option>
$t('task.repeat.fromCurrentDate')
}}
</option>
</select> </select>
</div> </div>
</div> </div>
</div> </div>
<div class="is-flex" v-if="task.repeatMode !== repeatModes.REPEAT_MODE_MONTH"> <div class="is-flex" v-if="task.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_MONTH">
<p class="pr-4"> <p class="pr-4">
{{ $t('task.repeat.each') }} {{ $t('task.repeat.each') }}
</p> </p>
@ -65,14 +62,18 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, watch} from 'vue' import {ref, reactive, watch, type PropType} from 'vue'
import repeatModes from '@/models/constants/taskRepeatModes.json'
import TaskModel from '@/models/task'
import {error} from '@/message'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {error} from '@/message'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
import type {IRepeatAfter} from '@/types/IRepeatAfter'
import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object as PropType<ITask>,
default: () => ({}), default: () => ({}),
required: true, required: true,
}, },
@ -82,11 +83,11 @@ const props = defineProps({
}, },
}) })
const {t} = useI18n() const {t} = useI18n({useScope: 'global'})
const emit = defineEmits(['update:modelValue', 'change']) const emit = defineEmits(['update:modelValue'])
const task = ref<TaskModel>() const task = ref<ITask>()
const repeatAfter = reactive({ const repeatAfter = reactive({
amount: 0, amount: 0,
type: '', type: '',
@ -104,7 +105,7 @@ watch(
) )
function updateData() { function updateData() {
if (task.value.repeatMode !== repeatModes.REPEAT_MODE_DEFAULT && repeatAfter.amount === 0) { if (!task.value || task.value.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT && repeatAfter.amount === 0) {
return return
} }
@ -115,11 +116,10 @@ function updateData() {
Object.assign(task.value.repeatAfter, repeatAfter) Object.assign(task.value.repeatAfter, repeatAfter)
emit('update:modelValue', task.value) emit('update:modelValue', task.value)
emit('change')
} }
function setRepeatAfter(amount: number, type) { function setRepeatAfter(amount: number, type: IRepeatAfter['type']) {
Object.assign(repeatAfter, {amount, type}) Object.assign(repeatAfter, { amount, type})
updateData() updateData()
} }
</script> </script>

View file

@ -1,12 +1,11 @@
<template> <template>
<div :class="{'is-loading': taskService.loading}" class="task loader-container"> <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"/>
<span <ColorBubble
v-if="showListColor && listColor !== ''" v-if="showListColor && listColor !== ''"
:style="{backgroundColor: listColor }" :color="listColor"
class="color-bubble" class="mr-1"
> />
</span>
<router-link <router-link
:to="taskDetailRoute" :to="taskDetailRoute"
:class="{ 'done': task.done}" :class="{ 'done': task.done}"
@ -15,11 +14,17 @@
<router-link <router-link
:to="{ name: 'list.list', params: { listId: task.listId } }" :to="{ name: 'list.list', params: { listId: task.listId } }"
class="task-list" class="task-list"
v-if="showList && $store.getters['lists/getListById'](task.listId) !== null" :class="{'mr-2': task.hexColor !== ''}"
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})"> v-if="showList && getListById(task.listId) !== null"
{{ $store.getters['lists/getListById'](task.listId).title }} v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
{{ getListById(task.listId).title }}
</router-link> </router-link>
<ColorBubble
v-if="task.hexColor !== ''"
:color="task.getHexColor()"
class="mr-1"
/>
<!-- Show any parent tasks to make it clear this task is a sub task of something --> <!-- 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'"> <span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'">
<template v-for="(pt, i) in task.relatedTasks.parenttask"> <template v-for="(pt, i) in task.relatedTasks.parenttask">
@ -30,7 +35,7 @@
{{ task.title }} {{ task.title }}
</span> </span>
<labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0"/> <labels class="labels ml-2 mr-1" :labels="task.labels" v-if="task.labels.length > 0" />
<user <user
:avatar-size="27" :avatar-size="27"
:is-inline="true" :is-inline="true"
@ -43,7 +48,7 @@
v-if="+new Date(task.dueDate) > 0" v-if="+new Date(task.dueDate) > 0"
class="dueDate" class="dueDate"
@click.prevent.stop="showDefer = !showDefer" @click.prevent.stop="showDefer = !showDefer"
v-tooltip="formatDate(task.dueDate)" v-tooltip="formatDateLong(task.dueDate)"
> >
<time <time
:datetime="formatISO(task.dueDate)" :datetime="formatISO(task.dueDate)"
@ -80,9 +85,9 @@
<router-link <router-link
:to="{ name: 'list.list', params: { listId: task.listId } }" :to="{ name: 'list.list', params: { listId: task.listId } }"
class="task-list" class="task-list"
v-if="!showList && currentList.id !== task.listId && $store.getters['lists/getListById'](task.listId) !== null" v-if="!showList && currentList.id !== task.listId && getListById(task.listId) !== null"
v-tooltip="$t('task.detail.belongsToList', {list: $store.getters['lists/getListById'](task.listId).title})"> v-tooltip="$t('task.detail.belongsToList', {list: getListById(task.listId).title})">
{{ $store.getters['lists/getListById'](task.listId).title }} {{ getListById(task.listId).title }}
</router-link> </router-link>
<BaseButton <BaseButton
:class="{'is-favorite': task.isFavorite}" :class="{'is-favorite': task.isFavorite}"
@ -96,19 +101,26 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import {defineComponent} from 'vue' import {defineComponent, type PropType} from 'vue'
import {mapState} from 'pinia'
import TaskModel from '../../../models/task' import TaskModel from '@/models/task'
import PriorityLabel from './priorityLabel' import type {ITask} from '@/modelTypes/ITask'
import PriorityLabel from './priorityLabel.vue'
import TaskService from '../../../services/task' import TaskService from '../../../services/task'
import Labels from './labels' import Labels from '@/components/tasks/partials/labels.vue'
import User from '../../misc/user' import User from '@/components/misc/user.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import Fancycheckbox from '../../input/fancycheckbox' import Fancycheckbox from '../../input/fancycheckbox.vue'
import DeferTask from './defer-task' import DeferTask from './defer-task.vue'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {playPop} from '@/helpers/playPop' import ChecklistSummary from './checklist-summary.vue'
import ChecklistSummary from './checklist-summary' import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import ColorBubble from '@/components/misc/colorBubble.vue'
import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces'
import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks'
export default defineComponent({ export default defineComponent({
name: 'singleTaskInList', name: 'singleTaskInList',
@ -120,6 +132,7 @@ export default defineComponent({
} }
}, },
components: { components: {
ColorBubble,
BaseButton, BaseButton,
ChecklistSummary, ChecklistSummary,
DeferTask, DeferTask,
@ -130,7 +143,7 @@ export default defineComponent({
}, },
props: { props: {
theTask: { theTask: {
type: TaskModel, type: Object as PropType<ITask>,
required: true, required: true,
}, },
isArchived: { isArchived: {
@ -168,15 +181,19 @@ export default defineComponent({
document.removeEventListener('click', this.hideDeferDueDatePopup) document.removeEventListener('click', this.hideDeferDueDatePopup)
}, },
computed: { computed: {
...mapState(useListStore, {
getListById: 'getListById',
}),
listColor() { listColor() {
const list = this.$store.getters['lists/getListById'](this.task.listId) const list = this.getListById(this.task.listId)
return list !== null ? list.hexColor : '' return list !== null ? list.hexColor : ''
}, },
currentList() { currentList() {
return typeof this.$store.state.currentList === 'undefined' ? { const baseStore = useBaseStore()
return typeof baseStore.currentList === 'undefined' ? {
id: 0, id: 0,
title: '', title: '',
} : this.$store.state.currentList } : baseStore.currentList
}, },
taskDetailRoute() { taskDetailRoute() {
return { return {
@ -188,12 +205,13 @@ export default defineComponent({
}, },
}, },
methods: { methods: {
async markAsDone(checked) { formatDateSince,
formatISO,
formatDateLong,
async markAsDone(checked: boolean) {
const updateFunc = async () => { const updateFunc = async () => {
const task = await this.taskService.update(this.task) const task = await useTaskStore().update(this.task)
if (this.task.done) {
playPop()
}
this.task = task this.task = task
this.$emit('task-updated', task) this.$emit('task-updated', task)
this.$message.success({ this.$message.success({
@ -213,7 +231,7 @@ export default defineComponent({
} }
}, },
undoDone(checked) { undoDone(checked: boolean) {
this.task.done = !this.task.done this.task.done = !this.task.done
this.markAsDone(!checked) this.markAsDone(!checked)
}, },
@ -222,7 +240,7 @@ export default defineComponent({
this.task.isFavorite = !this.task.isFavorite this.task.isFavorite = !this.task.isFavorite
this.task = await this.taskService.update(this.task) this.task = await this.taskService.update(this.task)
this.$emit('task-updated', this.task) this.$emit('task-updated', this.task)
this.$store.dispatch('namespaces/loadNamespacesIfFavoritesDontExist') useNamespaceStore().loadNamespacesIfFavoritesDontExist()
}, },
hideDeferDueDatePopup(e) { hideDeferDueDatePopup(e) {
if (!this.showDefer) { if (!this.showDefer) {
@ -276,11 +294,6 @@ export default defineComponent({
white-space: nowrap; white-space: nowrap;
} }
.color-bubble {
height: 10px;
flex: 0 0 10px;
}
.avatar { .avatar {
border-radius: 50%; border-radius: 50%;
vertical-align: bottom; vertical-align: bottom;

View file

@ -7,7 +7,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {PropType} from 'vue' import type {PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
type Order = 'asc' | 'desc' | 'none' type Order = 'asc' | 'desc' | 'none'

View file

@ -2,6 +2,7 @@ import {ref, shallowReactive, watch, computed} from 'vue'
import {useRoute} from 'vue-router' import {useRoute} from 'vue-router'
import TaskCollectionService from '@/services/taskCollection' import TaskCollectionService from '@/services/taskCollection'
import type { ITask } from '@/modelTypes/ITask'
// FIXME: merge with DEFAULT_PARAMS in filters.vue // FIXME: merge with DEFAULT_PARAMS in filters.vue
export const getDefaultParams = () => ({ export const getDefaultParams = () => ({
@ -70,7 +71,7 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
const loading = computed(() => taskCollectionService.loading) const loading = computed(() => taskCollectionService.loading)
const totalPages = computed(() => taskCollectionService.totalPages) const totalPages = computed(() => taskCollectionService.totalPages)
const tasks = ref([]) const tasks = ref<ITask[]>([])
async function loadTasks() { async function loadTasks() {
tasks.value = [] tasks.value = []
tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value) tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value)
@ -81,10 +82,10 @@ export function useTaskList(listId, sortByDefault = SORT_BY_DEFAULT) {
watch(() => route.query, (query) => { watch(() => route.query, (query) => {
const { page: pageQueryValue, search: searchQuery } = query const { page: pageQueryValue, search: searchQuery } = query
if (searchQuery !== undefined) { if (searchQuery !== undefined) {
search.value = searchQuery search.value = searchQuery as string
} }
if (pageQueryValue !== undefined) { if (pageQueryValue !== undefined) {
page.value = parseInt(pageQueryValue) page.value = Number(pageQueryValue)
} }
}, { immediate: true }) }, { immediate: true })

View file

@ -4,10 +4,41 @@ import {useI18n} from 'vue-i18n'
export function useCopyToClipboard() { export function useCopyToClipboard() {
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
function fallbackCopyTextToClipboard(text: string) {
const textArea = document.createElement('textarea')
textArea.value = text
// Avoid scrolling to bottom
textArea.style.top = '0'
textArea.style.left = '0'
textArea.style.position = 'fixed'
document.body.appendChild(textArea)
textArea.focus()
textArea.select()
try {
// NOTE: the execCommand is deprecated but as of 2022_09
// widely supported and works without https
const successful = document.execCommand('copy')
if (!successful) {
throw new Error()
}
} catch (err) {
error(t('misc.copyError'))
}
document.body.removeChild(textArea)
}
return async (text: string) => { return async (text: string) => {
if (!navigator.clipboard) {
fallbackCopyTextToClipboard(text)
return
}
try { try {
await navigator.clipboard.writeText(text) await navigator.clipboard.writeText(text)
} catch { } catch(e) {
error(t('misc.copyError')) error(t('misc.copyError'))
} }
} }

View file

@ -1,11 +1,11 @@
import {ref, computed} from 'vue' import {ref, computed} from 'vue'
import {useStore} from 'vuex' import {useNamespaceStore} from '@/stores/namespaces'
export function useNameSpaceSearch() { export function useNamespaceSearch() {
const query = ref('') const query = ref('')
const store = useStore() const namespaceStore = useNamespaceStore()
const namespaces = computed(() => store.getters['namespaces/searchNamespace'](query.value)) const namespaces = computed(() => namespaceStore.searchNamespace(query.value))
function findNamespaces(newQuery: string) { function findNamespaces(newQuery: string) {
query.value = newQuery query.value = newQuery

View file

@ -0,0 +1,40 @@
import {computed} from 'vue'
import {useRouter} from 'vue-router'
import {useEventListener} from '@vueuse/core'
import {useAuthStore} from '@/stores/auth'
export function useRenewTokenOnFocus() {
const router = useRouter()
const authStore = useAuthStore()
const userInfo = computed(() => authStore.info)
const authenticated = computed(() => authStore.authenticated)
// Try renewing the token every time vikunja is loaded initially
// (When opening the browser the focus event is not fired)
authStore.renewToken()
// Check if the token is still valid if the window gets focus again to maybe renew it
useEventListener('focus', () => {
if (!authenticated.value) {
return
}
const expiresIn = (userInfo.value !== null ? userInfo.value.exp : 0) - +new Date() / 1000
// If the token expiry is negative, it is already expired and we have no choice but to redirect
// the user to the login page
if (expiresIn < 0) {
authStore.checkAuth()
router.push({name: 'user.login'})
return
}
// Check if the token is valid for less than 60 hours and renew if thats the case
if (expiresIn < 60 * 3600) {
authStore.renewToken()
console.debug('renewed token')
}
})
}

View file

@ -0,0 +1,54 @@
import { computed, shallowRef, watchEffect, h, type VNode } from 'vue'
import { useRoute, useRouter } from 'vue-router'
export function useRouteWithModal() {
const router = useRouter()
const route = useRoute()
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
const routeWithModal = computed(() => {
return backdropView.value
? router.resolve(backdropView.value)
: route
})
const currentModal = shallowRef<VNode>()
watchEffect(() => {
if (!backdropView.value) {
currentModal.value = undefined
return
}
// logic 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
? routePropsOption === true
? route.params
: typeof routePropsOption === 'function'
? routePropsOption(route)
: routePropsOption
: null
const component = route.matched[0]?.components?.default
if (!component) {
currentModal.value = undefined
return
}
currentModal.value = h(component, routeProps)
})
function closeModal() {
const historyState = computed(() => route.fullPath && window.history.state)
if (historyState.value) {
router.back()
} else {
const backdropRoute = historyState.value?.backdropView && router.resolve(historyState.value.backdropView)
router.push(backdropRoute)
}
}
return {routeWithModal, currentModal, closeModal}
}

View file

@ -1,12 +1,21 @@
import { computed, watchEffect } from 'vue' import { computed } from 'vue'
import type { ComputedGetter } from 'vue' import type { Ref } from 'vue'
import { setTitle } from '@/helpers/setTitle' import {useTitle as useTitleVueUse, resolveRef} from '@vueuse/core'
export function useTitle(titleGetter: ComputedGetter<string>) { type UseTitleParameters = Parameters<typeof useTitleVueUse>
const titleRef = computed(titleGetter)
watchEffect(() => setTitle(titleRef.value)) export function useTitle(...args: UseTitleParameters) {
return titleRef const [newTitle, ...restArgs] = args
const pageTitle = resolveRef(newTitle) as Ref<string>
const completeTitle = computed(() =>
(typeof pageTitle.value === 'undefined' || pageTitle.value === '')
? 'Vikunja'
: `${pageTitle.value} | Vikunja`,
)
return useTitleVueUse(completeTitle, ...restArgs)
} }

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