Compare commits

..

183 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
181 changed files with 5524 additions and 4662 deletions

View file

@ -46,7 +46,7 @@ steps:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
CYPRESS_CACHE_FOLDER: .cache/cypress CYPRESS_CACHE_FOLDER: .cache/cypress
commands: commands:
- corepack enable && corepack prepare pnpm@7.9.3 --activate && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000 - pnpm install --fetch-timeout 100000
# depends_on: # depends_on:
# - restore-cache # - restore-cache
@ -57,7 +57,7 @@ steps:
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
commands: commands:
- corepack enable && corepack prepare pnpm@7.9.3 --activate && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run lint - pnpm run lint
depends_on: depends_on:
- dependencies - dependencies
@ -68,7 +68,7 @@ steps:
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
commands: commands:
- corepack enable && corepack prepare pnpm@7.9.3 --activate && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run build - pnpm run build
depends_on: depends_on:
- dependencies - dependencies
@ -77,7 +77,7 @@ steps:
image: node:18-alpine image: node:18-alpine
pull: true pull: true
commands: commands:
- corepack enable && corepack prepare pnpm@7.9.3 --activate && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run test:unit - pnpm run test:unit
depends_on: depends_on:
- dependencies - dependencies
@ -89,7 +89,7 @@ steps:
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
commands: commands:
- corepack enable && corepack prepare pnpm@7.9.3 --activate && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm run typecheck - pnpm run typecheck
depends_on: depends_on:
- dependencies - dependencies
@ -107,7 +107,7 @@ steps:
from_secret: cypress_project_key from_secret: cypress_project_key
commands: commands:
- sed -i 's/localhost/api/g' dist/index.html - sed -i 's/localhost/api/g' dist/index.html
- corepack enable && corepack prepare pnpm@7.9.3 --activate && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm cypress install - pnpm cypress install
- pnpm run serve:dist & npx wait-on http://localhost:4173 - pnpm run serve:dist & npx wait-on http://localhost:4173
- pnpm run test:frontend --browser chrome --record - pnpm run test:frontend --browser chrome --record
@ -147,7 +147,7 @@ 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 - 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
@ -202,7 +202,7 @@ steps:
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
commands: commands:
- corepack enable && corepack prepare pnpm@7.9.3 --activate && pnpm config set store-dir .cache/.pnp - corepack enable && pnpm config set store-dir .cache/.pnp
- pnpm install --fetch-timeout 100000 - pnpm install --fetch-timeout 100000
- pnpm run lint - 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"
@ -278,7 +278,7 @@ steps:
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
commands: commands:
- corepack enable && corepack prepare pnpm@7.9.3 --activate && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000 - pnpm install --fetch-timeout 100000
- pnpm run lint - 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"
@ -659,6 +659,6 @@ steps:
from_secret: crowdin_key from_secret: crowdin_key
--- ---
kind: signature kind: signature
hmac: 9e9585222c466c67b426df40c76670f1a539c363a52075ff34e7f4518e67da2d 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.

1
.gitignore vendored
View file

@ -2,6 +2,7 @@
node_modules node_modules
/dist* /dist*
*.zip *.zip
.direnv/
# local env files # local env files
.env.local .env.local

View file

@ -6,31 +6,22 @@ WORKDIR /build
ARG USE_RELEASE=false ARG USE_RELEASE=false
ARG RELEASE_VERSION=main ARG RELEASE_VERSION=main
ENV PNPM_CACHE_FOLDER .cache/pnpm/
ADD . ./
RUN \ RUN \
if [ $USE_RELEASE = true ]; then \ if [ $USE_RELEASE = true ]; then \
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 && \
ENV PNPM_CACHE_FOLDER .cache/pnpm/
# pnpm fetch does require only lockfile
COPY pnpm-lock.yaml ./
RUN \
# https://pnpm.io/installation#using-corepack # https://pnpm.io/installation#using-corepack
corepack enable && \ corepack enable && \
corepack prepare pnpm@7.9.3 --activate && \ # 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
pnpm fetch pnpm install && \
apk add --no-cache git && \
ADD . ./
RUN apk add --no-cache git
RUN \
pnpm install --offline && \
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 && \
pnpm run build pnpm run build

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

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

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

