Compare commits

...

87 Commits

Author SHA1 Message Date
a94198f2eb Fixed accidental change - restoring original 2022-01-08 15:52:33 -05:00
a6e7e6efa6 Changed button test to look for "Mark task done!" - to match button change 2022-01-08 15:49:50 -05:00
6f5f7b190a Changed "Done!" to "Mark task done" on green button 2022-01-08 15:48:47 -05:00
0473c385d6
fix: editor cursor color 2022-01-08 17:34:13 +01:00
68a76faacc
fix: don't reset active fields when saving
Resolves #590
2022-01-08 15:13:49 +01:00
4579dd3ce7
fix: button size on task detail view 2022-01-08 15:11:55 +01:00
f4fee26fe4 chore(deps): update dependency sass to v1.47.0 (#1333)
Reviewed-on: vikunja/frontend#1333
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-08 11:03:51 +00:00
00398085fd chore(deps): update dependency caniuse-lite to v1.0.30001298 (#1334)
Reviewed-on: vikunja/frontend#1334
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-08 10:54:58 +00:00
13c8e6dbcd fix(deps): update dependency v-tooltip to v4.0.0-beta.13 (#1332)
Reviewed-on: vikunja/frontend#1332
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-07 16:21:41 +00:00
1b5f8a069b chore(deps): update dependency netlify-cli to v8.6.15 (#1331)
Reviewed-on: vikunja/frontend#1331
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-07 16:21:27 +00:00
21fec9461d chore(deps): update dependency vitest to v0.0.139 (#1330)
Reviewed-on: vikunja/frontend#1330
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-07 16:21:11 +00:00
df3af739f8 chore(deps): update dependency autoprefixer to v10.4.2 (#1329)
Reviewed-on: vikunja/frontend#1329
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-07 16:20:45 +00:00
b08d34bc96 fix(deps): update dependency v-tooltip to v4.0.0-beta.11 (#1326)
Reviewed-on: vikunja/frontend#1326
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-07 08:26:54 +00:00
be03efd015 chore(deps): update dependency caniuse-lite to v1.0.30001297 (#1327)
Reviewed-on: vikunja/frontend#1327
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-07 08:26:35 +00:00
c353fd151d chore(deps): update dependency vitest to v0.0.136 (#1325)
Reviewed-on: vikunja/frontend#1325
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 21:23:31 +00:00
c32e9badf0 chore(deps): update dependency netlify-cli to v8.6.12 (#1322)
Reviewed-on: vikunja/frontend#1322
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 19:39:31 +00:00
8f64ab5dce chore(deps): update dependency vitest to v0.0.135 (#1323)
Reviewed-on: vikunja/frontend#1323
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 19:39:11 +00:00
63ca8ffc7c fix(deps): update dependency v-tooltip to v4.0.0-beta.10 (#1324)
Reviewed-on: vikunja/frontend#1324
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 19:38:46 +00:00
fe9ddf33ca fix(deps): update dependency marked to v4.0.9 (#1321)
Reviewed-on: vikunja/frontend#1321
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 16:26:02 +00:00
74777d6bed chore(deps): update dependency netlify-cli to v8.6.9 (#1320)
Reviewed-on: vikunja/frontend#1320
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 16:25:44 +00:00
76d1c56fab chore(deps): update dependency netlify-cli to v8.6.8 (#1318)
Reviewed-on: vikunja/frontend#1318
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 15:23:22 +00:00
f75e9135c2 fix(deps): update dependency highlight.js to v11.4.0 (#1319)
Reviewed-on: vikunja/frontend#1319
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 15:23:08 +00:00
480f0f8da9 fix(deps): update dependency v-tooltip to v4.0.0-beta.8 (#1317)
Reviewed-on: vikunja/frontend#1317
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 14:05:02 +00:00
ac832186d6 chore(deps): update dependency netlify-cli to v8.6.6 (#1316)
Reviewed-on: vikunja/frontend#1316
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 11:38:23 +00:00
738e1e8370 chore(deps): update dependency sass to v1.46.0 (#1315)
Reviewed-on: vikunja/frontend#1315
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 11:10:06 +00:00
Dominik Pschenitschni
9b85817ddb feat: run vue-tsc in ci (#1295)
We had this in for a while already, but never cared for the result.
Might make sense to integrate in the pipeline.

To make things easy from the start we could add [`failure: ignore`](https://docs.drone.io/pipeline/macstadium/syntax/steps/#failure) to the step

Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#1295
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-01-06 10:37:51 +00:00
Dominik Pschenitschni
49a6569db0 fix: remove obsolet code (#1312)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#1312
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-01-06 10:34:14 +00:00
e762f7f073 chore(deps): update dependency vitest to v0.0.134 (#1314)
Reviewed-on: vikunja/frontend#1314
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-06 10:33:07 +00:00
e5d2b23cb3 chore(deps): update dependency netlify-cli to v8.6.5 2022-01-05 21:05:56 +00:00
6eddf23c0d fix(deps): update dependency vue-advanced-cropper to v2.7.1 2022-01-05 17:03:11 +00:00
70934c6a0b fix(deps): pin dependency @types/is-touch-device to 1.0.0 (#1308)
Reviewed-on: vikunja/frontend#1308
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-05 16:45:48 +00:00
49955eb03a
fix: remove some of the typescript warnings 2022-01-05 16:17:14 +01:00
2b302974cc chore(deps): update dependency vitest to v0.0.133 2022-01-05 14:52:22 +00:00
Dominik Pschenitschni
64d632b0a5 [skip ci] Updated translations via Crowdin 2022-01-05 14:36:24 +00:00
Dominik Pschenitschni
e28f0f5be4 chore(addTask) improve order (#1297)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#1297
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-01-05 13:14:50 +00:00
c618b7e0b6 fix: namespace new buttons on mobile (#1262)
Before:

![image](/attachments/7626e28d-8a13-4f92-b162-697676f765c7)

After:

![image](/attachments/5331af47-4887-4ba5-98d7-ee70311e20d7)

Co-authored-by: kolaente <k@knt.li>
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#1262
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2022-01-05 12:46:33 +00:00
380af7fbf2 fix(deps): update dependency @vueuse/router to v7.5.3 (#1303)
Reviewed-on: vikunja/frontend#1303
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2022-01-05 11:44:12 +00:00
9d3ef30be6 fix(deps): update dependency @vueuse/core to v7.5.2 2022-01-05 10:03:09 +00:00
fc00169863 chore(deps): update dependency vitest to v0.0.132 2022-01-05 09:21:26 +00:00
b652225a12 fix(deps): update dependency @vueuse/router to v7.5.2 2022-01-05 08:02:58 +00:00
29d8422e94
fix(ready): remove class form fragment 2022-01-04 21:15:02 +01:00
Dominik Pschenitschni
cdbd1c2ac4 feat: create BaseButton component (#1123)
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Reviewed-on: vikunja/frontend#1123
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-01-04 18:58:06 +00:00
cb37fd773d feat: convert to composable useDateTimeSalutation 2022-01-04 18:44:07 +00:00
d2577f1df6 feat: use useNow to provide auto updates 2022-01-04 18:44:07 +00:00
27534a98e9 feat: return full translation key 2022-01-04 18:44:07 +00:00
de77393905 feat: move the calculation of the current salutation to a different function 2022-01-04 18:44:07 +00:00
dd450263fb chore(deps): pin dependency happy-dom to 2.25.1 2022-01-04 18:03:16 +00:00
d8106dcb73 chore(deps): update dependency vitest to v0.0.131 2022-01-04 14:03:26 +00:00
8114012997
feat: replace jest with vitest 2022-01-04 14:16:47 +01:00
bc4ea82639 chore(deps): update dependency netlify-cli to v8.6.4 2022-01-04 13:03:16 +00:00
cd97cfe612 chore(deps): update dependency netlify-cli to v8.6.3 2022-01-04 12:03:19 +00:00
e6136fdee4 chore(deps): update dependency caniuse-lite to v1.0.30001296 2022-01-04 11:48:22 +00:00
2c395c720a chore(deps): update dependency vue-tsc to v0.30.2 2022-01-04 11:48:01 +00:00
8b639fd4af chore(deps): update dependency rollup to v2.63.0 2022-01-04 08:03:11 +00:00
f7bd5f13ac fix(deps): update dependency v-tooltip to v4.0.0-beta.6 2022-01-03 20:03:15 +00:00
0ae774b95c chore(deps): update typescript-eslint monorepo to v5.9.0 2022-01-03 19:04:33 +00:00
be899c3eb0 chore(deps): update dependency eslint to v8.6.0 2022-01-03 15:40:28 +00:00
dc02827a33 chore(deps): update dependency slugify to v1.6.5 2022-01-03 15:39:59 +00:00
951e511bf9
chore(deps): update dependency postcss-preset-env to v7.2.0 2022-01-03 16:21:43 +01:00
5f1d936ca4 fix(deps): update dependency v-tooltip to v4.0.0-beta.5 2022-01-03 15:15:05 +00:00
c3845e5690 chore(deps): update dependency netlify-cli to v8.6.1 2022-01-03 14:03:10 +00:00
f4db2df37b chore(deps): update dependency caniuse-lite to v1.0.30001295 2022-01-02 00:02:54 +00:00
5668fc7a2c chore(deps): update dependency esbuild to v0.14.10 2021-12-31 17:02:57 +00:00
12a0099fbf chore(deps): update dependency sass to v1.45.2 (#1271)
Reviewed-on: vikunja/frontend#1271
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-31 15:31:03 +00:00
7f4027cbe8 fix(deps): update dependency @vueuse/core to v7.5.1 (#1272)
Reviewed-on: vikunja/frontend#1272
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-31 15:30:46 +00:00
b57aa33cae fix(deps): update dependency @vueuse/router to v7.5.1 (#1273)
Reviewed-on: vikunja/frontend#1273
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-31 13:06:00 +00:00
b80de79594 fix(deps): update dependency @vueuse/router to v7.4.3 2021-12-31 00:02:51 +00:00
ead3e45a59 chore(deps): update dependency @types/jest to v27.4.0 2021-12-30 21:02:53 +00:00
4a7d2d8414 feat: save and restore the user language on the server (#1181)
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: vikunja/frontend#1181
Reviewed-by: Dominik Pschenitschni <dpschen@noreply.kolaente.de>
Co-authored-by: konrad <k@knt.li>
Co-committed-by: konrad <k@knt.li>
2021-12-30 20:20:45 +00:00
0befa58908 fix: blockquote styling in dark mode 2021-12-30 15:59:59 +00:00
8ae84eaf42 fix: padding and centering of the kanban limit and dropdown 2021-12-30 15:59:59 +00:00
cd10bc9d7a fix(gantt): use function to create default date 2021-12-30 13:05:29 +00:00
f4545fbe2f fix(deps): update dependency @vueuse/core to v7.4.3 (#1266)
Reviewed-on: vikunja/frontend#1266
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-30 12:04:16 +00:00
c6ffe8acab chore(deps): update dependency vite to v2.7.10 (#1265)
Reviewed-on: vikunja/frontend#1265
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-30 08:52:41 +00:00
2fb16f9a77 fix(deps): update dependency vue-i18n to v9.2.0-beta.26 (#1263)
Reviewed-on: vikunja/frontend#1263
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-29 23:13:35 +00:00
be427449e4 chore(deps): update dependency netlify-cli to v8.6.0 (#1259)
Reviewed-on: vikunja/frontend#1259
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-29 20:56:08 +00:00
5e889ebe36 chore(deps): update dependency autoprefixer to v10.4.1 (#1260)
Reviewed-on: vikunja/frontend#1260
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-29 20:06:07 +00:00
8c62c96109 chore(deps): update dependency esbuild to v0.14.9 (#1258)
Reviewed-on: vikunja/frontend#1258
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-29 13:34:16 +00:00
7908a1e657 chore(deps): update dependency caniuse-lite to v1.0.30001294 (#1257)
Reviewed-on: vikunja/frontend#1257
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-28 22:03:48 +00:00
09b62da2ae fix(deps): update dependency date-fns to v2.28.0 (#1256)
Reviewed-on: vikunja/frontend#1256
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-28 17:30:07 +00:00
076494afa5 chore(deps): update dependency netlify-cli to v8.5.0 (#1255)
Reviewed-on: vikunja/frontend#1255
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-28 17:12:35 +00:00
b604d5da75 chore(deps): update dependency vite to v2.7.9 (#1254)
Reviewed-on: vikunja/frontend#1254
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-28 17:11:48 +00:00
59889cccf3 chore(deps): update typescript-eslint monorepo to v5.8.1 (#1253)
Reviewed-on: vikunja/frontend#1253
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-27 19:19:38 +00:00
f8ffb428d3 chore(deps): update dependency rollup to v2.62.0 (#1246)
Reviewed-on: vikunja/frontend#1246
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-27 11:00:46 +00:00
c2ea932c09 [skip ci] Updated translations via Crowdin 2021-12-26 19:09:18 +00:00
033d97e919 chore(deps): update dependency @vue/eslint-config-typescript to v10 (#1243)
Reviewed-on: vikunja/frontend#1243
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-26 17:45:55 +00:00
4694c14760 chore(deps): update dependency vue-tsc to v0.30.1 (#1248)
Reviewed-on: vikunja/frontend#1248
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2021-12-26 17:45:33 +00:00
95 changed files with 1776 additions and 2513 deletions

View File

@ -98,6 +98,15 @@ steps:
depends_on: depends_on:
- dependencies - dependencies
- name: typecheck
failure: ignore
image: node:16
pull: true
commands:
- yarn typecheck
depends_on:
- dependencies
- name: test-frontend - name: test-frontend
image: cypress/browsers:node16.5.0-chrome94-ff93 image: cypress/browsers:node16.5.0-chrome94-ff93
pull: true pull: true

View File

@ -31,7 +31,7 @@ describe('Lists', () => {
cy.url() cy.url()
.should('contain', '/namespaces/1/list') .should('contain', '/namespaces/1/list')
cy.get('.card-header-title') cy.get('.card-header-title')
.contains('Create a new list') .contains('New list')
cy.get('input.input') cy.get('input.input')
.type('New List') .type('New List')
cy.get('.button') cy.get('.button')
@ -101,7 +101,7 @@ describe('Lists', () => {
.click() .click()
cy.url() cy.url()
.should('contain', '/settings/delete') .should('contain', '/settings/delete')
cy.get('.modal-mask .modal-container .modal-content .actions a.button') cy.get('[data-cy="modalPrimary"]')
.contains('Do it') .contains('Do it')
.click() .click()
@ -392,7 +392,7 @@ describe('Lists', () => {
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input') cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input')
.first() .first()
.type(3) .type(3)
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field a.button.is-primary') cy.get('[data-cy="setBucketLimit"]')
.first() .first()
.click() .click()

View File

@ -15,7 +15,7 @@ describe('Namepaces', () => {
it('Should be all there', () => { it('Should be all there', () => {
cy.visit('/namespaces') cy.visit('/namespaces')
cy.get('.namespace h1 span') cy.get('[data-cy="namespace-title"]')
.should('contain', namespaces[0].title) .should('contain', namespaces[0].title)
}) })
@ -23,14 +23,14 @@ describe('Namepaces', () => {
const newNamespaceTitle = 'New Namespace' const newNamespaceTitle = 'New Namespace'
cy.visit('/namespaces') cy.visit('/namespaces')
cy.get('a.button') cy.get('[data-cy="new-namespace"]')
.contains('Create a new namespace') .should('contain', 'New namespace')
.click() .click()
cy.url() cy.url()
.should('contain', '/namespaces/new') .should('contain', '/namespaces/new')
cy.get('.card-header-title') cy.get('.card-header-title')
.should('contain', 'Create a new namespace') .should('contain', 'New namespace')
cy.get('input.input') cy.get('input.input')
.type(newNamespaceTitle) .type(newNamespaceTitle)
cy.get('.button') cy.get('.button')
@ -72,7 +72,7 @@ describe('Namepaces', () => {
cy.get('.namespace-container .menu.namespaces-lists') cy.get('.namespace-container .menu.namespaces-lists')
.should('contain', newNamespaceName) .should('contain', newNamespaceName)
.should('not.contain', newNamespaces[0].title) .should('not.contain', newNamespaces[0].title)
cy.get('.content.namespaces-list') cy.get('[data-cy="namespaces-list"]')
.should('contain', newNamespaceName) .should('contain', newNamespaceName)
.should('not.contain', newNamespaces[0].title) .should('not.contain', newNamespaces[0].title)
}) })
@ -89,7 +89,7 @@ describe('Namepaces', () => {
.click() .click()
cy.url() cy.url()
.should('contain', '/settings/delete') .should('contain', '/settings/delete')
cy.get('.modal-mask .modal-container .modal-content .actions a.button') cy.get('[data-cy="modalPrimary"]')
.contains('Do it') .contains('Do it')
.click() .click()
@ -116,30 +116,30 @@ describe('Namepaces', () => {
// Initial // Initial
cy.visit('/namespaces') cy.visit('/namespaces')
cy.get('.namespaces-list .namespace') cy.get('.namespace')
.should('not.contain', 'Archived') .should('not.contain', 'Archived')
// Show archived // Show archived
cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span') cy.get('[data-cy="show-archived-check"] label.check span')
.should('be.visible') .should('be.visible')
.click() .click()
cy.get('.namespaces-list .fancycheckbox.show-archived-check input') cy.get('[data-cy="show-archived-check"] input')
.should('be.checked') .should('be.checked')
cy.get('.namespaces-list .namespace') cy.get('.namespace')
.should('contain', 'Archived') .should('contain', 'Archived')
// Don't show archived // Don't show archived
cy.get('.namespaces-list .fancycheckbox.show-archived-check label.check span') cy.get('[data-cy="show-archived-check"] label.check span')
.should('be.visible') .should('be.visible')
.click() .click()
cy.get('.namespaces-list .fancycheckbox.show-archived-check input') cy.get('[data-cy="show-archived-check"] input')
.should('not.be.checked') .should('not.be.checked')
// Second time visiting after unchecking // Second time visiting after unchecking
cy.visit('/namespaces') cy.visit('/namespaces')
cy.get('.namespaces-list .fancycheckbox.show-archived-check input') cy.get('[data-cy="show-archived-check"] input')
.should('not.be.checked') .should('not.be.checked')
cy.get('.namespaces-list .namespace') cy.get('.namespace')
.should('not.contain', 'Archived') .should('not.contain', 'Archived')
}) })
}) })

View File

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

View File

@ -128,7 +128,7 @@ describe('Task', () => {
cy.visit(`/tasks/${tasks[0].id}`) cy.visit(`/tasks/${tasks[0].id}`)
cy.get('.task-view .action-buttons .button') cy.get('.task-view .action-buttons .button')
.contains('Done!') .contains('Mark task done!')
.click() .click()
cy.get('.task-view .heading .is-done') cy.get('.task-view .heading .is-done')
@ -168,7 +168,7 @@ describe('Task', () => {
.click() .click()
cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll') cy.get('.task-view .details.content.description .editor .vue-easymde .EasyMDEContainer .CodeMirror-scroll')
.type('{selectall}New Description') .type('{selectall}New Description')
cy.get('.task-view .details.content.description .editor a') cy.get('[data-cy="saveEditor"]')
.contains('Save') .contains('Save')
.click() .click()
@ -404,7 +404,7 @@ describe('Task', () => {
cy.get('.datepicker .datepicker-popup a') cy.get('.datepicker .datepicker-popup a')
.contains('Tomorrow') .contains('Tomorrow')
.click() .click()
cy.get('.datepicker .datepicker-popup a.button') cy.get('[data-cy="closeDatepicker"]')
.contains('Confirm') .contains('Confirm')
.click() .click()

View File

@ -18,7 +18,7 @@ describe('User Settings', () => {
.trigger('mousedown', {which: 1}) .trigger('mousedown', {which: 1})
.trigger('mousemove', {clientY: 100}) .trigger('mousemove', {clientY: 100})
.trigger('mouseup') .trigger('mouseup')
cy.get('a.button.is-primary') cy.get('[data-cy="uploadAvatar"]')
.contains('Upload Avatar') .contains('Upload Avatar')
.click() .click()
@ -33,7 +33,7 @@ describe('User Settings', () => {
cy.get('.general-settings .control input.input') cy.get('.general-settings .control input.input')
.first() .first()
.type('Lorem Ipsum') .type('Lorem Ipsum')
cy.get('.card.general-settings .button.is-primary') cy.get('[data-cy="saveGeneralSettings"]')
.contains('Save') .contains('Save')
.click() .click()

View File

@ -9,10 +9,10 @@
"build": "vite build && workbox copyLibraries dist/", "build": "vite build && workbox copyLibraries dist/",
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/", "build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
"build:dev": "vite build -m development --outDir dist-dev/", "build:dev": "vite build -m development --outDir dist-dev/",
"typecheck": "vue-tsc --noEmit",
"lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts", "lint": "eslint --ignore-pattern '*.test.*' ./src --ext .vue,.js,.ts",
"lint:markup": "vue-tsc --noEmit",
"cypress:open": "cypress open", "cypress:open": "cypress open",
"test:unit": "jest", "test:unit": "vitest run",
"test:frontend": "cypress run", "test:frontend": "cypress run",
"browserslist:update": "npx browserslist@latest --update-db" "browserslist:update": "npx browserslist@latest --update-db"
}, },
@ -21,32 +21,33 @@
"@kyvg/vue3-notification": "2.3.4", "@kyvg/vue3-notification": "2.3.4",
"@sentry/tracing": "6.16.1", "@sentry/tracing": "6.16.1",
"@sentry/vue": "6.16.1", "@sentry/vue": "6.16.1",
"@types/is-touch-device": "1.0.0",
"@vue/compat": "3.2.26", "@vue/compat": "3.2.26",
"@vueuse/core": "7.4.1", "@vueuse/core": "7.5.2",
"@vueuse/router": "7.4.1", "@vueuse/router": "7.5.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.0", "codemirror": "5.65.0",
"copy-to-clipboard": "3.3.1", "copy-to-clipboard": "3.3.1",
"date-fns": "2.27.0", "date-fns": "2.28.0",
"dompurify": "2.3.4", "dompurify": "2.3.4",
"easymde": "2.15.0", "easymde": "2.15.0",
"flatpickr": "4.6.9", "flatpickr": "4.6.9",
"flexsearch": "0.7.21", "flexsearch": "0.7.21",
"highlight.js": "11.3.1", "highlight.js": "11.4.0",
"is-touch-device": "1.0.1", "is-touch-device": "1.0.1",
"lodash.clonedeep": "4.5.0", "lodash.clonedeep": "4.5.0",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"marked": "4.0.8", "marked": "4.0.9",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
"snake-case": "3.0.4", "snake-case": "3.0.4",
"ufo": "0.7.9", "ufo": "0.7.9",
"v-tooltip": "4.0.0-beta.2", "v-tooltip": "4.0.0-beta.13",
"vue": "3.2.26", "vue": "3.2.26",
"vue-advanced-cropper": "2.7.0", "vue-advanced-cropper": "2.7.1",
"vue-drag-resize": "2.0.3", "vue-drag-resize": "2.0.3",
"vue-flatpickr-component": "9.0.5", "vue-flatpickr-component": "9.0.5",
"vue-i18n": "9.2.0-beta.25", "vue-i18n": "9.2.0-beta.26",
"vue-router": "4.0.12", "vue-router": "4.0.12",
"vuedraggable": "4.1.0", "vuedraggable": "4.1.0",
"vuex": "4.0.2", "vuex": "4.0.2",
@ -59,37 +60,36 @@
"@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/vue-fontawesome": "3.0.0-5", "@fortawesome/vue-fontawesome": "3.0.0-5",
"@types/flexsearch": "0.7.2", "@types/flexsearch": "0.7.2",
"@types/jest": "27.0.3", "@typescript-eslint/eslint-plugin": "5.9.0",
"@typescript-eslint/eslint-plugin": "5.8.0", "@typescript-eslint/parser": "5.9.0",
"@typescript-eslint/parser": "5.8.0",
"@vitejs/plugin-legacy": "1.6.4", "@vitejs/plugin-legacy": "1.6.4",
"@vitejs/plugin-vue": "2.0.1", "@vitejs/plugin-vue": "2.0.1",
"@vue/eslint-config-typescript": "9.1.0", "@vue/eslint-config-typescript": "10.0.0",
"autoprefixer": "10.4.0", "autoprefixer": "10.4.2",
"axios": "0.24.0", "axios": "0.24.0",
"browserslist": "4.19.1", "browserslist": "4.19.1",
"caniuse-lite": "1.0.30001292", "caniuse-lite": "1.0.30001298",
"cypress": "9.2.0", "cypress": "9.2.0",
"cypress-file-upload": "5.0.8", "cypress-file-upload": "5.0.8",
"esbuild": "0.14.8", "esbuild": "0.14.10",
"eslint": "8.5.0", "eslint": "8.6.0",
"eslint-plugin-vue": "8.2.0", "eslint-plugin-vue": "8.2.0",
"express": "4.17.2", "express": "4.17.2",
"faker": "5.5.3", "faker": "5.5.3",
"jest": "27.4.5", "netlify-cli": "8.6.15",
"netlify-cli": "8.4.2", "happy-dom": "2.25.1",
"postcss": "8.4.5", "postcss": "8.4.5",
"postcss-preset-env": "7.1.0", "postcss-preset-env": "7.2.0",
"rollup": "2.61.1", "rollup": "2.63.0",
"rollup-plugin-visualizer": "5.5.2", "rollup-plugin-visualizer": "5.5.2",
"sass": "1.45.1", "sass": "1.47.0",
"slugify": "1.6.4", "slugify": "1.6.5",
"ts-jest": "27.1.2",
"typescript": "4.5.4", "typescript": "4.5.4",
"vite": "2.7.7", "vite": "2.7.10",
"vite-plugin-pwa": "0.11.12", "vite-plugin-pwa": "0.11.12",
"vite-svg-loader": "3.1.1", "vite-svg-loader": "3.1.1",
"vue-tsc": "0.30.0", "vitest": "0.0.139",
"vue-tsc": "0.30.2",
"wait-on": "6.0.0", "wait-on": "6.0.0",
"workbox-cli": "6.4.2" "workbox-cli": "6.4.2"
}, },
@ -144,24 +144,6 @@
"autoprefixer": {} "autoprefixer": {}
} }
}, },
"jest": {
"testPathIgnorePatterns": [
"cypress"
],
"testEnvironment": "jsdom",
"preset": "ts-jest",
"roots": [
"<rootDir>/src"
],
"transform": {
"^.+\\.(js|tsx?)$": "ts-jest"
},
"moduleFileExtensions": [
"ts",
"js",
"json"
]
},
"license": "AGPL-3.0-or-later", "license": "AGPL-3.0-or-later",
"packageManager": "yarn@1.22.17" "packageManager": "yarn@1.22.17"
} }

View File

@ -1,16 +1,14 @@
<template> <template>
<ready :class="{'is-touch': isTouch}"> <ready>
<div :class="{'is-hidden': !online}"> <template v-if="authUser">
<template v-if="authUser"> <top-navigation/>
<top-navigation/> <content-auth/>
<content-auth/> </template>
</template> <content-link-share v-else-if="authLinkShare"/>
<content-link-share v-else-if="authLinkShare"/> <no-auth-wrapper v-else>
<no-auth-wrapper v-else> <router-view/>
<router-view/> </no-auth-wrapper>
</no-auth-wrapper> <Notification/>
<Notification/>
</div>
<transition name="fade"> <transition name="fade">
<keyboard-shortcuts v-if="keyboardShortcutsActive"/> <keyboard-shortcuts v-if="keyboardShortcutsActive"/>
@ -19,12 +17,11 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {computed, watch, watchEffect, Ref} from 'vue' import {computed, watch, Ref} from 'vue'
import {useRouter} from 'vue-router' import {useRouter} from 'vue-router'
import {useRouteQuery} from '@vueuse/router' import {useRouteQuery} from '@vueuse/router'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {useOnline} from '@vueuse/core'
import isTouchDevice from 'is-touch-device' import isTouchDevice from 'is-touch-device'
import {success} from '@/message' import {success} from '@/message'
@ -38,17 +35,14 @@ 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 {ONLINE} from '@/store/mutation-types'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {useBodyClass} from '@/composables/useBodyClass'
const store = useStore() const store = useStore()
const online = useOnline()
watchEffect(() => store.commit(ONLINE, online.value))
const router = useRouter() const router = useRouter()
const isTouch = computed(isTouchDevice) useBodyClass('is-touch', isTouchDevice)
const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive) const keyboardShortcutsActive = computed(() => store.state.keyboardShortcutsActive)
const authUser = computed(() => store.getters['auth/authUser']) const authUser = computed(() => store.getters['auth/authUser'])

View File

@ -0,0 +1,118 @@
<template>
<component
:is="componentNodeName"
class="base-button"
:class="{ 'base-button--type-button': isButton }"
v-bind="elementBindings"
:disabled="disabled || undefined"
>
<slot />
</component>
</template>
<script lang="ts">
// see https://v3.vuejs.org/api/sfc-script-setup.html#usage-alongside-normal-script
export default {
inheritAttrs: false,
}
</script>
<script lang="ts" setup>
// this component removes styling differences between links / vue-router links and button elements
// by doing so we make it easy abstract the functionality from style and enable easier and semantic
// correct button and link usage. Also see: https://css-tricks.com/a-complete-guide-to-links-and-buttons/#accessibility-considerations
// the component tries to heuristically determine what it should be checking the props (see the
// componentNodeName and elementBindings ref for this).
// NOTE: Do NOT use buttons with @click to push routes. => Use router-links instead!
import { ref, watchEffect, computed, useAttrs, PropType } from 'vue'
const BASE_BUTTON_TYPES_MAP = Object.freeze({
button: 'button',
submit: 'submit',
})
type BaseButtonTypes = keyof typeof BASE_BUTTON_TYPES_MAP
const props = defineProps({
type: {
type: String as PropType<BaseButtonTypes>,
default: 'button',
},
disabled: {
type: Boolean,
default: false,
},
})
const componentNodeName = ref<Node['nodeName']>('button')
interface ElementBindings {
type?: string;
rel?: string,
}
const elementBindings = ref({})
const attrs = useAttrs()
watchEffect(() => {
// by default this component is a button element with the attribute of the type "button" (default prop value)
let nodeName = 'button'
let bindings: ElementBindings = {type: props.type}
// if we find a "to" prop we set it as router-link
if ('to' in attrs) {
nodeName = 'router-link'
bindings = {}
}
// if there is a href we assume the user wants an external link via a link element
// we also set the attribute rel to "noopener" but make it possible to overwrite this by the user.
if ('href' in attrs) {
nodeName = 'a'
bindings = {rel: 'noopener'}
}
componentNodeName.value = nodeName
elementBindings.value = {
...bindings,
...attrs,
}
})
const isButton = computed(() => componentNodeName.value === 'button')
</script>
<style lang="scss">
// NOTE: we do not use scoped styles to reduce specifity and make it easy to overwrite
// We reset the default styles of a button element to enable easier styling
:where(.base-button--type-button) {
border: 0;
margin: 0;
padding: 0;
text-decoration: none;
background-color: transparent;
text-align: center;
appearance: none;
}
:where(.base-button) {
cursor: pointer;
display: block;
color: inherit;
font: inherit;
user-select: none;
pointer-events: auto; // disable possible resets
&:focus {
outline: transparent;
}
&[disabled] {
cursor: default;
}
}
</style>

View File

@ -1,6 +1,6 @@
<template> <template>
<div <div
:class="[background ? 'has-background' : '', $route.name+'-view']" :class="[background ? 'has-background' : '', $route.name as string +'-view']"
:style="{'background-image': `url(${background})`}" :style="{'background-image': `url(${background})`}"
class="link-share-container" class="link-share-container"
> >

View File

@ -555,4 +555,8 @@ $vikunja-nav-selected-width: 0.4rem;
width: 32px; width: 32px;
flex-shrink: 0; flex-shrink: 0;
} }
.namespaces-list.loader-container.is-loading {
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
}
</style> </style>

View File

@ -37,7 +37,7 @@
<dropdown class="is-right" ref="usernameDropdown"> <dropdown class="is-right" ref="usernameDropdown">
<template #trigger> <template #trigger>
<x-button <x-button
type="secondary" variant="secondary"
:shadow="false"> :shadow="false">
<span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span> <span class="username">{{ userInfo.name !== '' ? userInfo.name : userInfo.username }}</span>
<span class="icon is-small"> <span class="icon is-small">

View File

@ -1,79 +1,64 @@
<template> <template>
<a <BaseButton
class="button" class="button"
:class="{ :class="[
'is-loading': loading, variantClass,
'has-no-shadow': !shadow, {
'is-primary': type === 'primary', 'is-loading': loading,
'is-outlined': type === 'secondary', 'has-no-shadow': !shadow || variant === 'tertiary',
'is-text is-inverted has-no-shadow underline-none': }
type === 'tertary', ]"
}"
:disabled="disabled || null"
@click="click"
:href="href !== '' ? href : null"
> >
<icon :icon="icon" v-if="showIconOnly"/> <icon :icon="icon" v-if="showIconOnly"/>
<span class="icon is-small" v-else-if="icon !== ''"> <span class="icon is-small" v-else-if="icon !== ''">
<icon :icon="icon"/> <icon :icon="icon"/>
</span> </span>
<slot></slot> <slot />
</a> </BaseButton>
</template> </template>
<script> <script lang="ts">
export default { export default {
name: 'x-button', name: 'x-button',
props: {
type: {
type: String,
default: 'primary',
},
href: {
type: String,
default: '',
},
to: {
default: false,
},
icon: {
default: '',
},
loading: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: true,
},
disabled: {
type: Boolean,
default: false,
},
},
emits: ['click'],
computed: {
showIconOnly() {
return this.icon !== '' && typeof this.$slots.default === 'undefined'
},
},
methods: {
click(e) {
if (this.disabled) {
return
}
if (this.to !== false) {
this.$router.push(this.to)
}
this.$emit('click', e)
},
},
} }
</script> </script>
<script setup lang="ts">
import {computed, useSlots, PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
const BUTTON_TYPES_MAP = Object.freeze({
primary: 'is-primary',
secondary: 'is-outlined',
tertiary: 'is-text is-inverted underline-none',
})
type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
const props = defineProps({
variant: {
type: String as PropType<ButtonTypes>,
default: 'primary',
},
icon: {
default: '',
},
loading: {
type: Boolean,
default: false,
},
shadow: {
type: Boolean,
default: true,
},
})
const variantClass = computed(() => BUTTON_TYPES_MAP[props.variant])
const slots = useSlots()
const showIconOnly = computed(() => props.icon !== '' && typeof slots.default === 'undefined')
</script>
<style lang="scss" scoped> <style lang="scss" scoped>
.button { .button {
transition: all $transition; transition: all $transition;
@ -83,8 +68,8 @@ export default {
font-weight: bold; font-weight: bold;
height: $button-height; height: $button-height;
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
display: inline-flex;
&.is-hovered,
&:hover { &:hover {
box-shadow: var(--shadow-md); box-shadow: var(--shadow-md);
} }
@ -106,9 +91,10 @@ export default {
color: var(--white); color: var(--white);
} }
&.is-small { }
border-radius: $radius;
} .is-small {
border-radius: $radius;
} }
.underline-none { .underline-none {

View File

@ -27,7 +27,7 @@
@click="reset" @click="reset"
class="is-small ml-2" class="is-small ml-2"
:shadow="false" :shadow="false"
type="secondary" variant="secondary"
> >
{{ $t('input.resetColor') }} {{ $t('input.resetColor') }}
</x-button> </x-button>

View File

@ -101,6 +101,7 @@
class="is-fullwidth" class="is-fullwidth"
:shadow="false" :shadow="false"
@click="close" @click="close"
v-cy="'closeDatepicker'"
> >
{{ $t('misc.confirm') }} {{ $t('misc.confirm') }}
</x-button> </x-button>

View File

@ -35,7 +35,7 @@
<a @click="toggleEdit">{{ $t('input.editor.edit') }}</a> <a @click="toggleEdit">{{ $t('input.editor.edit') }}</a>
</li> </li>
</ul> </ul>
<x-button v-else-if="isEditActive" @click="toggleEdit" type="secondary" :shadow="false"> <x-button v-else-if="isEditActive" @click="toggleEdit" variant="secondary" :shadow="false" v-cy="'saveEditor'">
{{ $t('misc.save') }} {{ $t('misc.save') }}
</x-button> </x-button>
</template> </template>
@ -348,6 +348,10 @@ $editor-border-color: #ddd;
color: var(--grey-400) !important; color: var(--grey-400) !important;
font-style: italic; font-style: italic;
} }
&-cursor {
border-color: var(--grey-700);
}
} }
.editor-preview { .editor-preview {

View File

@ -1,7 +1,7 @@
<template> <template>
<x-button <x-button
v-if="hasFilters" v-if="hasFilters"
type="secondary" variant="secondary"
@click="clearFilters" @click="clearFilters"
> >
{{ $t('filters.clear') }} {{ $t('filters.clear') }}
@ -10,7 +10,7 @@
<template #trigger="{toggle}"> <template #trigger="{toggle}">
<x-button <x-button
@click.prevent.stop="toggle()" @click.prevent.stop="toggle()"
type="secondary" variant="secondary"
icon="filter" icon="filter"
> >
{{ $t('filters.title') }} {{ $t('filters.title') }}

View File

@ -33,7 +33,9 @@ import {useStore} from 'vuex'
import ListService from '@/services/list' import ListService from '@/services/list'
const background = ref(null) import {colorIsDark} from '@/helpers/color/colorIsDark'
const background = ref<string | null>(null)
const backgroundLoading = ref(false) const backgroundLoading = ref(false)
const props = defineProps({ const props = defineProps({

View File

@ -14,25 +14,25 @@
</div> </div>
<footer class="modal-card-foot is-flex is-justify-content-flex-end"> <footer class="modal-card-foot is-flex is-justify-content-flex-end">
<x-button <x-button
v-if="tertiary !== ''"
:shadow="false" :shadow="false"
type="tertary" variant="tertiary"
@click.prevent.stop="$emit('tertary')" @click.prevent.stop="$emit('tertiary')"
v-if="tertary !== ''"
> >
{{ tertary }} {{ tertiary }}
</x-button> </x-button>
<x-button <x-button
type="secondary" variant="secondary"
@click.prevent.stop="$router.back()" @click.prevent.stop="$router.back()"
> >
{{ $t('misc.cancel') }} {{ $t('misc.cancel') }}
</x-button> </x-button>
<x-button <x-button
type="primary" v-if="primaryLabel !== ''"
variant="primary"
@click.prevent.stop="primary" @click.prevent.stop="primary"
:icon="primaryIcon" :icon="primaryIcon"
:disabled="primaryDisabled" :disabled="primaryDisabled"
v-if="primaryLabel !== ''"
> >
{{ primaryLabel }} {{ primaryLabel }}
</x-button> </x-button>
@ -65,7 +65,7 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
tertary: { tertiary: {
type: String, type: String,
default: '', default: '',
}, },
@ -78,7 +78,7 @@ export default {
default: false, default: false,
}, },
}, },
emits: ['create', 'primary', 'tertary'], emits: ['create', 'primary', 'tertiary'],
methods: { methods: {
primary() { primary() {
this.$emit('create') this.$emit('create')

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="dropdown is-right is-active" ref="dropdown"> <div class="dropdown is-right is-active" ref="dropdown">
<div class="dropdown-trigger" @click="open = !open"> <div class="dropdown-trigger is-flex" @click="open = !open">
<slot name="trigger" :close="close"> <slot name="trigger" :close="close">
<icon :icon="triggerIcon" class="icon"/> <icon :icon="triggerIcon" class="icon"/>
</slot> </slot>

View File

@ -13,7 +13,7 @@
<section class="content"> <section class="content">
<div> <div>
<h2 class="title" v-if="title">{{ title }}</h2> <h2 class="title" v-if="title">{{ title }}</h2>
<api-config @foundApi="hasApiUrl = true"/> <api-config/>
<slot/> <slot/>
</div> </div>
<legal/> <legal/>
@ -23,9 +23,9 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import Logo from '@/components/home/Logo' import Logo from '@/components/home/Logo.vue'
import Message from '@/components/misc/message' import Message from '@/components/misc/message.vue'
import Legal from '@/components/misc/legal' import Legal from '@/components/misc/legal.vue'
import ApiConfig from '@/components/misc/api-config.vue' import ApiConfig from '@/components/misc/api-config.vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {computed} from 'vue' import {computed} from 'vue'

View File

@ -26,7 +26,7 @@
@click="action.callback" @click="action.callback"
:shadow="false" :shadow="false"
class="is-small" class="is-small"
type="secondary" variant="secondary"
v-for="(action, i) in item.data.actions" v-for="(action, i) in item.data.actions"
> >
{{ action.title }} {{ action.title }}

View File

@ -50,11 +50,12 @@ import Message from '@/components/misc/message.vue'
import NoAuthWrapper from '@/components/misc/no-auth-wrapper.vue' 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'
const store = useStore() const store = useStore()
const ready = computed(() => store.state.vikunjaReady) const ready = computed(() => store.state.vikunjaReady)
const online = computed(() => store.state.online) const online = useOnline()
const error = ref('') const error = ref('')
const showLoading = computed(() => !ready.value && error.value === '') const showLoading = computed(() => !ready.value && error.value === '')

View File

@ -1,6 +1,6 @@
<template> <template>
<x-button <x-button
type="secondary" variant="secondary"
:icon="icon" :icon="icon"
v-tooltip="tooltipText" v-tooltip="tooltipText"
@click="changeSubscription" @click="changeSubscription"

View File

@ -31,14 +31,15 @@
<div class="actions"> <div class="actions">
<x-button <x-button
@click="$emit('close')" @click="$emit('close')"
type="tertary" variant="tertiary"
class="has-text-danger" class="has-text-danger"
> >
{{ $t('misc.cancel') }} {{ $t('misc.cancel') }}
</x-button> </x-button>
<x-button <x-button
@click="$emit('submit')" @click="$emit('submit')"
type="primary" variant="primary"
v-cy="'modalPrimary'"
:shadow="false" :shadow="false"
> >
{{ $t('misc.doit') }} {{ $t('misc.doit') }}

View File

@ -42,7 +42,7 @@ import {useI18n} from 'vue-i18n'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import { tryOnMounted, debouncedWatch, useWindowSize, MaybeRef } from '@vueuse/core' import { tryOnMounted, debouncedWatch, useWindowSize, MaybeRef } from '@vueuse/core'
import TaskService from '../../services/task' 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'
function cleanupTitle(title: string) { function cleanupTitle(title: string) {
@ -117,9 +117,6 @@ function useAutoHeightTextarea(value: MaybeRef<string>) {
return textarea return textarea
} }
const emit = defineEmits(['taskAdded'])
const props = defineProps({ const props = defineProps({
defaultPosition: { defaultPosition: {
type: Number, type: Number,
@ -127,8 +124,7 @@ const props = defineProps({
}, },
}) })
const taskService = shallowReactive(new TaskService()) const emit = defineEmits(['taskAdded'])
const errorMessage = ref('')
const newTaskTitle = ref('') const newTaskTitle = ref('')
const newTaskInput = useAutoHeightTextarea(newTaskTitle) const newTaskInput = useAutoHeightTextarea(newTaskTitle)
@ -136,6 +132,9 @@ const newTaskInput = useAutoHeightTextarea(newTaskTitle)
const { t } = useI18n() const { t } = useI18n()
const store = useStore() const store = useStore()
const taskService = shallowReactive(new TaskService())
const errorMessage = ref('')
async function addTask() { async function addTask() {
if (newTaskTitle.value === '') { if (newTaskTitle.value === '') {
errorMessage.value = t('list.create.addTitleRequired') errorMessage.value = t('list.create.addTitleRequired')

View File

@ -78,7 +78,6 @@
<script> <script>
import AsyncEditor from '@/components/input/AsyncEditor' import AsyncEditor from '@/components/input/AsyncEditor'
import ListService from '../../services/list'
import TaskService from '../../services/task' import TaskService from '../../services/task'
import TaskModel from '../../models/task' import TaskModel from '../../models/task'
import priorities from '../../models/constants/priorities' import priorities from '../../models/constants/priorities'
@ -90,14 +89,10 @@ export default {
name: 'edit-task', name: 'edit-task',
data() { data() {
return { return {
listId: this.$route.params.id,
listService: new ListService(),
taskService: new TaskService(), taskService: new TaskService(),
priorities: priorities, priorities: priorities,
list: {},
editorActive: false, editorActive: false,
newTask: new TaskModel(),
isTaskEdit: false, isTaskEdit: false,
taskEditTask: TaskModel, taskEditTask: TaskModel,
} }

View File

@ -183,6 +183,8 @@ import {mapState} from 'vuex'
import Rights from '../../models/constants/rights.json' import Rights from '../../models/constants/rights.json'
import FilterPopup from '@/components/list/partials/filter-popup.vue' import FilterPopup from '@/components/list/partials/filter-popup.vue'
import {colorIsDark} from '@/helpers/color/colorIsDark'
export default { export default {
name: 'GanttChart', name: 'GanttChart',
components: { components: {
@ -201,10 +203,10 @@ export default {
default: false, default: false,
}, },
dateFrom: { dateFrom: {
default: new Date(new Date().setDate(new Date().getDate() - 15)), default: () => new Date(new Date().setDate(new Date().getDate() - 15)),
}, },
dateTo: { dateTo: {
default: new Date(new Date().setDate(new Date().getDate() + 30)), default: () => new Date(new Date().setDate(new Date().getDate() + 30)),
}, },
// The width of a day in pixels, used to calculate all sorts of things. // The width of a day in pixels, used to calculate all sorts of things.
dayWidth: { dayWidth: {
@ -252,6 +254,7 @@ export default {
canWrite: (state) => state.currentList.maxRight > Rights.READ, canWrite: (state) => state.currentList.maxRight > Rights.READ,
}), }),
methods: { methods: {
colorIsDark,
buildTheGanttChart() { buildTheGanttChart() {
this.setDates() this.setDates()
this.prepareGanttDays() this.prepareGanttDays()

View File

@ -83,7 +83,7 @@
@click="$refs.files.click()" @click="$refs.files.click()"
class="mb-4" class="mb-4"
icon="cloud-upload-alt" icon="cloud-upload-alt"
type="secondary" variant="secondary"
:shadow="false" :shadow="false"
> >
{{ $t('task.attachment.upload') }} {{ $t('task.attachment.upload') }}

View File

@ -8,21 +8,21 @@
<x-button <x-button
@click.prevent.stop="() => deferDays(1)" @click.prevent.stop="() => deferDays(1)"
:shadow="false" :shadow="false"
type="secondary" variant="secondary"
> >
{{ $t('task.deferDueDate.1day') }} {{ $t('task.deferDueDate.1day') }}
</x-button> </x-button>
<x-button <x-button
@click.prevent.stop="() => deferDays(3)" @click.prevent.stop="() => deferDays(3)"
:shadow="false" :shadow="false"
type="secondary" variant="secondary"
> >
{{ $t('task.deferDueDate.3days') }} {{ $t('task.deferDueDate.3days') }}
</x-button> </x-button>
<x-button <x-button
@click.prevent.stop="() => deferDays(7)" @click.prevent.stop="() => deferDays(7)"
:shadow="false" :shadow="false"
type="secondary" variant="secondary"
> >
{{ $t('task.deferDueDate.1week') }} {{ $t('task.deferDueDate.1week') }}
</x-button> </x-button>

View File

@ -73,6 +73,8 @@ import Done from '@/components/misc/Done.vue'
import Labels from '../../../components/tasks/partials/labels' import Labels from '../../../components/tasks/partials/labels'
import ChecklistSummary from './checklist-summary' import ChecklistSummary from './checklist-summary'
import {colorIsDark} from '@/helpers/color/colorIsDark'
export default { export default {
name: 'kanban-card', name: 'kanban-card',
components: { components: {
@ -98,6 +100,7 @@ export default {
}, },
}, },
methods: { methods: {
colorIsDark,
async toggleTaskDone(task) { async toggleTaskDone(task) {
this.loadingInternal = true this.loadingInternal = true
try { try {

View File

@ -6,7 +6,7 @@
class="is-pulled-right add-task-relation-button" class="is-pulled-right add-task-relation-button"
:class="{'is-active': showNewRelationForm}" :class="{'is-active': showNewRelationForm}"
v-tooltip="$t('task.relation.add')" v-tooltip="$t('task.relation.add')"
type="secondary" variant="secondary"
icon="plus" icon="plus"
:shadow="false" :shadow="false"
/> />

View File

@ -1,9 +1,9 @@
<template> <template>
<div class="control repeat-after-input"> <div class="control repeat-after-input">
<div class="buttons has-addons is-centered mt-2"> <div class="buttons has-addons is-centered mt-2">
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button> <x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'days')">{{ $t('task.repeat.everyDay') }}</x-button>
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button> <x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'weeks')">{{ $t('task.repeat.everyWeek') }}</x-button>
<x-button type="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button> <x-button variant="secondary" class="is-small" @click="() => setRepeatAfter(1, 'months')">{{ $t('task.repeat.everyMonth') }}</x-button>
</div> </div>
<div class="is-flex is-align-items-center mb-2"> <div class="is-flex is-align-items-center mb-2">
<label for="repeatMode" class="is-fullwidth"> <label for="repeatMode" class="is-fullwidth">

View File

@ -0,0 +1,16 @@
import {ref, watchEffect} from 'vue'
import {tryOnBeforeUnmount} from '@vueuse/core'
export function useBodyClass(className: string, defaultValue = false) {
const isActive = ref(defaultValue)
watchEffect(() => {
isActive.value
? document.body.classList.add(className)
: document.body.classList.remove(className)
})
tryOnBeforeUnmount(() => isActive.value && document.body.classList.remove(className))
return isActive
}

View File

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

View File

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

View File

@ -0,0 +1,14 @@
import {ref} from 'vue'
import {useOnline as useNetworkOnline, ConfigurableWindow} from '@vueuse/core'
export function useOnline(options?: ConfigurableWindow) {
const fakeOnlineState = !!import.meta.env.VITE_IS_ONLINE
if (fakeOnlineState) {
console.log('Setting fake online state', fakeOnlineState)
}
return fakeOnlineState
? ref(true)
: useNetworkOnline(options)
}

View File

@ -1,3 +1,5 @@
import {it, expect} from 'vitest'
import {calculateItemPosition} from './calculateItemPosition' import {calculateItemPosition} from './calculateItemPosition'
it('should calculate the task position', () => { it('should calculate the task position', () => {

View File

@ -1,3 +1,5 @@
import {describe, it, expect} from 'vitest'
import {findCheckboxesInText, getChecklistStatistics} from './checklistFromText' import {findCheckboxesInText, getChecklistStatistics} from './checklistFromText'
describe('Find checklists in text', () => { describe('Find checklists in text', () => {

View File

@ -1,3 +1,5 @@
import {test, expect} from 'vitest'
import {colorFromHex} from './colorFromHex' import {colorFromHex} from './colorFromHex'
test('hex', () => { test('hex', () => {

View File

@ -1,3 +1,5 @@
import {test, expect} from 'vitest'
import {colorIsDark} from './colorIsDark' import {colorIsDark} from './colorIsDark'
test('dark color', () => { test('dark color', () => {

View File

@ -1,3 +1,5 @@
import {describe, it, expect} from 'vitest'
import {filterLabelsByQuery} from './labels' import {filterLabelsByQuery} from './labels'
import {createNewIndexer} from '../indexes' import {createNewIndexer} from '../indexes'

View File

@ -1,3 +1,5 @@
import {test, expect} from 'vitest'
import {calculateDayInterval} from './calculateDayInterval' import {calculateDayInterval} from './calculateDayInterval'
const days = { const days = {

View File

@ -1,3 +1,5 @@
import {test, expect} from 'vitest'
import {calculateNearestHours} from './calculateNearestHours' import {calculateNearestHours} from './calculateNearestHours'
test('5:00', () => { test('5:00', () => {

View File

@ -1,3 +1,5 @@
import {test, expect} from 'vitest'
import {createDateFromString} from './createDateFromString' import {createDateFromString} from './createDateFromString'
test('YYYY-MM-DD HH:MM', () => { test('YYYY-MM-DD HH:MM', () => {

View File

@ -7,7 +7,7 @@
"lastViewed": "Naposledy zobrazeno", "lastViewed": "Naposledy zobrazeno",
"list": { "list": {
"newText": "Můžete vytvořit nový seznam pro své nové úkoly:", "newText": "Můžete vytvořit nový seznam pro své nové úkoly:",
"new": "Vytvořit nový seznam", "new": "New list",
"importText": "Nebo importujte své seznamy a úkoly z jiných služeb:", "importText": "Nebo importujte své seznamy a úkoly z jiných služeb:",
"import": "Importujte svá data do Vikunja" "import": "Importujte svá data do Vikunja"
} }
@ -157,7 +157,7 @@
"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",
"create": { "create": {
"header": "Vytvořit nový seznam", "header": "New list",
"titlePlaceholder": "Název seznamu přijde sem…", "titlePlaceholder": "Název seznamu přijde sem…",
"addTitleRequired": "Uveďte prosím název.", "addTitleRequired": "Uveďte prosím název.",
"createdSuccess": "Seznam byl úspěšně vytvořen.", "createdSuccess": "Seznam byl úspěšně vytvořen.",
@ -315,7 +315,7 @@
"namespaces": "Prostory", "namespaces": "Prostory",
"search": "Začni psát pro vyhledání prostoru…", "search": "Začni psát pro vyhledání prostoru…",
"create": { "create": {
"title": "Vytvořit nový prostor", "title": "New namespace",
"titleRequired": "Uveďte prosím název.", "titleRequired": "Uveďte prosím název.",
"explanation": "Prostor je kolekce seznamů, které můžete sdílet a používat k organizaci seznamů. Každý seznam patří do nějakého prostoru.", "explanation": "Prostor je kolekce seznamů, které můžete sdílet a používat k organizaci seznamů. Každý seznam patří do nějakého prostoru.",
"tooltip": "Co je prostor?", "tooltip": "Co je prostor?",
@ -383,7 +383,7 @@
"reminderRange": "Připomínky - období" "reminderRange": "Připomínky - období"
}, },
"create": { "create": {
"title": "Vytvořit uložený filtr", "title": "New Saved Filter",
"description": "Uložený filtr je virtuální seznam, který se počítá ze sady filtrů pokaždé, když je přístupný. Jakmile bude vytvořen, objeví se ve speciálním prostoru.", "description": "Uložený filtr je virtuální seznam, který se počítá ze sady filtrů pokaždé, když je přístupný. Jakmile bude vytvořen, objeví se ve speciálním prostoru.",
"action": "Vytvořit uložený filtr" "action": "Vytvořit uložený filtr"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Zuletzt angesehen", "lastViewed": "Zuletzt angesehen",
"list": { "list": {
"newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:", "newText": "Du kannst eine neue Liste für deine neuen Aufgaben erstellen:",
"new": "Eine neue Liste erstellen", "new": "New list",
"importText": "Oder importiere deine Listen und Aufgaben aus anderen Diensten in Vikunja:", "importText": "Oder importiere deine Listen und Aufgaben aus anderen Diensten in Vikunja:",
"import": "Deine Daten in Vikunja importieren" "import": "Deine Daten in Vikunja importieren"
} }
@ -36,7 +36,7 @@
"password": "Passwort", "password": "Passwort",
"passwordRepeat": "Gib dein Passwort erneut ein", "passwordRepeat": "Gib dein Passwort erneut ein",
"passwordPlaceholder": "z.B. •••••••••••", "passwordPlaceholder": "z.B. •••••••••••",
"forgotPassword": "Forgot your password?", "forgotPassword": "Passwort vergessen?",
"resetPassword": "Setze dein Passwort zurück", "resetPassword": "Setze dein Passwort zurück",
"resetPasswordAction": "Sende mir einen Link zum Zurücksetzen des Passworts", "resetPasswordAction": "Sende mir einen Link zum Zurücksetzen des Passworts",
"resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.", "resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.",
@ -103,7 +103,7 @@
"title": "Avatar", "title": "Avatar",
"initials": "Initialen", "initials": "Initialen",
"gravatar": "Gravatar", "gravatar": "Gravatar",
"marble": "Marble", "marble": "Murmel",
"upload": "Hochladen", "upload": "Hochladen",
"uploadAvatar": "Avatar hochladen", "uploadAvatar": "Avatar hochladen",
"statusUpdateSuccess": "Avatar-Status wurde erfolgreich aktualisiert.", "statusUpdateSuccess": "Avatar-Status wurde erfolgreich aktualisiert.",
@ -157,7 +157,7 @@
"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",
"create": { "create": {
"header": "Eine neue Liste erstellen", "header": "New list",
"titlePlaceholder": "Der Titel der Liste steht hier…", "titlePlaceholder": "Der Titel der Liste steht hier…",
"addTitleRequired": "Bitte gebe einen Namen an.", "addTitleRequired": "Bitte gebe einen Namen an.",
"createdSuccess": "Die Liste wurde erfolgreich erstellt.", "createdSuccess": "Die Liste wurde erfolgreich erstellt.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Beginne zu schreiben, um einen Namespace zu suchen…", "search": "Beginne zu schreiben, um einen Namespace zu suchen…",
"create": { "create": {
"title": "Einen neuen Namespace erstellen", "title": "New namespace",
"titleRequired": "Bitte gebe einen Titel an.", "titleRequired": "Bitte gebe einen Titel an.",
"explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste zu einem Namespace.", "explanation": "Ein Namespace ist eine Sammlung von Listen, die du teilen und zur Organisation verwenden kannst. Jede Liste zu einem Namespace.",
"tooltip": "Was ist ein Namespace?", "tooltip": "Was ist ein Namespace?",
@ -374,7 +374,7 @@
"includeNulls": "Aufgaben ohne Werte einbeziehen", "includeNulls": "Aufgaben ohne Werte einbeziehen",
"requireAll": "Alle Filterkriterien müssen erfüllt sein, damit eine Aufgabe angezeigt wird", "requireAll": "Alle Filterkriterien müssen erfüllt sein, damit eine Aufgabe angezeigt wird",
"showDoneTasks": "Erledigte Aufgaben anzeigen", "showDoneTasks": "Erledigte Aufgaben anzeigen",
"sortAlphabetically": "Sort Alphabetically", "sortAlphabetically": "Alphabetisch sortieren",
"enablePriority": "Filter nach Priorität aktivieren", "enablePriority": "Filter nach Priorität aktivieren",
"enablePercentDone": "Filter nach % Erledigt aktivieren", "enablePercentDone": "Filter nach % Erledigt aktivieren",
"dueDateRange": "Fälligkeitsbereich", "dueDateRange": "Fälligkeitsbereich",
@ -383,7 +383,7 @@
"reminderRange": "Erinnerungs-Datumsbereich" "reminderRange": "Erinnerungs-Datumsbereich"
}, },
"create": { "create": {
"title": "Einen gespeicherten Filter erstellen", "title": "New Saved Filter",
"description": "Ein gespeicherter Filter ist eine virtuelle Liste, die bei jedem Zugriff aus einem Satz von Filtern errechnet wird. Einmal erstellt, erscheint diese in einem speziellen Namespace.", "description": "Ein gespeicherter Filter ist eine virtuelle Liste, die bei jedem Zugriff aus einem Satz von Filtern errechnet wird. Einmal erstellt, erscheint diese in einem speziellen Namespace.",
"action": "Neuen gespeicherten Filter erstellen" "action": "Neuen gespeicherten Filter erstellen"
}, },
@ -475,8 +475,8 @@
"download": "Herunterladen", "download": "Herunterladen",
"showMenu": "Menü anzeigen", "showMenu": "Menü anzeigen",
"hideMenu": "Menü ausblenden", "hideMenu": "Menü ausblenden",
"forExample": "For example:", "forExample": "Zum Beispiel:",
"welcomeBack": "Welcome Back!" "welcomeBack": "Willkommen zurück!"
}, },
"input": { "input": {
"resetColor": "Farbe zurücksetzen", "resetColor": "Farbe zurücksetzen",
@ -726,8 +726,8 @@
"dateCurrentYear": "wird das laufende Jahr nutzen", "dateCurrentYear": "wird das laufende Jahr nutzen",
"dateNth": "wird den {day}. des aktuellen Monats verwenden", "dateNth": "wird den {day}. des aktuellen Monats verwenden",
"dateTime": "Kombiniere eines der Datumsformate mit \"{time}\" (oder {timePM}), um eine Zeit festzulegen.", "dateTime": "Kombiniere eines der Datumsformate mit \"{time}\" (oder {timePM}), um eine Zeit festzulegen.",
"repeats": "Repeating tasks", "repeats": "Wiederholende Aufgaben",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)." "repeatsDescription": "Um eine Aufgabe als Wiederholung in einem Intervall festzulegen, füge einfach '{suffix}' dem Aufgabentext hinzu. Der Betrag muss eine Zahl sein und kann weggelassen werden, um nur den Typ zu verwenden (siehe Beispiele)."
} }
}, },
"team": { "team": {
@ -814,7 +814,7 @@
"url": "Vikunja-URL", "url": "Vikunja-URL",
"urlPlaceholder": "z.B. https://localhost:3456", "urlPlaceholder": "z.B. https://localhost:3456",
"change": "ändern", "change": "ändern",
"use": "Using Vikunja installation at {0}", "use": "Verwende die Vikunja-Installation unter „{0}“",
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.", "error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
"success": "Verwende die Vikunja-Installation unter „{domain}“.", "success": "Verwende die Vikunja-Installation unter „{domain}“.",
"urlRequired": "Eine Url ist erforderlich." "urlRequired": "Eine Url ist erforderlich."

View File

@ -7,7 +7,7 @@
"lastViewed": "Zletscht ahglueget", "lastViewed": "Zletscht ahglueget",
"list": { "list": {
"newText": "Du chasch e Liste für dini neue Uufgabe erstelle:", "newText": "Du chasch e Liste für dini neue Uufgabe erstelle:",
"new": "Neui Liste erstelle", "new": "New list",
"importText": "Oder importier dini Liste und Uufgabe us anderne Dienst nach Vikunja:", "importText": "Oder importier dini Liste und Uufgabe us anderne Dienst nach Vikunja:",
"import": "Dini Date in Vikunja importiere" "import": "Dini Date in Vikunja importiere"
} }
@ -36,7 +36,7 @@
"password": "Passwort", "password": "Passwort",
"passwordRepeat": "Gib dis Passwort nomal iih", "passwordRepeat": "Gib dis Passwort nomal iih",
"passwordPlaceholder": "z.B. •••••••••••", "passwordPlaceholder": "z.B. •••••••••••",
"forgotPassword": "Forgot your password?", "forgotPassword": "Passwort vergessen?",
"resetPassword": "Setz diis Passwort zrugg", "resetPassword": "Setz diis Passwort zrugg",
"resetPasswordAction": "Schick mir en Passwort zruggsetz Link", "resetPasswordAction": "Schick mir en Passwort zruggsetz Link",
"resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.", "resetPasswordSuccess": "Prüfe deinen Posteingang! Du solltest eine E-Mail mit Anweisungen zum Zurücksetzen deines Passworts erhalten haben.",
@ -103,7 +103,7 @@
"title": "Herr Der Elemente", "title": "Herr Der Elemente",
"initials": "Initialä", "initials": "Initialä",
"gravatar": "Gravatar", "gravatar": "Gravatar",
"marble": "Marble", "marble": "Murmel",
"upload": "Ufeladä", "upload": "Ufeladä",
"uploadAvatar": "Profiilbild ufeladä", "uploadAvatar": "Profiilbild ufeladä",
"statusUpdateSuccess": "Avatar Zuestand erfolgriich aktualisiert!", "statusUpdateSuccess": "Avatar Zuestand erfolgriich aktualisiert!",
@ -157,7 +157,7 @@
"searchSelect": "Druck uf Enter um die Liste uuszwähle", "searchSelect": "Druck uf Enter um die Liste uuszwähle",
"shared": "Teilti Liste", "shared": "Teilti Liste",
"create": { "create": {
"header": "Neui Liste erstelle", "header": "New list",
"titlePlaceholder": "Listetitl da ahgeh…", "titlePlaceholder": "Listetitl da ahgeh…",
"addTitleRequired": "Bitte gib en Titl ah.", "addTitleRequired": "Bitte gib en Titl ah.",
"createdSuccess": "Liste erfolgriich erstellt.", "createdSuccess": "Liste erfolgriich erstellt.",
@ -315,7 +315,7 @@
"namespaces": "Namensrüüm", "namespaces": "Namensrüüm",
"search": "Schriib, um nachemne Namensruum z'sueche…", "search": "Schriib, um nachemne Namensruum z'sueche…",
"create": { "create": {
"title": "Neue Namensruum erstelle", "title": "New namespace",
"titleRequired": "Bitte gib en Titl ah.", "titleRequired": "Bitte gib en Titl ah.",
"explanation": "En Namensruum isch e Gruppe vo Liste, wo du chasch zur Organisation benutze. Tatsächlich sind alli Listene emne Namensruum zuegwise.", "explanation": "En Namensruum isch e Gruppe vo Liste, wo du chasch zur Organisation benutze. Tatsächlich sind alli Listene emne Namensruum zuegwise.",
"tooltip": "Was isch en Namensruum?", "tooltip": "Was isch en Namensruum?",
@ -374,7 +374,7 @@
"includeNulls": "Uufgabe ohni Wert iihbezieh", "includeNulls": "Uufgabe ohni Wert iihbezieh",
"requireAll": "Alli Filter mend wahr sii, demits die Uufgab ahzeigt", "requireAll": "Alli Filter mend wahr sii, demits die Uufgab ahzeigt",
"showDoneTasks": "Zeig die fertige Uufgabe", "showDoneTasks": "Zeig die fertige Uufgabe",
"sortAlphabetically": "Sort Alphabetically", "sortAlphabetically": "Alphabetisch sortieren",
"enablePriority": "Filter nach Priorität aktiviere", "enablePriority": "Filter nach Priorität aktiviere",
"enablePercentDone": "Filter nach Prozent iihschalte", "enablePercentDone": "Filter nach Prozent iihschalte",
"dueDateRange": "Fälligkeitsberiich", "dueDateRange": "Fälligkeitsberiich",
@ -383,7 +383,7 @@
"reminderRange": "Errinnerigs Datumbereich" "reminderRange": "Errinnerigs Datumbereich"
}, },
"create": { "create": {
"title": "Neue gspeicherete Filter erstelle", "title": "New Saved Filter",
"description": "En gspeicherete Filter isch e virtuelli Liste, welche vomene Satz a Filter zemmegsetzt wird, sobald me uf sie zuegriift. Wenn sie mal erstellt worde isch, erhaltet si ihren eigene Namensruum.", "description": "En gspeicherete Filter isch e virtuelli Liste, welche vomene Satz a Filter zemmegsetzt wird, sobald me uf sie zuegriift. Wenn sie mal erstellt worde isch, erhaltet si ihren eigene Namensruum.",
"action": "Neue gspeicherete Filter erstelle" "action": "Neue gspeicherete Filter erstelle"
}, },
@ -475,8 +475,8 @@
"download": "Herunterladen", "download": "Herunterladen",
"showMenu": "Menü anzeigen", "showMenu": "Menü anzeigen",
"hideMenu": "Menü ausblenden", "hideMenu": "Menü ausblenden",
"forExample": "For example:", "forExample": "Zum Beispiel:",
"welcomeBack": "Welcome Back!" "welcomeBack": "Willkommen zurück!"
}, },
"input": { "input": {
"resetColor": "Farb zruggsetze", "resetColor": "Farb zruggsetze",
@ -726,8 +726,8 @@
"dateCurrentYear": "nimmt das laufende Jahr", "dateCurrentYear": "nimmt das laufende Jahr",
"dateNth": "nimmt de {day}ti vom jetzige Monet", "dateNth": "nimmt de {day}ti vom jetzige Monet",
"dateTime": "Kombiniere irgendeis vo dene Datumsformat mit \"{time}\" (oder {timePM}) um e Ziit z'setze.", "dateTime": "Kombiniere irgendeis vo dene Datumsformat mit \"{time}\" (oder {timePM}) um e Ziit z'setze.",
"repeats": "Repeating tasks", "repeats": "Wiederholende Aufgaben",
"repeatsDescription": "To set a task as repeating in an interval, simply add '{suffix}' to the task text. The amount needs to be a number and can be omitted to use just the type (see examples)." "repeatsDescription": "Um eine Aufgabe als Wiederholung in einem Intervall festzulegen, füge einfach '{suffix}' dem Aufgabentext hinzu. Der Betrag muss eine Zahl sein und kann weggelassen werden, um nur den Typ zu verwenden (siehe Beispiele)."
} }
}, },
"team": { "team": {
@ -814,7 +814,7 @@
"url": "Vikunja URL", "url": "Vikunja URL",
"urlPlaceholder": "z.B. https://localhost:3456", "urlPlaceholder": "z.B. https://localhost:3456",
"change": "ändere", "change": "ändere",
"use": "Using Vikunja installation at {0}", "use": "Verwende die Vikunja-Installation unter „{0}“",
"error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.", "error": "Konnte keine Vikunja-Installation unter „{domain}“ finden oder verwenden. Bitte probiere eine andere Url.",
"success": "Benutze d'Vikunja Installation uf \"{domain}\".", "success": "Benutze d'Vikunja Installation uf \"{domain}\".",
"urlRequired": "Eine Url ist erforderlich." "urlRequired": "Eine Url ist erforderlich."

View File

@ -7,7 +7,7 @@
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
"new": "Create a new list", "new": "New list",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
"create": { "create": {
"header": "Create a new list", "header": "New list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The list was successfully created.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Create A Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter" "action": "Create new saved filter"
}, },
@ -545,7 +545,7 @@
"chooseStartDate": "Click here to set a start date", "chooseStartDate": "Click here to set a start date",
"chooseEndDate": "Click here to set an end date", "chooseEndDate": "Click here to set an end date",
"move": "Move task to a different list", "move": "Move task to a different list",
"done": "Done!", "done": "Mark task done!",
"undone": "Mark as undone", "undone": "Mark as undone",
"created": "Created {0} by {1}", "created": "Created {0} by {1}",
"updated": "Updated {0}", "updated": "Updated {0}",
@ -781,7 +781,7 @@
"then": "then", "then": "then",
"task": { "task": {
"title": "Task Page", "title": "Task Page",
"done": "Mark a task as done", "done": "Done",
"assign": "Assign to a user", "assign": "Assign to a user",
"labels": "Add labels to this task", "labels": "Add labels to this task",
"dueDate": "Change the due date of this task", "dueDate": "Change the due date of this task",

View File

@ -7,7 +7,7 @@
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
"new": "Create a new list", "new": "New list",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
"create": { "create": {
"header": "Create a new list", "header": "New list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The list was successfully created.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Create A Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter" "action": "Create new saved filter"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Dernière consultation", "lastViewed": "Dernière consultation",
"list": { "list": {
"newText": "Tu peux créer une nouvelle liste pour tes nouvelles tâches :", "newText": "Tu peux créer une nouvelle liste pour tes nouvelles tâches :",
"new": "Créer une nouvelle liste", "new": "New list",
"importText": "Ou importe tes listes et tâches dautres services dans Vikunja :", "importText": "Ou importe tes listes et tâches dautres services dans Vikunja :",
"import": "Importer tes données dans Vikunja" "import": "Importer tes données dans Vikunja"
} }
@ -157,7 +157,7 @@
"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",
"create": { "create": {
"header": "Créer une nouvelle liste", "header": "New list",
"titlePlaceholder": "Entre le nom de la liste…", "titlePlaceholder": "Entre le nom de la liste…",
"addTitleRequired": "Indique un nom.", "addTitleRequired": "Indique un nom.",
"createdSuccess": "Liste créée.", "createdSuccess": "Liste créée.",
@ -315,7 +315,7 @@
"namespaces": "Espaces de noms", "namespaces": "Espaces de noms",
"search": "Écris pour rechercher un espace de noms…", "search": "Écris pour rechercher un espace de noms…",
"create": { "create": {
"title": "Créer un nouvel espace de noms", "title": "New namespace",
"titleRequired": "Indique un nom.", "titleRequired": "Indique un nom.",
"explanation": "Des collections de listes pour partager et organiser vos listes. En fait, chaque liste appartient à un espace de noms.", "explanation": "Des collections de listes pour partager et organiser vos listes. En fait, chaque liste appartient à un espace de noms.",
"tooltip": "Quest-ce quun espace de noms ?", "tooltip": "Quest-ce quun espace de noms ?",
@ -383,7 +383,7 @@
"reminderRange": "Plage de dates de rappel" "reminderRange": "Plage de dates de rappel"
}, },
"create": { "create": {
"title": "Créer un filtre enregistré", "title": "New Saved Filter",
"description": "Un filtre enregistré est une liste virtuelle qui est calculée à partir dun ensemble de filtres à chaque fois quon y accède. Une fois créé, il apparaît dans un espace de noms spécial.", "description": "Un filtre enregistré est une liste virtuelle qui est calculée à partir dun ensemble de filtres à chaque fois quon y accède. Une fois créé, il apparaît dans un espace de noms spécial.",
"action": "Créer un nouveau filtre enregistré" "action": "Créer un nouveau filtre enregistré"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Ultima visualizzazione", "lastViewed": "Ultima visualizzazione",
"list": { "list": {
"newText": "È possibile creare una nuova lista per le nuove attività:", "newText": "È possibile creare una nuova lista per le nuove attività:",
"new": "Crea una nuova lista", "new": "New list",
"importText": "O importare le liste e le attività da altri servizi in Vikunja:", "importText": "O importare le liste e le attività da altri servizi in Vikunja:",
"import": "Importa i tuoi dati in Vikunja" "import": "Importa i tuoi dati in Vikunja"
} }
@ -157,7 +157,7 @@
"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",
"create": { "create": {
"header": "Crea una nuova lista", "header": "New list",
"titlePlaceholder": "Il titolo della lista va qui…", "titlePlaceholder": "Il titolo della lista va qui…",
"addTitleRequired": "Specifica un titolo.", "addTitleRequired": "Specifica un titolo.",
"createdSuccess": "La lista è stata creata correttamente.", "createdSuccess": "La lista è stata creata correttamente.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Crea Un Filtro Salvato", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Crea nuovo filtro salvato" "action": "Crea nuovo filtro salvato"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
"new": "Create a new list", "new": "New list",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
"create": { "create": {
"header": "Create a new list", "header": "New list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The list was successfully created.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Create A Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter" "action": "Create new saved filter"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
"new": "Create a new list", "new": "New list",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
"create": { "create": {
"header": "Create a new list", "header": "New list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The list was successfully created.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Create A Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter" "action": "Create new saved filter"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
"new": "Create a new list", "new": "New list",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
"create": { "create": {
"header": "Create a new list", "header": "New list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The list was successfully created.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Create A Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter" "action": "Create new saved filter"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
"new": "Create a new list", "new": "New list",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
"create": { "create": {
"header": "Create a new list", "header": "New list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The list was successfully created.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Create A Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter" "action": "Create new saved filter"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Последние просмотренные", "lastViewed": "Последние просмотренные",
"list": { "list": {
"newText": "Ты можешь создать новый список для своих задач:", "newText": "Ты можешь создать новый список для своих задач:",
"new": "Создать новый список", "new": "New list",
"importText": "Или импортировать списки и задачи из других сервисов в Vikunja:", "importText": "Или импортировать списки и задачи из других сервисов в Vikunja:",
"import": "Импорт данных в Vikunja" "import": "Импорт данных в Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Кликни или нажми Enter для выбора этого списка", "searchSelect": "Кликни или нажми Enter для выбора этого списка",
"shared": "Общие списки", "shared": "Общие списки",
"create": { "create": {
"header": "Создать новый список", "header": "New list",
"titlePlaceholder": "Введи имя списка…", "titlePlaceholder": "Введи имя списка…",
"addTitleRequired": "Укажи название.", "addTitleRequired": "Укажи название.",
"createdSuccess": "Список создан.", "createdSuccess": "Список создан.",
@ -315,7 +315,7 @@
"namespaces": "Пространства имён", "namespaces": "Пространства имён",
"search": "Введи запрос для поиска пространства имён…", "search": "Введи запрос для поиска пространства имён…",
"create": { "create": {
"title": "Создать новое пространство имён", "title": "New namespace",
"titleRequired": "Укажи название.", "titleRequired": "Укажи название.",
"explanation": "Коллекции списков для совместного использования и организации ваших списков. Фактически, каждый список принадлежит какому-нибудь пространству имён.", "explanation": "Коллекции списков для совместного использования и организации ваших списков. Фактически, каждый список принадлежит какому-нибудь пространству имён.",
"tooltip": "Что такое пространство имён?", "tooltip": "Что такое пространство имён?",
@ -383,7 +383,7 @@
"reminderRange": "Диапазон даты напоминания" "reminderRange": "Диапазон даты напоминания"
}, },
"create": { "create": {
"title": "Создать сохранённый фильтр", "title": "New Saved Filter",
"description": "Сохраненный фильтр это виртуальный список, построенный из набора фильтров. При создании отображается в специальном пространстве имен.", "description": "Сохраненный фильтр это виртуальный список, построенный из набора фильтров. При создании отображается в специальном пространстве имен.",
"action": "Создать новый сохранённый фильтр" "action": "Создать новый сохранённый фильтр"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
"new": "Create a new list", "new": "New list",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
"create": { "create": {
"header": "Create a new list", "header": "New list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The list was successfully created.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Create A Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter" "action": "Create new saved filter"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"list": { "list": {
"newText": "You can create a new list for your new tasks:", "newText": "You can create a new list for your new tasks:",
"new": "Create a new list", "new": "New list",
"importText": "Or import your lists and tasks from other services into Vikunja:", "importText": "Or import your lists and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Click or press enter to select this list", "searchSelect": "Click or press enter to select this list",
"shared": "Shared Lists", "shared": "Shared Lists",
"create": { "create": {
"header": "Create a new list", "header": "New list",
"titlePlaceholder": "The list's title goes here…", "titlePlaceholder": "The list's title goes here…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Please specify a title.",
"createdSuccess": "The list was successfully created.", "createdSuccess": "The list was successfully created.",
@ -315,7 +315,7 @@
"namespaces": "Namespaces", "namespaces": "Namespaces",
"search": "Type to search for a namespace…", "search": "Type to search for a namespace…",
"create": { "create": {
"title": "Create a new namespace", "title": "New namespace",
"titleRequired": "Please specify a title.", "titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.", "explanation": "A namespace is a collection of lists you can share and use to organize your lists with. In fact, every list belongs to a namepace.",
"tooltip": "What's a namespace?", "tooltip": "What's a namespace?",
@ -383,7 +383,7 @@
"reminderRange": "Reminder Date Range" "reminderRange": "Reminder Date Range"
}, },
"create": { "create": {
"title": "Create A Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "A saved filter is a virtual list which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.",
"action": "Create new saved filter" "action": "Create new saved filter"
}, },

View File

@ -7,7 +7,7 @@
"lastViewed": "Xem gần đây", "lastViewed": "Xem gần đây",
"list": { "list": {
"newText": "Bạn có thể tạo một danh sách công việc mới cho mình:", "newText": "Bạn có thể tạo một danh sách công việc mới cho mình:",
"new": "Tạo một danh sách mới", "new": "New list",
"importText": "Hoặc nhập danh sách và nhiệm vụ của bạn từ các dịch vụ khác vào Vikunja:", "importText": "Hoặc nhập danh sách và nhiệm vụ của bạn từ các dịch vụ khác vào Vikunja:",
"import": "Nhập dữ liệu của bạn vào Vikunja" "import": "Nhập dữ liệu của bạn vào Vikunja"
} }
@ -157,7 +157,7 @@
"searchSelect": "Nhấp hoặc nhấn enter để chọn danh sách này", "searchSelect": "Nhấp hoặc nhấn enter để chọn danh sách này",
"shared": "Đang tham gia", "shared": "Đang tham gia",
"create": { "create": {
"header": "Tạo một danh sách mới", "header": "New list",
"titlePlaceholder": "Tên danh sách ở đây…", "titlePlaceholder": "Tên danh sách ở đây…",
"addTitleRequired": "Hãy xác định một tên.", "addTitleRequired": "Hãy xác định một tên.",
"createdSuccess": "Danh sách đã được tạo thành công.", "createdSuccess": "Danh sách đã được tạo thành công.",
@ -315,7 +315,7 @@
"namespaces": "Góc làm việc", "namespaces": "Góc làm việc",
"search": "Gõ để tìm kiếm một góc làm việc…", "search": "Gõ để tìm kiếm một góc làm việc…",
"create": { "create": {
"title": "Tạo một góc làm việc mới", "title": "New namespace",
"titleRequired": "Hãy đặt một tiêu đề.", "titleRequired": "Hãy đặt một tiêu đề.",
"explanation": "Góc làm việc là một tập hợp các danh sách mà bạn có thể chia sẻ và sử dụng để sắp xếp các danh sách của mình. Trên thực tế, mọi danh sách đều thuộc về một góc làm việc.", "explanation": "Góc làm việc là một tập hợp các danh sách mà bạn có thể chia sẻ và sử dụng để sắp xếp các danh sách của mình. Trên thực tế, mọi danh sách đều thuộc về một góc làm việc.",
"tooltip": "Góc làm việc là gì?", "tooltip": "Góc làm việc là gì?",
@ -383,7 +383,7 @@
"reminderRange": "Phạm vi Ngày nhắc nhở" "reminderRange": "Phạm vi Ngày nhắc nhở"
}, },
"create": { "create": {
"title": "Tạo một Bộ lọc sẵn", "title": "New Saved Filter",
"description": "Bộ lọc sẵn là một danh sách ảo được chọn từ một tập hợp các bộ lọc. Sau khi được tạo, nó sẽ xuất hiện trong một không gian làm việc đặc biệt.", "description": "Bộ lọc sẵn là một danh sách ảo được chọn từ một tập hợp các bộ lọc. Sau khi được tạo, nó sẽ xuất hiện trong một không gian làm việc đặc biệt.",
"action": "Tạo thêm bộ lọc sẵn" "action": "Tạo thêm bộ lọc sẵn"
}, },

View File

@ -1,11 +1,8 @@
import {createApp, configureCompat} from 'vue' import {createApp, configureCompat} from 'vue'
// default everything to Vue 3 behavior
configureCompat({ configureCompat({
COMPONENT_V_MODEL: false, MODE: 3,
COMPONENT_ASYNC: false,
RENDER_FUNCTION: false,
WATCH_ARRAY: false, // TODO: check this again; this might lead to some problemes
TRANSITION_GROUP_ROOT: false,
}) })
import App from './App.vue' import App from './App.vue'
@ -79,7 +76,6 @@ app.component('card', Card)
// Mixins // Mixins
import {getNamespaceTitle} from './helpers/getNamespaceTitle' import {getNamespaceTitle} from './helpers/getNamespaceTitle'
import {getListTitle} from './helpers/getListTitle' import {getListTitle} from './helpers/getListTitle'
import {colorIsDark} from './helpers/color/colorIsDark'
import {setTitle} from './helpers/setTitle' import {setTitle} from './helpers/setTitle'
app.mixin({ app.mixin({
@ -90,7 +86,6 @@ app.mixin({
formatDateShort: formatDateShort, formatDateShort: formatDateShort,
getNamespaceTitle, getNamespaceTitle,
getListTitle, getListTitle,
colorIsDark,
setTitle, setTitle,
}, },
}) })

View File

@ -1,14 +1,15 @@
import {test, expect, fn} from 'vitest'
import {getHistory, removeListFromHistory, saveListToHistory} from './listHistory' import {getHistory, removeListFromHistory, saveListToHistory} from './listHistory'
test('return an empty history when none was saved', () => { test('return an empty history when none was saved', () => {
Storage.prototype.getItem = jest.fn(() => null) Storage.prototype.getItem = fn(() => null)
const h = getHistory() const h = getHistory()
expect(h).toStrictEqual([]) expect(h).toStrictEqual([])
}) })
test('return a saved history', () => { test('return a saved history', () => {
const saved = [{id: 1}, {id: 2}] const saved = [{id: 1}, {id: 2}]
Storage.prototype.getItem = jest.fn(() => JSON.stringify(saved)) Storage.prototype.getItem = fn(() => JSON.stringify(saved))
const h = getHistory() const h = getHistory()
expect(h).toStrictEqual(saved) expect(h).toStrictEqual(saved)
@ -16,8 +17,8 @@ test('return a saved history', () => {
test('store list in history', () => { test('store list in history', () => {
let saved = {} let saved = {}
Storage.prototype.getItem = jest.fn(() => null) Storage.prototype.getItem = fn(() => null)
Storage.prototype.setItem = jest.fn((key, lists) => { Storage.prototype.setItem = fn((key, lists) => {
saved = lists saved = lists
}) })
@ -27,8 +28,8 @@ test('store list in history', () => {
test('store only the last 5 lists in history', () => { test('store only the last 5 lists in history', () => {
let saved: string | null = null let saved: string | null = null
Storage.prototype.getItem = jest.fn(() => saved) Storage.prototype.getItem = fn(() => saved)
Storage.prototype.setItem = jest.fn((key: string, lists: string) => { Storage.prototype.setItem = fn((key: string, lists: string) => {
saved = lists saved = lists
}) })
@ -43,8 +44,8 @@ test('store only the last 5 lists in history', () => {
test('don\'t store the same list twice', () => { test('don\'t store the same list twice', () => {
let saved: string | null = null let saved: string | null = null
Storage.prototype.getItem = jest.fn(() => saved) Storage.prototype.getItem = fn(() => saved)
Storage.prototype.setItem = jest.fn((key: string, lists: string) => { Storage.prototype.setItem = fn((key: string, lists: string) => {
saved = lists saved = lists
}) })
@ -55,8 +56,8 @@ test('don\'t store the same list twice', () => {
test('move a list to the beginning when storing it multiple times', () => { test('move a list to the beginning when storing it multiple times', () => {
let saved: string | null = null let saved: string | null = null
Storage.prototype.getItem = jest.fn(() => saved) Storage.prototype.getItem = fn(() => saved)
Storage.prototype.setItem = jest.fn((key: string, lists: string) => { Storage.prototype.setItem = fn((key: string, lists: string) => {
saved = lists saved = lists
}) })
@ -68,11 +69,11 @@ test('move a list to the beginning when storing it multiple times', () => {
test('remove list from history', () => { test('remove list from history', () => {
let saved: string | null = '[{"id": 1}]' let saved: string | null = '[{"id": 1}]'
Storage.prototype.getItem = jest.fn(() => null) Storage.prototype.getItem = fn(() => null)
Storage.prototype.setItem = jest.fn((key: string, lists: string) => { Storage.prototype.setItem = fn((key: string, lists: string) => {
saved = lists saved = lists
}) })
Storage.prototype.removeItem = jest.fn((key: string) => { Storage.prototype.removeItem = fn((key: string) => {
saved = null saved = null
}) })

View File

@ -1,3 +1,5 @@
import {describe, it, expect} from 'vitest'
import {parseTaskText} from './parseTaskText' import {parseTaskText} from './parseTaskText'
import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate' import {getDateFromText, getDateFromTextIn} from '../helpers/time/parseDate'
import {calculateDayInterval} from '../helpers/time/calculateDayInterval' import {calculateDayInterval} from '../helpers/time/calculateDayInterval'

View File

@ -2,73 +2,73 @@ import { createRouter, createWebHistory, RouteLocation } from 'vue-router'
import {saveLastVisited} from '@/helpers/saveLastVisited' import {saveLastVisited} from '@/helpers/saveLastVisited'
import {store} from '@/store' import {store} from '@/store'
import HomeComponent from '../views/Home' import HomeComponent from '../views/Home.vue'
import NotFoundComponent from '../views/404' import NotFoundComponent from '../views/404.vue'
import About from '../views/About' import About from '../views/About.vue'
// User Handling // User Handling
import LoginComponent from '../views/user/Login' import LoginComponent from '../views/user/Login.vue'
import RegisterComponent from '../views/user/Register' import RegisterComponent from '../views/user/Register.vue'
import OpenIdAuth from '../views/user/OpenIdAuth' import OpenIdAuth from '../views/user/OpenIdAuth.vue'
import DataExportDownload from '../views/user/DataExportDownload' import DataExportDownload from '../views/user/DataExportDownload.vue'
// Tasks // Tasks
import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange' import ShowTasksInRangeComponent from '../views/tasks/ShowTasksInRange.vue'
import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth' import LinkShareAuthComponent from '../views/sharing/LinkSharingAuth.vue'
import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal' import TaskDetailViewModal from '../views/tasks/TaskDetailViewModal.vue'
import TaskDetailView from '../views/tasks/TaskDetailView' import TaskDetailView from '../views/tasks/TaskDetailView.vue'
import ListNamespaces from '../views/namespaces/ListNamespaces' import ListNamespaces from '../views/namespaces/ListNamespaces.vue'
// Team Handling // Team Handling
import ListTeamsComponent from '../views/teams/ListTeams' import ListTeamsComponent from '../views/teams/ListTeams.vue'
// Label Handling // Label Handling
import ListLabelsComponent from '../views/labels/ListLabels' import ListLabelsComponent from '../views/labels/ListLabels.vue'
import NewLabelComponent from '../views/labels/NewLabel' import NewLabelComponent from '../views/labels/NewLabel.vue'
// Migration // Migration
import MigrationComponent from '../views/migrator/Migrate' import MigrationComponent from '../views/migrator/Migrate.vue'
import MigrateServiceComponent from '../views/migrator/MigrateService' import MigrateServiceComponent from '../views/migrator/MigrateService.vue'
// List Views // List Views
import ShowListComponent from '../views/list/ShowList' import ShowListComponent from '../views/list/ShowList.vue'
import Kanban from '../views/list/views/Kanban' import Kanban from '../views/list/views/Kanban.vue'
import List from '../views/list/views/List' import List from '../views/list/views/List.vue'
import Gantt from '../views/list/views/Gantt' import Gantt from '../views/list/views/Gantt.vue'
import Table from '../views/list/views/Table' import Table from '../views/list/views/Table.vue'
// List Settings // List Settings
import ListSettingEdit from '../views/list/settings/edit' import ListSettingEdit from '../views/list/settings/edit.vue'
import ListSettingBackground from '../views/list/settings/background' import ListSettingBackground from '../views/list/settings/background.vue'
import ListSettingDuplicate from '../views/list/settings/duplicate' import ListSettingDuplicate from '../views/list/settings/duplicate.vue'
import ListSettingShare from '../views/list/settings/share' import ListSettingShare from '../views/list/settings/share.vue'
import ListSettingDelete from '../views/list/settings/delete' import ListSettingDelete from '../views/list/settings/delete.vue'
import ListSettingArchive from '../views/list/settings/archive' import ListSettingArchive from '../views/list/settings/archive.vue'
// Namespace Settings // Namespace Settings
import NamespaceSettingEdit from '../views/namespaces/settings/edit' import NamespaceSettingEdit from '../views/namespaces/settings/edit.vue'
import NamespaceSettingShare from '../views/namespaces/settings/share' import NamespaceSettingShare from '../views/namespaces/settings/share.vue'
import NamespaceSettingArchive from '../views/namespaces/settings/archive' import NamespaceSettingArchive from '../views/namespaces/settings/archive.vue'
import NamespaceSettingDelete from '../views/namespaces/settings/delete' import NamespaceSettingDelete from '../views/namespaces/settings/delete.vue'
// Saved Filters // Saved Filters
import FilterNew from '@/views/filters/FilterNew' import FilterNew from '@/views/filters/FilterNew.vue'
import FilterEdit from '@/views/filters/FilterEdit' import FilterEdit from '@/views/filters/FilterEdit.vue'
import FilterDelete from '@/views/filters/FilterDelete' import FilterDelete from '@/views/filters/FilterDelete.vue'
const PasswordResetComponent = () => import('../views/user/PasswordReset') const PasswordResetComponent = () => import('../views/user/PasswordReset.vue')
const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset') const GetPasswordResetComponent = () => import('../views/user/RequestPasswordReset.vue')
const UserSettingsComponent = () => import('../views/user/Settings') const UserSettingsComponent = () => import('../views/user/Settings.vue')
const UserSettingsAvatarComponent = () => import('../views/user/settings/Avatar') const UserSettingsAvatarComponent = () => import('../views/user/settings/Avatar.vue')
const UserSettingsCaldavComponent = () => import('../views/user/settings/Caldav') const UserSettingsCaldavComponent = () => import('../views/user/settings/Caldav.vue')
const UserSettingsDataExportComponent = () => import('../views/user/settings/DataExport') const UserSettingsDataExportComponent = () => import('../views/user/settings/DataExport.vue')
const UserSettingsDeletionComponent = () => import('../views/user/settings/Deletion') const UserSettingsDeletionComponent = () => import('../views/user/settings/Deletion.vue')
const UserSettingsEmailUpdateComponent = () => import('../views/user/settings/EmailUpdate') const UserSettingsEmailUpdateComponent = () => import('../views/user/settings/EmailUpdate.vue')
const UserSettingsGeneralComponent = () => import('../views/user/settings/General') const UserSettingsGeneralComponent = () => import('../views/user/settings/General.vue')
const UserSettingsPasswordUpdateComponent = () => import('../views/user/settings/PasswordUpdate') const UserSettingsPasswordUpdateComponent = () => import('../views/user/settings/PasswordUpdate.vue')
const UserSettingsTOTPComponent = () => import('../views/user/settings/TOTP') const UserSettingsTOTPComponent = () => import('../views/user/settings/TOTP.vue')
// List Handling // List Handling
const NewListComponent = () => import('../views/list/NewList') const NewListComponent = () => import('../views/list/NewList.vue')
// Namespace Handling // Namespace Handling
const NewNamespaceComponent = () => import('../views/namespaces/NewNamespace') const NewNamespaceComponent = () => import('../views/namespaces/NewNamespace.vue')
const EditTeamComponent = () => import('../views/teams/EditTeam') const EditTeamComponent = () => import('../views/teams/EditTeam.vue')
const NewTeamComponent = () => import('../views/teams/NewTeam') const NewTeamComponent = () => import('../views/teams/NewTeam.vue')
const router = createRouter({ const router = createRouter({
history: createWebHistory(), history: createWebHistory(),

View File

@ -7,7 +7,7 @@ import {
LOADING, LOADING,
LOADING_MODULE, LOADING_MODULE,
MENU_ACTIVE, MENU_ACTIVE,
ONLINE, QUICK_ACTIONS_ACTIVE, QUICK_ACTIONS_ACTIVE,
} from './mutation-types' } from './mutation-types'
import config from './modules/config' import config from './modules/config'
import auth from './modules/auth' import auth from './modules/auth'
@ -36,7 +36,6 @@ export const store = createStore({
state: { state: {
loading: false, loading: false,
loadingModule: null, loadingModule: null,
online: true,
// This is used to highlight the current list in menu for all list related views // This is used to highlight the current list in menu for all list related views
currentList: {id: 0}, currentList: {id: 0},
background: '', background: '',
@ -53,12 +52,6 @@ export const store = createStore({
[LOADING_MODULE](state, module) { [LOADING_MODULE](state, module) {
state.loadingModule = module state.loadingModule = module
}, },
[ONLINE](state, online) {
if (import.meta.env.VITE_IS_ONLINE) {
console.log('Setting fake online state', import.meta.env.VITE_IS_ONLINE)
}
state.online = !!import.meta.env.VITE_IS_ONLINE || online
},
[CURRENT_LIST](state, currentList) { [CURRENT_LIST](state, currentList) {
// Server updates don't return the right. Therefore the right is reset after updating the list which is // Server updates don't return the right. Therefore the right is reset after updating the list which is
// confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right // confusing because all the buttons will disappear in that case. To prevent this, we're keeping the right

View File

@ -1,7 +1,12 @@
import {HTTPFactory} from '@/http-common' import {HTTPFactory} from '@/http-common'
import {getCurrentLanguage, saveLanguage} from '@/i18n'
import {LOADING} from '../mutation-types' import {LOADING} from '../mutation-types'
import UserModel from '../../models/user' import UserModel from '@/models/user'
import UserSettingsService from '@/services/userSettings'
import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth' import {getToken, refreshToken, removeToken, saveToken} from '@/helpers/auth'
import {setLoading} from '@/store/helper'
import {i18n} from '@/i18n'
import {success} from '@/message'
const AUTH_TYPES = { const AUTH_TYPES = {
'UNKNOWN': 0, 'UNKNOWN': 0,
@ -98,10 +103,10 @@ export default {
const response = await HTTP.post('login', data) const response = await HTTP.post('login', data)
// Save the token to local storage for later use // Save the token to local storage for later use
saveToken(response.data.token, true) saveToken(response.data.token, true)
// Tell others the user is autheticated // Tell others the user is autheticated
ctx.dispatch('checkAuth') ctx.dispatch('checkAuth')
} catch(e) { } catch (e) {
if ( if (
e.response && e.response &&
e.response.data.code === 1017 && e.response.data.code === 1017 &&
@ -124,7 +129,7 @@ export default {
try { try {
await HTTP.post('register', credentials) await HTTP.post('register', credentials)
return ctx.dispatch('login', credentials) return ctx.dispatch('login', credentials)
} catch(e) { } catch (e) {
if (e.response?.data?.message) { if (e.response?.data?.message) {
throw e.response.data throw e.response.data
} }
@ -149,7 +154,7 @@ export default {
const response = await HTTP.post(`/auth/openid/${provider}/callback`, data) const response = await HTTP.post(`/auth/openid/${provider}/callback`, data)
// Save the token to local storage for later use // Save the token to local storage for later use
saveToken(response.data.token, true) saveToken(response.data.token, true)
// Tell others the user is autheticated // Tell others the user is autheticated
ctx.dispatch('checkAuth') ctx.dispatch('checkAuth')
} finally { } finally {
@ -200,7 +205,7 @@ export default {
} }
}, },
async refreshUserInfo(ctx) { async refreshUserInfo({state, commit, dispatch}) {
const jwt = getToken() const jwt = getToken()
if (!jwt) { if (!jwt) {
return return
@ -208,22 +213,53 @@ export default {
const HTTP = HTTPFactory() const HTTP = HTTPFactory()
try { try {
const response = await HTTP.get('user', { const response = await HTTP.get('user', {
headers: { headers: {
Authorization: `Bearer ${jwt}`, Authorization: `Bearer ${jwt}`,
}, },
}) })
const info = new UserModel(response.data) const info = new UserModel(response.data)
info.type = ctx.state.info.type info.type = state.info.type
info.email = ctx.state.info.email info.email = state.info.email
info.exp = ctx.state.info.exp info.exp = state.info.exp
ctx.commit('info', info) commit('info', info)
ctx.commit('lastUserRefresh') commit('lastUserRefresh')
if (typeof info.settings.language !== 'undefined') {
// save current language
await dispatch('saveUserSettings', {
settings: {
...state.settings,
language: getCurrentLanguage(),
},
showMessage: false,
})
}
return info return info
} catch(e) { } catch (e) {
throw new Error('Error while refreshing user info:', { cause: e }) throw new Error('Error while refreshing user info:', {cause: e})
}
},
async saveUserSettings(ctx, payload) {
const {settings} = payload
const showMessage = payload.showMessage ?? true
const userSettingsService = new UserSettingsService()
const cancel = setLoading(ctx, 'general-settings')
try {
saveLanguage(settings.language)
await userSettingsService.update(settings)
ctx.commit('setUserSettings', {...settings})
if (showMessage) {
success({message: i18n.global.t('user.settings.general.savedSuccess')})
}
} catch (e) {
throw new Error('Error while saving user settings:', {cause: e})
} finally {
cancel()
} }
}, },
@ -240,7 +276,7 @@ export default {
try { try {
await refreshToken(!ctx.state.isLinkShareAuth) await refreshToken(!ctx.state.isLinkShareAuth)
ctx.dispatch('checkAuth') ctx.dispatch('checkAuth')
} catch(e) { } catch (e) {
// Don't logout on network errors as the user would then get logged out if they don't have // Don't logout on network errors as the user would then get logged out if they don't have
// internet for a short period of time - such as when the laptop is still reconnecting // internet for a short period of time - such as when the laptop is still reconnecting
if (e?.request?.status) { if (e?.request?.status) {

View File

@ -1,6 +1,5 @@
export const LOADING = 'loading' export const LOADING = 'loading'
export const LOADING_MODULE = 'loadingModule' export const LOADING_MODULE = 'loadingModule'
export const ONLINE = 'online'
export const CURRENT_LIST = 'currentList' export const CURRENT_LIST = 'currentList'
export const HAS_TASKS = 'hasTasks' export const HAS_TASKS = 'hasTasks'
export const MENU_ACTIVE = 'menuActive' export const MENU_ACTIVE = 'menuActive'

View File

@ -2,5 +2,4 @@
@import "labels"; @import "labels";
@import "list"; @import "list";
@import "task"; @import "task";
@import "tasks"; @import "tasks";
@import "namespaces";

View File

@ -1,4 +0,0 @@
// FIXME: used in navigation.vue and in ListNamespaces.vue
.namespaces-list.loader-container.is-loading {
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
}

View File

@ -24,3 +24,7 @@
max-width: $widescreen; max-width: $widescreen;
} }
.content blockquote {
background-color: var(--grey-200);
border-left: .25rem solid var(--grey-300);
}

View File

@ -1,4 +1,6 @@
declare module 'vue' { declare module 'vue' {
import { CompatVue } from '@vue/runtime-dom'
const Vue: CompatVue
export default Vue export default Vue
export * from '@vue/runtime-dom' export * from '@vue/runtime-dom'
} }

View File

@ -21,7 +21,7 @@
</div> </div>
<footer class="modal-card-foot is-flex is-justify-content-flex-end"> <footer class="modal-card-foot is-flex is-justify-content-flex-end">
<x-button <x-button
type="secondary" variant="secondary"
@click.prevent.stop="$router.back()" @click.prevent.stop="$router.back()"
> >
{{ $t('misc.close') }} {{ $t('misc.close') }}

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="content has-text-centered"> <div class="content has-text-centered">
<h2 v-if="userInfo"> <h2 v-if="userInfo">
{{ $t(`home.welcome${welcome}`, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}! {{ $t(welcome, {username: userInfo.name !== '' ? userInfo.name : userInfo.username}) }}!
</h2> </h2>
<message variant="danger" v-if="deletionScheduledAt !== null" class="mb-4"> <message variant="danger" v-if="deletionScheduledAt !== null" class="mb-4">
{{ {{
@ -57,7 +57,6 @@
<script lang="ts" setup> <script lang="ts" setup>
import {ref, computed} from 'vue' import {ref, computed} from 'vue'
import {useStore} from 'vuex' import {useStore} from 'vuex'
import {useNow} from '@vueuse/core'
import Message from '@/components/misc/message.vue' import Message from '@/components/misc/message.vue'
import ShowTasks from '@/views/tasks/ShowTasks.vue' import ShowTasks from '@/views/tasks/ShowTasks.vue'
@ -67,36 +66,15 @@ import AddTask from '@/components/tasks/add-task.vue'
import {getHistory} from '@/modules/listHistory' import {getHistory} from '@/modules/listHistory'
import {parseDateOrNull} from '@/helpers/parseDateOrNull' import {parseDateOrNull} from '@/helpers/parseDateOrNull'
import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate' import {formatDateShort, formatDateSince} from '@/helpers/time/formatDate'
import {useDateTimeSalutation} from '@/composables/useDateTimeSalutation'
const now = useNow() const welcome = useDateTimeSalutation()
const welcome = computed(() => {
const hours = new Date(now.value).getHours()
if (hours < 5) {
return 'Night'
}
if (hours < 11) {
return 'Morning'
}
if (hours < 18) {
return 'Day'
}
if (hours < 23) {
return 'Evening'
}
return 'Night'
})
const store = useStore() const store = useStore()
const listHistory = computed(() => { const listHistory = computed(() => {
const history = getHistory() return getHistory()
return history.map(l => { .map(l => store.getters['lists/getListById'](l.id))
return store.getters['lists/getListById'](l.id) .filter(l => l !== null)
}).filter(l => l !== null)
}) })

View File

@ -4,8 +4,8 @@
primary-icon="" primary-icon=""
:primary-label="$t('misc.save')" :primary-label="$t('misc.save')"
@primary="saveSavedFilter" @primary="saveSavedFilter"
:tertary="$t('misc.delete')" :tertiary="$t('misc.delete')"
@tertary="$router.push({ name: 'filter.settings.delete', params: { id: $route.params.listId } })" @tertiary="$router.push({ name: 'filter.settings.delete', params: { id: $route.params.listId } })"
> >
<form @submit.prevent="saveSavedFilter()"> <form @submit.prevent="saveSavedFilter()">
<div class="field"> <div class="field">

View File

@ -6,8 +6,8 @@
class="list-background-setting" class="list-background-setting"
:wide="true" :wide="true"
v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled" v-if="uploadBackgroundEnabled || unsplashBackgroundEnabled"
:tertary="hasBackground ? $t('list.background.remove') : ''" :tertiary="hasBackground ? $t('list.background.remove') : ''"
@tertary="removeBackground()" @tertiary="removeBackground()"
> >
<div class="mb-4" v-if="uploadBackgroundEnabled"> <div class="mb-4" v-if="uploadBackgroundEnabled">
<input <input
@ -20,7 +20,7 @@
<x-button <x-button
:loading="backgroundUploadService.loading" :loading="backgroundUploadService.loading"
@click="$refs.backgroundUploadInput.click()" @click="$refs.backgroundUploadInput.click()"
type="primary" variant="primary"
> >
{{ $t('list.background.upload') }} {{ $t('list.background.upload') }}
</x-button> </x-button>
@ -54,7 +54,7 @@
@click="() => searchBackgrounds(currentPage + 1)" @click="() => searchBackgrounds(currentPage + 1)"
class="is-load-more-button mt-4" class="is-load-more-button mt-4"
:shadow="false" :shadow="false"
type="secondary" variant="secondary"
v-if="backgroundSearchResult.length > 0" v-if="backgroundSearchResult.length > 0"
> >
{{ backgroundService.loading ? $t('misc.loading') : $t('list.background.loadMore') }} {{ backgroundService.loading ? $t('misc.loading') : $t('list.background.loadMore') }}

View File

@ -4,8 +4,8 @@
primary-icon="" primary-icon=""
:primary-label="$t('misc.save')" :primary-label="$t('misc.save')"
@primary="save" @primary="save"
:tertary="$t('misc.delete')" :tertiary="$t('misc.delete')"
@tertary="$router.push({ name: 'list.list.settings.delete', params: { id: $route.params.listId } })" @tertiary="$router.push({ name: 'list.list.settings.delete', params: { id: $route.params.listId } })"
> >
<div class="field"> <div class="field">
<label class="label" for="title">{{ $t('list.title') }}</label> <label class="label" for="title">{{ $t('list.title') }}</label>

View File

@ -79,6 +79,7 @@
:disabled="bucket.limit < 0" :disabled="bucket.limit < 0"
:icon="['far', 'save']" :icon="['far', 'save']"
:shadow="false" :shadow="false"
v-cy="'setBucketLimit'"
/> />
</div> </div>
</div> </div>
@ -165,7 +166,7 @@
:shadow="false" :shadow="false"
v-if="!showNewTaskInput[bucket.id]" v-if="!showNewTaskInput[bucket.id]"
icon="plus" icon="plus"
type="secondary" variant="secondary"
> >
{{ {{
bucket.tasks.length === 0 ? $t('list.kanban.addTask') : $t('list.kanban.addAnotherTask') bucket.tasks.length === 0 ? $t('list.kanban.addTask') : $t('list.kanban.addAnotherTask')
@ -195,7 +196,7 @@
:shadow="false" :shadow="false"
class="is-transparent is-fullwidth has-text-centered" class="is-transparent is-fullwidth has-text-centered"
v-else v-else
type="secondary" variant="secondary"
icon="plus" icon="plus"
> >
{{ $t('list.kanban.addBucket') }} {{ $t('list.kanban.addBucket') }}
@ -701,7 +702,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
height: $bucket-header-height; height: $bucket-header-height;
.limit { .limit {
padding-left: .5rem; padding: 0 .5rem;
font-weight: bold; font-weight: bold;
&.is-max { &.is-max {

View File

@ -37,7 +37,7 @@
<x-button <x-button
@click="showTaskSearch = !showTaskSearch" @click="showTaskSearch = !showTaskSearch"
icon="search" icon="search"
type="secondary" variant="secondary"
v-if="!showTaskSearch" v-if="!showTaskSearch"
/> />
</div> </div>

View File

@ -7,7 +7,7 @@
<x-button <x-button
@click.prevent.stop="toggle()" @click.prevent.stop="toggle()"
icon="th" icon="th"
type="secondary" variant="secondary"
> >
{{ $t('list.table.columns') }} {{ $t('list.table.columns') }}
</x-button> </x-button>

View File

@ -53,7 +53,7 @@
</p> </p>
<div class="buttons"> <div class="buttons">
<x-button @click="migrate">{{ $t('migrate.confirm') }}</x-button> <x-button @click="migrate">{{ $t('migrate.confirm') }}</x-button>
<x-button :to="{name: 'home'}" type="tertary" class="has-text-danger">{{ $t('misc.cancel') }}</x-button> <x-button :to="{name: 'home'}" variant="tertiary" class="has-text-danger">{{ $t('misc.cancel') }}</x-button>
</div> </div>
</div> </div>
<div v-else> <div v-else>

View File

@ -24,17 +24,17 @@ export const MIGRATORS: IMigratorRecord = {
todoist: { todoist: {
id: 'todoist', id: 'todoist',
name: 'Todoist', name: 'Todoist',
icon: todoistIcon, icon: todoistIcon as string,
}, },
trello: { trello: {
id: 'trello', id: 'trello',
name: 'Trello', name: 'Trello',
icon: trelloIcon, icon: trelloIcon as string,
}, },
'microsoft-todo': { 'microsoft-todo': {
id: 'microsoft-todo', id: 'microsoft-todo',
name: 'Microsoft Todo', name: 'Microsoft Todo',
icon: microsoftTodoIcon, icon: microsoftTodoIcon as string,
}, },
'vikunja-file': { 'vikunja-file': {
id: 'vikunja-file', id: 'vikunja-file',

View File

@ -1,15 +1,19 @@
<template> <template>
<div class="content namespaces-list loader-container" :class="{'is-loading': loading}"> <div class="content loader-container" :class="{'is-loading': loading}" v-cy="'namespaces-list'">
<x-button :to="{name: 'namespace.create'}" class="new-namespace" icon="plus"> <header class="namespace-header">
{{ $t('namespace.create.title') }} <fancycheckbox v-model="showArchived" @change="saveShowArchivedState" v-cy="'show-archived-check'">
</x-button> {{ $t('namespace.showArchived') }}
<x-button :to="{name: 'filters.create'}" class="new-namespace" icon="filter"> </fancycheckbox>
{{ $t('filters.create.title') }}
</x-button>
<fancycheckbox class="show-archived-check" v-model="showArchived" @change="saveShowArchivedState"> <div class="action-buttons">
{{ $t('namespace.showArchived') }} <x-button :to="{name: 'filters.create'}" icon="filter">
</fancycheckbox> {{ $t('filters.create.title') }}
</x-button>
<x-button :to="{name: 'namespace.create'}" icon="plus" v-cy="'new-namespace'">
{{ $t('namespace.create.title') }}
</x-button>
</div>
</header>
<p class="has-text-centered has-text-grey mt-4 is-italic" v-if="namespaces.length === 0"> <p class="has-text-centered has-text-grey mt-4 is-italic" v-if="namespaces.length === 0">
{{ $t('namespace.noneAvailable') }} {{ $t('namespace.noneAvailable') }}
@ -22,7 +26,7 @@
<x-button <x-button
:to="{name: 'list.create', params: {id: n.id}}" :to="{name: 'list.create', params: {id: n.id}}"
class="is-pulled-right" class="is-pulled-right"
type="secondary" variant="secondary"
v-if="n.id > 0 && n.lists.length > 0" v-if="n.id > 0 && n.lists.length > 0"
icon="plus" icon="plus"
> >
@ -31,19 +35,19 @@
<x-button <x-button
:to="{name: 'namespace.settings.archive', params: {id: n.id}}" :to="{name: 'namespace.settings.archive', params: {id: n.id}}"
class="is-pulled-right mr-4" class="is-pulled-right mr-4"
type="secondary" variant="secondary"
v-if="n.isArchived" v-if="n.isArchived"
icon="archive" icon="archive"
> >
{{ $t('namespace.unarchive') }} {{ $t('namespace.unarchive') }}
</x-button> </x-button>
<h1> <h2 class="namespace-title">
<span>{{ getNamespaceTitle(n) }}</span> <span v-cy="'namespace-title'">{{ getNamespaceTitle(n) }}</span>
<span class="is-archived" v-if="n.isArchived"> <span class="is-archived" v-if="n.isArchived">
{{ $t('namespace.archived') }} {{ $t('namespace.archived') }}
</span> </span>
</h1> </h2>
<p class="has-text-centered has-text-grey mt-4 is-italic" v-if="n.lists.length === 0"> <p class="has-text-centered has-text-grey mt-4 is-italic" v-if="n.lists.length === 0">
{{ $t('namespace.noLists') }} {{ $t('namespace.noLists') }}
@ -103,47 +107,53 @@ export default {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.namespaces-list { .namespace-header {
.button.new-namespace { display: flex;
float: right; justify-content: space-between;
margin-left: 1rem; align-items: center;
gap: 1rem;
@media screen and (max-width: $mobile) { @media screen and (max-width: $tablet) {
float: none; flex-direction: column;
width: 100%; }
margin-bottom: 1rem; }
}
}
.show-archived-check { .action-buttons {
margin-bottom: 1rem; display: flex;
} justify-content: space-between;
gap: 1rem;
.namespace { @media screen and (max-width: $tablet) {
&:not(:last-child) { width: 100%;
margin-bottom: 1rem; flex-direction: column;
} align-items: stretch;
}
}
h1 { .namespace {
display: flex; & + & {
align-items: center; margin-top: 1rem;
} }
}
.is-archived { .namespace-title {
font-size: 0.75rem; display: flex;
border: 1px solid var(--grey-500); align-items: center;
color: $grey !important; }
padding: 2px 4px;
border-radius: 3px;
font-family: $vikunja-font;
background: var(--white-translucent);
margin-left: .5rem;
}
.lists { .is-archived {
display: flex; font-size: 0.75rem;
flex-flow: row wrap; border: 1px solid var(--grey-500);
} color: $grey !important;
} padding: 2px 4px;
border-radius: 3px;
font-family: $vikunja-font;
background: var(--white-translucent);
margin-left: .5rem;
}
.lists {
display: flex;
flex-flow: row wrap;
} }
</style> </style>

View File

@ -4,8 +4,8 @@
primary-icon="" primary-icon=""
:primary-label="$t('misc.save')" :primary-label="$t('misc.save')"
@primary="save" @primary="save"
:tertary="$t('misc.delete')" :tertiary="$t('misc.delete')"
@tertary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })" @tertiary="$router.push({ name: 'namespace.settings.delete', params: { id: $route.params.id } })"
> >
<form @submit.prevent="save()"> <form @submit.prevent="save()">
<div class="field"> <div class="field">

View File

@ -32,9 +32,9 @@
/> />
</h3> </h3>
<div v-if="!showAll" class="mb-4"> <div v-if="!showAll" class="mb-4">
<x-button type="secondary" @click="showTodaysTasks()" class="mr-2">{{ $t('task.show.today') }}</x-button> <x-button variant="secondary" @click="showTodaysTasks()" class="mr-2">{{ $t('task.show.today') }}</x-button>
<x-button type="secondary" @click="setDatesToNextWeek()" class="mr-2">{{ $t('task.show.nextWeek') }}</x-button> <x-button variant="secondary" @click="setDatesToNextWeek()" class="mr-2">{{ $t('task.show.nextWeek') }}</x-button>
<x-button type="secondary" @click="setDatesToNextMonth()">{{ $t('task.show.nextMonth') }}</x-button> <x-button variant="secondary" @click="setDatesToNextMonth()">{{ $t('task.show.nextMonth') }}</x-button>
</div> </div>
<template v-if="!loading && (!tasks || tasks.length === 0) && showNothingToDo"> <template v-if="!loading && (!tasks || tasks.length === 0) && showNothingToDo">
<h3 class="nothing">{{ $t('task.show.noTasks') }}</h3> <h3 class="nothing">{{ $t('task.show.noTasks') }}</h3>

View File

@ -9,7 +9,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { ref } from 'vue'
import ShowTasks from './ShowTasks' import ShowTasks from './ShowTasks.vue'
function getNextWeekDate() { function getNextWeekDate() {
return new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000) return new Date((new Date()).getTime() + 7 * 24 * 60 * 60 * 1000)

View File

@ -258,7 +258,7 @@
@click="toggleTaskDone()" @click="toggleTaskDone()"
class="is-outlined has-no-border" class="is-outlined has-no-border"
icon="check-double" icon="check-double"
type="secondary" variant="secondary"
> >
{{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }} {{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }}
</x-button> </x-button>
@ -270,7 +270,7 @@
/> />
<x-button <x-button
@click="setFieldActive('assignees')" @click="setFieldActive('assignees')"
type="secondary" variant="secondary"
v-shortcut="'a'" v-shortcut="'a'"
v-cy="'taskDetail.assign'" v-cy="'taskDetail.assign'"
> >
@ -279,7 +279,7 @@
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('labels')" @click="setFieldActive('labels')"
type="secondary" variant="secondary"
icon="tags" icon="tags"
v-shortcut="'l'" v-shortcut="'l'"
> >
@ -287,14 +287,14 @@
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('priority')" @click="setFieldActive('priority')"
type="secondary" variant="secondary"
icon="exclamation" icon="exclamation"
> >
{{ $t('task.detail.actions.priority') }} {{ $t('task.detail.actions.priority') }}
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('dueDate')" @click="setFieldActive('dueDate')"
type="secondary" variant="secondary"
icon="calendar" icon="calendar"
v-shortcut="'d'" v-shortcut="'d'"
> >
@ -302,42 +302,42 @@
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('startDate')" @click="setFieldActive('startDate')"
type="secondary" variant="secondary"
icon="play" icon="play"
> >
{{ $t('task.detail.actions.startDate') }} {{ $t('task.detail.actions.startDate') }}
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('endDate')" @click="setFieldActive('endDate')"
type="secondary" variant="secondary"
icon="stop" icon="stop"
> >
{{ $t('task.detail.actions.endDate') }} {{ $t('task.detail.actions.endDate') }}
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('reminders')" @click="setFieldActive('reminders')"
type="secondary" variant="secondary"
:icon="['far', 'clock']" :icon="['far', 'clock']"
> >
{{ $t('task.detail.actions.reminders') }} {{ $t('task.detail.actions.reminders') }}
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('repeatAfter')" @click="setFieldActive('repeatAfter')"
type="secondary" variant="secondary"
icon="history" icon="history"
> >
{{ $t('task.detail.actions.repeatAfter') }} {{ $t('task.detail.actions.repeatAfter') }}
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('percentDone')" @click="setFieldActive('percentDone')"
type="secondary" variant="secondary"
icon="percent" icon="percent"
> >
{{ $t('task.detail.actions.percentDone') }} {{ $t('task.detail.actions.percentDone') }}
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('attachments')" @click="setFieldActive('attachments')"
type="secondary" variant="secondary"
icon="paperclip" icon="paperclip"
v-shortcut="'f'" v-shortcut="'f'"
> >
@ -345,7 +345,7 @@
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('relatedTasks')" @click="setFieldActive('relatedTasks')"
type="secondary" variant="secondary"
icon="sitemap" icon="sitemap"
v-shortcut="'r'" v-shortcut="'r'"
> >
@ -353,21 +353,21 @@
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('moveList')" @click="setFieldActive('moveList')"
type="secondary" variant="secondary"
icon="list" icon="list"
> >
{{ $t('task.detail.actions.moveList') }} {{ $t('task.detail.actions.moveList') }}
</x-button> </x-button>
<x-button <x-button
@click="setFieldActive('color')" @click="setFieldActive('color')"
type="secondary" variant="secondary"
icon="fill-drip" icon="fill-drip"
> >
{{ $t('task.detail.actions.color') }} {{ $t('task.detail.actions.color') }}
</x-button> </x-button>
<x-button <x-button
@click="toggleFavorite" @click="toggleFavorite"
type="secondary" variant="secondary"
:icon="task.isFavorite ? 'star' : ['far', 'star']" :icon="task.isFavorite ? 'star' : ['far', 'star']"
> >
{{ {{
@ -627,7 +627,6 @@ export default {
} }
this.task = await this.$store.dispatch('tasks/update', this.task) this.task = await this.$store.dispatch('tasks/update', this.task)
this.setActiveFields()
if (!showNotification) { if (!showNotification) {
return return
@ -871,7 +870,7 @@ $flash-background-duration: 750ms;
} }
.action-buttons { .action-buttons {
a.button { .button {
width: 100%; width: 100%;
margin-bottom: .5rem; margin-bottom: .5rem;
justify-content: left; justify-content: left;

View File

@ -67,7 +67,7 @@
<x-button <x-button
:to="{ name: 'user.register' }" :to="{ name: 'user.register' }"
v-if="registrationEnabled" v-if="registrationEnabled"
type="secondary" variant="secondary"
> >
{{ $t('user.auth.register') }} {{ $t('user.auth.register') }}
</x-button> </x-button>
@ -87,7 +87,7 @@
@click="redirectToProvider(p)" @click="redirectToProvider(p)"
v-for="(p, k) in openidConnect.providers" v-for="(p, k) in openidConnect.providers"
:key="k" :key="k"
type="secondary" variant="secondary"
class="is-fullwidth mt-2" class="is-fullwidth mt-2"
> >
{{ $t('user.auth.loginWith', {provider: p.name}) }} {{ $t('user.auth.loginWith', {provider: p.name}) }}

View File

@ -79,7 +79,7 @@
> >
{{ $t('user.auth.register') }} {{ $t('user.auth.register') }}
</x-button> </x-button>
<x-button :to="{ name: 'user.login' }" type="secondary"> <x-button :to="{ name: 'user.login' }" variant="secondary">
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
</x-button> </x-button>
</div> </div>

View File

@ -35,7 +35,7 @@
> >
{{ $t('user.auth.resetPasswordAction') }} {{ $t('user.auth.resetPasswordAction') }}
</x-button> </x-button>
<x-button :to="{ name: 'user.login' }" type="secondary"> <x-button :to="{ name: 'user.login' }" variant="secondary">
{{ $t('user.auth.login') }} {{ $t('user.auth.login') }}
</x-button> </x-button>
</div> </div>

View File

@ -48,6 +48,7 @@
<x-button <x-button
:loading="avatarService.loading || loading" :loading="avatarService.loading || loading"
@click="uploadAvatar" @click="uploadAvatar"
v-cy="'uploadAvatar'"
> >
{{ $t('user.settings.avatar.uploadAvatar') }} {{ $t('user.settings.avatar.uploadAvatar') }}
</x-button> </x-button>

View File

@ -1,5 +1,5 @@
<template> <template>
<card :title="$t('user.settings.general.title')" class="general-settings" :loading="userSettingsService.loading"> <card :title="$t('user.settings.general.title')" class="general-settings" :loading="loading">
<div class="field"> <div class="field">
<label class="label" :for="`newName${id}`">{{ $t('user.settings.general.name') }}</label> <label class="label" :for="`newName${id}`">{{ $t('user.settings.general.name') }}</label>
<div class="control"> <div class="control">
@ -67,8 +67,8 @@
{{ $t('user.settings.general.language') }} {{ $t('user.settings.general.language') }}
</span> </span>
<div class="select ml-2"> <div class="select ml-2">
<select v-model="language"> <select v-model="settings.language">
<option :value="lang.code" v-for="lang in availableLanguages" :key="lang.code">{{ <option :value="lang.code" v-for="lang in availableLanguageOptions" :key="lang.code">{{
lang.title lang.title
}} }}
</option> </option>
@ -107,9 +107,10 @@
</div> </div>
<x-button <x-button
:loading="userSettingsService.loading" :loading="loading"
@click="updateSettings()" @click="updateSettings()"
class="is-fullwidth mt-4" class="is-fullwidth mt-4"
v-cy="'saveGeneralSettings'"
> >
{{ $t('misc.save') }} {{ $t('misc.save') }}
</x-button> </x-button>
@ -120,14 +121,12 @@
import {computed, watch} from 'vue' import {computed, watch} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {playSoundWhenDoneKey} from '@/helpers/playPop' import {playSoundWhenDoneKey, playPop} from '@/helpers/playPop'
import {availableLanguages, saveLanguage, getCurrentLanguage} from '@/i18n' import {availableLanguages} from '@/i18n'
import {getQuickAddMagicMode, setQuickAddMagicMode} from '@/helpers/quickAddMagicMode' import {getQuickAddMagicMode, setQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import UserSettingsService from '@/services/userSettings'
import {PrefixMode} from '@/modules/parseTaskText' import {PrefixMode} from '@/modules/parseTaskText'
import ListSearch from '@/components/tasks/partials/listSearch' import ListSearch from '@/components/tasks/partials/listSearch'
import {createRandomID} from '@/helpers/randomId' import {createRandomID} from '@/helpers/randomId'
import {playPop} from '@/helpers/playPop'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {success} from '@/message' import {success} from '@/message'
@ -165,23 +164,19 @@ export default {
data() { data() {
return { return {
playSoundWhenDone: getPlaySoundWhenDoneSetting(), playSoundWhenDone: getPlaySoundWhenDoneSetting(),
language: getCurrentLanguage(),
quickAddMagicMode: getQuickAddMagicMode(), quickAddMagicMode: getQuickAddMagicMode(),
quickAddMagicPrefixes: PrefixMode, quickAddMagicPrefixes: PrefixMode,
userSettingsService: new UserSettingsService(),
settings: {...this.$store.state.auth.settings}, settings: {...this.$store.state.auth.settings},
id: createRandomID(), id: createRandomID(),
availableLanguageOptions: Object.entries(availableLanguages)
.map(l => ({code: l[0], title: l[1]}))
.sort((a, b) => a.title.localeCompare(b.title)),
} }
}, },
components: { components: {
ListSearch, ListSearch,
}, },
computed: { computed: {
availableLanguages() {
return Object.entries(availableLanguages)
.map(l => ({code: l[0], title: l[1]}))
.sort((a, b) => a.title.localeCompare(b.title))
},
defaultList: { defaultList: {
get() { get() {
return this.$store.getters['lists/getListById'](this.settings.defaultListId) return this.$store.getters['lists/getListById'](this.settings.defaultListId)
@ -190,6 +185,9 @@ export default {
this.settings.defaultListId = l ? l.id : DEFAULT_LIST_ID this.settings.defaultListId = l ? l.id : DEFAULT_LIST_ID
}, },
}, },
loading() {
return this.$store.state.loading && this.$store.state.loadingModule === 'general-settings'
},
}, },
setup() { setup() {
@ -211,16 +209,11 @@ export default {
methods: { methods: {
async updateSettings() { async updateSettings() {
localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone) localStorage.setItem(playSoundWhenDoneKey, this.playSoundWhenDone)
saveLanguage(this.language)
setQuickAddMagicMode(this.quickAddMagicMode) setQuickAddMagicMode(this.quickAddMagicMode)
const settings = { await this.$store.dispatch('auth/saveUserSettings', {
...this.settings, settings: {...this.settings},
} })
await this.userSettingsService.update(settings)
this.$store.commit('auth/setUserSettings', settings)
this.$message.success({message: this.$t('user.settings.general.savedSuccess')})
}, },
}, },
} }

View File

@ -55,7 +55,7 @@
<x-button @click="totpDisable" class="is-danger"> <x-button @click="totpDisable" class="is-danger">
{{ $t('user.settings.totp.disable') }} {{ $t('user.settings.totp.disable') }}
</x-button> </x-button>
<x-button @click="totpDisableForm = false" type="tertary" class="ml-2"> <x-button @click="totpDisableForm = false" variant="tertiary" class="ml-2">
{{ $t('misc.cancel') }} {{ $t('misc.cancel') }}
</x-button> </x-button>
</div> </div>

View File

@ -15,7 +15,6 @@
"baseUrl": ".", "baseUrl": ".",
"isolatedModules": true, "isolatedModules": true,
"types": [ "types": [
"jest",
"vite/client" "vite/client"
], ],
"paths": { "paths": {

View File

@ -1,3 +1,4 @@
/// <reference types="vitest" />
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import vue from '@vitejs/plugin-vue'
import legacyFn from '@vitejs/plugin-legacy' import legacyFn from '@vitejs/plugin-legacy'
@ -27,6 +28,10 @@ if (isModernBuild) {
} }
export default defineConfig({ export default defineConfig({
// https://vitest.dev/config/
test: {
environment: 'happy-dom',
},
css: { css: {
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {

2977
yarn.lock

File diff suppressed because it is too large Load Diff