@ -24,15 +24,15 @@
"@fortawesome/vue-fontawesome": "3.0.1", "@fortawesome/vue-fontawesome": "3.0.1",
"@github/hotkey": "2.0.1", "@github/hotkey": "2.0.1",
"@kyvg/vue3-notification": "2.4.1", "@kyvg/vue3-notification": "2.4.1",
"@sentry/tracing": "7.13.0", "@sentry/tracing": "7.15.0",
"@sentry/vue": "7.13.0", "@sentry/vue": "7.15.0",
"@types/is-touch-device": "1.0.0", "@types/is-touch-device": "1.0.0",
"@types/lodash.clonedeep": "4.5.7", "@types/lodash.clonedeep": "4.5.7",
"@types/sortablejs": "1.15.0", "@types/sortablejs": "1.15.0",
"@vueuse/core": "9.2.0", "@vueuse/core": "9.3.0",
"@vueuse/router": "9.2.0", "@vueuse/router": "9.3.0",
"axios": "0.27.2", "axios": "0.27.2",
"blurhash": "2.0.0", "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",
"codemirror": "5.65.9", "codemirror": "5.65.9",
@ -41,63 +41,66 @@
"easymde": "2.18.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.1.0", "marked": "4.1.1",
"minimist": "1.2.6", "minimist": "1.2.7",
"pinia": "2.0.22", "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", "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.39", "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.2", "vue-i18n": "9.2.2",
"vue-router": "4.1.5", "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.1.1", "@cypress/vite-dev-server": "3.3.1",
"@cypress/vue": "4.2.0", "@cypress/vue": "4.2.0",
"@faker-js/faker": "7.5.0", "@faker-js/faker": "7.5.0",
"@rushstack/eslint-patch": "1.2.0",
"@types/dompurify": "2.3.4", "@types/dompurify": "2.3.4",
"@types/flexsearch": "0.7.3", "@types/flexsearch": "0.7.3",
"@typescript-eslint/eslint-plugin": "5.38.0", "@types/lodash.debounce": "4.0.7",
"@typescript-eslint/parser": "5.38.0", "@types/marked": "4.0.7",
"@types/node": "16.11.65",
"@typescript-eslint/eslint-plugin": "5.40.0",
"@typescript-eslint/parser": "5.40.0",
"@vitejs/plugin-legacy": "2.2.0", "@vitejs/plugin-legacy": "2.2.0",
"@vitejs/plugin-vue": "3.1.0", "@vitejs/plugin-vue": "3.1.2",
"@vue/eslint-config-typescript": "11.0.2", "@vue/eslint-config-typescript": "11.0.2",
"@vue/test-utils": "2.0.2", "@vue/test-utils": "2.1.0",
"@vue/tsconfig": "0.1.3", "@vue/tsconfig": "0.1.3",
"autoprefixer": "10.4.12", "autoprefixer": "10.4.12",
"browserslist": "4.21.4", "browserslist": "4.21.4",
"caniuse-lite": "1.0.30001406", "caniuse-lite": "1.0.30001418",
"cypress": "10.8.0", "cypress": "10.10.0",
"esbuild": "0.15.8", "esbuild": "0.15.10",
"eslint": "8.23.1", "eslint": "8.25.0",
"eslint-plugin-vue": "9.5.1", "eslint-plugin-vue": "9.6.0",
"express": "4.18.1", "express": "4.18.2",
"happy-dom": "6.0.4", "happy-dom": "7.4.0",
"netlify-cli": "11.8.0", "netlify-cli": "12.0.7",
"postcss": "8.4.16", "postcss": "8.4.17",
"postcss-preset-env": "7.8.2", "postcss-preset-env": "7.8.2",
"rollup": "2.79.1", "rollup": "3.0.0",
"rollup-plugin-visualizer": "5.8.1", "rollup-plugin-visualizer": "5.8.2",
"sass": "1.55.0", "sass": "1.55.0",
"typescript": "4.8.3", "typescript": "4.8.4",
"vite": "3.1.3", "vite": "3.1.7",
"vite-plugin-pwa": "0.13.1", "vite-plugin-pwa": "0.13.1",
"vite-svg-loader": "3.6.0", "vite-svg-loader": "3.6.0",
"vitest": "0.23.4", "vitest": "0.24.1",
"vue-tsc": "0.40.13", "vue-tsc": "1.0.5",
"wait-on": "6.0.1", "wait-on": "6.0.1",
"workbox-cli": "6.5.4" "workbox-cli": "6.5.4"
}, },
@ -107,5 +110,5 @@
} }
}, },
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"packageManager": "pnpm@7.12.1" "packageManager": "pnpm@7.13.4"
} }

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"]
}, },
{ {

View file

@ -18,7 +18,6 @@
import {computed, watch, type 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 '@/store'
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

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

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 '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import flatPickr from 'vue-flatpickr-component' import flatPickr from 'vue-flatpickr-component'
@ -81,8 +80,9 @@ 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(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
@ -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}`
}
}, },
) )

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

@ -70,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>
@ -92,10 +92,7 @@
<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 '@/store'
import {useRouter} from 'vue-router'
import {QUICK_ACTIONS_ACTIVE} from '@/store/mutation-types'
import {RIGHTS as Rights} from '@/constants/rights' import {RIGHTS as Rights} from '@/constants/rights'
import Update from '@/components/home/update.vue' import Update from '@/components/home/update.vue'
@ -109,16 +106,23 @@ import MenuButton from '@/components/home/MenuButton.vue'
import {getListTitle} from '@/helpers/getListTitle' import {getListTitle} from '@/helpers/getListTitle'
const store = useStore() import {useBaseStore} from '@/stores/base'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
const userInfo = computed(() => store.state.auth.info) const baseStore = useBaseStore()
const userAvatar = computed(() => store.state.auth.avatarUrl) const currentList = computed(() => baseStore.currentList)
const currentList = computed(() => store.state.currentList) const background = computed(() => baseStore.background)
const background = computed(() => store.state.background) const canWriteCurrentList = computed(() => baseStore.currentList.maxRight > Rights.READ)
const imprintUrl = computed(() => store.state.config.legal.imprintUrl) const menuActive = computed(() => baseStore.menuActive)
const privacyPolicyUrl = computed(() => store.state.config.legal.privacyPolicyUrl)
const canWriteCurrentList = computed(() => store.state.currentList.maxRight > Rights.READ) const authStore = useAuthStore()
const menuActive = computed(() => store.state.menuActive) 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()
@ -132,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>

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,82 +60,34 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {watch, computed, shallowRef, watchEffect, type VNode, h} from 'vue' import {watch, computed} from 'vue'
import {useStore} from '@/store' 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 {useLabelStore} from '@/stores/labels'
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.
@ -157,47 +109,14 @@ 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()
const labelStore = useLabelStore() const labelStore = useLabelStore()
labelStore.loadAllLabels() labelStore.loadAllLabels()
</script> </script>

View file

@ -24,15 +24,16 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from '@/store'
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(() => store.state.logoVisible) const logoVisible = computed(() => baseStore.logoVisible)
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View file

@ -141,7 +141,6 @@
<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 '@/store'
import draggable from 'zhyswan-vuedraggable' import draggable from 'zhyswan-vuedraggable'
import type {SortableEvent} from 'sortablejs' import type {SortableEvent} from 'sortablejs'
@ -151,7 +150,6 @@ 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 {getListTitle} from '@/helpers/getListTitle'
@ -159,6 +157,8 @@ import {useEventListener} from '@vueuse/core'
import type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import type {INamespace} from '@/modelTypes/INamespace' import type {INamespace} from '@/modelTypes/INamespace'
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useBaseStore} from '@/stores/base'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
@ -168,10 +168,10 @@ const dragOptions = {
ghostClass: 'ghost', ghostClass: 'ghost',
} }
const store = useStore() const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const currentList = computed(() => store.state.currentList) const currentList = computed(() => baseStore.currentList)
const menuActive = computed(() => store.state.menuActive) const menuActive = computed(() => baseStore.menuActive)
const loading = computed(() => namespaceStore.isLoading) const loading = computed(() => namespaceStore.isLoading)
@ -202,7 +202,7 @@ const listStore = useListStore()
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: INamespace['id']) { function toggleLists(namespaceId: INamespace['id']) {
@ -262,7 +262,7 @@ 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 listStore.updateList({ await listStore.updateList({
...list, ...list,
position, position,

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,47 +61,43 @@ export default defineComponent({
type: String, type: String,
default: 'top', default: 'top',
}, },
}, })
emits: ['update:modelValue'],
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)
}, 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

@ -1,10 +1,8 @@
<template> <template>
<div class="datepicker"> <div class="datepicker">
<slot :date="date" :openPopup="toggleDatePopup">
<BaseButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined"> <BaseButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
{{ date === null ? chooseDateLabel : formatDateShort(date) }} {{ date === null ? chooseDateLabel : formatDateShort(date) }}
</BaseButton> </BaseButton>
</slot>
<transition name="fade"> <transition name="fade">
<div v-if="show" class="datepicker-popup" ref="datepickerPopup"> <div v-if="show" class="datepicker-popup" ref="datepickerPopup">
@ -90,12 +88,10 @@
</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'
@ -104,141 +100,140 @@ 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', '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 formatDate(this.date, 'yyy-LL-dd H:mm') return formatDate(date.value, 'yyy-LL-dd H:mm')
}, },
}, })
},
methods: {
formatDateShort, function setDateValue(dateString: string | Date | null) {
setDateValue(newVal) { if (dateString === null) {
if (newVal === null) { date.value = null
this.date = 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
}, 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) {
closeWhenClickedOutside(e, this.$refs.datepickerPopup, this.close)
} }
},
close() { const datepickerPopup = ref<HTMLElement | null>(null)
function hideDatePopup(e) {
if (show.value) {
closeWhenClickedOutside(e, datepickerPopup.value, 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) {
this.date = new Date()
} }
const interval = calculateDayInterval(date) function setDate(dateString: string) {
if (date.value === null) {
date.value = new 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 formatDate(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"
@ -66,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 {setupMarkdownRenderer} from '@/helpers/markdownRenderer'
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: '',
@ -135,96 +131,108 @@ export default defineComponent({
type: String, type: String,
default: '', default: '',
}, },
}, })
emits: ['update:modelValue'],
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. // This gets triggered when only pasting content into the editor.
// A change event would not get generated by that, an input event does. // A change event would not get generated by that, an input event does.
// Therefore, we're using this handler to catch paste events. // Therefore, we're using this handler to catch paste events.
// But because this also gets triggered when typing into the editor, we give // But because this also gets triggered when typing into the editor, we give
// it a higher timeout to make the timouts cancel each other in that case so // it a higher timeout to make the timouts cancel each other in that case so
// that in the end, only one change event is triggered to the outside per change. // that in the end, only one change event is triggered to the outside per change.
handleInput(val) { 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) {
clearTimeout(this.changeTimeout)
} }
this.changeTimeout = setTimeout(() => { function bubble(timeout = 500) {
this.$emit('update:modelValue', this.text) if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
changeTimeout.value = setTimeout(() => {
emit('update:modelValue', text.value)
}, 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() {
setupMarkdownRenderer(this.checkboxId)
this.preview = DOMPurify.sanitize(marked(this.text), {ADD_ATTR: ['target']}) function renderPreview() {
setupMarkdownRenderer(checkboxId.value)
preview.value = DOMPurify.sanitize(marked(text.value), {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.
@ -233,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()
} else { renderPreview()
this.text = this.replaceAt(this.text, index, `${listPrefix} [ ] `)
} }
this.bubble()
this.renderPreview() function toggleEdit() {
}, if (isEditActive.value) {
toggleEdit() { isPreviewActive.value = true
if (this.isEditActive) { isEditActive.value = false
this.isPreviewActive = true renderPreview()
this.isEditActive = false bubble(0) // save instantly
this.renderPreview()
this.bubble(0) // save instantly
} else { } else {
this.isPreviewActive = false isPreviewActive.value = false
this.isEditActive = true isEditActive.value = 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: boolean) {
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: {
const searchInput = ref<HTMLInputElement | null>(null)
// Searching will be triggered with a 200ms delay to avoid searching on every keyup event. // Searching will be triggered with a 200ms delay to avoid searching on every keyup event.
search() { 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) {
if (this.internalValue === null) {
this.internalValue = []
} }
this.internalValue.push(object) function select(object: {[key: string]: any}) {
if (props.multiple) {
if (internalValue.value === null) {
internalValue.value = []
}
(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"
:model-value="list.subscription" :model-value="list.subscription"
@update:model-value="sub => subscription = sub" @update:model-value="setSubscriptionInStore"
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
@ -77,14 +77,16 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, watchEffect, type PropType} from 'vue' import {ref, computed, watchEffect, type PropType} from 'vue'
import {useStore} from '@/store'
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 type {IList} from '@/modelTypes/IList' import type {IList} from '@/modelTypes/IList'
import type {ISubscription} from '@/modelTypes/ISubscription' 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: {
@ -93,12 +95,23 @@ const props = defineProps({
}, },
}) })
const listStore = useListStore()
const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null) 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) const modalOpen = ref(false)
return { function clearFilters() {
modalOpen, value.value = {...getDefaultParams()}
} }
},
methods: {
clearFilters() {
this.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>
@ -42,7 +46,7 @@
/> />
<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>
@ -58,7 +62,7 @@
/> />
<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>
@ -206,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'
@ -387,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
} }
@ -534,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')
@ -573,7 +586,7 @@ export default defineComponent({
return return
} }
let ids = [] const ids = []
this[kind].forEach(u => { this[kind].forEach(u => {
ids.push(kind === 'users' ? u.username : u.id) ids.push(kind === 'users' ? u.username : u.id)
}) })
@ -608,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,
@ -101,7 +101,7 @@ const listStore = useListStore()
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

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

@ -9,7 +9,7 @@
@close="$router.back()" @close="$router.back()"
:loading="loading" :loading="loading"
> >
<div :class="{'p-4': padding}"> <div class="p-4">
<slot /> <slot />
</div> </div>
@ -72,10 +72,6 @@ defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
padding: {
type: Boolean,
default: true,
},
}) })
const emit = defineEmits(['create', 'primary', 'tertiary']) const emit = defineEmits(['create', 'primary', 'tertiary'])

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

@ -8,14 +8,13 @@
<script lang="ts" setup> <script lang="ts" setup>
import {computed} from 'vue' import {computed} from 'vue'
import {useStore} from '@/store'
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

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

@ -69,17 +69,38 @@ 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 ''
}
switch (props.entity) {
case 'namespace':
return props.modelValue !== null ? return props.modelValue !== null ?
t('task.subscription.subscribed', {entity: props.entity}) : t('task.subscription.subscribedNamespace') :
t('task.subscription.notSubscribed', {entity: props.entity}) 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 buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
@ -105,7 +126,20 @@ async function subscribe() {
}) })
await subscriptionService.value.create(subscription) await subscriptionService.value.create(subscription)
emit('update:modelValue', 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() {
@ -115,6 +149,19 @@ async function unsubscribe() {
}) })
await subscriptionService.value.delete(subscription) await subscriptionService.value.delete(subscription)
emit('update:modelValue', 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,14 +33,13 @@
> >
{{ $t('menu.archive') }} {{ $t('menu.archive') }}
</dropdown-item> </dropdown-item>
<task-subscription <Subscription
v-if="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"
:model-value="subscription" :model-value="subscription"
@update:model-value="sub => subscription = sub" @update:model-value="setSubscriptionInStore"
type="dropdown" type="dropdown"
/> />
<dropdown-item <dropdown-item
@ -59,9 +58,10 @@ 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 {INamespace} from '@/modelTypes/INamespace'
import type {ISubscription} from '@/modelTypes/ISubscription' import type {ISubscription} from '@/modelTypes/ISubscription'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
namespace: { namespace: {
@ -70,8 +70,20 @@ const props = defineProps({
}, },
}) })
const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null) 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,7 +24,7 @@
<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) }}
@ -48,19 +48,20 @@
<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 { NOTIFICATION_NAMES as names, type INotification} from '@/modelTypes/INotification' import { NOTIFICATION_NAMES as names, type INotification} from '@/modelTypes/INotification'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {useStore} from '@/store'
import {useRouter} from 'vue-router'
import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate' import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
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<INotification[]>([]) const allNotifications = ref<INotification[]>([])
@ -73,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'
@ -70,7 +69,12 @@ import {getHistory} from '@/modules/listHistory'
import {parseTaskText, PrefixMode} from '@/modules/parseTaskText' 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 {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'
@ -109,8 +113,10 @@ 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
@ -139,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
@ -178,15 +184,14 @@ 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() {
if (this.selectedCmd !== null) { if (this.selectedCmd !== null) {
switch (this.selectedCmd.action) { switch (this.selectedCmd.action) {
case CMD_NEW_TASK: case CMD_NEW_TASK:
return this.$t('task.create.title') return this.$t('quickActions.newTask')
case CMD_NEW_LIST: case CMD_NEW_LIST:
return this.$t('quickActions.newList') return this.$t('quickActions.newList')
case CMD_NEW_NAMESPACE: case CMD_NEW_NAMESPACE:
@ -206,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})
} }
} }
@ -216,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 = []
@ -309,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())
@ -357,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) {
@ -410,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,
}) })
@ -436,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

@ -92,8 +92,8 @@
</p> </p>
<p class="mb-2"> <p class="mb-2">
<i18n-t keypath="list.share.links.sharedBy"> <i18n-t keypath="list.share.links.sharedBy" scope="global">
<strong>{{ s.sharedBy.getDisplayName() }}</strong> <strong>{{ getDisplayName(s.sharedBy) }}</strong>
</i18n-t> </i18n-t>
</p> </p>
@ -189,7 +189,6 @@
<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 '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {RIGHTS} from '@/constants/rights' import {RIGHTS} from '@/constants/rights'
@ -202,8 +201,10 @@ 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 type {ListView} from '@/types/ListView'
import {LIST_VIEWS} from '@/types/ListView' import {LIST_VIEWS} from '@/types/ListView'
import {useConfigStore} from '@/stores/config'
const props = defineProps({ const props = defineProps({
listId: { listId: {
@ -241,8 +242,8 @@ 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: IList['id']) { 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

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>
@ -139,7 +139,6 @@ export default {name: 'userTeamShare'}
<script setup lang="ts"> <script setup lang="ts">
import {ref, reactive, computed, shallowReactive, type 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 '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import UserNamespaceService from '@/services/userNamespace' import UserNamespaceService from '@/services/userNamespace'
@ -151,7 +150,7 @@ import UserListModel from '@/models/userList'
import type {IUserList} from '@/modelTypes/IUserList' 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 type {IUser} from '@/modelTypes/IUser'
import TeamNamespaceService from '@/services/teamNamespace' import TeamNamespaceService from '@/services/teamNamespace'
@ -171,6 +170,7 @@ 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: {
@ -208,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(() => {
@ -365,7 +365,7 @@ 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: string) { async function find(query: string) {
if (query === '') { if (query === '') {
found.value = [] found.value = []

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 '@/store' import {debouncedWatch, type MaybeRef, tryOnMounted, useWindowSize} from '@vueuse/core'
import {tryOnMounted, debouncedWatch, useWindowSize, type 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')
@ -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

@ -173,6 +173,7 @@
<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.vue' import EditTask from './edit-task.vue'
@ -182,7 +183,6 @@ import TaskModel from '../../models/task'
import {PRIORITIES as priorities} from '@/constants/priorities' import {PRIORITIES as priorities} from '@/constants/priorities'
import PriorityLabel from './partials/priorityLabel.vue' 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 as Rights} from '@/constants/rights'
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'
@ -190,6 +190,8 @@ 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 {formatDate} from '@/helpers/time/formatDate'
import {useBaseStore} from '@/stores/base'
export default defineComponent({ export default defineComponent({
name: 'GanttChart', name: 'GanttChart',
components: { components: {
@ -256,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: {
@ -276,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() + ''] = {}
} }
@ -351,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,
) )
@ -360,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,
) )
@ -428,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,
}) })

View file

@ -9,7 +9,7 @@
<input <input
v-if="editEnabled" v-if="editEnabled"
:disabled="attachmentService.loading || undefined" :disabled="loading || undefined"
@change="uploadNewAttachment()" @change="uploadNewAttachment()"
id="files" id="files"
multiple multiple
@ -35,10 +35,18 @@
: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="formatDateLong(a.created)"> <span v-tooltip="formatDateLong(a.created)">
{{ formatDateSince(a.created) }} {{ formatDateSince(a.created) }}
</span> </span>
@ -78,6 +86,17 @@
> >
{{ $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,7 +104,7 @@
<x-button <x-button
v-if="editEnabled" v-if="editEnabled"
:disabled="attachmentService.loading" :disabled="loading"
@click="filesRef?.click()" @click="filesRef?.click()"
class="mb-4" class="mb-4"
icon="cloud-upload-alt" icon="cloud-upload-alt"
@ -140,38 +159,45 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, computed} from 'vue' import {ref, shallowReactive, computed} from 'vue'
import {useDropZone} from '@vueuse/core' import {useDropZone} from '@vueuse/core'
import {useStore} from '@/store'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import type AttachmentModel from '@/models/attachment' import type AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask'
import {useAttachmentStore} from '@/stores/attachments'
import {formatDateSince, formatDateLong} from '@/helpers/time/formatDate' import {formatDateSince, formatDateLong} from '@/helpers/time/formatDate'
import {uploadFiles, generateAttachmentUrl} from '@/helpers/attachments' import {uploadFiles, generateAttachmentUrl} from '@/helpers/attachments'
import {getHumanSize} from '@/helpers/getHumanSize' import {getHumanSize} from '@/helpers/getHumanSize'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard' import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {error, success} from '@/message' import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
import {useI18n} from 'vue-i18n'
const props = defineProps({ const taskStore = useTaskStore()
taskId: { const {t} = useI18n({useScope: 'global'})
type: Number,
required: true, const props = withDefaults(defineProps<{
}, task: ITask,
initialAttachments: { initialAttachments?: IAttachment[],
type: Array, editEnabled: boolean,
}, }>(), {
editEnabled: { editEnabled: true,
default: true,
},
}) })
// FIXME: this should go through the store
const emit = defineEmits(['task-changed'])
const attachmentService = shallowReactive(new AttachmentService()) const attachmentService = shallowReactive(new AttachmentService())
const store = useStore() const attachmentStore = useAttachmentStore()
const attachments = computed(() => store.state.attachments.attachments) const attachments = computed(() => attachmentStore.attachments)
const loading = computed(() => attachmentService.loading || taskStore.isLoading)
function onDrop(files: File[] | null) { function onDrop(files: File[] | null) {
if (files && files.length !== 0) { if (files && files.length !== 0) {
@ -186,6 +212,7 @@ function downloadAttachment(attachment: IAttachment) {
} }
const filesRef = ref<HTMLInputElement | null>(null) const filesRef = ref<HTMLInputElement | null>(null)
function uploadNewAttachment() { function uploadNewAttachment() {
const files = filesRef.value?.files const files = filesRef.value?.files
@ -197,7 +224,7 @@ function uploadNewAttachment() {
} }
function uploadFilesToTask(files: File[] | FileList) { function uploadFilesToTask(files: File[] | FileList) {
uploadFiles(attachmentService, props.taskId, files) uploadFiles(attachmentService, props.task.id, files)
} }
const attachmentToDelete = ref<AttachmentModel | null>(null) const attachmentToDelete = ref<AttachmentModel | null>(null)
@ -213,10 +240,7 @@ async function deleteAttachment() {
try { try {
const r = await attachmentService.delete(attachmentToDelete.value) const r = await attachmentService.delete(attachmentToDelete.value)
store.commit( attachmentStore.removeById(attachmentToDelete.value.id)
'attachments/removeById',
attachmentToDelete.value.id,
)
success(r) success(r)
setAttachmentToDelete(null) setAttachmentToDelete(null)
} catch (e) { } catch (e) {
@ -225,10 +249,9 @@ async function deleteAttachment() {
} }
const attachmentImageBlobUrl = ref<string | null>(null) const attachmentImageBlobUrl = ref<string | null>(null)
const SUPPORTED_SUFFIX = ['.jpg', '.png', '.bmp', '.gif']
async function viewOrDownload(attachment: AttachmentModel) { async function viewOrDownload(attachment: AttachmentModel) {
if (SUPPORTED_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix)) ) { if (SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.endsWith(suffix))) {
attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment) attachmentImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
} else { } else {
downloadAttachment(attachment) downloadAttachment(attachment)
@ -236,8 +259,15 @@ async function viewOrDownload(attachment: AttachmentModel) {
} }
const copy = useCopyToClipboard() const copy = useCopyToClipboard()
function copyUrl(attachment: AttachmentModel) {
copy(generateAttachmentUrl(props.taskId, attachment.id)) 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>
@ -396,5 +426,13 @@ function copyUrl(attachment: AttachmentModel) {
} }
} }
.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

@ -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,13 +27,13 @@
<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="formatDateLong(c.created)" class="has-text-grey"> <span v-tooltip="formatDateLong(c.created)" class="has-text-grey">
{{ formatDateSince(c.created) }} {{ formatDateSince(c.created) }}
</span> </span>
@ -153,7 +153,6 @@
<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 '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
@ -167,6 +166,9 @@ 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 {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: {
@ -179,7 +181,8 @@ 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<ITaskComment[]>([]) const comments = ref<ITaskComment[]>([])
@ -194,9 +197,9 @@ const newComment = reactive(new TaskCommentModel())
const saved = ref<ITask['id'] | null>(null) const saved = ref<ITask['id'] | null>(null)
const saving = ref<ITask['id'] | null>(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 {}

View file

@ -1,16 +1,16 @@
<template> <template>
<p class="created"> <p class="created">
<time :datetime="formatISO(task.created)" v-tooltip="formatDateLong(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>
@ -30,6 +30,7 @@
import {computed, toRefs, type PropType} from 'vue' import {computed, toRefs, type PropType} from 'vue'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {formatISO, 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: {

View file

@ -38,13 +38,13 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount, type PropType} from 'vue' import {ref, shallowReactive, computed, watch, onMounted, onBeforeUnmount, toRef, type PropType} from 'vue'
import {useStore} from '@/store'
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 type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {useAuthStore} from '@/stores/auth'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -55,7 +55,7 @@ const props = defineProps({
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<ITask>() const task = ref<ITask>()
@ -63,12 +63,12 @@ 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

@ -32,11 +32,12 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref,computed, watch, type PropType} from 'vue' import {ref,computed, watch, type PropType} from 'vue'
import {useStore} from '@/store'
import Editor from '@/components/input/AsyncEditor' import Editor from '@/components/input/AsyncEditor'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks'
import TaskModel from '@/models/task'
const props = defineProps({ const props = defineProps({
@ -55,14 +56,14 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const task = ref<ITask>({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,
@ -77,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

@ -28,8 +28,7 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, shallowReactive, watch, type PropType} from 'vue' import {ref, shallowReactive, watch, nextTick, type PropType} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import User from '@/components/misc/user.vue' import User from '@/components/misc/user.vue'
@ -39,6 +38,8 @@ import BaseButton from '@/components/base/BaseButton.vue'
import {includesById} from '@/helpers/utils' import {includesById} from '@/helpers/utils'
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' import type {IUser} from '@/modelTypes/IUser'
const props = defineProps({ const props = defineProps({
@ -60,12 +61,13 @@ const props = defineProps({
}) })
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<IUser[]>([]) const assignees = ref<IUser[]>([])
let isAdding = false
watch( watch(
() => props.modelValue, () => props.modelValue,
@ -79,13 +81,23 @@ watch(
) )
async function addAssignee(user: IUser) { 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: IUser) { 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) {
@ -118,6 +130,7 @@ function clearAllFoundUsers() {
} }
const multiselect = ref() const multiselect = ref()
function focus() { function focus() {
multiselect.value.focus() multiselect.value.focus()
} }

View file

@ -40,7 +40,6 @@
<script setup lang="ts"> <script setup lang="ts">
import {type PropType, ref, computed, shallowReactive, watch} from 'vue' import {type PropType, ref, computed, shallowReactive, watch} from 'vue'
import {useStore} from '@/store'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import LabelModel from '@/models/label' import LabelModel from '@/models/label'
@ -51,6 +50,7 @@ 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 type {ILabel} from '@/modelTypes/ILabel'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -69,7 +69,6 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue']) 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())
@ -87,6 +86,7 @@ watch(
}, },
) )
const taskStore = useTaskStore()
const labelStore = useLabelStore() const labelStore = useLabelStore()
const foundLabels = computed(() => labelStore.filterLabelsByQuery(labels.value, query.value)) const foundLabels = computed(() => labelStore.filterLabelsByQuery(labels.value, query.value))
@ -97,17 +97,13 @@ function findLabel(newQuery: string) {
} }
async function addLabel(label: ILabel, showNotification = true) { async function addLabel(label: ILabel, showNotification = true) {
const bubble = () => {
emit('update:modelValue', 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')})
} }
@ -115,7 +111,7 @@ async function addLabel(label: ILabel, showNotification = true) {
async function removeLabel(label: ILabel) { 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) {

View file

@ -38,16 +38,16 @@
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, type PropType} from 'vue' import {ref, computed, type PropType} from 'vue'
import {useStore} from '@/store'
import {useRouter} from 'vue-router' 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 {useCopyToClipboard} from '@/composables/useCopyToClipboard' import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {useTaskStore} from '@/stores/tasks'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import ColorBubble from '@/components/misc/colorBubble.vue'
const props = defineProps({ const props = defineProps({
task: { task: {
@ -72,8 +72,8 @@ async function copyUrl() {
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() || '')
@ -93,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,
}) })

View file

@ -6,11 +6,18 @@
'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 === ''">
@ -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,81 +70,83 @@
</span> </span>
</div> </div>
</div> </div>
</div>
</template> </template>
<script lang="ts"> <script lang="ts" setup>
import {defineComponent, type PropType} from 'vue' import {ref, computed, watch} from 'vue'
import {useRouter} from 'vue-router'
import PriorityLabel from '../../../components/tasks/partials/priorityLabel.vue' import PriorityLabel from '@/components/tasks/partials/priorityLabel.vue'
import User from '../../../components/misc/user.vue' import User from '@/components/misc/user.vue'
import Done from '@/components/misc/Done.vue' import Done from '@/components/misc/Done.vue'
import Labels from '../../../components/tasks/partials/labels.vue' import Labels from '@/components/tasks/partials/labels.vue'
import ChecklistSummary from './checklist-summary.vue' 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 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 {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, const color = computed(() => getHexColor(props.task.hexColor))
TASK_DEFAULT_COLOR,
} async function toggleTaskDone(task: ITask) {
}, loadingInternal.value = true
props: {
task: {
type: Object as PropType<ITask>,
required: true,
},
loading: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
color() {
return this.task.getHexColor
? this.task.getHexColor()
: TASK_DEFAULT_COLOR
},
},
methods: {
formatDateLong,
formatISO,
formatDateSince,
colorIsDark,
async toggleTaskDone(task: ITask) {
this.loadingInternal = true
try { try {
const done = !task.done await useTaskStore().update({
await this.$store.dispatch('tasks/update', {
...task, ...task,
done, done: !task.done,
}) })
} finally { } finally {
this.loadingInternal = false loadingInternal.value = false
} }
}, }
openTaskDetail() {
this.$router.push({ function openTaskDetail() {
router.push({
name: 'task.detail', name: 'task.detail',
params: { id: this.task.id }, params: {id: props.task.id},
state: { backdropView: this.$router.currentRoute.value.fullPath }, 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>
@ -149,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

@ -37,7 +37,11 @@
@create="createAndRelateTask" @create="createAndRelateTask"
> >
<template #searchResult="{option: task}"> <template #searchResult="{option: task}">
<span v-if="typeof task !== '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="task.listId !== listId" v-if="task.listId !== listId"
@ -149,7 +153,6 @@
import {ref, reactive, shallowReactive, watch, computed, type PropType} from 'vue' import {ref, reactive, shallowReactive, watch, computed, type PropType} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useRoute} from 'vue-router' import {useRoute} from 'vue-router'
import {useStore} from '@/store'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TaskModel from '@/models/task' import TaskModel from '@/models/task'
@ -167,6 +170,7 @@ import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {useNamespaceStore} from '@/stores/namespaces' import {useNamespaceStore} from '@/stores/namespaces'
import {error, success} from '@/message' import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -190,7 +194,7 @@ const props = defineProps({
}, },
}) })
const store = useStore() const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore() const namespaceStore = useNamespaceStore()
const route = useRoute() const route = useRoute()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -344,11 +348,11 @@ async function createAndRelateTask(title: string) {
} }
async function toggleTaskDone(task: ITask) { async function toggleTaskDone(task: ITask) {
await store.dispatch('tasks/update', task) await taskStore.update(task)
// Find the task in the list and update it so that it is correctly strike through // Find the task in the list and update it so that it is correctly strike through
Object.entries(relatedTasks.value).some(([kind, tasks]) => { Object.entries(relatedTasks.value).some(([kind, tasks]) => {
return tasks.some((t, key) => { return (tasks as ITask[]).some((t, key) => {
const found = t.id === task.id const found = t.id === task.id
if (found) { if (found) {
relatedTasks.value[kind as IRelationKind]![key] = task relatedTasks.value[kind as IRelationKind]![key] = task

View file

@ -119,6 +119,8 @@ import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatD
import ColorBubble from '@/components/misc/colorBubble.vue' import ColorBubble from '@/components/misc/colorBubble.vue'
import {useListStore} from '@/stores/lists' import {useListStore} from '@/stores/lists'
import {useNamespaceStore} from '@/stores/namespaces' 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',
@ -187,10 +189,11 @@ export default defineComponent({
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 {
@ -208,7 +211,7 @@ export default defineComponent({
async markAsDone(checked: boolean) { async markAsDone(checked: boolean) {
const updateFunc = async () => { const updateFunc = async () => {
const task = await this.$store.dispatch('tasks/update', this.task) const task = await useTaskStore().update(this.task)
this.task = task this.task = task
this.$emit('task-updated', task) this.$emit('task-updated', task)
this.$message.success({ this.$message.success({
@ -237,8 +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)
const namespaceStore = useNamespaceStore() useNamespaceStore().loadNamespacesIfFavoritesDontExist()
namespaceStore.loadNamespacesIfFavoritesDontExist()
}, },
hideDeferDueDatePopup(e) { hideDeferDueDatePopup(e) {
if (!this.showDefer) { if (!this.showDefer) {

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

@ -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,6 +1,8 @@
export default { import type {Directive} from 'vue'
const focus = <Directive<HTMLElement,string>>{
// When the bound element is inserted into the DOM... // When the bound element is inserted into the DOM...
mounted: (el, {modifiers}) => { mounted(el, {modifiers}) {
// Focus the element only if the viewport is big enough // Focus the element only if the viewport is big enough
// auto focusing elements on mobile can be annoying since in these cases the // auto focusing elements on mobile can be annoying since in these cases the
// keyboard always pops up and takes half of the available space on the screen. // keyboard always pops up and takes half of the available space on the screen.
@ -10,3 +12,5 @@ export default {
} }
}, },
} }
export default focus

View file

@ -2,9 +2,9 @@ import AttachmentModel from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import AttachmentService from '@/services/attachment' import AttachmentService from '@/services/attachment'
import { store } from '@/store' import {useTaskStore} from '@/stores/tasks'
export function uploadFile(taskId: number, file: File, onSuccess: (url: string) => void) { export function uploadFile(taskId: number, file: File, onSuccess?: (url: string) => void) {
const attachmentService = new AttachmentService() const attachmentService = new AttachmentService()
const files = [file] const files = [file]
@ -15,18 +15,18 @@ export async function uploadFiles(
attachmentService: AttachmentService, attachmentService: AttachmentService,
taskId: number, taskId: number,
files: File[] | FileList, files: File[] | FileList,
onSuccess: Function = () => {}, onSuccess?: (attachmentUrl: string) => void,
) { ) {
const attachmentModel = new AttachmentModel({taskId}) const attachmentModel = new AttachmentModel({taskId})
const response = await attachmentService.create(attachmentModel, files) const response = await attachmentService.create(attachmentModel, files)
console.debug(`Uploaded attachments for task ${taskId}, response was`, response) console.debug(`Uploaded attachments for task ${taskId}, response was`, response)
response.success?.map((attachment: IAttachment) => { response.success?.map((attachment: IAttachment) => {
store.dispatch('tasks/addTaskAttachment', { useTaskStore().addTaskAttachment({
taskId, taskId,
attachment, attachment,
}) })
onSuccess(generateAttachmentUrl(taskId, attachment.id)) onSuccess?.(generateAttachmentUrl(taskId, attachment.id))
}) })
if (response.errors !== null) { if (response.errors !== null) {

View file

@ -45,7 +45,6 @@ export async function refreshToken(persist: boolean): Promise<AxiosResponse> {
return response return response
} catch(e) { } catch(e) {
// @ts-ignore
throw new Error('Error renewing token: ', { cause: e }) throw new Error('Error renewing token: ', { cause: e })
} }
} }

View file

@ -1,19 +1,18 @@
export const calculateItemPosition = (positionBefore: number | null, positionAfter: number | null): number => { export const calculateItemPosition = (positionBefore: number | null, positionAfter: number | null): number => {
if (positionBefore === null && positionAfter === null) { if (positionBefore === null) {
if (positionAfter === null) {
return 0 return 0
} }
// If there is no task before, our task is the first task in which case we let it have half of the position of the task after it // If there is no task after it, we just add 2^16 to the last position to have enough room in the future
if (positionBefore === null && positionAfter !== null) {
return positionAfter / 2 return positionAfter / 2
} }
// If there is no task after it, we just add 2^16 to the last position to have enough room in the future // If there is no task after it, we just add 2^16 to the last position to have enough room in the future
if (positionBefore !== null && positionAfter === null) { if (positionAfter === null) {
return positionBefore + Math.pow(2, 16) return positionBefore + Math.pow(2, 16)
} }
// If we have both a task before and after it, we acually calculate the position // If we have both a task before and after it, we acually calculate the position
// @ts-ignore - can never be null but TS does not seem to understand that
return positionBefore + (positionAfter - positionBefore) / 2 return positionBefore + (positionAfter - positionBefore) / 2
} }

View file

@ -1,10 +1,9 @@
import {store} from '@/store' import {useConfigStore} from '@/stores/config'
const API_DEFAULT_PORT = '3456' const API_DEFAULT_PORT = '3456'
export const ERROR_NO_API_URL = 'noApiUrlProvided' export const ERROR_NO_API_URL = 'noApiUrlProvided'
const updateConfig = () => store.dispatch('config/update')
export const checkAndSetApiUrl = (url: string): Promise<string> => { export const checkAndSetApiUrl = (url: string): Promise<string> => {
if(url.startsWith('/')) { if(url.startsWith('/')) {
@ -25,6 +24,9 @@ export const checkAndSetApiUrl = (url: string): Promise<string> => {
const oldUrl = window.API_URL const oldUrl = window.API_URL
window.API_URL = urlToCheck.toString() window.API_URL = urlToCheck.toString()
const configStore = useConfigStore()
const updateConfig = () => configStore.update()
// Check if the api is reachable at the provided url // Check if the api is reachable at the provided url
return updateConfig() return updateConfig()
.catch(e => { .catch(e => {

View file

@ -1,23 +0,0 @@
import UserService from '@/services/user'
import {findPropertyByValue} from '@/helpers/findPropertyByValue'
// Check if the user exists
function validateUsername(users: IUser[], username: IUser['username']) {
return findPropertyByValue(users, 'username', username)
}
export async function findAssignees(parsedTaskAssignees: string[]) {
if (parsedTaskAssignees.length <= 0) {
return []
}
const userService = new UserService()
const assignees = parsedTaskAssignees.map(async a => {
const users = await userService.getAll({}, {s: a})
return validateUsername(users, a)
})
const validatedUsers = await Promise.all(assignees)
return validatedUsers.filter((item) => Boolean(item))
}

View file

@ -1,7 +0,0 @@
// IDEA: maybe use a small fuzzy search here to prevent errors
export function findPropertyByValue(object, key, value) {
return Object.values(object).find(
(l) => l[key]?.toLowerCase() === value.toLowerCase(),
)
}

View file

@ -8,33 +8,34 @@ export function setupMarkdownRenderer(checkboxId: string) {
let checkboxNum = -1 let checkboxNum = -1
marked.use({ marked.use({
renderer: { renderer: {
image: (src, title, text) => { image(src: string, title: string, text: string) {
title = title ? ` title="${title}` : '' title = title ? ` title="${title}` : ''
// If the url starts with the api url, the image is likely an attachment and // If the url starts with the api url, the image is likely an attachment and
// we'll need to download and parse it properly. // we'll need to download and parse it properly.
if (src.substr(0, window.API_URL.length + 7) === `${window.API_URL}/tasks/`) { if (src.slice(0, window.API_URL.length + 7) === `${window.API_URL}/tasks/`) {
return `<img data-src="${src}" alt="${text}" ${title} class="attachment-image"/>` return `<img data-src="${src}" alt="${text}" ${title} class="attachment-image"/>`
} }
return `<img src="${src}" alt="${text}" ${title}/>` return `<img src="${src}" alt="${text}" ${title}/>`
}, },
checkbox: (checked) => { checkbox(checked: boolean) {
let checkedString = ''
if (checked) { if (checked) {
checked = ' checked="checked"' checkedString = 'checked'
} }
checkboxNum++ checkboxNum++
return `<input type="checkbox" data-checkbox-num="${checkboxNum}" ${checked} class="text-checkbox-${checkboxId}"/>` return `<input type="checkbox" data-checkbox-num="${checkboxNum}" ${checkedString} class="text-checkbox-${checkboxId}"/>`
}, },
link: (href, title, text) => { link(href: string, title: string, text: string) {
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`) const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
const html = linkRenderer.call(renderer, href, title, text) const html = linkRenderer.call(renderer, href, title, text)
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ') return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
}, },
}, },
highlight: function (code, language) { highlight(code, language) {
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext' const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
return hljs.highlight(code, {language: validLanguage}).value return hljs.highlight(code, {language: validLanguage}).value
}, },

View file

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

View file

@ -0,0 +1,109 @@
import {describe, it, expect} from 'vitest'
import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention'
describe('Parse Subtasks via Relation', () => {
it('Should not return a parent for a single task', () => {
const tasks = parseSubtasksViaIndention('single task')
expect(tasks).to.have.length(1)
expect(tasks[0].parent).toBeNull()
})
it('Should not return a parent for multiple tasks without indention', () => {
const tasks = parseSubtasksViaIndention(`task one
task two`)
expect(tasks).to.have.length(2)
expect(tasks[0].parent).toBeNull()
expect(tasks[1].parent).toBeNull()
})
it('Should return a parent for two tasks with indention', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task`)
expect(tasks).to.have.length(2)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task')
})
it('Should return a parent for multiple subtasks', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task one
sub task two`)
expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub task two')
expect(tasks[2].parent).to.eq('parent task')
})
it('Should work with multiple indention levels', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task
sub sub task`)
expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub sub task')
expect(tasks[2].parent).to.eq('sub task')
})
it('Should work with multiple indention levels and multiple tasks', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task
sub sub task one
sub sub task two`)
expect(tasks).to.have.length(4)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub sub task one')
expect(tasks[2].parent).to.eq('sub task')
expect(tasks[3].title).to.eq('sub sub task two')
expect(tasks[3].parent).to.eq('sub task')
})
it('Should work with multiple indention levels and multiple tasks', () => {
const tasks = parseSubtasksViaIndention(`parent task
sub task
sub sub task one
sub sub sub task
sub sub task two`)
expect(tasks).to.have.length(5)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub sub task one')
expect(tasks[2].parent).to.eq('sub task')
expect(tasks[3].title).to.eq('sub sub sub task')
expect(tasks[3].parent).to.eq('sub sub task one')
expect(tasks[4].title).to.eq('sub sub task two')
expect(tasks[4].parent).to.eq('sub task')
})
it('Should return a parent for multiple subtasks with special stuff', () => {
const tasks = parseSubtasksViaIndention(`* parent task
* sub task one
sub task two`)
expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull()
expect(tasks[0].title).to.eq('parent task')
expect(tasks[1].title).to.eq('sub task one')
expect(tasks[1].parent).to.eq('parent task')
expect(tasks[2].title).to.eq('sub task two')
expect(tasks[2].parent).to.eq('parent task')
})
it('Should not break when the first line is indented', () => {
const tasks = parseSubtasksViaIndention(' single task')
expect(tasks).to.have.length(1)
expect(tasks[0].parent).toBeNull()
})
})

View file

@ -0,0 +1,48 @@
export interface TaskWithParent {
title: string,
parent: string | null,
}
function cleanupTitle(title: string) {
return title.replace(/^((\* |\+ |- )(\[ \] )?)/g, '')
}
const spaceRegex = /^ */
/**
* @param taskTitles should be multiple lines of task tiles with indention to declare their parent/subtask
* relation between each other.
*/
export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[] {
const titles = taskTitles.split(/[\r\n]+/)
return titles.map((title, index) => {
const task: TaskWithParent = {
title: cleanupTitle(title),
parent: null,
}
if (index === 0) {
return task
}
const matched = spaceRegex.exec(title)
const matchedSpaces = matched ? matched[0].length : 0
if (matchedSpaces > 0) {
// Go up the tree to find the first task with less indention than the current one
let pi = 1
let parentSpaces = 0
do {
task.parent = cleanupTitle(titles[index - pi])
pi++
const parentMatched = spaceRegex.exec(task.parent)
parentSpaces = parentMatched ? parentMatched[0].length : 0
} while (parentSpaces >= matchedSpaces)
task.title = cleanupTitle(title.replace(spaceRegex, ''))
task.parent = task.parent.replace(spaceRegex, '')
}
return task
})
}

View file

@ -1,5 +1,5 @@
const DEFAULT_ID_LENGTH = 9 const DEFAULT_ID_LENGTH = 9
export function createRandomID(idLength = DEFAULT_ID_LENGTH) { export function createRandomID(idLength = DEFAULT_ID_LENGTH) {
return Math.random().toString(36).substr(2, idLength) return Math.random().toString(36).slice(2, idLength)
} }

View file

@ -1,14 +1,9 @@
import {createRandomID} from '@/helpers/randomId'
import {parseURL} from 'ufo' import {parseURL} from 'ufo'
export interface Provider { import {createRandomID} from '@/helpers/randomId'
name: string import type {IProvider} from '@/types/IProvider'
key: string
authUrl: string
clientId: string
}
export const redirectToProvider = (provider: Provider, redirectUrl: string = '') => { export const redirectToProvider = (provider: IProvider, redirectUrl = '') => {
// We're not using the redirect url provided by the server to allow redirects when using the electron app. // We're not using the redirect url provided by the server to allow redirects when using the electron app.
// The implications are not quite clear yet hence the logic to pass in another redirect url still exists. // The implications are not quite clear yet hence the logic to pass in another redirect url still exists.

View file

@ -1,5 +1,5 @@
// Save the current list view to local storage // Save the current list view to local storage
// We use local storage and not vuex here to make it persistent across reloads. // We use local storage and not a store here to make it persistent across reloads.
export const saveListView = (listId, routeName) => { export const saveListView = (listId, routeName) => {
if (routeName.includes('settings.')) { if (routeName.includes('settings.')) {
return return

View file

@ -8,3 +8,7 @@ export function getSavedFilterIdFromListId(listId: IList['id']) {
} }
return filterId return filterId
} }
export function isSavedFilter(list: IList) {
return getSavedFilterIdFromListId(list.id) > 0
}

View file

@ -0,0 +1,19 @@
export function scrollIntoView(el: HTMLElement | null | undefined) {
if (!el) {
return
}
const boundingRect = el.getBoundingClientRect()
const scrollY = window.scrollY
if (
boundingRect.top > (scrollY + window.innerHeight) ||
boundingRect.top < scrollY
) {
el.scrollIntoView({
behavior: 'smooth',
block: 'center',
inline: 'nearest',
})
}
}

View file

@ -1,5 +1,5 @@
export function calculateDayInterval(date, currentDay = (new Date().getDay())) { export function calculateDayInterval(dateString: string, currentDay = (new Date().getDay())) {
switch (date) { switch (dateString) {
case 'today': case 'today':
return 0 return 0
case 'tomorrow': case 'tomorrow':

View file

@ -6,7 +6,7 @@ import {i18n} from '@/i18n'
const locales = {en: enGB, de, ch: de, fr, ru} const locales = {en: enGB, de, ch: de, fr, ru}
const dateIsValid = date => { export function dateIsValid(date) {
if (date === null) { if (date === null) {
return false return false
} }

View file

@ -125,16 +125,16 @@ const addTimeToDate = (text: string, date: Date, previousMatch: string | null):
} }
export const getDateFromText = (text: string, now: Date = new Date()) => { export const getDateFromText = (text: string, now: Date = new Date()) => {
const fullDateRegex: RegExp = / ([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig const fullDateRegex = / ([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig
// 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021 // 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021
let results: string[] | null = fullDateRegex.exec(text) let results: string[] | null = fullDateRegex.exec(text)
let result: string | null = results === null ? null : results[0] let result: string | null = results === null ? null : results[0]
let foundText: string | null = result let foundText: string | null = result
let containsYear: boolean = true let containsYear = true
if (result === null) { if (result === null) {
// 2. Try parsing the date as something like "jan 21" or "21 jan" // 2. Try parsing the date as something like "jan 21" or "21 jan"
const monthRegex: RegExp = new RegExp(` (${monthsRegexGroup} [0-9][0-9]?|[0-9][0-9]? ${monthsRegexGroup})`, 'ig') const monthRegex = new RegExp(` (${monthsRegexGroup} [0-9][0-9]?|[0-9][0-9]? ${monthsRegexGroup})`, 'ig')
results = monthRegex.exec(text) results = monthRegex.exec(text)
result = results === null ? null : `${results[0]} ${now.getFullYear()}`.trim() result = results === null ? null : `${results[0]} ${now.getFullYear()}`.trim()
foundText = results === null ? '' : results[0].trim() foundText = results === null ? '' : results[0].trim()
@ -142,7 +142,7 @@ export const getDateFromText = (text: string, now: Date = new Date()) => {
if (result === null) { if (result === null) {
// 3. Try parsing the date as "27/01" or "01/27" // 3. Try parsing the date as "27/01" or "01/27"
const monthNumericRegex: RegExp = / ([0-9][0-9]?\/[0-9][0-9]?)/ig const monthNumericRegex = / ([0-9][0-9]?\/[0-9][0-9]?)/ig
results = monthNumericRegex.exec(text) results = monthNumericRegex.exec(text)
// Put the year before or after the date, depending on what works // Put the year before or after the date, depending on what works
@ -229,7 +229,7 @@ export const getDateFromTextIn = (text: string, now: Date = new Date()) => {
} }
const getDateFromWeekday = (text: string): dateFoundResult => { const getDateFromWeekday = (text: string): dateFoundResult => {
const matcher: RegExp = / (next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g const matcher = / (next )?(monday|mon|tuesday|tue|wednesday|wed|thursday|thu|friday|fri|saturday|sat|sunday|sun)($| )/g
const results: string[] | null = matcher.exec(text.toLowerCase()) // The i modifier does not seem to work. const results: string[] | null = matcher.exec(text.toLowerCase()) // The i modifier does not seem to work.
if (results === null) { if (results === null) {
return { return {
@ -240,7 +240,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
const date: Date = new Date() const date: Date = new Date()
const currentDay: number = date.getDay() const currentDay: number = date.getDay()
let day: number = 0 let day = 0
switch (results[2]) { switch (results[2]) {
case 'mon': case 'mon':
@ -285,7 +285,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
// matched string comes with a space at the end (last part of the regex). // matched string comes with a space at the end (last part of the regex).
let foundText = results[0] let foundText = results[0]
if (foundText.endsWith(' ')) { if (foundText.endsWith(' ')) {
foundText = foundText.substr(0, foundText.length - 1) foundText = foundText.slice(0, foundText.length - 1)
} }
return { return {

View file

@ -1,12 +1,11 @@
export function parseDateOrString(rawValue: string | undefined, fallback: any): string | Date { export function parseDateOrString(rawValue: string | undefined, fallback: unknown) {
if (typeof rawValue === 'undefined') { if (typeof rawValue === 'undefined') {
return fallback return fallback
} }
const d = new Date(rawValue) const d = new Date(rawValue)
// @ts-ignore if rawValue is an invalid date, isNan will return false. return !isNaN(+d)
return !isNaN(d)
? d ? d
: rawValue : rawValue
} }

View file

@ -15,7 +15,7 @@ export function isNil(value: unknown) {
return value == null return value == null
} }
export function omitBy(obj: {}, check: (value: unknown) => boolean) { export function omitBy(obj: Record<string, unknown>, check: (value: unknown) => boolean) {
if (isNil(obj)) { if (isNil(obj)) {
return {} return {}
} }

View file

@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Název seznamu", "title": "Název seznamu",
"color": "Barva", "color": "Barva",
"lists": "Seznamy", "lists": "Seznamy",
"list": {
"title": "Seznam",
"add": "Přidat",
"addPlaceholder": "Přidat nový úkol…",
"empty": "Tento seznam je nyní prázdný.",
"newTaskCta": "Vytvořit nový úkol.",
"editTask": "Upravit úkol"
},
"search": "Začni psát pro vyhledání seznamu…", "search": "Začni psát pro vyhledání seznamu…",
"searchSelect": "Klikněte nebo stiskněte Enter pro výběr tohoto seznamu", "searchSelect": "Klikněte nebo stiskněte Enter pro výběr tohoto seznamu",
"shared": "Sdílené seznamy", "shared": "Sdílené seznamy",
@ -270,14 +278,6 @@
"delete": "Smazat" "delete": "Smazat"
} }
}, },
"list": {
"title": "Seznam",
"add": "Přidat",
"addPlaceholder": "Přidat nový úkol…",
"empty": "Tento seznam je nyní prázdný.",
"newTaskCta": "Vytvořit nový úkol.",
"editTask": "Upravit úkol"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Zobrazit úkoly, které nemají nastavené datum", "showTasksWithoutDates": "Zobrazit úkoly, které nemají nastavené datum",
@ -672,13 +672,23 @@
"updated": "Aktualizováno" "updated": "Aktualizováno"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru {entity} prostřednictvím jeho {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Aktuálně jste přihlášeni k odběru {entity} a budete dostávat oznámení o změnách.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Nejste přihlášeni k odběru {entity} a nebudete dostávat upozornění na změny.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Odebírat", "subscribe": "Odebírat",
"unsubscribe": "Odhlásit odběr", "unsubscribe": "Odhlásit odběr",
"subscribeSuccess": "Nyní jste přihlášeni k odběru {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Nyní jste odhlášeni od odběru {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Přílohy", "title": "Přílohy",
@ -690,7 +700,11 @@
"deleteTooltip": "Smazat tuto přílohu", "deleteTooltip": "Smazat tuto přílohu",
"deleteText1": "Opravdu chcete odstranit přílohu {filename}?", "deleteText1": "Opravdu chcete odstranit přílohu {filename}?",
"copyUrl": "Kopírovat URL", "copyUrl": "Kopírovat URL",
"copyUrlTooltip": "Kopírovat URL této přílohy pro použití v textu" "copyUrlTooltip": "Kopírovat URL této přílohy pro použití v textu",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Komentáře", "title": "Komentáře",
@ -839,6 +853,12 @@
"text1": "Opravdu chcete odebrat tohoto uživatele z týmu?", "text1": "Opravdu chcete odebrat tohoto uživatele z týmu?",
"text2": "Ztratí přístup ke všem seznamům a prostorům, k nimž má tento tým přístup. To NEMŮŽE BÝT VZATO ZPĚT!", "text2": "Ztratí přístup ke všem seznamům a prostorům, k nimž má tento tým přístup. To NEMŮŽE BÝT VZATO ZPĚT!",
"success": "Uživatel byl úspěšně odstraněn z týmu." "success": "Uživatel byl úspěšně odstraněn z týmu."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Listentitel", "title": "Listentitel",
"color": "Farbe", "color": "Farbe",
"lists": "Listen", "lists": "Listen",
"list": {
"title": "Liste",
"add": "Hinzufügen",
"addPlaceholder": "Eine neue Aufgabe hinzufügen …",
"empty": "Diese Liste ist derzeit leer.",
"newTaskCta": "Eine neue Aufgabe erstellen.",
"editTask": "Aufgabe bearbeiten"
},
"search": "Tippe, um nach einer Liste zu suchen…", "search": "Tippe, um nach einer Liste zu suchen…",
"searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen", "searchSelect": "Klicke auf oder drücke die Eingabetaste, um diese Liste auszuwählen",
"shared": "Geteilte Listen", "shared": "Geteilte Listen",
@ -270,14 +278,6 @@
"delete": "Löschen" "delete": "Löschen"
} }
}, },
"list": {
"title": "Liste",
"add": "Hinzufügen",
"addPlaceholder": "Eine neue Aufgabe hinzufügen …",
"empty": "Diese Liste ist derzeit leer.",
"newTaskCta": "Eine neue Aufgabe erstellen.",
"editTask": "Aufgabe bearbeiten"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Aufgaben anzeigen, für die keine Daten festgelegt sind", "showTasksWithoutDates": "Aufgaben anzeigen, für die keine Daten festgelegt sind",
@ -672,13 +672,23 @@
"updated": "Aktualisiert" "updated": "Aktualisiert"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Du kannst hier nicht de-abonnieren, da du diese {entity} durch {parent} abonniert hast.", "subscribedListThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Liste über ihren Namespace abonniert hast.",
"subscribed": "Du erhältst Benachrichtigungen zu dieser {entity}.", "subscribedTaskThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihren Namespace abonniert hast.",
"notSubscribed": "Du hast diese {entity} nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.", "subscribedTaskThroughParentList": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihre Liste abonniert hast.",
"subscribedNamespace": "Du hast diesen Namespace abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedNamespace": "Du hast diesen Namespace nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedList": "Du hast diese Liste abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedList": "Du hast diese Liste nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedTask": "Du hast diese Aufgabe abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedTask": "Du hast diese Aufgabe nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribe": "Abonnieren", "subscribe": "Abonnieren",
"unsubscribe": "Abbestellen", "unsubscribe": "Abbestellen",
"subscribeSuccess": "Du hast jetzt diese {entity} abonniert", "subscribeSuccessNamespace": "Du hast diesen Namespace jetzt abonniert",
"unsubscribeSuccess": "Du hast diese {entity} jetzt abbestellt" "unsubscribeSuccessNamespace": "Du hast diesen Namespace jetzt nicht mehr abonniert",
"subscribeSuccessList": "Du hast diese Liste jetzt abonniert",
"unsubscribeSuccessList": "Du hast diese Liste jetzt nicht mehr abonniert",
"subscribeSuccessTask": "Du hast diese Aufgabe jetzt abonniert",
"unsubscribeSuccessTask": "Du hast diese Aufgabe jetzt nicht mehr abonniert"
}, },
"attachment": { "attachment": {
"title": "Anhänge", "title": "Anhänge",
@ -690,7 +700,11 @@
"deleteTooltip": "Diesen Anhang löschen", "deleteTooltip": "Diesen Anhang löschen",
"deleteText1": "Soll der Anhang {filename} gelöscht werden?", "deleteText1": "Soll der Anhang {filename} gelöscht werden?",
"copyUrl": "URL kopieren", "copyUrl": "URL kopieren",
"copyUrlTooltip": "Die URL dieses Anhangs zur Verwendung im Text kopieren" "copyUrlTooltip": "Die URL dieses Anhangs zur Verwendung im Text kopieren",
"setAsCover": "Als Titelbild setzen",
"unsetAsCover": "Titelbild entfernen",
"successfullyChangedCoverImage": "Das Titelbild wurde erfolgreich geändert.",
"usedAsCover": "Titelbild"
}, },
"comment": { "comment": {
"title": "Kommentare", "title": "Kommentare",
@ -839,6 +853,12 @@
"text1": "Bist du sicher, dass du diese:n Benutzer:in aus dem Team entfernen willst?", "text1": "Bist du sicher, dass du diese:n Benutzer:in aus dem Team entfernen willst?",
"text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Listen und Namespaces auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!", "text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Listen und Namespaces auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!",
"success": "Der:die Benutzer:in wurde erfolgreich aus dem Team gelöscht." "success": "Der:die Benutzer:in wurde erfolgreich aus dem Team gelöscht."
},
"leave": {
"title": "Team verlassen",
"text1": "Bist du sicher, dass du dieses Team verlassen willst?",
"text2": "Du wirst Zugriff auf alle Listen und Namespaces verlieren, auf die dieses Team Zugriff hat. Wenn du deine Meinung änderst, musst du durch einen Team-Admin wieder hinzugefügt werden.",
"success": "Du hast das Team erfolgreich verlassen."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Liste Titl", "title": "Liste Titl",
"color": "Farb", "color": "Farb",
"lists": "Listene", "lists": "Listene",
"list": {
"title": "Liste",
"add": "Hinzuefüege",
"addPlaceholder": "E neui Uufgab erstelle…",
"empty": "D'Liste isch momentan leer.",
"newTaskCta": "Neui Uufgab erstelle.",
"editTask": "Uufgab bearbeite"
},
"search": "Schriib, um nachere Liste z'sueche…", "search": "Schriib, um nachere Liste z'sueche…",
"searchSelect": "Druck uf Enter um die Liste uuszwähle", "searchSelect": "Druck uf Enter um die Liste uuszwähle",
"shared": "Teilti Liste", "shared": "Teilti Liste",
@ -270,14 +278,6 @@
"delete": "Chüble" "delete": "Chüble"
} }
}, },
"list": {
"title": "Liste",
"add": "Hinzuefüege",
"addPlaceholder": "E neui Uufgab erstelle…",
"empty": "D'Liste isch momentan leer.",
"newTaskCta": "Neui Uufgab erstelle.",
"editTask": "Uufgab bearbeite"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Zeig Uufgabe, wo kei Date hend", "showTasksWithoutDates": "Zeig Uufgabe, wo kei Date hend",
@ -672,13 +672,23 @@
"updated": "Aktualisiert" "updated": "Aktualisiert"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Du chasch da nid deabonnierä, weil du zu dere {entity} durch {parent} dezue abonniert bisch.", "subscribedListThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Liste über ihren Namespace abonniert hast.",
"subscribed": "Du bisch momentan zu dere {entity} abonniert und bechunsch Benachrichtigunge für Änderige.", "subscribedTaskThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihren Namespace abonniert hast.",
"notSubscribed": "Du bisch momentan nid zu dere {entity} abonniert und bechunsch kei Benachrichtigunge für Änderige.", "subscribedTaskThroughParentList": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihre Liste abonniert hast.",
"subscribedNamespace": "Du hast diesen Namespace abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedNamespace": "Du hast diesen Namespace nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedList": "Du hast diese Liste abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedList": "Du hast diese Liste nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedTask": "Du hast diese Aufgabe abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedTask": "Du hast diese Aufgabe nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribe": "Abooniere", "subscribe": "Abooniere",
"unsubscribe": "Deabonniere", "unsubscribe": "Deabonniere",
"subscribeSuccess": "Du hesch die {entity} abonniert", "subscribeSuccessNamespace": "Du hast diesen Namespace jetzt abonniert",
"unsubscribeSuccess": "Du hesch die {entity} deabonniert" "unsubscribeSuccessNamespace": "Du hast diesen Namespace jetzt nicht mehr abonniert",
"subscribeSuccessList": "Du hast diese Liste jetzt abonniert",
"unsubscribeSuccessList": "Du hast diese Liste jetzt nicht mehr abonniert",
"subscribeSuccessTask": "Du hast diese Aufgabe jetzt abonniert",
"unsubscribeSuccessTask": "Du hast diese Aufgabe jetzt nicht mehr abonniert"
}, },
"attachment": { "attachment": {
"title": "Aahhäng", "title": "Aahhäng",
@ -690,7 +700,11 @@
"deleteTooltip": "De Aahhang lösche", "deleteTooltip": "De Aahhang lösche",
"deleteText1": "Bisch du dir sicher, dass du de Aahang {filename} lösche wetsch?", "deleteText1": "Bisch du dir sicher, dass du de Aahang {filename} lösche wetsch?",
"copyUrl": "URL Kopierä", "copyUrl": "URL Kopierä",
"copyUrlTooltip": "D'Url vo dem Aahang kopiere, um sie im Text zbruuche" "copyUrlTooltip": "D'Url vo dem Aahang kopiere, um sie im Text zbruuche",
"setAsCover": "Als Titelbild setzen",
"unsetAsCover": "Titelbild entfernen",
"successfullyChangedCoverImage": "Das Titelbild wurde erfolgreich geändert.",
"usedAsCover": "Titelbild"
}, },
"comment": { "comment": {
"title": "Kommentär", "title": "Kommentär",
@ -839,6 +853,12 @@
"text1": "Bisch du dir sicher, dass du de Benutzer usm Team werfe wetsch?", "text1": "Bisch du dir sicher, dass du de Benutzer usm Team werfe wetsch?",
"text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Listen und Namespaces auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!", "text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Listen und Namespaces auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!",
"success": "Benutzer erfolgriich usegworfe." "success": "Benutzer erfolgriich usegworfe."
},
"leave": {
"title": "Team verlassen",
"text1": "Bist du sicher, dass du dieses Team verlassen willst?",
"text2": "Du wirst Zugriff auf alle Listen und Namespaces verlieren, auf die dieses Team Zugriff hat. Wenn du deine Meinung änderst, musst du durch einen Team-Admin wieder hinzugefügt werden.",
"success": "Du hast das Team erfolgreich verlassen."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,7 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": "List",
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -616,12 +617,6 @@
"select": "Select a date range", "select": "Select a date range",
"noTasks": "Nothing to do — Have a nice day!" "noTasks": "Nothing to do — Have a nice day!"
}, },
"create": {
"heading": "Create a task in {list}",
"title": "Enter the title of the new task…",
"descriptionPlaceholder": "Enter your description here…",
"description": "Add description…"
},
"detail": { "detail": {
"chooseDueDate": "Click here to set a due date", "chooseDueDate": "Click here to set a due date",
"chooseStartDate": "Click here to set a start date", "chooseStartDate": "Click here to set a start date",
@ -681,13 +676,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -699,7 +704,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -848,6 +857,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {
@ -939,6 +954,7 @@
"lists": "Lists", "lists": "Lists",
"teams": "Teams", "teams": "Teams",
"newList": "Enter the title of the new list…", "newList": "Enter the title of the new list…",
"newTask": "Enter the title of the new task…",
"newNamespace": "Enter the title of the new namespace…", "newNamespace": "Enter the title of the new namespace…",
"newTeam": "Enter the name of the new team…", "newTeam": "Enter the name of the new team…",
"createTask": "Create a task in the current list ({title})", "createTask": "Create a task in the current list ({title})",

View file

@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Nom de la liste", "title": "Nom de la liste",
"color": "Couleur", "color": "Couleur",
"lists": "Listes", "lists": "Listes",
"list": {
"title": "Liste",
"add": "Ajouter",
"addPlaceholder": "Ajouter une nouvelle tâche…",
"empty": "Cette liste est actuellement vide.",
"newTaskCta": "Créer une nouvelle tâche.",
"editTask": "Modifier la tâche"
},
"search": "Écris pour rechercher une liste…", "search": "Écris pour rechercher une liste…",
"searchSelect": "Clique ou appuie sur la touche Entrée pour sélectionner cette liste", "searchSelect": "Clique ou appuie sur la touche Entrée pour sélectionner cette liste",
"shared": "Listes partagées", "shared": "Listes partagées",
@ -270,14 +278,6 @@
"delete": "Supprimer" "delete": "Supprimer"
} }
}, },
"list": {
"title": "Liste",
"add": "Ajouter",
"addPlaceholder": "Ajouter une nouvelle tâche…",
"empty": "Cette liste est actuellement vide.",
"newTaskCta": "Créer une nouvelle tâche.",
"editTask": "Modifier la tâche"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Afficher les tâches pour lesquelles aucune date na été fixée", "showTasksWithoutDates": "Afficher les tâches pour lesquelles aucune date na été fixée",
@ -672,13 +672,23 @@
"updated": "Mis à jour" "updated": "Mis à jour"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Tu ne peux pas te désabonner ici car tu es abonné·e à cette {entity} par le biais de son {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Tu es actuellement abonné·e à cette {entity} et recevras des notifications pour les changements.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Tu nes pas abonné·e à cette {entity} et ne recevras pas de notifications pour les changements.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Sabonner", "subscribe": "Sabonner",
"unsubscribe": "Se désabonner", "unsubscribe": "Se désabonner",
"subscribeSuccess": "Tu es maintenant abonné·e à cette {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Tu es maintenant désabonné·e de cette {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Pièces jointes", "title": "Pièces jointes",
@ -690,7 +700,11 @@
"deleteTooltip": "Supprimer cette pièce jointe", "deleteTooltip": "Supprimer cette pièce jointe",
"deleteText1": "Supprimer la pièce jointe {filename} ?", "deleteText1": "Supprimer la pièce jointe {filename} ?",
"copyUrl": "Copier lURL", "copyUrl": "Copier lURL",
"copyUrlTooltip": "Copier lURL de cette pièce jointe pour lutiliser dans le texte" "copyUrlTooltip": "Copier lURL de cette pièce jointe pour lutiliser dans le texte",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Commentaires", "title": "Commentaires",
@ -839,6 +853,12 @@
"text1": "Retirer cette personne de léquipe ?", "text1": "Retirer cette personne de léquipe ?",
"text2": "Ils perdront l'accès à toutes les listes et espaces de noms auxquels cette équipe a accès. Ceci NE PEUT PAS ÊTRE ANNULÉ !", "text2": "Ils perdront l'accès à toutes les listes et espaces de noms auxquels cette équipe a accès. Ceci NE PEUT PAS ÊTRE ANNULÉ !",
"success": "Utilisateur·rice retiré·e de léquipe." "success": "Utilisateur·rice retiré·e de léquipe."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Titolo della Lista", "title": "Titolo della Lista",
"color": "Colore", "color": "Colore",
"lists": "Liste", "lists": "Liste",
"list": {
"title": "Lista",
"add": "Aggiungi",
"addPlaceholder": "Aggiungi una nuova attività…",
"empty": "Questa lista è attualmente vuota.",
"newTaskCta": "Crea una nuova attività.",
"editTask": "Modifica Attività"
},
"search": "Digita per cercare una lista…", "search": "Digita per cercare una lista…",
"searchSelect": "Fare clic o premere invio per selezionare questa lista", "searchSelect": "Fare clic o premere invio per selezionare questa lista",
"shared": "Liste Condivise", "shared": "Liste Condivise",
@ -270,14 +278,6 @@
"delete": "Elimina" "delete": "Elimina"
} }
}, },
"list": {
"title": "Lista",
"add": "Aggiungi",
"addPlaceholder": "Aggiungi una nuova attività…",
"empty": "Questa lista è attualmente vuota.",
"newTaskCta": "Crea una nuova attività.",
"editTask": "Modifica Attività"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Mostra attività che non hanno date impostate", "showTasksWithoutDates": "Mostra attività che non hanno date impostate",
@ -672,13 +672,23 @@
"updated": "Aggiornato" "updated": "Aggiornato"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Non puoi annullare l'iscrizione qui perché sei iscritto a questo {entity} attraverso il suo {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Sei attualmente iscritto a questo {entity} e riceverai notifiche per le modifiche.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Non sei iscritto a questo {entity} e non riceverai notifiche per le modifiche.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Iscriviti", "subscribe": "Iscriviti",
"unsubscribe": "Disiscriviti", "unsubscribe": "Disiscriviti",
"subscribeSuccess": "Ti sei iscritto a questo {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Ti sei disiscritto a questo {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Allegati", "title": "Allegati",
@ -690,7 +700,11 @@
"deleteTooltip": "Elimina questo allegato", "deleteTooltip": "Elimina questo allegato",
"deleteText1": "Sei sicuro di voler eliminare l'allegato {filename}?", "deleteText1": "Sei sicuro di voler eliminare l'allegato {filename}?",
"copyUrl": "Copia URL", "copyUrl": "Copia URL",
"copyUrlTooltip": "Copia l'URL di questo allegato per usarlo nel testo" "copyUrlTooltip": "Copia l'URL di questo allegato per usarlo nel testo",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Commenti", "title": "Commenti",
@ -839,6 +853,12 @@
"text1": "Confermi di voler rimuovere questo utente dal gruppo?", "text1": "Confermi di voler rimuovere questo utente dal gruppo?",
"text2": "Perderanno l'accesso a tutte le liste e i namespace a cui questo gruppo ha accesso. NON PUÒ ESSERE RIPRISTINATO!", "text2": "Perderanno l'accesso a tutte le liste e i namespace a cui questo gruppo ha accesso. NON PUÒ ESSERE RIPRISTINATO!",
"success": "Utente rimosso dal gruppo." "success": "Utente rimosso dal gruppo."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Lijst titel", "title": "Lijst titel",
"color": "Kleur", "color": "Kleur",
"lists": "Lijsten", "lists": "Lijsten",
"list": {
"title": "Lijst",
"add": "Toevoegen",
"addPlaceholder": "Voeg een nieuwe taak toe…",
"empty": "Deze lijst is momenteel leeg.",
"newTaskCta": "Creëer een nieuwe taak.",
"editTask": "Taak bewerken"
},
"search": "Typ om naar een lijst te zoeken…", "search": "Typ om naar een lijst te zoeken…",
"searchSelect": "Klik of druk op enter om deze lijst te selecteren", "searchSelect": "Klik of druk op enter om deze lijst te selecteren",
"shared": "Gedeelde lijsten", "shared": "Gedeelde lijsten",
@ -270,14 +278,6 @@
"delete": "Verwijderen" "delete": "Verwijderen"
} }
}, },
"list": {
"title": "Lijst",
"add": "Toevoegen",
"addPlaceholder": "Voeg een nieuwe taak toe…",
"empty": "Deze lijst is momenteel leeg.",
"newTaskCta": "Creëer een nieuwe taak.",
"editTask": "Taak bewerken"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Toon taken waarvoor geen datums zijn ingesteld", "showTasksWithoutDates": "Toon taken waarvoor geen datums zijn ingesteld",
@ -672,13 +672,23 @@
"updated": "Bijgewerkt" "updated": "Bijgewerkt"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Bijlagen", "title": "Bijlagen",
@ -690,7 +700,11 @@
"deleteTooltip": "Verwijder deze bijlage", "deleteTooltip": "Verwijder deze bijlage",
"deleteText1": "Weet je zeker dat je de bijlage {filename} wilt verwijderen?", "deleteText1": "Weet je zeker dat je de bijlage {filename} wilt verwijderen?",
"copyUrl": "URL kopiëren", "copyUrl": "URL kopiëren",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Reacties", "title": "Reacties",
@ -839,6 +853,12 @@
"text1": "Weet je zeker dat je deze gebruiker wilt verwijderen uit het team?", "text1": "Weet je zeker dat je deze gebruiker wilt verwijderen uit het team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Tytuł listy", "title": "Tytuł listy",
"color": "Kolor", "color": "Kolor",
"lists": "Listy", "lists": "Listy",
"list": {
"title": "Lista",
"add": "Dodaj",
"addPlaceholder": "Dodaj nowe zadanie…",
"empty": "Ta lista jest obecnie pusta.",
"newTaskCta": "Utwórz nowe zadanie.",
"editTask": "Edytuj zadanie"
},
"search": "Wpisz, aby wyszukać listę…", "search": "Wpisz, aby wyszukać listę…",
"searchSelect": "Kliknij lub naciśnij Enter, aby wybrać tę listę", "searchSelect": "Kliknij lub naciśnij Enter, aby wybrać tę listę",
"shared": "Współdzielone listy", "shared": "Współdzielone listy",
@ -270,14 +278,6 @@
"delete": "Usuń" "delete": "Usuń"
} }
}, },
"list": {
"title": "Lista",
"add": "Dodaj",
"addPlaceholder": "Dodaj nowe zadanie…",
"empty": "Ta lista jest obecnie pusta.",
"newTaskCta": "Utwórz nowe zadanie.",
"editTask": "Edytuj zadanie"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Pokaż zadania, które nie mają ustawionych dat", "showTasksWithoutDates": "Pokaż zadania, które nie mają ustawionych dat",
@ -672,13 +672,23 @@
"updated": "Zaktualizowano" "updated": "Zaktualizowano"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Nie możesz zrezygnować z subskrypcji, ponieważ subskrybujesz {entity} za pośrednictwem {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Obecnie subskrybujesz {entity} i będziesz otrzymywać powiadomienia o zmianach.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Nie subskrybujesz {entity} i nie będziesz otrzymywać powiadomień o zmianach.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subskrybuj", "subscribe": "Subskrybuj",
"unsubscribe": "Anuluj subskrypcję", "unsubscribe": "Anuluj subskrypcję",
"subscribeSuccess": "Od teraz subskrybujesz {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Już nie subskrybujesz {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Załączniki", "title": "Załączniki",
@ -690,7 +700,11 @@
"deleteTooltip": "Usuń ten załącznik", "deleteTooltip": "Usuń ten załącznik",
"deleteText1": "Czy na pewno chcesz usunąć załącznik {filename}?", "deleteText1": "Czy na pewno chcesz usunąć załącznik {filename}?",
"copyUrl": "Kopiuj URL", "copyUrl": "Kopiuj URL",
"copyUrlTooltip": "Skopiuj adres URL tego załącznika do użycia w tekście" "copyUrlTooltip": "Skopiuj adres URL tego załącznika do użycia w tekście",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Komentarze", "title": "Komentarze",
@ -839,6 +853,12 @@
"text1": "Czy na pewno chcesz usunąć tego użytkownika z zespołu?", "text1": "Czy na pewno chcesz usunąć tego użytkownika z zespołu?",
"text2": "Utraci on dostęp do wszystkich list i sekcji, do których ma dostęp ten zespół. Tego NIE DA SIĘ COFNĄĆ!", "text2": "Utraci on dostęp do wszystkich list i sekcji, do których ma dostęp ten zespół. Tego NIE DA SIĘ COFNĄĆ!",
"success": "Użytkownik został pomyślnie usunięty z zespołu." "success": "Użytkownik został pomyślnie usunięty z zespołu."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Título da Lista", "title": "Título da Lista",
"color": "Cor", "color": "Cor",
"lists": "Listas", "lists": "Listas",
"list": {
"title": "Lista",
"add": "Adicionar",
"addPlaceholder": "Adicionar uma nova tarefa…",
"empty": "Esta lista está atualmente vazia.",
"newTaskCta": "Cria uma nova tarefa.",
"editTask": "Editar Tarefa"
},
"search": "Escreve para pesquisar por uma lista…", "search": "Escreve para pesquisar por uma lista…",
"searchSelect": "Clica ou pressiona Enter para selecionar esta lista", "searchSelect": "Clica ou pressiona Enter para selecionar esta lista",
"shared": "Listas Partilhadas", "shared": "Listas Partilhadas",
@ -270,14 +278,6 @@
"delete": "Eliminar" "delete": "Eliminar"
} }
}, },
"list": {
"title": "Lista",
"add": "Adicionar",
"addPlaceholder": "Adicionar uma nova tarefa…",
"empty": "Esta lista está atualmente vazia.",
"newTaskCta": "Cria uma nova tarefa.",
"editTask": "Editar Tarefa"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Mostrar tarefas que não têm datas atríbuidas", "showTasksWithoutDates": "Mostrar tarefas que não têm datas atríbuidas",
@ -672,13 +672,23 @@
"updated": "Atualizado" "updated": "Atualizado"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta {entity} através de {parent}.", "subscribedListThroughParentNamespace": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta lista através do seu espaço.",
"subscribed": "Estás atualmente subscrito a esta {entity} e serás notificado de alterações.", "subscribedTaskThroughParentNamespace": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta tarefa através do seu espaço.",
"notSubscribed": "Não estás subscrito a esta {entity} e não serás notificado de alterações.", "subscribedTaskThroughParentList": "Não podes cancelar a tua subscrição aqui porque estás subscrito nesta tarefa através da sua lista.",
"subscribedNamespace": "Estás atualmente subscrito a este espaço e serás notificado de alterações.",
"notSubscribedNamespace": "Não estás subscrito a este espaço e não serás notificado de alterações.",
"subscribedList": "Estás atualmente subscrito a esta lista e serás notificado de alterações.",
"notSubscribedList": "Não estás subscrito a esta lista e não serás notificado de alterações.",
"subscribedTask": "Estás atualmente subscrito a esta tarefa e serás notificado de alterações.",
"notSubscribedTask": "Não estás subscrito a esta tarefa e não serás notificado de alterações.",
"subscribe": "Subscrever", "subscribe": "Subscrever",
"unsubscribe": "Remover Subscrição", "unsubscribe": "Remover Subscrição",
"subscribeSuccess": "Estás agora subscrito a esta {entity}", "subscribeSuccessNamespace": "Estás agora subscrito a este espaço",
"unsubscribeSuccess": "Não estás mais subcrito a esta {entity}" "unsubscribeSuccessNamespace": "Não estás mais subcrito a este espaço",
"subscribeSuccessList": "Estás agora subscrito a esta lista",
"unsubscribeSuccessList": "Não estás mais subcrito a esta lista",
"subscribeSuccessTask": "Estás agora subscrito a esta tarefa",
"unsubscribeSuccessTask": "Não estás mais subcrito a esta tarefa"
}, },
"attachment": { "attachment": {
"title": "Anexos", "title": "Anexos",
@ -690,7 +700,11 @@
"deleteTooltip": "Eliminar este anexo", "deleteTooltip": "Eliminar este anexo",
"deleteText1": "Tens a certeza que pretendes eliminar o anexo {filename}?", "deleteText1": "Tens a certeza que pretendes eliminar o anexo {filename}?",
"copyUrl": "Copiar URL", "copyUrl": "Copiar URL",
"copyUrlTooltip": "Copia o url deste anexo para o utilizar no texto" "copyUrlTooltip": "Copia o url deste anexo para o utilizar no texto",
"setAsCover": "Criar capa",
"unsetAsCover": "Remover capa",
"successfullyChangedCoverImage": "A imagem de capa foi alterada com sucesso.",
"usedAsCover": "Imagem de capa"
}, },
"comment": { "comment": {
"title": "Comentários", "title": "Comentários",
@ -839,6 +853,12 @@
"text1": "Tens a certeza que pretendes remover este utilizador da equipa?", "text1": "Tens a certeza que pretendes remover este utilizador da equipa?",
"text2": "Eles perderão o acesso a todas as listas e espaços a que esta equipa tem acesso. Isto NÃO PODER SER REVERTIDO!", "text2": "Eles perderão o acesso a todas as listas e espaços a que esta equipa tem acesso. Isto NÃO PODER SER REVERTIDO!",
"success": "O utilizador foi removido da equipa com sucesso." "success": "O utilizador foi removido da equipa com sucesso."
},
"leave": {
"title": "Sair da equipa",
"text1": "Tens a certeza de que queres sair desta equipa?",
"text2": "Vais perder acesso a todas as listas e espaços a que esta equipa tem acesso. Se mudares de ideias, vais necessitar que um administrador da equipa te adicione novamente.",
"success": "Saíste da equipa com sucesso."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "Название списка", "title": "Название списка",
"color": "Цвет", "color": "Цвет",
"lists": "Списки", "lists": "Списки",
"list": {
"title": "Список",
"add": "Добавить",
"addPlaceholder": "Добавить новую задачу…",
"empty": "Список сейчас пуст.",
"newTaskCta": "Создать новую задачу.",
"editTask": "Изменить задачу"
},
"search": "Введи запрос для поиска списка…", "search": "Введи запрос для поиска списка…",
"searchSelect": "Кликни или нажми Enter для выбора этого списка", "searchSelect": "Кликни или нажми Enter для выбора этого списка",
"shared": "Общие списки", "shared": "Общие списки",
@ -270,14 +278,6 @@
"delete": "Удалить" "delete": "Удалить"
} }
}, },
"list": {
"title": "Список",
"add": "Добавить",
"addPlaceholder": "Добавить новую задачу…",
"empty": "Список сейчас пуст.",
"newTaskCta": "Создать новую задачу.",
"editTask": "Изменить задачу"
},
"gantt": { "gantt": {
"title": "Гант", "title": "Гант",
"showTasksWithoutDates": "Показать задачи без установленной даты", "showTasksWithoutDates": "Показать задачи без установленной даты",
@ -672,13 +672,23 @@
"updated": "Дата изменения" "updated": "Дата изменения"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "Ты не можешь отписаться здесь, потому что ты подписан на {entity} через {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "Ты подписан на {entity} и будешь получать уведомления об изменениях.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "Ты не подписан на {entity} и не будешь получать уведомления об изменениях.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Подписаться", "subscribe": "Подписаться",
"unsubscribe": "Отписаться", "unsubscribe": "Отписаться",
"subscribeSuccess": "Ты подписался на {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "Ты отписался от {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Вложения", "title": "Вложения",
@ -690,7 +700,11 @@
"deleteTooltip": "Удалить это вложение", "deleteTooltip": "Удалить это вложение",
"deleteText1": "Удалить вложение {filename}?", "deleteText1": "Удалить вложение {filename}?",
"copyUrl": "Скопировать URL", "copyUrl": "Скопировать URL",
"copyUrlTooltip": "Скопировать ссылку на это вложение для использования в тексте" "copyUrlTooltip": "Скопировать ссылку на это вложение для использования в тексте",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Комментарии", "title": "Комментарии",
@ -839,6 +853,12 @@
"text1": "Удалить этого пользователя из команды?", "text1": "Удалить этого пользователя из команды?",
"text2": "Пользователь потеряет доступ ко всем спискам и пространствам имён, к котором есть доступ у команды. Это действие отменить нельзя!", "text2": "Пользователь потеряет доступ ко всем спискам и пространствам имён, к котором есть доступ у команды. Это действие отменить нельзя!",
"success": "Пользователь удалён из команды." "success": "Пользователь удалён из команды."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

View file

@ -169,6 +169,14 @@
"title": "List Title", "title": "List Title",
"color": "Color", "color": "Color",
"lists": "Lists", "lists": "Lists",
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"search": "Type to search for a list…", "search": "Type to search for a list…",
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
@ -270,14 +278,6 @@
"delete": "Delete" "delete": "Delete"
} }
}, },
"list": {
"title": "List",
"add": "Add",
"addPlaceholder": "Add a new task…",
"empty": "This list is currently empty.",
"newTaskCta": "Create a new task.",
"editTask": "Edit Task"
},
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Show tasks which don't have dates set",
@ -672,13 +672,23 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedThroughParent": "You can't unsubscribe here because you are subscribed to this {entity} through its {parent}.", "subscribedListThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this list through its namespace.",
"subscribed": "You are currently subscribed to this {entity} and will receive notifications for changes.", "subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"notSubscribed": "You are not subscribed to this {entity} and won't receive notifications for changes.", "subscribedTaskThroughParentList": "You can't unsubscribe here because you are subscribed to this task through its list.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedList": "You are currently subscribed to this list and will receive notifications for changes.",
"notSubscribedList": "You are not subscribed to this list and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccess": "You are now subscribed to this {entity}", "subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccess": "You are now unsubscribed to this {entity}" "unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessList": "You are now subscribed to this list",
"unsubscribeSuccessList": "You are now unsubscribed to this list",
"subscribeSuccessTask": "You are now subscribed to this task",
"unsubscribeSuccessTask": "You are now unsubscribed to this task"
}, },
"attachment": { "attachment": {
"title": "Attachments", "title": "Attachments",
@ -690,7 +700,11 @@
"deleteTooltip": "Delete this attachment", "deleteTooltip": "Delete this attachment",
"deleteText1": "Are you sure you want to delete the attachment {filename}?", "deleteText1": "Are you sure you want to delete the attachment {filename}?",
"copyUrl": "Copy URL", "copyUrl": "Copy URL",
"copyUrlTooltip": "Copy the url of this attachment for usage in text" "copyUrlTooltip": "Copy the url of this attachment for usage in text",
"setAsCover": "Make cover",
"unsetAsCover": "Remove cover",
"successfullyChangedCoverImage": "The cover image was successfully changed.",
"usedAsCover": "Cover image"
}, },
"comment": { "comment": {
"title": "Comments", "title": "Comments",
@ -839,6 +853,12 @@
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all lists and namespaces this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
},
"leave": {
"title": "Leave team",
"text1": "Are you sure you want to leave this team?",
"text2": "You will loose access to all lists and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team."
} }
}, },
"attributes": { "attributes": {

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