Compare commits

..

515 Commits

Author SHA1 Message Date
kolaente 653415e764
chore(deps): install dependencies after rebase
continuous-integration/drone/pr Build is pending Details
2023-06-18 14:38:30 +02:00
Dominik Pschenitschni 73947f0ba4
feat: add vite-plugin sentry 2023-06-18 14:33:21 +02:00
renovate 389ca1b692 chore(deps): update dependency vue-tsc to v1.8.0
continuous-integration/drone/push Build is failing Details
2023-06-18 08:30:20 +00:00
renovate 9c0e140e2e chore(deps): update dependency eslint to v8.43.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-16 22:05:48 +00:00
renovate 51d08a1637 fix(deps): update dependency floating-vue to v2.0.0-beta.22
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-16 19:05:33 +00:00
kolaente 35de8a40d8
fix(docker): copy patches prior to installing dependencies so that the installation actually works
continuous-integration/drone/push Build is passing Details
2023-06-16 20:35:08 +02:00
kolaente 80772f7578
fix(ci): directly build docker images and not use releases to avoid caching issues
continuous-integration/drone/push Build is failing Details
2023-06-16 20:07:43 +02:00
kolaente faa62985df
fix: correctly sync filters on upcoming tasks page
continuous-integration/drone/push Build is passing Details
Resolves #3600
2023-06-16 19:49:43 +02:00
kolaente 154d43a392
fix(reminders): don't assigne the task
continuous-integration/drone/push Build is passing Details
2023-06-16 19:42:55 +02:00
renovate 7fe5565654 chore(deps): update dependency vitest to v0.32.2
continuous-integration/drone/push Build is passing Details
2023-06-16 17:21:20 +00:00
kolaente 1fcd1cdd4b
fix(reminders): assignment to const when changing a reminder
continuous-integration/drone/push Build is passing Details
2023-06-16 19:20:40 +02:00
kolaente ba057f3527
feat(reminders): add preset two hours before due / start / end date 2023-06-16 19:12:07 +02:00
kolaente dd7b77e12d
feat(reminders): add on the due / start / end date as a reminder preset 2023-06-16 19:12:06 +02:00
renovate 3845a45934 chore(deps): update dependency vitest to v0.32.1
continuous-integration/drone/push Build is passing Details
2023-06-16 16:41:52 +00:00
renovate 564808bd35 fix(deps): update dependency @vueuse/core to v10.2.0
continuous-integration/drone/push Build is passing Details
2023-06-16 16:41:30 +00:00
renovate c0a66e4746 fix(deps): update dependency floating-vue to v2.0.0-beta.21
continuous-integration/drone/push Build is passing Details
2023-06-16 16:41:08 +00:00
renovate 28242cfb23 chore(deps): update dependency esbuild to v0.18.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-16 16:05:32 +00:00
renovate 818fb2b524 chore(deps): update dependency esbuild to v0.18.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-16 01:05:45 +00:00
Frederick [Bot] ad95bdd039 [skip ci] Updated translations via Crowdin 2023-06-16 00:29:42 +00:00
renovate 3faed19298 chore(deps): update dependency @rushstack/eslint-patch to v1.3.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-15 01:05:19 +00:00
renovate 9114a86813 chore(deps): update dependency postcss-preset-env to v8.5.0
continuous-integration/drone/push Build is passing Details
2023-06-14 21:21:31 +00:00
renovate bae9a5c9be fix(deps): update sentry-javascript monorepo to v7.55.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build encountered an error Details
2023-06-14 15:05:21 +00:00
renovate fe2d6d4467 chore(deps): update dependency sass to v1.63.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-14 01:05:12 +00:00
Frederick [Bot] 96acea90ed [skip ci] Updated translations via Crowdin 2023-06-14 00:29:44 +00:00
renovate 21c98d5166 chore(deps): update dependency histoire to v0.16.2
continuous-integration/drone/push Build is passing Details
2023-06-13 17:14:09 +00:00
renovate b3cb36c1e1 fix(deps): update sentry-javascript monorepo to v7.55.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-06-13 15:06:33 +00:00
kolaente 79ceaf6a2b
fix(task): repeat mode now saves correctly
continuous-integration/drone/push Build is passing Details
2023-06-13 12:33:35 +02:00
kolaente 5694b39489
feat(reminders): show resolved reminder time in a tooltip and properly bubble updated task down to the reminder component
continuous-integration/drone/push Build is passing Details
2023-06-13 12:30:07 +02:00
kolaente 32e5f9f757
fix(reminders): don't sync negative relative reminder amounts in ui
continuous-integration/drone/push Build is passing Details
2023-06-13 12:10:10 +02:00
kolaente 928b338cf2
fix(reminders): don't assume 30 days are always a month
continuous-integration/drone/push Build is passing Details
2023-06-13 12:06:00 +02:00
kolaente 1a792e0667
feat(reminders): only show relative reminders when there's a date to relate them to
continuous-integration/drone/push Build is passing Details
2023-06-13 12:03:28 +02:00
renovate 6407644138 chore(deps): update dependency esbuild to v0.18.2
continuous-integration/drone/push Build is failing Details
2023-06-13 05:54:06 +00:00
renovate 2db88b583b chore(deps): update dependency @types/node to v18.16.18
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-13 03:05:22 +00:00
kolaente bef25c49d5
feat: new image for the unauthenticated views
continuous-integration/drone/push Build is passing Details
There have been so many big changes lately, I think it's time for a new image.
2023-06-12 21:58:17 +02:00
renovate f01ea20a38 chore(deps): update typescript-eslint monorepo to v5.59.11
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-12 18:06:30 +00:00
kolaente 3c9083b90d
feat: add message to add to home screen on mobile
continuous-integration/drone/push Build is passing Details
2023-06-12 19:37:58 +02:00
konrad 169feaaf0f feat(user): persist frontend settings in the api (#3594)
continuous-integration/drone/push Build is passing Details
Implements saving of frontend settings for 04e2c51fac.

Resolves https://github.com/go-vikunja/frontend/issues/105
Resolves vikunja/api#1250
Resolves vikunja/api#1452
Reviewed-on: #3594
2023-06-12 16:22:51 +00:00
kolaente 5d59392566
fix: lint
continuous-integration/drone/pr Build is passing Details
2023-06-12 18:06:07 +02:00
kolaente 6593380013
fix: load the correct language
continuous-integration/drone/pr Build is failing Details
2023-06-12 18:01:56 +02:00
kolaente 69e94e58c4
fix: tests
continuous-integration/drone/pr Build was killed Details
2023-06-12 16:35:47 +02:00
kolaente cd8e497b24
fix(user): lint
continuous-integration/drone/pr Build is failing Details
2023-06-12 16:22:15 +02:00
kolaente aab2020e68
chore(user): cleanup 2023-06-12 16:20:46 +02:00
kolaente a050419fdf
fix(user): set the language when saving 2023-06-12 16:19:47 +02:00
kolaente f0c3980700
fix(user): fix flickering of default settings 2023-06-12 16:18:01 +02:00
kolaente 68597c9709
feat(user): use user language from store after logging in 2023-06-12 16:08:31 +02:00
kolaente 5325f6d7d9
feat(user): migrate color scheme settings to persistance in db 2023-06-12 15:57:18 +02:00
renovate 8c687350a0 chore(deps): update dependency esbuild to v0.18.1
continuous-integration/drone/push Build is passing Details
2023-06-12 06:45:04 +00:00
renovate bac679caf7 chore(deps): update dependency rollup to v3.25.1
continuous-integration/drone/push Build is passing Details
2023-06-12 06:42:03 +00:00
renovate 4f8ff17138 chore(deps): update dependency caniuse-lite to v1.0.30001500
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-12 06:05:17 +00:00
Frederick [Bot] 83e7138a18 [skip ci] Updated translations via Crowdin 2023-06-12 00:28:51 +00:00
renovate 8e44b87d07 chore(deps): update pnpm to v8.6.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-11 20:04:39 +00:00
kolaente 4b0022664a
feat(user): set default settings when loading persisted 2023-06-11 17:43:55 +02:00
kolaente d8ad934643
feat(user): save quick add magic mode in api 2023-06-11 17:37:49 +02:00
kolaente 77ee1bfc3e
feat(user): migrate pop sound setting to store in api 2023-06-11 17:31:04 +02:00
renovate 8728647f00 chore(deps): update dependency vite-plugin-pwa to v0.16.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-11 12:05:13 +00:00
kolaente bd7d09c17c
fix(repeat): prevent disappearing repeat mode settings when modes other than default repeat mode were selected
continuous-integration/drone/push Build is passing Details
Resolves #3585
2023-06-11 09:48:54 +02:00
renovate 77bedbd1cf chore(deps): update dependency rollup to v3.25.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-11 06:05:08 +00:00
renovate 2773612420 chore(deps): update dependency rollup-plugin-visualizer to v5.9.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-10 19:05:24 +00:00
kolaente 48cfdddff7
chore(reminders): remove reminderDates property
continuous-integration/drone/push Build is passing Details
2023-06-10 19:05:22 +02:00
konrad 3f8e457d52 feat: edit relative reminders (#3248)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3248
2023-06-10 17:04:09 +00:00
kolaente 098b5fa2b1
fix(reminders): properly parse relative reminders which don't have an amount
continuous-integration/drone/pr Build is passing Details
2023-06-10 18:54:39 +02:00
kolaente 5e4eb4a728
fix(reminders): custom relative highlight now only when a custom relative reminder was actually selected 2023-06-10 18:54:10 +02:00
kolaente 8930f61548
fix(reminders): flatpickr styling improvements 2023-06-10 18:46:15 +02:00
kolaente 9a736cf65f
fix(reminders): style flatpickr so that it blends in more
continuous-integration/drone/pr Build is passing Details
2023-06-10 18:39:33 +02:00
kolaente 2677f6254d
feat(reminders): highlight which preset or custom date is selected
continuous-integration/drone/pr Build is passing Details
2023-06-10 17:35:50 +02:00
kolaente bfcb36e093
fix(reminders): align remove icon with the rest
continuous-integration/drone/pr Build is passing Details
2023-06-10 17:29:30 +02:00
kolaente 9ec29cad30
fix: lint
continuous-integration/drone/pr Build is passing Details
2023-06-10 17:26:06 +02:00
renovate c4f609a0c8 chore(deps): update dependency esbuild to v0.18.0
continuous-integration/drone/push Build is passing Details
2023-06-10 15:00:24 +00:00
renovate 7e7535b860 chore(deps): update dependency @types/node to v18.16.17
continuous-integration/drone/push Build is passing Details
2023-06-10 15:00:05 +00:00
renovate df9181b34e chore(deps): update dependency rollup to v3.24.1
continuous-integration/drone/push Build is passing Details
2023-06-10 14:59:45 +00:00
renovate e6a56f2822 fix(deps): update dependency marked to v5.1.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-10 04:06:05 +00:00
renovate 3633d68269 chore(deps): update dependency sass to v1.63.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-09 16:05:26 +00:00
kolaente dd3a5fe6b5
fix(reminders): duplicate reminder for each change
continuous-integration/drone/pr Build is failing Details
2023-06-09 14:46:34 +02:00
kolaente 04642ae1ec
fix(reminders): set date over relative reminder
continuous-integration/drone/pr Build is failing Details
2023-06-09 14:43:02 +02:00
kolaente eac19e28d6
feat(reminders): add more spacing
continuous-integration/drone/pr Build is failing Details
2023-06-09 14:39:35 +02:00
kolaente 11f94e4037
feat(reminders): make adding new reminders less confusing
continuous-integration/drone/pr Build is failing Details
2023-06-09 14:37:26 +02:00
kolaente 39cc7a00d8
feat(reminders): make relative presets actually work
continuous-integration/drone/pr Build is failing Details
2023-06-09 14:31:32 +02:00
kolaente 02da1e171e
feat(reminders): add confirm button 2023-06-09 14:27:11 +02:00
kolaente ae177c73ea
feat(reminders): move reminder settings to a popup 2023-06-09 14:23:32 +02:00
kolaente e6c4c18974
feat(reminders): translate all reminder form strings
continuous-integration/drone/pr Build is failing Details
2023-06-09 13:54:17 +02:00
kolaente 95487d7569
feat(reminders): add proper time picker for relative dates
continuous-integration/drone/pr Build is failing Details
2023-06-09 13:19:47 +02:00
kolaente 7b2a688b6e
feat(datepicker): separate datepicker popup and datepicker logic in different components
continuous-integration/drone/pr Build is failing Details
2023-06-09 12:07:23 +02:00
Frederick [Bot] f5b3b21ce0 [skip ci] Updated translations via Crowdin 2023-06-09 00:29:38 +00:00
kolaente 979561342a
fix(kanban): decrease task count per bucket when deleting a task
continuous-integration/drone/push Build is passing Details
2023-06-08 17:09:48 +02:00
kolaente ad27f588a2
feat(kanban): use total task count from the api instead of manually calculating it per bucket
continuous-integration/drone/push Build is passing Details
This fixes an ux issue where the total count would show a wrong number of total tasks because that was the number of tasks which were loaded at the time. In combination with bucket limits, this caused error messages when the user would attempt to drag tasks into a bucket which appeared not full but was.
2023-06-08 16:57:58 +02:00
kolaente c7a989d7dc
fix(kanban): don't export buckets as readonly because that makes it impossible to update them, even from within the store
continuous-integration/drone/push Build is passing Details
This fixes a bug where the task on the kanban board would not get updated because the "tasks" property of all buckets were still read only if they were exported once as readonly. This has been unnoticed in the past because the visual representation of the board still perfectly matched what the user was doing and what was saved in the api - just not what was stored in pina.
2023-06-08 16:54:52 +02:00
renovate 0e674d8300 chore(deps): update dependency @rushstack/eslint-patch to v1.3.1
continuous-integration/drone/push Build is passing Details
2023-06-08 06:45:00 +00:00
renovate 121fd70235 chore(deps): update dependency sass to v1.63.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-08 02:06:22 +00:00
renovate d4cd90da45 chore(deps): update dependency sass to v1.63.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-07 21:05:13 +00:00
kolaente c74612f24a
fix: lint
continuous-integration/drone/push Build is passing Details
2023-06-07 21:53:40 +02:00
kolaente 64f9f4fd88
fix: disable autocomplete in assignee search
continuous-integration/drone/push Build is failing Details
Resolves https://github.com/go-vikunja/frontend/issues/114
2023-06-07 21:46:18 +02:00
kolaente b50adaf4b5
fix(navigation): highlight saved filters in project view and prevent them from being dragged around
continuous-integration/drone/push Build is passing Details
2023-06-07 20:58:49 +02:00
renovate 7b92028e67 chore(deps): update dependency cypress to v12.14.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-07 17:05:24 +00:00
renovate 08d84f7994 chore(deps): update dependency rollup to v3.24.0
continuous-integration/drone/push Build is passing Details
2023-06-07 06:51:59 +00:00
renovate f95b138b9f fix(deps): update dependency marked to v5.0.5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-07 05:05:20 +00:00
Frederick [Bot] e6aecbd8dc [skip ci] Updated translations via Crowdin 2023-06-07 00:29:36 +00:00
renovate eab0600f63 chore(deps): update dependency vitest to v0.32.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-06 18:05:23 +00:00
kolaente 46f5dcb4dc
fix(navigation): show text ellipsis for very long project titles
continuous-integration/drone/push Build is passing Details
2023-06-06 17:33:55 +02:00
kolaente 0dc7e83dc4
fix(navigation): menu item overflow 2023-06-06 17:32:18 +02:00
kolaente 82c10b87c8
fix(navigation): hide archived subprojects
continuous-integration/drone/push Build is passing Details
2023-06-06 17:29:08 +02:00
kolaente 5888946861
fix(navigation): sidebar top spacing
continuous-integration/drone/push Build is passing Details
2023-06-06 17:27:05 +02:00
renovate e24607ed3a chore(deps): update dependency @types/codemirror to v5.60.8
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-06 02:05:16 +00:00
Frederick [Bot] d1ae6a8b84 [skip ci] Updated translations via Crowdin 2023-06-06 00:29:33 +00:00
renovate fc052cf8f5 chore(deps): update typescript-eslint monorepo to v5.59.9
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-05 18:05:53 +00:00
konrad d9f608e8b4 feat: improve user assignments via quick add magic (#3348)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3348
2023-06-05 15:03:14 +00:00
konrad a988565227 feat: hide quick add magic help behind a tooltip (#3353)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3353
2023-06-05 15:02:42 +00:00
Dominik Pschenitschni b76fffb788 Update 'src/i18n/lang/en.json'
continuous-integration/drone/push Build is passing Details
2023-06-05 14:31:30 +00:00
kolaente 25c3b7bcbf chore: move styles to components
continuous-integration/drone/pr Build is passing Details
2023-06-05 14:20:41 +00:00
kolaente dfa6cd777b fix: add more padding to the textarea 2023-06-05 14:20:41 +00:00
kolaente 21ad8301f2 chore: add comment on overriding 2023-06-05 14:20:41 +00:00
kolaente 7110c9a5ce chore: move positioning css 2023-06-05 14:20:41 +00:00
kolaente a4c8fccb11 chore: remove nesting 2023-06-05 14:20:41 +00:00
Dominik Pschenitschni c294f9d28d fix: lint 2023-06-05 14:20:41 +00:00
kolaente 422d7fc693 feat: highlight hint icon when hovering the input 2023-06-05 14:20:41 +00:00
kolaente abb5128426 feat: add transition to input icons 2023-06-05 14:20:41 +00:00
kolaente 2174608801 fix: improve tooltip text 2023-06-05 14:20:41 +00:00
kolaente a6cdf6c4bd fix: improve tooltip icon contrast 2023-06-05 14:20:41 +00:00
kolaente 2c9693a83e chore: remove wrapper div 2023-06-05 14:20:41 +00:00
kolaente 6989558963 feat: move quick add magic to a popup behind an icon 2023-06-05 14:20:41 +00:00
kolaente 7fb85dacec feat: allow hiding the quick add magic help tooltip with a button 2023-06-05 14:20:41 +00:00
steffeydev 57218d1454 fix: allow icon changes configuration via env (#3567)
continuous-integration/drone/push Build is passing Details
`window.ALLOW_ICON_CHANGES` needs to be written as a boolean during the docker deploy instead of a string. The strings `"true"` and `"false"` both evaluate to `true` in JS, so we need to use the boolean `true` and `false` for the assertion in `Logo.vue` to be meaningful.

Co-authored-by: SteffeyDev <steffeydev@icloud.com>
Reviewed-on: #3567
Reviewed-by: konrad <k@knt.li>
Co-authored-by: steffeydev <steffeydev@icloud.com>
Co-committed-by: steffeydev <steffeydev@icloud.com>
2023-06-05 14:19:55 +00:00
kolaente 9df6950d1a
feat: start adding relative reminder picker with more options
continuous-integration/drone/pr Build is failing Details
2023-06-05 16:16:10 +02:00
kolaente cd2b7fe185 fix: lint
continuous-integration/drone/pr Build is passing Details
2023-06-05 14:09:19 +00:00
kolaente 52987060b1 chore: group return parameter 2023-06-05 14:09:19 +00:00
kolaente aeb73a374f chore: make fuzzy matching a paramater 2023-06-05 14:09:19 +00:00
kolaente bc416f282f fix: make type singular 2023-06-05 14:09:19 +00:00
kolaente f88c373742 chore(i18n): clarify translation string 2023-06-05 14:09:19 +00:00
kolaente 10ac1ff66a chore: use startsWith for prefix matching 2023-06-05 14:09:19 +00:00
kolaente ae025e30c6 fix: clarify user search setting 2023-06-05 14:09:19 +00:00
kolaente a1dd1d6664 chore: remove user margin from the component 2023-06-05 14:09:19 +00:00
kolaente 57c64bbf71 chore: remove user margin from the component 2023-06-05 14:09:19 +00:00
kolaente 218a19d907 feat(quick add magic): allow fuzzy matching of assignees when the api results are unambigous 2023-06-05 14:09:19 +00:00
kolaente 7b6a13dd52 fix: ensure all matched quick add magic parts are correctly removed from the task 2023-06-05 14:09:19 +00:00
kolaente 4ff0c81e37 fix: lint 2023-06-05 14:09:19 +00:00
kolaente 6a15489610 feat(assignees): show user avatar in search results 2023-06-05 14:09:19 +00:00
kolaente 59c942af73 feat: show initial list of users when opening the assignees view 2023-06-05 14:09:19 +00:00
kolaente 302ba2bec7 chore: clarify users when can still be found even if they disabled it 2023-06-05 14:09:19 +00:00
kolaente 34d1e4bddd fix(quick add magic): cleanup all assignee properties 2023-06-05 14:09:19 +00:00
kolaente 02c24a4814 fix(quick add magic): use the project user service to find assignees for quick add magic 2023-06-05 14:09:19 +00:00
kolaente 0724776ccb fix(quick add magic): don't replace the prefix in every occurrence when it is present in the matched part 2023-06-05 14:09:19 +00:00
renovate 11979cbee0 chore(deps): update caniuse-and-related
continuous-integration/drone/push Build is passing Details
2023-06-05 13:24:09 +00:00
renovate 2a490bf8ef chore(deps): update dependency happy-dom to v9.20.3
continuous-integration/drone/push Build is passing Details
2023-06-05 13:23:57 +00:00
renovate b5d3d1a7b7 chore(deps): update pnpm to v8.6.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-05 13:04:47 +00:00
renovate 554ffe3b9d chore(deps): update dependency rollup to v3.23.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-04 19:08:49 +00:00
renovate 0f57be107b chore(deps): update dependency vite-plugin-pwa to v0.16.3
continuous-integration/drone/push Build is passing Details
2023-06-03 09:33:26 +00:00
renovate 269aa6b426 chore(deps): update dependency eslint to v8.42.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-03 00:05:32 +00:00
renovate b316b8f2ba chore(deps): update dependency vite-plugin-pwa to v0.16.1
continuous-integration/drone/push Build is passing Details
2023-06-02 13:20:52 +00:00
renovate cad68e269c fix(deps): update dependency dayjs to v1.11.8
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-06-02 13:05:16 +00:00
kolaente efb3407b87
feat: allow disabling icon changes
continuous-integration/drone/push Build is passing Details
2023-06-02 13:51:47 +02:00
renovate 6f1ff02c04 chore(deps): update dependency vitest to v0.31.4
continuous-integration/drone/push Build is passing Details
2023-06-02 11:40:07 +00:00
renovate 93c66b0613 chore(deps): update dependency @4tw/cypress-drag-drop to v2.2.4
continuous-integration/drone/push Build is passing Details
2023-06-02 11:39:31 +00:00
renovate c14644a300 chore(deps): update dependency postcss-preset-env to v8.4.2
continuous-integration/drone/push Build is passing Details
2023-06-02 11:39:20 +00:00
renovate 02d2300608 fix(deps): update sentry-javascript monorepo to v7.54.0
continuous-integration/drone/push Build is passing Details
2023-06-02 11:38:49 +00:00
renovate ff918608c5 chore(deps): update dependency typescript to v5.1.3
continuous-integration/drone/push Build is passing Details
2023-06-02 11:38:39 +00:00
renovate aa591ee2ed chore(deps): update workbox monorepo to v7 (major) (#3556)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3556
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-06-02 11:03:36 +00:00
kolaente f4a7943680
fix: bubble changes from the editor immediately and move the delay to callers
continuous-integration/drone/push Build is passing Details
This gives the callers more control over when to save data and show/hide additional controls based on the input text
2023-06-02 12:40:21 +02:00
kolaente 68fd4698ac
fix: don't try to set a user language if none is saved 2023-06-02 11:43:42 +02:00
Frederick [Bot] dd039f31fe [skip ci] Updated translations via Crowdin 2023-06-01 00:28:46 +00:00
kolaente 6c2dc483a2
fix: redundant ) 2023-05-31 16:27:20 +02:00
Dominik Pschenitschni 811254e6a9
wip: base review
continuous-integration/drone/pr Build is failing Details
(cherry picked from commit 3bb64f078cf333f174d247dc404355f2c8437cfd)
2023-05-31 16:25:33 +02:00
cernst 85ffed4d9a
fix: review findings 2023-05-31 16:25:33 +02:00
cernst 5fb45afb12
fix: review findings 2023-05-31 16:25:33 +02:00
cernst fb14eca634
fix: review findings 2023-05-31 16:25:32 +02:00
cernst 14e2698833
feat: edit relative reminders 2023-05-31 16:25:32 +02:00
cernst 0d6c0c8399
chore; extract code to reminder-period.vue 2023-05-31 16:25:32 +02:00
cernst 5d38b8327f
feat: allow to edit existing relative reminders 2023-05-31 16:25:32 +02:00
cernst f747d5b2fc
feat: Use new Reminders API instead of reminder_dates 2023-05-31 16:25:30 +02:00
kolaente 8a75790453
chore: remove triggered notifications as it's not supported anywhere
continuous-integration/drone/push Build encountered an error Details
2023-05-31 15:21:09 +02:00
kolaente acb212ab24
feat: set the current language to the one saved by the user on login 2023-05-31 15:17:54 +02:00
kolaente 4ba02ebbb6
fix: don't try to convert a null date
continuous-integration/drone/push Build is passing Details
Resolves #3371
2023-05-31 15:07:23 +02:00
kolaente 244da46e38
fix(navigation): nav item width for items without sub projects
continuous-integration/drone/push Build is passing Details
2023-05-31 14:37:57 +02:00
kolaente f40035dc79
chore: update nix flake
continuous-integration/drone/push Build is passing Details
2023-05-31 13:44:14 +02:00
renovate 5f71e406fc fix(deps): update dependency marked to v5.0.4
continuous-integration/drone/push Build is passing Details
2023-05-31 05:15:46 +00:00
Frederick [Bot] 3d11a4f03a [skip ci] Updated translations via Crowdin 2023-05-31 00:30:36 +00:00
renovate 1dfd2dc4b7 fix(deps): update dependency vue-router to v4.2.2
continuous-integration/drone/push Build is passing Details
2023-05-30 22:46:00 +00:00
renovate e9701660d3 chore(deps): update dependency vite-plugin-pwa to v0.15.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-30 21:04:22 +00:00
renovate c8dbb4c7ef chore(deps): update typescript-eslint monorepo to v5.59.8
continuous-integration/drone/push Build is passing Details
2023-05-30 20:31:05 +00:00
renovate 1241d90268 chore(deps): update workbox monorepo to v6.6.1 (#3553)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3553
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-05-30 20:30:46 +00:00
renovate 3de5b65977 chore(deps): update dependency vitest to v0.31.2
continuous-integration/drone/push Build is passing Details
2023-05-30 18:45:05 +00:00
renovate 4a353553c3 chore(deps): update pnpm to v8.6.0
continuous-integration/drone/push Build is passing Details
2023-05-30 18:42:51 +00:00
renovate 1240f31c0a chore(deps): update workbox monorepo to v6.6.0 (#3548)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3548
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-05-30 18:12:03 +00:00
kolaente 01ac84ce1e
fix: don't require variant prop on loading component as it already has a default one set
continuous-integration/drone/push Build is passing Details
2023-05-30 20:00:02 +02:00
kolaente 4c969f0a42
fix: don't allow creating a new label from filter view
continuous-integration/drone/push Build is passing Details
Resolves #1035
2023-05-30 19:54:01 +02:00
kolaente 8e2c76a33e
feat: optimize print view for project views
continuous-integration/drone/push Build is passing Details
2023-05-30 19:50:37 +02:00
renovate b3666ec27e chore(deps): update dependency @vitejs/plugin-legacy to v4.0.4
continuous-integration/drone/push Build is failing Details
2023-05-30 16:36:54 +00:00
renovate 2c6862c509 chore(deps): update dependency vite-plugin-pwa to v0.15.1
continuous-integration/drone/push Build is failing Details
2023-05-30 16:36:44 +00:00
renovate 9f8c43818c chore(deps): update dependency @types/node to v18.16.16
continuous-integration/drone/push Build is failing Details
2023-05-30 16:36:17 +00:00
renovate 0debca91c8 chore(deps): update dependency @faker-js/faker to v8.0.2
continuous-integration/drone/push Build is passing Details
2023-05-30 16:36:08 +00:00
renovate 7b6c9fcd24 chore(deps): update dependency postcss to v8.4.24
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-30 15:05:47 +00:00
renovate 55675bf41b fix(deps): update sentry-javascript monorepo to v7.53.1
continuous-integration/drone/push Build is failing Details
2023-05-30 14:25:03 +00:00
renovate bb24b06031 chore(deps): update dependency vite to v4.3.9
continuous-integration/drone/push Build is passing Details
2023-05-30 14:24:37 +00:00
renovate dbce0376d5 fix(deps): update dependency marked to v5.0.3
continuous-integration/drone/push Build is failing Details
2023-05-30 14:24:15 +00:00
renovate 40db144a41 fix(deps): update dependency @intlify/unplugin-vue-i18n to v0.11.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-30 11:08:12 +00:00
kolaente f7ba3bd08f
fix: increase default auto-save timeout to 5 seconds
continuous-integration/drone/push Build is failing Details
Related discussion: https://community.vikunja.io/t/task-description-constantly-saving-loosing-content/1350
2023-05-30 12:19:14 +02:00
konrad ac1d374191 feat: remove namespaces, make projects infinitely nestable (#3323)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3323
2023-05-30 10:09:39 +00:00
kolaente 391992effb
fix: missing await
continuous-integration/drone/pr Build is passing Details
2023-05-30 11:37:45 +02:00
kolaente 2e9ade11c3
fix: missing variant prop for loading component
continuous-integration/drone/pr Build was killed Details
2023-05-30 11:05:10 +02:00
kolaente f11a8c543b
fix(tests): project archived filter checkbox selector 2023-05-30 11:00:30 +02:00
kolaente e30a4452f2
fix(tests): new project input field 2023-05-30 10:57:08 +02:00
kolaente 6cc11e64ab
fix: undefined parent project when none was selected 2023-05-30 10:56:42 +02:00
kolaente 7b05ed9d3d
fix: avoid crashing browser processes during tests
continuous-integration/drone/pr Build was killed Details
2023-05-30 10:42:32 +02:00
Frederick [Bot] dba35c0107 [skip ci] Updated translations via Crowdin 2023-05-29 00:28:01 +00:00
Frederick [Bot] bfbc874b1d [skip ci] Updated translations via Crowdin 2023-05-28 00:29:34 +00:00
kolaente dbccdb239a
chore(tests): enable experimental memory managment for cypress tests
continuous-integration/drone/pr Build is failing Details
2023-05-24 18:32:23 +02:00
kolaente f13db9268a
fix: translation string 2023-05-24 17:41:14 +02:00
kolaente ed8de7e3eb
fix: lint
continuous-integration/drone/pr Build is failing Details
2023-05-24 15:54:37 +02:00
kolaente b34118485c
feat: allow creating a new project directly as a child project from another one 2023-05-24 15:54:37 +02:00
kolaente 9c3259c660
chore: don't recalculate everything 2023-05-24 15:54:37 +02:00
kolaente a3e289c06c
chore: remove type annotation for computed 2023-05-24 15:54:37 +02:00
kolaente 31b7c1f217
fix: don't set the current project when setting a project 2023-05-24 15:54:37 +02:00
kolaente c30dcff451
chore: don't show selection for parent project when no projects are available 2023-05-24 15:54:37 +02:00
kolaente 086f50d4fe
chore: re-add top menu spacing 2023-05-24 15:54:36 +02:00
kolaente 46e825820c
fix: sort in store 2023-05-24 15:54:36 +02:00
kolaente a3e2cbeb27
feat: replace color dot with handle icon on hover 2023-05-24 15:54:36 +02:00
kolaente a342ae67de
chore: use project id type 2023-05-24 15:54:36 +02:00
kolaente e4d97e0520
chore: don't set the current project to null if it's undefined already 2023-05-24 15:54:36 +02:00
kolaente b69a05689b
chore: move duplicate project logic to composable 2023-05-24 15:54:36 +02:00
kolaente 6b824a49ab
chore: redirect to new project after creating from store 2023-05-24 15:54:36 +02:00
kolaente 652db56d42
chore: remove unused code 2023-05-24 15:54:36 +02:00
kolaente afaf1846ec
chore: don't wrap a computed in another computed 2023-05-24 15:54:36 +02:00
kolaente ba452ab883
fix: move parent project handling out of useProject 2023-05-24 15:54:36 +02:00
kolaente 39f699a61a
fix: rename getParentProjects method to make it clear what it does 2023-05-24 15:54:36 +02:00
kolaente 4ab547810c
fix: return updated project instead of the old one 2023-05-24 15:54:35 +02:00
kolaente bbaddb9406
fix: remove leftovers of childIds 2023-05-24 15:54:35 +02:00
kolaente a2cc9ddc88
fix: properly determine if there are projects 2023-05-24 15:54:35 +02:00
kolaente 175e31ca62
fix: recreate project instead of editing before 2023-05-24 15:54:35 +02:00
kolaente d414b65e7d
fix: remove unnecessary fallback 2023-05-24 15:54:35 +02:00
kolaente 78158bcba5
fix: remove getProjectById and replace all usages of it 2023-05-24 15:54:35 +02:00
kolaente 9402344b7e
fix: add default for level 2023-05-24 15:54:35 +02:00
kolaente 3eca9f6180
fix: only bind child projects data down 2023-05-24 15:54:35 +02:00
kolaente 26e3d42ed5
fix: move parent project child id mutation to store 2023-05-24 15:54:35 +02:00
kolaente 6e095436e9
chore: rename flag 2023-05-24 15:54:35 +02:00
kolaente 1344026494
fix: move the collapsable placeholder to the button 2023-05-24 15:54:35 +02:00
kolaente 1a94496801
fix: bottom margin of project header 2023-05-24 15:54:34 +02:00
kolaente 48570808e5
fix: use the color bubble as handle if the project has a color 2023-05-24 15:54:34 +02:00
kolaente a7440ed296
chore: use stores directly 2023-05-24 15:54:34 +02:00
kolaente 12ebefd86a
chore: move v-if 2023-05-24 15:54:34 +02:00
kolaente 6c9cbaadc8
chore: set project id from the outside 2023-05-24 15:54:34 +02:00
kolaente 9b10693172
chore: replace section with a div 2023-05-24 15:54:34 +02:00
kolaente db1c6d6a41
chore: move all options to component props 2023-05-24 15:54:34 +02:00
kolaente c56787443f
chore: add types for emit 2023-05-24 15:54:34 +02:00
kolaente cb218ec0c3
feat: add setting for infinite nesting 2023-05-24 15:54:34 +02:00
kolaente 0dd6f82a0e
fix: use menu tag everywhere 2023-05-24 15:54:34 +02:00
kolaente 225091864f
fix: collapsing child projects 2023-05-24 15:54:34 +02:00
kolaente ebd9c4702e
feat: don't use child_projects property from api 2023-05-24 15:54:33 +02:00
kolaente 4ad9773022
chore: format 2023-05-24 15:54:33 +02:00
kolaente 0a17df87e9
fix: don't show child projects when the project is only a favorite 2023-05-24 15:54:33 +02:00
kolaente b567146d69
chore: move more logic to ProjectsNavigationItem.vue 2023-05-24 15:54:33 +02:00
kolaente 65522a57f1
chore: move ProjectsNavigationWrapper back to navigation.vue 2023-05-24 15:54:33 +02:00
kolaente 1d936618fa
feat: load all projects earlier than in the navigation and use the loading state of the store 2023-05-24 15:54:33 +02:00
kolaente 76814a2d3f
chore: move loading styles to variant into the component 2023-05-24 15:54:33 +02:00
kolaente 4134fcbd75
chore: remove old comment 2023-05-24 15:54:33 +02:00
kolaente 49fac7db1c
chore: use <menu> instead of <ul> 2023-05-24 15:54:33 +02:00
kolaente e25273df48
fix: indention 2023-05-24 15:54:33 +02:00
kolaente 638f6bea24
chore: improve prop type definition 2023-05-24 15:54:33 +02:00
kolaente ddcd6a17dc
chore: only apply padding where needed 2023-05-24 15:54:32 +02:00
kolaente 4e21b463df
chore: remove old todo 2023-05-24 15:54:32 +02:00
kolaente 3db4e011d4
feat: move navigation item to component 2023-05-24 15:54:32 +02:00
kolaente a0d39e6081
chore: use long variable name 2023-05-24 15:54:32 +02:00
kolaente a803bc637e
chore: rename alias 2023-05-24 15:54:32 +02:00
kolaente d4e452545a
chore: remove unused class 2023-05-24 15:54:32 +02:00
kolaente 9d73ac661f
fix: remove leftover suspense 2023-05-24 15:54:32 +02:00
kolaente 55e912221b
chore: use klona to clone project objet 2023-05-24 15:54:32 +02:00
kolaente d85be26761
fix: passing readonly projects data to navigation 2023-05-24 15:54:32 +02:00
kolaente ac78e85e17
chore: move loader class 2023-05-24 15:54:32 +02:00
kolaente 131022da42
chore: export favorite projects from store 2023-05-24 15:54:32 +02:00
kolaente 336db56316
chore: remove unnecessary map 2023-05-24 15:54:32 +02:00
kolaente b5d9afd0f7
chore: export not archived root projects 2023-05-24 15:54:31 +02:00
kolaente 0be83db40f
fix: show favorite on hover 2023-05-24 15:54:31 +02:00
kolaente 03f4d0b8bc
fix: don't show > for top-level projects 2023-05-24 15:54:31 +02:00
kolaente ee8f80cc70
feat: allow selecting a parent project when editing a project 2023-05-24 15:54:31 +02:00
kolaente ce887c38f3
feat: allow selecting a parent project when creating a project 2023-05-24 15:54:31 +02:00
kolaente 799c0be830
feat: allow selecting a parent project when duplicating a project 2023-05-24 15:54:31 +02:00
kolaente 760efa854d
feat: don't handle child projects and instead only save the ids 2023-05-24 15:54:31 +02:00
kolaente 26bec05174
fix: make computed side-effect free 2023-05-24 15:54:31 +02:00
kolaente c32a198a34
chore: refactor get parents project and move to projects store 2023-05-24 15:54:31 +02:00
kolaente 6a8c656dbb
feat: show all parent projects in project search 2023-05-24 15:54:31 +02:00
kolaente 63ba2982c9
feat: show all parent projects in task detail view 2023-05-24 15:54:30 +02:00
kolaente 9d9fb959d8
fix: add await 2023-05-24 15:54:30 +02:00
kolaente 8ed201c83f
fix(filters): load projects after updating a filter 2023-05-24 15:54:30 +02:00
kolaente bfb40c9166
fix(filters): load projects after deleting a filter 2023-05-24 15:54:30 +02:00
kolaente 5ea450844c
fix(filters): load projects after creating a filter 2023-05-24 15:54:30 +02:00
kolaente 36bec9e64f
chore(task): move toggleFavorite to store 2023-05-24 15:54:30 +02:00
kolaente a95014dc5d
feat(projects): move hasProjects check to store 2023-05-24 15:54:30 +02:00
kolaente 2579c33ee1
feat: wrap projects navigation in a <Suspense> so that we can use top level await 2023-05-24 15:54:30 +02:00
kolaente 6f1baa3219
chore: use long variable name 2023-05-24 15:54:30 +02:00
kolaente 4dee3a90e9
chore: rename archived message key 2023-05-24 15:54:30 +02:00
kolaente 326b6eda6f
fix: use correct shortcut to open projects overview 2023-05-24 15:54:30 +02:00
kolaente 85e882cc59
fix: simplify sort 2023-05-24 15:54:29 +02:00
kolaente e4379f0a22
chore: export projects as array directly from projects store 2023-05-24 15:54:29 +02:00
kolaente 2bb7ff1803
chore: rename prop 2023-05-24 15:54:29 +02:00
kolaente 5dd6e9a077
feat(tests): add project tests derived from old namespace tests 2023-05-24 15:54:29 +02:00
kolaente f7629c28f4
fix(projects): make sure the project hierarchy is properly updated when moving projects between parents 2023-05-24 15:54:29 +02:00
kolaente be2a38b48e
feat(navigation): show favorite projects on top 2023-05-24 15:54:29 +02:00
kolaente 3ba5f531bb
fix(navigation): make sure updating a project's state works for sub projects as well. 2023-05-24 15:54:29 +02:00
kolaente 10f1e69bc3
fix(navigation): make marking a project as favorite work 2023-05-24 15:54:29 +02:00
kolaente fd7d90b017
fix(navigation): make sure the Favorites project shows up when marking or unmarking a task as favorite 2023-05-24 15:54:29 +02:00
kolaente d898316918
fix(navigation): favorites project 2023-05-24 15:54:29 +02:00
kolaente a6f524e7af
fix(task detail view): make project display show the task's project 2023-05-24 15:54:29 +02:00
kolaente 5e65814b8c
fix: make check if projects are available work again 2023-05-24 15:54:28 +02:00
kolaente aaa9d553d0
fix: cleanup unused translation strings 2023-05-24 15:54:28 +02:00
kolaente 5685890493
fix: make tests work again 2023-05-24 15:54:28 +02:00
kolaente 2e336150e0
chore: cleanup namespace leftovers 2023-05-24 15:54:28 +02:00
kolaente 749dcdcd70
fix(navigation): hide left ul border 2023-05-24 15:54:28 +02:00
kolaente ab94343d07
feat(navigation): make dragging a project under another project work 2023-05-24 15:54:28 +02:00
kolaente fa71cec5c8
feat(navigation): allow dragging a project out from its parent project 2023-05-24 15:54:28 +02:00
kolaente c6f3829387
feat(navigation): make dragging a project to a parent work 2023-05-24 15:54:28 +02:00
kolaente 7171b63947
fix(navigation): hover state of other menu items 2023-05-24 15:54:28 +02:00
kolaente 06c4c0d921
feat(navigation): add hiding child projects 2023-05-24 15:54:28 +02:00
kolaente f2ca2d850d
feat: translate inbox project title 2023-05-24 15:54:28 +02:00
kolaente 638d187a24
chore: format 2023-05-24 15:54:28 +02:00
kolaente b188d40d3c
feat(navigation): correctly show child projects 2023-05-24 15:54:27 +02:00
kolaente 3ad948305f
fix(navigation): make the styles work again 2023-05-24 15:54:27 +02:00
kolaente be1f1d94c9
fix(navigation): watcher 2023-05-24 15:54:27 +02:00
kolaente 06e8cdb9d2
feat: rebuild main navigation so that it works recursively with projects 2023-05-24 15:54:27 +02:00
kolaente 10311b79df
fix: remove namespace routes 2023-05-24 15:54:27 +02:00
kolaente ad2690b21c
fix: remove namespace store reference 2023-05-24 15:54:27 +02:00
kolaente 1bd17d6e50
feat: remove all namespace leftovers 2023-05-24 15:54:27 +02:00
kolaente a5e710bfe5
fix: route to create new project 2023-05-24 15:54:27 +02:00
kolaente e1bdabc8d6
feat: move namespaces list to projects list 2023-05-24 15:54:27 +02:00
renovate c6ef99dde2 chore(deps): update dependency cypress to v12.13.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-24 00:04:34 +00:00
renovate 49b508a783 fix(deps): update sentry-javascript monorepo to v7.53.0
continuous-integration/drone/push Build is passing Details
2023-05-23 15:50:03 +00:00
renovate 52128925f5 chore(deps): update typescript-eslint monorepo to v5.59.7
continuous-integration/drone/push Build is passing Details
2023-05-23 15:49:52 +00:00
renovate cf0c7f9d08 chore(deps): update dependency rollup to v3.23.0
continuous-integration/drone/push Build is failing Details
2023-05-23 15:49:28 +00:00
renovate 57d5140301 chore(deps): update dependency @types/node to v18.16.14
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-23 12:04:31 +00:00
renovate dbd9106621 chore(deps): update dependency postcss-preset-env to v8.4.1
continuous-integration/drone/push Build is passing Details
2023-05-23 11:17:17 +00:00
renovate e4fef0e88e chore(deps): update dependency eslint to v8.41.0
continuous-integration/drone/push Build is passing Details
2023-05-23 11:16:31 +00:00
renovate 7ef0074ecc chore(deps): update dependency caniuse-lite to v1.0.30001489
continuous-integration/drone/push Build is passing Details
2023-05-23 11:16:09 +00:00
renovate 17c35f6d42 chore(deps): update dependency happy-dom to v9.20.1
continuous-integration/drone/push Build is failing Details
2023-05-23 11:15:58 +00:00
renovate 3a0844adba chore(deps): update dependency @rushstack/eslint-patch to v1.3.0
continuous-integration/drone/push Build is failing Details
2023-05-23 11:15:13 +00:00
renovate 5b5b9022e0 chore(deps): update dependency vite-plugin-pwa to v0.15.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-23 01:09:13 +00:00
Frederick [Bot] 0b0bd7dff6 [skip ci] Updated translations via Crowdin 2023-05-23 00:29:34 +00:00
renovate 079e3782d1 chore(deps): update dependency rollup to v3.22.0
continuous-integration/drone/push Build is failing Details
2023-05-19 10:30:44 +00:00
renovate a0ae9ae54c chore(deps): update dependency @types/marked to v5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-19 09:06:04 +00:00
renovate a1b9a0ec4c fix(deps): update dependency @kyvg/vue3-notification to v2.9.1
continuous-integration/drone/push Build is failing Details
2023-05-19 08:07:44 +00:00
renovate 1fa690670d fix(deps): update dependency vue-router to v4.2.1
continuous-integration/drone/push Build is failing Details
2023-05-19 08:06:23 +00:00
renovate 3f0a87a5ec chore(deps): update dependency vite to v4.3.8
continuous-integration/drone/push Build is failing Details
2023-05-19 08:05:46 +00:00
renovate caf02f78bf chore(deps): update dependency @types/marked to v4.3.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-18 23:05:09 +00:00
renovate 9b9fd14d27 chore(deps): update dependency vitest to v0.31.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-17 15:05:09 +00:00
renovate 7f77efbfab chore(deps): update dependency @types/node to v18.16.11
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-16 20:04:54 +00:00
renovate 53967d20cc chore(deps): update dependency vite to v4.3.7
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-16 17:05:08 +00:00
renovate ef3411f39a chore(deps): update dependency @types/node to v18.16.10
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-16 11:04:50 +00:00
renovate 66e63f1363 chore(deps): update typescript-eslint monorepo to v5.59.6
continuous-integration/drone/push Build is passing Details
2023-05-16 10:46:23 +00:00
renovate 2fe21f6b28 fix(deps): update sentry-javascript monorepo to v7.52.1
continuous-integration/drone/push Build is passing Details
2023-05-16 10:46:09 +00:00
renovate 67df372636 chore(deps): update dependency eslint-plugin-vue to v9.13.0
continuous-integration/drone/push Build is failing Details
2023-05-16 10:45:49 +00:00
renovate df80e9da23 chore(deps): update dependency rollup to v3.21.8
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-16 08:04:56 +00:00
renovate 13ab2efd0f chore(deps): update dependency @faker-js/faker to v8.0.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-15 17:04:44 +00:00
renovate 0ffe96cf59 chore(deps): update dependency vite to v4.3.6
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-15 16:04:58 +00:00
renovate 1808d0971d fix(deps): update sentry-javascript monorepo to v7.52.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-15 14:05:00 +00:00
renovate ec83a28d78 chore(deps): update pnpm to v8.5.1
continuous-integration/drone/push Build is passing Details
2023-05-15 10:33:06 +00:00
renovate f0320b3a58 chore(deps): update dependency caniuse-lite to v1.0.30001487
continuous-integration/drone/push Build is passing Details
2023-05-15 10:30:51 +00:00
renovate d93a1a4f4f chore(deps): update dependency happy-dom to v9.18.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-15 00:05:29 +00:00
renovate a9f9ddf6b9 chore(deps): update dependency @types/node to v18.16.9
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-13 15:05:04 +00:00
renovate 6a8fe35fcf chore(deps): update dependency rollup to v3.21.7
continuous-integration/drone/push Build is passing Details
2023-05-13 14:48:00 +00:00
renovate 94661e9e09 chore(deps): update dependency vue-tsc to v1.6.5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-13 08:04:55 +00:00
renovate 318f63d098 chore(deps): update dependency esbuild to v0.17.19
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-13 01:04:45 +00:00
renovate e2c9e83c2a chore(deps): update dependency @vitejs/plugin-vue to v4.2.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-12 11:04:45 +00:00
renovate cd434a0e3e chore(deps): update dependency @types/node to v18.16.8
continuous-integration/drone/push Build is passing Details
2023-05-12 07:06:52 +00:00
renovate 9f293af804 chore(deps): update dependency @vue/tsconfig to v0.4.0
continuous-integration/drone/push Build is failing Details
2023-05-12 07:06:24 +00:00
renovate b175e00cfe chore(deps): update dependency @tsconfig/node18 to v2.0.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-12 05:04:41 +00:00
Frederick [Bot] 19dd82d62a [skip ci] Updated translations via Crowdin 2023-05-12 00:29:44 +00:00
renovate b3ddc9465a chore(deps): update dependency @vitejs/plugin-vue to v4.2.2
continuous-integration/drone/push Build is passing Details
2023-05-11 19:57:40 +00:00
renovate 6b38f17d32 fix(deps): update dependency vue-router to v4.2.0
continuous-integration/drone/push Build is passing Details
2023-05-11 19:57:25 +00:00
renovate 86449d4912 fix(deps): update dependency marked to v5.0.2
continuous-integration/drone/push Build is failing Details
2023-05-11 19:57:07 +00:00
renovate 145d756251 chore(deps): update dependency @faker-js/faker to v8
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-11 18:05:10 +00:00
renovate 838a11a2f6 chore(deps): update dependency eslint-plugin-vue to v9.12.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-10 14:04:39 +00:00
renovate 3bfd3210b0 chore(deps): update dependency @types/node to v18.16.7
continuous-integration/drone/push Build is failing Details
2023-05-10 09:46:06 +00:00
renovate e933bfa99e fix(deps): update sentry-javascript monorepo to v7.51.2
continuous-integration/drone/push Build is failing Details
2023-05-10 09:14:43 +00:00
renovate f6a37a54d0 fix(deps): update dependency pinia to v2.0.36
continuous-integration/drone/push Build is failing Details
2023-05-10 09:14:33 +00:00
primeapple e00c9bb1af feat: add hotkeys for priority, delete and favorite on the `TaskDetailView` (#3400)
continuous-integration/drone/push Build is failing Details
Reviewed-on: #3400
Reviewed-by: konrad <k@knt.li>
Co-authored-by: primeapple <toni.mueller.web@mailbox.org>
Co-committed-by: primeapple <toni.mueller.web@mailbox.org>
2023-05-10 09:14:07 +00:00
kolaente 018707c3d5
fix(ci): disable puppeteer chrome download
continuous-integration/drone/push Build is failing Details
2023-05-10 10:42:44 +02:00
renovate 386727f6c5 chore(deps): update dependency @types/node to v18.16.6
continuous-integration/drone/push Build is passing Details
2023-05-10 08:09:35 +00:00
renovate a29ce36d6c chore(deps): update typescript-eslint monorepo to v5.59.5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-10 07:05:09 +00:00
renovate 7aed16bd6f chore(deps): update dependency rollup to v3.21.6
continuous-integration/drone/push Build is failing Details
2023-05-10 06:03:37 +00:00
renovate b1f3ca6e59 chore(deps): update pnpm to v8.5.0
continuous-integration/drone/push Build is failing Details
2023-05-10 06:03:09 +00:00
renovate 4c0b8a06c5 chore(deps): update dependency cypress to v12.12.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-09 23:05:25 +00:00
renovate 60647c50ac chore(deps): update dependency caniuse-lite to v1.0.30001486
continuous-integration/drone/push Build is failing Details
2023-05-08 07:11:25 +00:00
renovate 59eaf1849e chore(deps): update dependency happy-dom to v9.10.9
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-08 00:05:41 +00:00
renovate fb57339050 chore(deps): update dependency eslint-plugin-vue to v9.11.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-07 17:04:47 +00:00
renovate f9831a6ad8 fix(deps): update dependency marked to v5.0.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-06 21:04:51 +00:00
renovate f25c67f80a chore(deps): update dependency eslint to v8.40.0
continuous-integration/drone/push Build is passing Details
2023-05-06 15:26:02 +00:00
renovate d3b0b97192 chore(deps): update dependency @types/node to v18.16.5
continuous-integration/drone/push Build is passing Details
2023-05-06 15:25:42 +00:00
renovate fa3be219a8 fix(deps): update dependency dompurify to v3.0.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-05-06 13:04:56 +00:00
renovate c22702d911 chore(deps): update dependency @types/node to v18.16.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-05 14:04:43 +00:00
renovate b25c5ff547 chore(deps): update dependency vite to v4.3.5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-05 11:04:43 +00:00
renovate 2e0a097806 chore(deps): update dependency rollup to v3.21.5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-05 05:04:46 +00:00
renovate c2083f7924 fix(deps): update sentry-javascript monorepo to v7.51.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-04 16:05:24 +00:00
renovate 8923261e5b fix(deps): update dependency ufo to v1.1.2
continuous-integration/drone/push Build is passing Details
2023-05-04 07:35:00 +00:00
renovate 5391df56b0 chore(deps): update dependency vue-tsc to v1.6.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-04 01:04:54 +00:00
renovate d2b1f5780e chore(deps): update dependency vitest to v0.31.0
continuous-integration/drone/push Build is passing Details
2023-05-03 19:52:49 +00:00
renovate 1717e968e1 chore(deps): update dependency rollup to v3.21.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-03 19:04:31 +00:00
renovate 2c29bb3971 chore(deps): update pnpm to v8.4.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-02 14:04:09 +00:00
renovate 37b8218a0a chore(deps): update node.js to v20 (#3411)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #3411
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2023-05-02 13:35:04 +00:00
konrad ca7bbb5b91 chore(ci): remove netlify dependency (#3459)
continuous-integration/drone/push Build is passing Details
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: #3459
2023-05-02 10:10:14 +00:00
renovate 2f3c008d2b chore(deps): update dependency vite to v4.3.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-02 08:04:40 +00:00
renovate c2722b7c3d fix(deps): update dependency @vueuse/core to v10.1.2
continuous-integration/drone/push Build is failing Details
2023-05-02 07:18:12 +00:00
renovate 312abd907f chore(deps): update dependency rollup to v3.21.3
continuous-integration/drone/push Build is passing Details
2023-05-02 07:18:02 +00:00
renovate 1b73c1ed64 chore(deps): update dependency vue-tsc to v1.6.3
continuous-integration/drone/push Build is passing Details
2023-05-02 07:17:43 +00:00
renovate d442d6653b fix(deps): update dependency marked to v5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-02 05:05:40 +00:00
renovate 758b8d6e2b chore(deps): update typescript-eslint monorepo to v5.59.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-01 18:04:39 +00:00
renovate 416fd2e2a7 chore(deps): update dependency @types/marked to v4.3.0
continuous-integration/drone/push Build is passing Details
2023-05-01 16:45:51 +00:00
renovate 15a8335f1a chore(deps): update dependency vue-tsc to v1.6.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-01 14:04:42 +00:00
renovate c689583669 chore(deps): update dependency netlify-cli to v14.3.1
continuous-integration/drone/push Build is passing Details
2023-05-01 10:44:11 +00:00
renovate 7a43a7acc9 chore(deps): update dependency happy-dom to v9.10.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-05-01 00:04:51 +00:00
renovate 8843418161 fix(deps): update dependency date-fns to v2.30.0
continuous-integration/drone/push Build is passing Details
2023-04-30 06:57:42 +00:00
renovate 7c1eab13ae chore(deps): update dependency rollup to v3.21.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-30 06:04:44 +00:00
renovate 5a69036da7 fix(deps): update dependency highlight.js to v11.8.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-29 14:04:50 +00:00
renovate 2ad3458873 chore(deps): update dependency @types/node to v18.16.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-29 07:04:40 +00:00
renovate eb464343e8 chore(deps): update dependency rollup to v3.21.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-29 06:04:42 +00:00
kolaente 2f18d0cbad
fix(docker): don't set nginx worker rlimit
continuous-integration/drone/push Build is passing Details
Resolves https://community.vikunja.io/t/helm-chart-frontend-pod-does-not-start-because-of-permission-issues-in-raspberry-pie-4-k3s/1286
2023-04-28 10:31:15 +02:00
renovate 6cd463a514 chore(deps): pin dependency @tsconfig/node18 to 2.0.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-28 08:04:35 +00:00
kolaente 05b70632c5
fix: tsconfig as per https://github.com/vuejs/tsconfig#configuration-for-node-environments
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-28 09:30:45 +02:00
kolaente ca9fe6ff21
fix: tsconfig as per https://github.com/vuejs/tsconfig#configuration-for-node-environments
continuous-integration/drone/pr Build is failing Details
2023-04-28 09:16:54 +02:00
renovate e5754300de chore(deps): update dependency @vue/tsconfig to v0.3.2
continuous-integration/drone/pr Build is failing Details
2023-04-28 07:04:31 +00:00
renovate 65134048bf chore(deps): update dependency @vitejs/plugin-vue to v4.2.1
continuous-integration/drone/push Build is passing Details
2023-04-28 06:04:35 +00:00
renovate 8339a99747 chore(deps): update dependency @types/node to v18.16.2
continuous-integration/drone/push Build is passing Details
2023-04-28 05:17:36 +00:00
renovate 3e1ae41e70 fix(deps): update dependency axios to v1.4.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-28 00:04:55 +00:00
renovate f757ba3441 chore(deps): update dependency vue-tsc to v1.6.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-27 19:04:37 +00:00
renovate 6499c9cb5b fix(deps): update sentry-javascript monorepo to v7.50.0
continuous-integration/drone/push Build is passing Details
2023-04-27 09:39:32 +00:00
renovate 28e5440d8b fix(deps): update dependency codemirror to v5.65.13
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-27 09:04:38 +00:00
renovate fef8c4d0f4 chore(deps): update dependency vue-tsc to v1.6.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-26 21:06:27 +00:00
renovate 99e5059c64 chore(deps): update dependency cypress to v12.11.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-26 18:04:42 +00:00
renovate 5df4f39d95 chore(deps): update dependency vite to v4.3.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-26 15:04:55 +00:00
renovate 7ec5a70ccb chore(deps): update dependency sass to v1.62.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-26 10:05:07 +00:00
renovate 72fcab6e78 chore(deps): update dependency @types/node to v18.16.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-26 09:04:23 +00:00
kolaente 292c90425e
fix: lint
continuous-integration/drone/push Build is passing Details
2023-04-26 10:19:49 +02:00
kolaente b80f070431
feat: show avatar and full name in team overview
continuous-integration/drone/push Build is failing Details
2023-04-25 18:32:36 +02:00
renovate 03936c0403 chore(deps): update dependency vite to v4.3.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-25 16:04:40 +00:00
kolaente 62825d2e64
fix: add spacing between checkbox and title of related task
continuous-integration/drone/push Build is passing Details
Related to https://github.com/go-vikunja/frontend/issues/111
2023-04-25 17:33:47 +02:00
renovate 5cd5caef45 chore(deps): update dependency @vue/eslint-config-typescript to v11.0.3
continuous-integration/drone/push Build is passing Details
2023-04-25 15:17:58 +00:00
renovate 798e8b529d chore(deps): update dependency @vitejs/plugin-legacy to v4.0.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-25 15:04:36 +00:00
renovate 0e3766c5a5 chore(deps): update typescript-eslint monorepo to v5.59.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-25 13:05:07 +00:00
renovate 90207a4427 chore(deps): update dependency @vitejs/plugin-vue to v4.2.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-25 10:05:12 +00:00
renovate 60993a886a chore(deps): update dependency caniuse-lite to v1.0.30001481
continuous-integration/drone/push Build is passing Details
2023-04-24 06:16:59 +00:00
renovate a6b42f9181 chore(deps): update dependency happy-dom to v9.9.2
continuous-integration/drone/push Build is passing Details
2023-04-24 06:16:43 +00:00
renovate 98fbd7c53c chore(deps): update dependency netlify-cli to v14
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-24 01:05:19 +00:00
renovate 8d533f50e8 chore(deps): update dependency rollup to v3.21.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-23 20:04:46 +00:00
renovate 707459ec77 chore(deps): update dependency @types/node to v18.16.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-23 06:04:51 +00:00
renovate faf7db649e chore(deps): update dependency esbuild to v0.17.18
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-22 21:04:36 +00:00
renovate 202e71be48 chore(deps): update dependency vue-tsc to v1.4.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-22 20:04:41 +00:00
renovate d6e8b418d3 chore(deps): update dependency vue-tsc to v1.4.3
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-22 16:04:50 +00:00
renovate a9f41f6114 chore(deps): update dependency eslint to v8.39.0
continuous-integration/drone/push Build is passing Details
2023-04-22 15:45:11 +00:00
renovate f6f0d52518 fix(deps): update dependency @vueuse/core to v10.1.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-22 10:04:50 +00:00
renovate ccb9be42c2 chore(deps): update dependency vue-tsc to v1.4.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-21 11:04:30 +00:00
renovate 179009bfe3 chore(deps): update dependency @types/node to v18.15.13
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-21 07:04:39 +00:00
renovate 8c2bd94a9f chore(deps): update dependency vue-tsc to v1.4.1
continuous-integration/drone/push Build is passing Details
2023-04-21 06:13:48 +00:00
renovate 7757166d75 chore(deps): update dependency rollup to v3.20.7
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-21 05:04:36 +00:00
renovate 7f03002972 chore(deps): update dependency vite to v4.3.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-04-20 20:04:26 +00:00
renovate 8555006d9e chore(deps): update dependency vue-tsc to v1.4.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-04-20 18:04:40 +00:00
renovate 713ad64658 fix(deps): update sentry-javascript monorepo to v7.49.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-20 16:04:46 +00:00
renovate 0713d481e3 fix(deps): update dependency pinia to v2.0.35
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-20 11:04:28 +00:00
renovate ace0cf3588 chore(deps): update dependency vite to v4.3.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-20 09:04:31 +00:00
renovate bba3bbfe89 chore(deps): update dependency @types/dompurify to v3.0.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-20 08:04:42 +00:00
renovate 754afc5496 chore(deps): update dependency @types/node to v18.15.12
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-20 00:05:04 +00:00
renovate f1e8892ab5 chore(deps): update dependency postcss to v8.4.23
continuous-integration/drone/push Build is passing Details
2023-04-19 20:32:32 +00:00
renovate c11e192c4e fix(deps): update dependency axios to v1.3.6
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-19 20:05:18 +00:00
renovate e9c704075d chore(deps): update pnpm to v8.3.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-19 13:04:16 +00:00
renovate 35edcb5672 chore(deps): update dependency rollup to v3.20.6
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-18 12:05:05 +00:00
renovate 4695798176 chore(deps): update dependency vite to v4.2.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-18 10:05:15 +00:00
renovate 7a323fd170 chore(deps): update dependency rollup to v3.20.5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-18 06:04:54 +00:00
renovate 1d6e4b6e32 chore(deps): update pnpm to v8.3.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-18 01:04:56 +00:00
renovate 5524aa7998 chore(deps): update dependency cypress to v12.10.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-17 19:04:51 +00:00
renovate 15ff2008e3 chore(deps): update typescript-eslint monorepo to v5.59.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-17 18:05:11 +00:00
renovate 9bc2e6e165 chore(deps): update dependency postcss-preset-env to v8.3.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-17 07:04:46 +00:00
renovate 344001856c chore(deps): update dependency rollup to v3.20.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-17 06:04:41 +00:00
renovate ad261fcc2f chore(deps): update dependency esbuild to v0.17.17
continuous-integration/drone/push Build is failing Details
2023-04-17 05:29:14 +00:00
renovate 5142a0ae72 chore(deps): update dependency caniuse-lite to v1.0.30001479
continuous-integration/drone/push Build is passing Details
2023-04-17 05:27:04 +00:00
renovate 6d195f96c9 chore(deps): update dependency happy-dom to v9.7.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-17 01:05:56 +00:00
Frederick [Bot] 1917b217a8 [skip ci] Updated translations via Crowdin 2023-04-17 00:25:34 +00:00
renovate 1f6b01bc73 chore(deps): update dependency rollup to v3.20.3
continuous-integration/drone/push Build is passing Details
2023-04-16 17:19:59 +00:00
renovate d47a16aa8e chore(deps): update dependency postcss to v8.4.22
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-16 14:04:37 +00:00
renovate c57d00a74b chore(deps): update dependency eslint-plugin-vue to v9.11.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-15 01:04:45 +00:00
renovate 77ea7fa0ee fix(deps): update dependency @vueuse/core to v10.0.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-14 22:04:52 +00:00
kolaente b92d780cda chore: formatting
continuous-integration/drone/push Build is passing Details
2023-04-14 21:53:04 +00:00
kolaente f14e721caf fix: rename resolveRef 2023-04-14 21:53:04 +00:00
renovate 1ff6399112 fix(deps): update dependency @vueuse/core to v10 2023-04-14 21:53:04 +00:00
renovate 503fb8da76 fix(deps): update dependency dompurify to v3.0.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-14 18:04:56 +00:00
renovate f050cb7015 chore(deps): update histoire to v0.16.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-14 11:04:40 +00:00
renovate 3670916f36 fix(deps): update sentry-javascript monorepo to v7.48.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-14 10:04:35 +00:00
Frederick [Bot] 838a063eaa [skip ci] Updated translations via Crowdin 2023-04-14 00:26:31 +00:00
renovate e1b16b11d6 chore(deps): update node.js to v18.16.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-13 01:04:28 +00:00
Dominik Pschenitschni 314cbf471f feat: better vscode vitest integration
continuous-integration/drone/push Build is passing Details
2023-04-12 15:39:49 +00:00
Dominik Pschenitschni a416d26f7c
chore: better function naming in password components
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-04-12 16:15:40 +02:00
Dominik Pschenitschni 795b26e1dd feat: improve datemathHelp.vue
continuous-integration/drone/push Build is passing Details
2023-04-12 07:33:45 +00:00
renovate 14666cf9d8 chore(deps): update dependency sass to v1.62.0
continuous-integration/drone/push Build is passing Details
2023-04-12 05:57:33 +00:00
renovate c938f31935 chore(deps): update pnpm to v8
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-11 23:05:49 +00:00
kolaente 35a52ef01b
fix(quick add magic): date parsing with a date at the beginning
continuous-integration/drone/push Build is passing Details
Resolves https://github.com/go-vikunja/frontend/issues/110
2023-04-11 18:12:08 +02:00
renovate 3b05ce3f10 chore(deps): update histoire to v0.16.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-11 14:04:49 +00:00
renovate aec4fd7a2d chore(deps): update dependency vitest to v0.30.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-11 12:04:57 +00:00
renovate 2661af3a17 fix(deps): update sentry-javascript monorepo to v7.47.0
continuous-integration/drone/push Build is passing Details
2023-04-11 12:01:30 +00:00
renovate 56f43bae3f chore(deps): update typescript-eslint monorepo to v5.58.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-10 18:04:41 +00:00
renovate 84472d2e9c chore(deps): update dependency happy-dom to v9.1.9
continuous-integration/drone/push Build is passing Details
2023-04-10 13:17:25 +00:00
renovate c5afcd63b0 chore(deps): update dependency postcss-preset-env to v8.3.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-04-10 12:05:14 +00:00
renovate 9bdb257814 chore(deps): update dependency caniuse-lite to v1.0.30001477
continuous-integration/drone/push Build is passing Details
2023-04-10 11:14:22 +00:00
renovate 5ad9891b16 chore(deps): update pnpm to v7.32.0
continuous-integration/drone/push Build is passing Details
2023-04-10 11:13:57 +00:00
renovate 7c04064917 chore(deps): update dependency vitest to v0.30.0
continuous-integration/drone/push Build is passing Details
2023-04-10 11:13:43 +00:00
renovate fb5383d86b chore(deps): update dependency esbuild to v0.17.16
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-10 05:04:46 +00:00
renovate 68af314ec0 chore(deps): update dependency eslint to v8.38.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-08 00:04:43 +00:00
renovate 8b1de5ce09 fix(deps): update dependency pinia to v2.0.34
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-07 20:04:46 +00:00
renovate 724b6fe091 chore(deps): update dependency typescript to v5.0.4
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-07 18:04:25 +00:00
renovate 6648cd30c3 chore(deps): update dependency sass to v1.61.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-06 22:04:45 +00:00
kolaente 8b90b45739
fix: make sure the unread notifications indicator is correctly positioned
continuous-integration/drone/push Build is passing Details
Resolves #3358
2023-04-06 16:11:12 +02:00
renovate 39be67eecf fix(deps): update dependency axios to v1.3.5
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-06 09:06:06 +00:00
Frederick [Bot] 750f0ddeab [skip ci] Updated translations via Crowdin 2023-04-05 00:06:16 +00:00
renovate 6a5ece2f24 chore(deps): update pnpm to v7.31.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-04 01:04:32 +00:00
Frederick [Bot] 4ce33abfe6 [skip ci] Updated translations via Crowdin 2023-04-04 00:06:21 +00:00
renovate 5b7e1af87d chore(deps): update dependency @types/dompurify to v3.0.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-03 20:04:51 +00:00
renovate 59c6605b14 chore(deps): update typescript-eslint monorepo to v5.57.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-03 18:05:29 +00:00
Dominik Pschenitschni 820d598ecd fix(i18n): orderedList translationid
continuous-integration/drone/push Build is passing Details
2023-04-03 09:48:03 +00:00
Dominik Pschenitschni a263ec1273
fix(Expandable): spelling ⛈
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-03 11:31:39 +02:00
renovate b68892492c chore(deps): update dependency happy-dom to v9
continuous-integration/drone/push Build is passing Details
2023-04-03 05:18:07 +00:00
renovate 7c97695cec chore(deps): update dependency netlify-cli to v13.2.2
continuous-integration/drone/push Build is failing Details
2023-04-03 05:17:16 +00:00
renovate e764f34a2d chore(deps): update dependency caniuse-lite to v1.0.30001473
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2023-04-03 00:05:10 +00:00
renovate 6892a28bb6 chore(deps): update dependency esbuild to v0.17.15
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-01 23:04:45 +00:00
renovate 74d688b8d2 chore(deps): update dependency csstype to v3.1.2
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2023-04-01 21:04:41 +00:00
187 changed files with 8496 additions and 14527 deletions

View File

@ -42,11 +42,12 @@ steps:
# - .cache # - .cache
- name: dependencies - name: dependencies
image: node:18-alpine image: node:20-alpine
pull: always pull: always
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
CYPRESS_CACHE_FOLDER: .cache/cypress CYPRESS_CACHE_FOLDER: .cache/cypress
PUPPETEER_SKIP_DOWNLOAD: true
commands: commands:
- corepack enable && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000 - pnpm install --fetch-timeout 100000
@ -54,7 +55,7 @@ steps:
# - restore-cache # - restore-cache
- name: lint - name: lint
image: node:18-alpine image: node:20-alpine
pull: always pull: always
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
@ -65,7 +66,7 @@ steps:
- dependencies - dependencies
- name: build-prod - name: build-prod
image: node:18-alpine image: node:20-alpine
pull: always pull: always
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
@ -82,7 +83,7 @@ steps:
- dependencies - dependencies
- name: test-unit - name: test-unit
image: node:18-alpine image: node:20-alpine
pull: always pull: always
commands: commands:
- corepack enable && pnpm config set store-dir .cache/pnpm - corepack enable && pnpm config set store-dir .cache/pnpm
@ -92,7 +93,7 @@ steps:
- name: typecheck - name: typecheck
failure: ignore failure: ignore
image: node:18-alpine image: node:20-alpine
pull: always pull: always
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
@ -142,8 +143,9 @@ steps:
# - dependencies # - dependencies
- name: deploy-preview - name: deploy-preview
image: node:18-alpine image: williamjackson/netlify-cli
pull: always pull: always
user: root # The rest runs as root and thus the permissions wouldn't work
environment: environment:
NETLIFY_AUTH_TOKEN: NETLIFY_AUTH_TOKEN:
from_secret: netlify_auth_token from_secret: netlify_auth_token
@ -206,7 +208,7 @@ steps:
# - .cache # - .cache
- name: build - name: build
image: node:18-alpine image: node:20-alpine
pull: always pull: always
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
@ -283,7 +285,7 @@ steps:
# - .cache # - .cache
- name: build - name: build
image: node:18-alpine image: node:20-alpine
pull: always pull: always
environment: environment:
PNPM_CACHE_FOLDER: .cache/pnpm PNPM_CACHE_FOLDER: .cache/pnpm
@ -353,8 +355,7 @@ type: docker
name: docker-release name: docker-release
depends_on: depends_on:
- release-latest - build
- release-version
trigger: trigger:
ref: ref:
@ -382,8 +383,7 @@ steps:
repo: vikunja/frontend repo: vikunja/frontend
tags: unstable tags: unstable
build_args: build_args:
- USE_RELEASE=true - USE_RELEASE=false
- RELEASE_VERSION=unstable
platforms: platforms:
- linux/386 - linux/386
- linux/amd64 - linux/amd64
@ -417,8 +417,7 @@ steps:
from_secret: docker_password from_secret: docker_password
repo: vikunja/frontend repo: vikunja/frontend
build_args: build_args:
- USE_RELEASE=true - USE_RELEASE=false
- RELEASE_VERSION=${DRONE_TAG##v}
platforms: platforms:
- linux/386 - linux/386
- linux/amd64 - linux/amd64
@ -528,6 +527,6 @@ steps:
from_secret: crowdin_key from_secret: crowdin_key
--- ---
kind: signature kind: signature
hmac: 303afeb09b75a57ba88720b45dc06c8bf2c7320e19d738d8299f325438246f75 hmac: a41964ffb64789df5553d7f51e05ac60d8243a4d8b7dfdd5be8de851aea5f9d7
... ...

2
.nvmrc
View File

@ -1 +1 @@
18.15.0 18.16.0

View File

@ -8,6 +8,7 @@
"lokalise.i18n-ally", "lokalise.i18n-ally",
"mgmcdermott.vscode-language-babel", "mgmcdermott.vscode-language-babel",
"mikestead.dotenv", "mikestead.dotenv",
"Syler.sass-indented" "Syler.sass-indented",
"zixuanchen.vitest-explorer"
] ]
} }

View File

@ -3,16 +3,17 @@
# │─││ │││ │ │ # │─││ │││ │ │
# ┘─┘┘─┘┘┘─┘┘─┘ # ┘─┘┘─┘┘┘─┘┘─┘
FROM --platform=$BUILDPLATFORM node:18-alpine AS builder FROM --platform=$BUILDPLATFORM node:20-alpine AS builder
WORKDIR /build WORKDIR /build
ARG USE_RELEASE=false ARG USE_RELEASE=false
ARG RELEASE_VERSION=main ARG RELEASE_VERSION=unstable
ENV PNPM_CACHE_FOLDER .cache/pnpm/ ENV PNPM_CACHE_FOLDER .cache/pnpm/
COPY package.json ./ COPY package.json ./
COPY pnpm-lock.yaml ./ COPY pnpm-lock.yaml ./
COPY patches ./patches/
RUN if [ "$USE_RELEASE" != true ]; then \ RUN if [ "$USE_RELEASE" != true ]; then \
# https://pnpm.io/installation#using-corepack # https://pnpm.io/installation#using-corepack
@ -54,6 +55,8 @@ ENV VIKUNJA_LOG_FORMAT main
ENV VIKUNJA_API_URL /api/v1 ENV VIKUNJA_API_URL /api/v1
ENV VIKUNJA_SENTRY_ENABLED false ENV VIKUNJA_SENTRY_ENABLED false
ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480 ENV VIKUNJA_SENTRY_DSN https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480
ENV VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED false
ENV VIKUNJA_ALLOW_ICON_CHANGES true
COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh COPY docker/injector.sh /docker-entrypoint.d/50-injector.sh
COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh COPY docker/ipv6-disable.sh /docker-entrypoint.d/60-ipv6-disable.sh

View File

@ -24,4 +24,5 @@ export default defineConfig({
}, },
viewportWidth: 1600, viewportWidth: 1600,
viewportHeight: 900, viewportHeight: 900,
experimentalMemoryManagement: true,
}) })

View File

@ -2,7 +2,6 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {ProjectFactory} from '../../factories/project' import {ProjectFactory} from '../../factories/project'
import {NamespaceFactory} from '../../factories/namespace'
import {UserProjectFactory} from '../../factories/users_project' import {UserProjectFactory} from '../../factories/users_project'
import {BucketFactory} from '../../factories/bucket' import {BucketFactory} from '../../factories/bucket'
@ -10,7 +9,6 @@ describe('Editor', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
beforeEach(() => { beforeEach(() => {
NamespaceFactory.create(1)
ProjectFactory.create(1) ProjectFactory.create(1)
BucketFactory.create(1) BucketFactory.create(1)
TaskFactory.truncate() TaskFactory.truncate()

View File

@ -8,20 +8,20 @@ describe('The Menu', () => {
}) })
it('Is visible by default on desktop', () => { it('Is visible by default on desktop', () => {
cy.get('.namespace-container') cy.get('.menu-container')
.should('have.class', 'is-active') .should('have.class', 'is-active')
}) })
it('Can be hidden on desktop', () => { it('Can be hidden on desktop', () => {
cy.get('button.menu-show-button:visible') cy.get('button.menu-show-button:visible')
.click() .click()
cy.get('.namespace-container') cy.get('.menu-container')
.should('not.have.class', 'is-active') .should('not.have.class', 'is-active')
}) })
it('Is hidden by default on mobile', () => { it('Is hidden by default on mobile', () => {
cy.viewport('iphone-8') cy.viewport('iphone-8')
cy.get('.namespace-container') cy.get('.menu-container')
.should('not.have.class', 'is-active') .should('not.have.class', 'is-active')
}) })
@ -29,7 +29,7 @@ describe('The Menu', () => {
cy.viewport('iphone-8') cy.viewport('iphone-8')
cy.get('button.menu-show-button:visible') cy.get('button.menu-show-button:visible')
.click() .click()
cy.get('.namespace-container') cy.get('.menu-container')
.should('have.class', 'is-active') .should('have.class', 'is-active')
}) })
}) })

View File

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

View File

@ -1,9 +1,7 @@
import {ProjectFactory} from '../../factories/project' import {ProjectFactory} from '../../factories/project'
import {NamespaceFactory} from '../../factories/namespace'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
export function createProjects() { export function createProjects() {
NamespaceFactory.create(1)
const projects = ProjectFactory.create(1, { const projects = ProjectFactory.create(1, {
title: 'First Project' title: 'First Project'
}) })

View File

@ -8,37 +8,30 @@ describe('Project History', () => {
prepareProjects() prepareProjects()
it('should show a project history on the home page', () => { it('should show a project history on the home page', () => {
cy.intercept(Cypress.env('API_URL') + '/namespaces*').as('loadNamespaces') cy.intercept(Cypress.env('API_URL') + '/projects*').as('loadProjectArray')
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject') cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
const projects = ProjectFactory.create(6) const projects = ProjectFactory.create(6)
cy.visit('/') cy.visit('/')
cy.wait('@loadNamespaces') cy.wait('@loadProjectArray')
cy.get('body') cy.get('body')
.should('not.contain', 'Last viewed') .should('not.contain', 'Last viewed')
cy.visit(`/projects/${projects[0].id}`) cy.visit(`/projects/${projects[0].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadProject') cy.wait('@loadProject')
cy.visit(`/projects/${projects[1].id}`) cy.visit(`/projects/${projects[1].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadProject') cy.wait('@loadProject')
cy.visit(`/projects/${projects[2].id}`) cy.visit(`/projects/${projects[2].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadProject') cy.wait('@loadProject')
cy.visit(`/projects/${projects[3].id}`) cy.visit(`/projects/${projects[3].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadProject') cy.wait('@loadProject')
cy.visit(`/projects/${projects[4].id}`) cy.visit(`/projects/${projects[4].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadProject') cy.wait('@loadProject')
cy.visit(`/projects/${projects[5].id}`) cy.visit(`/projects/${projects[5].id}`)
cy.wait('@loadNamespaces')
cy.wait('@loadProject') cy.wait('@loadProject')
// cy.visit('/') // cy.visit('/')
// cy.wait('@loadNamespaces')
// Not using cy.visit here to work around the redirect issue fixed in #1337 // Not using cy.visit here to work around the redirect issue fixed in #1337
cy.get('nav.menu.top-menu a') cy.get('nav.menu.top-menu a')
.contains('Overview') .contains('Overview')

View File

@ -58,7 +58,6 @@ describe('Project View Project', () => {
}) })
const projects = ProjectFactory.create(2, { const projects = ProjectFactory.create(2, {
owner_id: '{increment}', owner_id: '{increment}',
namespace_id: '{increment}',
}) })
cy.visit(`/projects/${projects[1].id}/`) cy.visit(`/projects/${projects[1].id}/`)

View File

@ -1,6 +1,7 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser' import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {ProjectFactory} from '../../factories/project'
import {prepareProjects} from './prepareProjects' import {prepareProjects} from './prepareProjects'
describe('Projects', () => { describe('Projects', () => {
@ -10,23 +11,20 @@ describe('Projects', () => {
prepareProjects((newProjects) => (projects = newProjects)) prepareProjects((newProjects) => (projects = newProjects))
it('Should create a new project', () => { it('Should create a new project', () => {
cy.visit('/') cy.visit('/projects')
cy.get('.namespace-title .dropdown-trigger') cy.get('.project-header [data-cy=new-project]')
.click()
cy.get('.namespace-title .dropdown .dropdown-item')
.contains('New project')
.click() .click()
cy.url() cy.url()
.should('contain', '/projects/new/1') .should('contain', '/projects/new')
cy.get('.card-header-title') cy.get('.card-header-title')
.contains('New project') .contains('New project')
cy.get('input.input') cy.get('input[name=projectTitle]')
.type('New Project') .type('New Project')
cy.get('.button') cy.get('.button')
.contains('Create') .contains('Create')
.click() .click()
cy.get('.global-notification', { timeout: 1000 }) // Waiting until the request to create the new project is done cy.get('.global-notification', {timeout: 1000}) // Waiting until the request to create the new project is done
.should('contain', 'Success') .should('contain', 'Success')
cy.url() cy.url()
.should('contain', '/projects/') .should('contain', '/projects/')
@ -56,9 +54,9 @@ describe('Projects', () => {
cy.get('.project-title') cy.get('.project-title')
.should('contain', 'First Project') .should('contain', 'First Project')
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger') cy.get('.menu-container .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
.click() .click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content') cy.get('.menu-container .menu-list li:first-child .dropdown .dropdown-content')
.contains('Edit') .contains('Edit')
.click() .click()
cy.get('#title') cy.get('#title')
@ -72,21 +70,21 @@ describe('Projects', () => {
cy.get('.project-title') cy.get('.project-title')
.should('contain', newProjectName) .should('contain', newProjectName)
.should('not.contain', projects[0].title) .should('not.contain', projects[0].title)
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child') cy.get('.menu-container .menu-list li:first-child')
.should('contain', newProjectName) .should('contain', newProjectName)
.should('not.contain', projects[0].title) .should('not.contain', projects[0].title)
cy.visit('/') cy.visit('/')
cy.get('.card-content') cy.get('.project-grid')
.should('contain', newProjectName) .should('contain', newProjectName)
.should('not.contain', projects[0].title) .should('not.contain', projects[0].title)
}) })
it('Should remove a project', () => { it('Should remove a project when deleting it', () => {
cy.visit(`/projects/${projects[0].id}`) cy.visit(`/projects/${projects[0].id}`)
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .menu-list-dropdown-trigger') cy.get('.menu-container .menu-list li:first-child .dropdown .menu-list-dropdown-trigger')
.click() .click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list li:first-child .dropdown .dropdown-content') cy.get('.menu-container .menu-list li:first-child .dropdown .dropdown-content')
.contains('Delete') .contains('Delete')
.click() .click()
cy.url() cy.url()
@ -97,15 +95,15 @@ describe('Projects', () => {
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')
cy.get('.namespace-container .menu.namespaces-lists .menu-list') cy.get('.menu-container .menu-list')
.should('not.contain', projects[0].title) .should('not.contain', projects[0].title)
cy.location('pathname') cy.location('pathname')
.should('equal', '/') .should('equal', '/')
}) })
it('Should archive a project', () => { it('Should archive a project', () => {
cy.visit(`/projects/${projects[0].id}`) cy.visit(`/projects/${projects[0].id}`)
cy.get('.project-title-dropdown') cy.get('.project-title-dropdown')
.click() .click()
cy.get('.project-title-dropdown .dropdown-menu .dropdown-item') cy.get('.project-title-dropdown .dropdown-menu .dropdown-item')
@ -115,10 +113,59 @@ describe('Projects', () => {
.should('contain.text', 'Archive this project') .should('contain.text', 'Archive this project')
cy.get('.modal-content [data-cy=modalPrimary]') cy.get('.modal-content [data-cy=modalPrimary]')
.click() .click()
cy.get('.namespace-container .menu.namespaces-lists .menu-list') cy.get('.menu-container .menu-list')
.should('not.contain', projects[0].title) .should('not.contain', projects[0].title)
cy.get('main.app-content') cy.get('main.app-content')
.should('contain.text', 'This project is archived. It is not possible to create new or edit tasks for it.') .should('contain.text', 'This project is archived. It is not possible to create new or edit tasks for it.')
}) })
it('Should show all projects on the projects page', () => {
const projects = ProjectFactory.create(10)
cy.visit('/projects')
projects.forEach(p => {
cy.get('[data-cy="projects-list"]')
.should('contain', p.title)
})
})
it('Should not show archived projects if the filter is not checked', () => {
ProjectFactory.create(1, {
id: 2,
}, false)
ProjectFactory.create(1, {
id: 3,
is_archived: true,
}, false)
// Initial
cy.visit('/projects')
cy.get('.project-grid')
.should('not.contain', 'Archived')
// Show archived
cy.get('[data-cy="show-archived-check"] label span')
.should('be.visible')
.click()
cy.get('[data-cy="show-archived-check"] input')
.should('be.checked')
cy.get('.project-grid')
.should('contain', 'Archived')
// Don't show archived
cy.get('[data-cy="show-archived-check"] label span')
.should('be.visible')
.click()
cy.get('[data-cy="show-archived-check"] input')
.should('not.be.checked')
// Second time visiting after unchecking
cy.visit('/projects')
cy.get('[data-cy="show-archived-check"] input')
.should('not.be.checked')
cy.get('.project-grid')
.should('not.contain', 'Archived')
})
}) })

View File

@ -3,12 +3,10 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ProjectFactory} from '../../factories/project' import {ProjectFactory} from '../../factories/project'
import {seed} from '../../support/seed' import {seed} from '../../support/seed'
import {TaskFactory} from '../../factories/task' import {TaskFactory} from '../../factories/task'
import {NamespaceFactory} from '../../factories/namespace'
import {BucketFactory} from '../../factories/bucket' import {BucketFactory} from '../../factories/bucket'
import {updateUserSettings} from '../../support/updateUserSettings' import {updateUserSettings} from '../../support/updateUserSettings'
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) { function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
NamespaceFactory.create(1)
const project = ProjectFactory.create()[0] const project = ProjectFactory.create()[0]
BucketFactory.create(1, { BucketFactory.create(1, {
project_id: project.id, project_id: project.id,
@ -137,8 +135,7 @@ describe('Home Page Task Overview', () => {
cy.visit('/') cy.visit('/')
cy.get('.home.app-content .content') cy.get('.home.app-content .content')
.should('contain.text', 'You can create a new project for your new tasks:') .should('contain.text', 'Import your projects and tasks from other services into Vikunja:')
.should('contain.text', 'Or import your projects and tasks from other services into Vikunja:')
}) })
it('Should not show the cta buttons for new project when there are tasks', () => { it('Should not show the cta buttons for new project when there are tasks', () => {

View File

@ -4,7 +4,6 @@ import {TaskFactory} from '../../factories/task'
import {ProjectFactory} from '../../factories/project' import {ProjectFactory} from '../../factories/project'
import {TaskCommentFactory} from '../../factories/task_comment' import {TaskCommentFactory} from '../../factories/task_comment'
import {UserFactory} from '../../factories/user' import {UserFactory} from '../../factories/user'
import {NamespaceFactory} from '../../factories/namespace'
import {UserProjectFactory} from '../../factories/users_project' import {UserProjectFactory} from '../../factories/users_project'
import {TaskAssigneeFactory} from '../../factories/task_assignee' import {TaskAssigneeFactory} from '../../factories/task_assignee'
import {LabelFactory} from '../../factories/labels' import {LabelFactory} from '../../factories/labels'
@ -47,13 +46,11 @@ function uploadAttachmentAndVerify(taskId: number) {
describe('Task', () => { describe('Task', () => {
createFakeUserAndLogin() createFakeUserAndLogin()
let namespaces
let projects let projects
let buckets let buckets
beforeEach(() => { beforeEach(() => {
// UserFactory.create(1) // UserFactory.create(1)
namespaces = NamespaceFactory.create(1)
projects = ProjectFactory.create(1) projects = ProjectFactory.create(1)
buckets = BucketFactory.create(1, { buckets = BucketFactory.create(1, {
project_id: projects[0].id, project_id: projects[0].id,
@ -110,7 +107,7 @@ describe('Task', () => {
cy.get('.tasks .task .favorite') cy.get('.tasks .task .favorite')
.first() .first()
.click() .click()
cy.get('.menu.namespaces-lists') cy.get('.menu-container')
.should('contain', 'Favorites') .should('contain', 'Favorites')
}) })
@ -133,7 +130,6 @@ describe('Task', () => {
cy.get('.task-view h1.title.task-id') cy.get('.task-view h1.title.task-id')
.should('contain', '#1') .should('contain', '#1')
cy.get('.task-view h6.subtitle') cy.get('.task-view h6.subtitle')
.should('contain', namespaces[0].title)
.should('contain', projects[0].title) .should('contain', projects[0].title)
cy.get('.task-view .details.content.description') cy.get('.task-view .details.content.description')
.should('contain', tasks[0].description) .should('contain', tasks[0].description)
@ -260,7 +256,6 @@ describe('Task', () => {
.click() .click()
cy.get('.task-view h6.subtitle') cy.get('.task-view h6.subtitle')
.should('contain', namespaces[0].title)
.should('contain', projects[1].title) .should('contain', projects[1].title)
cy.get('.global-notification') cy.get('.global-notification')
.should('contain', 'Success') .should('contain', 'Success')

View File

@ -1,5 +1,5 @@
{ {
"extends": "@vue/tsconfig/tsconfig.web.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["./**/*", "../support/**/*", "../factories/**/*"], "include": ["./**/*", "../support/**/*", "../factories/**/*"],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "baseUrl": ".",

View File

@ -1,18 +0,0 @@
import {faker} from '@faker-js/faker'
import {Factory} from '../support/factory'
export class NamespaceFactory extends Factory {
static table = 'namespaces'
static factory() {
const now = new Date()
return {
id: '{increment}',
title: faker.lorem.words(3),
owner_id: 1,
created: now.toISOString(),
updated: now.toISOString(),
}
}
}

View File

@ -11,7 +11,6 @@ export class ProjectFactory extends Factory {
id: '{increment}', id: '{increment}',
title: faker.lorem.words(3), title: faker.lorem.words(3),
owner_id: 1, owner_id: 1,
namespace_id: 1,
created: now.toISOString(), created: now.toISOString(),
updated: now.toISOString(), updated: now.toISOString(),
} }

2
docker/injector.sh Normal file → Executable file
View File

@ -11,5 +11,7 @@ VIKUNJA_SENTRY_DSN="$(echo "$VIKUNJA_SENTRY_DSN" | sed -r 's/([:;])/\\\1/g')"
sed -ri "s:^(\s*window.API_URL\s*=)\s*.+:\1 '${VIKUNJA_API_URL}':g" /usr/share/nginx/html/index.html sed -ri "s:^(\s*window.API_URL\s*=)\s*.+:\1 '${VIKUNJA_API_URL}':g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g" /usr/share/nginx/html/index.html sed -ri "s:^(\s*window.SENTRY_ENABLED\s*=)\s*.+:\1 ${VIKUNJA_SENTRY_ENABLED}:g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html sed -ri "s:^(\s*window.SENTRY_DSN\s*=)\s*.+:\1 '${VIKUNJA_SENTRY_DSN}':g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.PROJECT_INFINITE_NESTING_ENABLED\s*=)\s*.+:\1 '${VIKUNJA_PROJECT_INFINITE_NESTING_ENABLED}':g" /usr/share/nginx/html/index.html
sed -ri "s:^(\s*window.ALLOW_ICON_CHANGES\s*=)\s*.+:\1 ${VIKUNJA_ALLOW_ICON_CHANGES}:g" /usr/share/nginx/html/index.html
date -uIseconds | xargs echo 'info: started at' date -uIseconds | xargs echo 'info: started at'

0
docker/ipv6-disable.sh Normal file → Executable file
View File

View File

@ -4,7 +4,6 @@
pid /tmp/nginx.pid; pid /tmp/nginx.pid;
worker_processes auto; worker_processes auto;
worker_rlimit_nofile 65535;
events { events {
multi_accept on; multi_accept on;

10
env.config.d.ts vendored
View File

@ -6,14 +6,4 @@ declare module 'postcss-easings' {
declare module 'postcss-easing-gradients' { declare module 'postcss-easing-gradients' {
import postcssEasingGradients from 'postcss-easing-gradients' import postcssEasingGradients from 'postcss-easing-gradients'
export default postcssEasingGradients export default postcssEasingGradients
}
declare module 'postcss-focus-within/browser' {
import focusWithinInit from 'postcss-focus-within/browser'
export default focusWithinInit
}
declare module 'css-has-pseudo/browser' {
import cssHasPseudo from 'css-has-pseudo/browser'
export default cssHasPseudo
} }

15
env.d.ts vendored
View File

@ -1,17 +1,16 @@
/// <reference types="vite/client" /> /// <reference types="vite/client" />
/// <reference types="vite-svg-loader" /> /// <reference types="vite-svg-loader" />
/// <reference types="vite-plugin-sentry/client" />
/// <reference types="cypress" /> /// <reference types="cypress" />
/// <reference types="@histoire/plugin-vue/components" /> /// <reference types="@histoire/plugin-vue/components" />
declare module 'postcss-focus-within/browser' { declare module 'postcss-focus-within/browser' {
import focusWithinInit from 'postcss-focus-within/browser' import focusWithinInit from 'postcss-focus-within/browser'
export default focusWithinInit export default focusWithinInit
} }
declare module 'css-has-pseudo/browser' { declare module 'css-has-pseudo/browser' {
import cssHasPseudo from 'css-has-pseudo/browser' import cssHasPseudo from 'css-has-pseudo/browser'
export default cssHasPseudo export default cssHasPseudo
} }
interface ImportMetaEnv { interface ImportMetaEnv {
@ -28,9 +27,9 @@ interface ImportMetaEnv {
readonly SENTRY_RELEASE?: string readonly SENTRY_RELEASE?: string
readonly VITE_WORKBOX_DEBUG?: boolean readonly VITE_WORKBOX_DEBUG?: boolean
readonly VITE_IS_ONLINE?: boolean readonly VITE_IS_ONLINE: boolean
} }
interface ImportMeta { interface ImportMeta {
readonly env: ImportMetaEnv readonly env: ImportMetaEnv
} }

View File

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1680030621, "lastModified": 1685498995,
"narHash": "sha256-qQa1NeS5Rvk2lgK5lSk986PC6I72yIHejzM8PFu+dHs=", "narHash": "sha256-rdyjnkq87tJp+T2Bm1OD/9NXKSsh/vLlPeqCc/mm7qs=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "402cc3633cc60dfc50378197305c984518b30773", "rev": "9cfaa8a1a00830d17487cb60a19bb86f96f09b27",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@ -27,6 +27,11 @@
// our sentry instance to notify us of potential problems. // our sentry instance to notify us of potential problems.
window.SENTRY_ENABLED = false window.SENTRY_ENABLED = false
window.SENTRY_DSN = 'https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480' window.SENTRY_DSN = 'https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480'
// If enabled, allows the user to nest projects infinitely, instead of the default 2 levels.
// This setting might change in the future or be removed completely.
window.PROJECT_INFINITE_NESTING_ENABLED = false
// Allow changing the logo and other icons based on various occasions throughout the year.
window.ALLOW_ICON_CHANGES = true
</script> </script>
</body> </body>
</html> </html>

View File

@ -13,7 +13,7 @@
}, },
"homepage": "https://vikunja.io/", "homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja", "funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@7.30.5", "packageManager": "pnpm@8.6.2",
"keywords": [ "keywords": [
"todo", "todo",
"productivity", "productivity",
@ -51,98 +51,98 @@
"@fortawesome/vue-fontawesome": "3.0.3", "@fortawesome/vue-fontawesome": "3.0.3",
"@github/hotkey": "2.0.1", "@github/hotkey": "2.0.1",
"@infectoone/vue-ganttastic": "2.1.4", "@infectoone/vue-ganttastic": "2.1.4",
"@intlify/unplugin-vue-i18n": "0.10.0", "@intlify/unplugin-vue-i18n": "0.11.0",
"@kyvg/vue3-notification": "2.9.0", "@kyvg/vue3-notification": "2.9.1",
"@sentry/tracing": "7.46.0", "@sentry/tracing": "7.55.2",
"@sentry/vue": "7.46.0", "@sentry/vue": "7.55.2",
"@vueuse/core": "9.13.0", "@vueuse/core": "10.2.0",
"axios": "1.3.4", "axios": "1.4.0",
"blurhash": "2.0.5", "blurhash": "2.0.5",
"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.12", "codemirror": "5.65.13",
"date-fns": "2.29.3", "date-fns": "2.30.0",
"dayjs": "1.11.7", "dayjs": "1.11.8",
"dompurify": "3.0.1", "dompurify": "3.0.3",
"easymde": "2.18.0", "easymde": "2.18.0",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13", "flatpickr": "4.6.13",
"flexsearch": "0.7.31", "flexsearch": "0.7.31",
"floating-vue": "2.0.0-beta.20", "floating-vue": "2.0.0-beta.22",
"highlight.js": "11.7.0", "highlight.js": "11.8.0",
"is-touch-device": "1.0.1", "is-touch-device": "1.0.1",
"klona": "2.0.6", "klona": "2.0.6",
"lodash.debounce": "4.0.8", "lodash.debounce": "4.0.8",
"marked": "4.3.0", "marked": "5.1.0",
"pinia": "2.0.33", "pinia": "2.0.36",
"register-service-worker": "1.7.2", "register-service-worker": "1.7.2",
"snake-case": "3.0.4", "snake-case": "3.0.4",
"sortablejs": "1.15.0", "sortablejs": "1.15.0",
"ufo": "1.1.1", "ufo": "1.1.2",
"vue": "3.2.47", "vue": "3.2.47",
"vue-advanced-cropper": "2.8.8", "vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.3", "vue-flatpickr-component": "11.0.3",
"vue-i18n": "9.2.2", "vue-i18n": "9.2.2",
"vue-router": "4.1.6", "vue-router": "4.2.2",
"workbox-precaching": "6.5.4", "workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3" "zhyswan-vuedraggable": "4.1.3"
}, },
"devDependencies": { "devDependencies": {
"@4tw/cypress-drag-drop": "2.2.3", "@4tw/cypress-drag-drop": "2.2.4",
"@cypress/vite-dev-server": "5.0.5", "@cypress/vite-dev-server": "5.0.5",
"@cypress/vue": "5.0.5", "@cypress/vue": "5.0.5",
"@faker-js/faker": "7.6.0", "@faker-js/faker": "8.0.2",
"@histoire/plugin-screenshot": "0.15.9", "@histoire/plugin-screenshot": "0.16.1",
"@histoire/plugin-vue": "0.15.8", "@histoire/plugin-vue": "0.16.1",
"@rushstack/eslint-patch": "1.2.0", "@rushstack/eslint-patch": "1.3.2",
"@types/codemirror": "5.60.7", "@tsconfig/node18": "2.0.1",
"@types/dompurify": "3.0.0", "@types/codemirror": "5.60.8",
"@types/dompurify": "3.0.2",
"@types/flexsearch": "0.7.3", "@types/flexsearch": "0.7.3",
"@types/is-touch-device": "1.0.0", "@types/is-touch-device": "1.0.0",
"@types/lodash.debounce": "4.0.7", "@types/lodash.debounce": "4.0.7",
"@types/marked": "4.0.8", "@types/marked": "5.0.0",
"@types/node": "18.15.11", "@types/node": "18.16.18",
"@types/postcss-preset-env": "7.7.0", "@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.1", "@types/sortablejs": "1.15.1",
"@typescript-eslint/eslint-plugin": "5.57.0", "@typescript-eslint/eslint-plugin": "5.59.11",
"@typescript-eslint/parser": "5.57.0", "@typescript-eslint/parser": "5.59.11",
"@vitejs/plugin-legacy": "4.0.2", "@vitejs/plugin-legacy": "4.0.4",
"@vitejs/plugin-vue": "4.1.0", "@vitejs/plugin-vue": "4.2.3",
"@vue/eslint-config-typescript": "11.0.2", "@vue/eslint-config-typescript": "11.0.3",
"@vue/test-utils": "2.3.2", "@vue/test-utils": "2.3.2",
"@vue/tsconfig": "0.1.3", "@vue/tsconfig": "0.4.0",
"autoprefixer": "10.4.14", "autoprefixer": "10.4.14",
"browserslist": "4.21.5", "browserslist": "4.21.7",
"caniuse-lite": "1.0.30001470", "caniuse-lite": "1.0.30001500",
"css-has-pseudo": "5.0.2", "css-has-pseudo": "5.0.2",
"csstype": "3.1.1", "csstype": "3.1.2",
"cypress": "12.9.0", "cypress": "12.14.0",
"esbuild": "0.17.14", "esbuild": "0.18.4",
"eslint": "8.37.0", "eslint": "8.43.0",
"eslint-plugin-vue": "9.10.0", "eslint-plugin-vue": "9.13.0",
"happy-dom": "8.9.0", "happy-dom": "9.20.3",
"histoire": "0.15.9", "histoire": "0.16.2",
"netlify-cli": "13.2.1", "postcss": "8.4.24",
"postcss": "8.4.21",
"postcss-easing-gradients": "3.0.1", "postcss-easing-gradients": "3.0.1",
"postcss-easings": "3.0.1", "postcss-easings": "3.0.1",
"postcss-focus-within": "7.0.2", "postcss-focus-within": "7.0.2",
"postcss-preset-env": "8.3.0", "postcss-preset-env": "8.5.0",
"rimraf": "3.0.2", "rimraf": "3.0.2",
"rollup": "3.20.2", "rollup": "3.25.1",
"rollup-plugin-visualizer": "5.9.0", "rollup-plugin-visualizer": "5.9.2",
"sass": "1.60.0", "sass": "1.63.4",
"start-server-and-test": "2.0.0", "start-server-and-test": "2.0.0",
"typescript": "5.0.3", "typescript": "5.1.3",
"vite": "4.2.1", "vite": "4.3.9",
"vite-plugin-inject-preload": "1.3.1", "vite-plugin-inject-preload": "1.3.1",
"vite-plugin-pwa": "0.14.7", "vite-plugin-pwa": "0.16.4",
"vite-plugin-sentry": "1.1.7", "vite-plugin-sentry": "1.1.6",
"vite-svg-loader": "4.0.0", "vite-svg-loader": "4.0.0",
"vitest": "0.29.8", "vitest": "0.32.2",
"vue-tsc": "1.2.0", "vue-tsc": "1.8.0",
"wait-on": "7.0.1", "wait-on": "7.0.1",
"workbox-cli": "6.5.4" "workbox-cli": "7.0.0"
}, },
"pnpm": { "pnpm": {
"patchedDependencies": { "patchedDependencies": {

File diff suppressed because it is too large Load Diff

View File

@ -6,7 +6,7 @@
], ],
"packageRules": [ "packageRules": [
{ {
"matchPackageNames": ["netlify-cli", "happy-dom"], "matchPackageNames": ["happy-dom"],
"extends": ["schedule:weekly"] "extends": ["schedule:weekly"]
}, },
{ {

View File

@ -33,9 +33,9 @@ const promiseExec = cmd => {
} }
(async function () { (async function () {
let stdout = await promiseExec(`./node_modules/.bin/netlify link --id ${siteId}`) let stdout = await promiseExec(`/home/node/docker-netlify-cli/node_modules/.bin/netlify link --id ${siteId}`)
console.log(stdout) console.log(stdout)
stdout = await promiseExec(`./node_modules/.bin/netlify deploy --alias ${alias}`) stdout = await promiseExec(`/home/node/docker-netlify-cli/node_modules/.bin/netlify deploy --alias ${alias}`)
console.log(stdout) console.log(stdout)
const data = await fetch(prIssueCommentsUrl).then(response => response.json()) const data = await fetch(prIssueCommentsUrl).then(response => response.json())

View File

@ -1 +1 @@
57af69409e66bc87f4f2fc5822dd8d3c2eb47c601f81af1ac4a56f3e2d80837b1a2de06f4ff57695ec379b7c15b881e3 ./scripts/deploy-preview-netlify.mjs 4a7c1293c7b12e9ab476cdf35251a407c6a1cd005d22c06df994222cccfb25cde5f47d15866a098c9d739778fee4dc19 ./scripts/deploy-preview-netlify.mjs

View File

@ -12,6 +12,7 @@
<keyboard-shortcuts v-if="keyboardShortcutsActive"/> <keyboard-shortcuts v-if="keyboardShortcutsActive"/>
<Teleport to="body"> <Teleport to="body">
<AddToHomeScreen/>
<UpdateNotification/> <UpdateNotification/>
<Notification/> <Notification/>
</Teleport> </Teleport>
@ -43,6 +44,7 @@ import {useBaseStore} from '@/stores/base'
import {useColorScheme} from '@/composables/useColorScheme' import {useColorScheme} from '@/composables/useColorScheme'
import {useBodyClass} from '@/composables/useBodyClass' import {useBodyClass} from '@/composables/useBodyClass'
import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
const baseStore = useBaseStore() const baseStore = useBaseStore()
const authStore = useAuthStore() const authStore = useAuthStore()
@ -92,7 +94,7 @@ watch(userEmailConfirm, (userEmailConfirm) => {
router.push({name: 'user.login'}) router.push({name: 'user.login'})
}, { immediate: true }) }, { immediate: true })
setLanguage() setLanguage(authStore.settings.language)
useColorScheme() useColorScheme()
</script> </script>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 KiB

After

Width:  |  Height:  |  Size: 313 KiB

View File

@ -32,7 +32,7 @@ import {computed, ref} from 'vue'
import {getInheritedBackgroundColor} from '@/helpers/getInheritedBackgroundColor' import {getInheritedBackgroundColor} from '@/helpers/getInheritedBackgroundColor'
const props = defineProps({ const props = defineProps({
/** Wheather the Expandable is open or not */ /** Whether the Expandable is open or not */
open: { open: {
type: Boolean, type: Boolean,
default: false, default: false,

View File

@ -0,0 +1,11 @@
<script setup lang="ts">
import datemathHelp from './datemathHelp.vue'
</script>
<template>
<Story>
<Variant title="Default">
<datemathHelp />
</Variant>
</Story>
</template>

View File

@ -1,7 +1,8 @@
<template> <template>
<card <card
class="has-no-shadow how-it-works-modal" class="has-no-shadow how-it-works-modal"
:title="$t('input.datemathHelp.title')"> :title="$t('input.datemathHelp.title')"
>
<p> <p>
{{ $t('input.datemathHelp.intro') }} {{ $t('input.datemathHelp.intro') }}
</p> </p>
@ -27,11 +28,11 @@
</p> </p>
<p>{{ $t('misc.forExample') }}</p> <p>{{ $t('misc.forExample') }}</p>
<ul> <ul>
<li><code>+1d</code>{{ $t('input.datemathHelp.add1Day') }}</li> <li><code>+1d</code> {{ $t('input.datemathHelp.add1Day') }}</li>
<li><code>-1d</code>{{ $t('input.datemathHelp.minus1Day') }}</li> <li><code>-1d</code> {{ $t('input.datemathHelp.minus1Day') }}</li>
<li><code>/d</code>{{ $t('input.datemathHelp.roundDay') }}</li> <li><code>/d</code> {{ $t('input.datemathHelp.roundDay') }}</li>
</ul> </ul>
<p>{{ $t('input.datemathHelp.supportedUnits') }}</p> <h3>{{ $t('input.datemathHelp.supportedUnits') }}</h3>
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>
@ -69,7 +70,7 @@
</tbody> </tbody>
</table> </table>
<p>{{ $t('input.datemathHelp.someExamples') }}</p> <h3>{{ $t('input.datemathHelp.someExamples') }}</h3>
<table class="table"> <table class="table">
<tbody> <tbody>
<tr> <tr>
@ -100,7 +101,7 @@
<td><code>{{ exampleDate }}||+1M/d</code></td> <td><code>{{ exampleDate }}||+1M/d</code></td>
<td> <td>
<i18n-t keypath="input.datemathHelp.examples.datePlusMonth" scope="global"> <i18n-t keypath="input.datemathHelp.examples.datePlusMonth" scope="global">
<code>{{ exampleDate }}</code> <strong>{{ exampleDate }}</strong>
</i18n-t> </i18n-t>
</td> </td>
</tr> </tr>
@ -110,13 +111,15 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {formatDate} from '@/helpers/time/formatDate' import {formatDateShort} from '@/helpers/time/formatDate'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
const exampleDate = formatDate(new Date(), 'yyyy-MM-dd') const exampleDate = formatDateShort(new Date())
</script> </script>
<style scoped lang="scss"> <style scoped lang="scss">
// FIXME: Remove style overwrites
.how-it-works-modal { .how-it-works-modal {
font-size: 1rem; font-size: 1rem;
} }

View File

@ -0,0 +1,80 @@
<template>
<div
v-if="shouldShowMessage"
class="add-to-home-screen"
:class="{'has-update-available': hasUpdateAvailable}"
>
<icon icon="arrow-up-from-bracket" class="add-icon"/>
<p>
{{ $t('home.addToHomeScreen') }}
</p>
<BaseButton @click="() => hideMessage = true" class="hide-button">
<icon icon="x"/>
</BaseButton>
</div>
</template>
<script lang="ts" setup>
import BaseButton from '@/components/base/BaseButton.vue'
import {useLocalStorage} from '@vueuse/core'
import {computed} from 'vue'
import {useBaseStore} from '@/stores/base'
const baseStore = useBaseStore()
const hideMessage = useLocalStorage('hideAddToHomeScreenMessage', false)
const hasUpdateAvailable = computed(() => baseStore.updateAvailable)
const shouldShowMessage = computed(() => {
if (hideMessage.value) {
return false
}
if (typeof window !== 'undefined' && window.matchMedia('(display-mode: standalone)').matches) {
return false
}
return true
})
</script>
<style lang="scss" scoped>
.add-to-home-screen {
position: fixed;
// FIXME: We should prevent usage of z-index or
// at least define it centrally
// the highest z-index of a modal is .hint-modal with 4500
z-index: 5000;
bottom: 1rem;
inset-inline: 1rem;
max-width: max-content;
margin-inline: auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: .5rem 1rem;
background: var(--grey-900);
border-radius: $radius;
font-size: .9rem;
color: var(--grey-200);
@media screen and (min-width: $tablet) {
display: none;
}
&.has-update-available {
bottom: 5rem;
}
}
.add-icon {
color: var(--primary-light);
}
.hide-button {
padding: .25rem .5rem;
cursor: pointer;
}
</style>

View File

@ -9,7 +9,7 @@ import {MILLISECONDS_A_HOUR} from '@/constants/date'
const now = useNow({ const now = useNow({
interval: MILLISECONDS_A_HOUR, interval: MILLISECONDS_A_HOUR,
}) })
const Logo = computed(() => now.value.getMonth() === 5 ? LogoFullPride : LogoFull) const Logo = computed(() => window.ALLOW_ICON_CHANGES && now.value.getMonth() === 5 ? LogoFullPride : LogoFull)
</script> </script>
<template> <template>

View File

@ -0,0 +1,109 @@
<template>
<draggable
v-model="availableProjects"
animation="100"
ghostClass="ghost"
group="projects"
@start="() => drag = true"
@end="saveProjectPosition"
handle=".handle"
tag="menu"
item-key="id"
:disabled="!canEditOrder"
filter=".drag-disabled"
:component-data="{
type: 'transition-group',
name: !drag ? 'flip-list' : null,
class: [
'menu-list can-be-hidden',
{ 'dragging-disabled': !canEditOrder }
],
}"
>
<template #item="{element: project}">
<ProjectsNavigationItem
:class="{'drag-disabled': project.id < 0}"
:project="project"
:is-loading="projectUpdating[project.id]"
:can-collapse="canCollapse"
:level="level"
:data-project-id="project.id"
/>
</template>
</draggable>
</template>
<script lang="ts" setup>
import {ref, watch} from 'vue'
import draggable from 'zhyswan-vuedraggable'
import type {SortableEvent} from 'sortablejs'
import ProjectsNavigationItem from '@/components/home/ProjectsNavigationItem.vue'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import type {IProject} from '@/modelTypes/IProject'
import {useProjectStore} from '@/stores/projects'
const props = defineProps<{
modelValue?: IProject[],
canEditOrder: boolean,
canCollapse?: boolean,
level?: number,
}>()
const emit = defineEmits<{
(e: 'update:modelValue', projects: IProject[]): void
}>()
const drag = ref(false)
const projectStore = useProjectStore()
// Vue draggable will modify the projects list as it changes their position which will not work on a prop.
// Hence, we'll clone the prop and work on the clone.
const availableProjects = ref<IProject[]>([])
watch(
() => props.modelValue,
projects => {
availableProjects.value = projects || []
},
{immediate: true},
)
const projectUpdating = ref<{ [id: IProject['id']]: boolean }>({})
async function saveProjectPosition(e: SortableEvent) {
if (!e.newIndex && e.newIndex !== 0) return
const projectsActive = availableProjects.value
// If the project was dragged to the last position, Safari will report e.newIndex as the size of the projectsActive
// array instead of using the position. Because the index is wrong in that case, dragging the project will fail.
// To work around that we're explicitly checking that case here and decrease the index.
const newIndex = e.newIndex === projectsActive.length ? e.newIndex - 1 : e.newIndex
const projectId = parseInt(e.item.dataset.projectId)
const project = projectStore.projects[projectId]
const parentProjectId = e.to.parentNode.dataset.projectId ? parseInt(e.to.parentNode.dataset.projectId) : 0
const projectBefore = projectsActive[newIndex - 1] ?? null
const projectAfter = projectsActive[newIndex + 1] ?? null
projectUpdating.value[project.id] = true
const position = calculateItemPosition(
projectBefore !== null ? projectBefore.position : null,
projectAfter !== null ? projectAfter.position : null,
)
try {
// create a copy of the project in order to not violate pinia manipulation
await projectStore.updateProject({
...project,
position,
parentProjectId,
})
emit('update:modelValue', availableProjects.value)
} finally {
projectUpdating.value[project.id] = false
}
}
</script>

View File

@ -0,0 +1,175 @@
<template>
<li
class="list-menu loader-container is-loading-small"
:class="{'is-loading': isLoading}"
>
<div>
<BaseButton
v-if="canCollapse && childProjects?.length > 0"
@click="childProjectsOpen = !childProjectsOpen"
class="collapse-project-button"
>
<icon icon="chevron-down" :class="{ 'project-is-collapsed': !childProjectsOpen }"/>
</BaseButton>
<BaseButton
:to="{ name: 'project.index', params: { projectId: project.id} }"
class="list-menu-link"
:class="{'router-link-exact-active': currentProject?.id === project.id}"
>
<span
v-if="!canCollapse || childProjects?.length === 0"
class="collapse-project-button-placeholder"
></span>
<div class="color-bubble-handle-wrapper" :class="{'is-draggable': project.id > 0}">
<ColorBubble
v-if="project.hexColor !== ''"
:color="project.hexColor"
/>
<span v-else-if="project.id < -1" class="saved-filter-icon icon menu-item-icon">
<icon icon="filter"/>
</span>
<span
v-if="project.id > 0"
class="icon menu-item-icon handle lines-handle"
:class="{'has-color-bubble': project.hexColor !== ''}"
>
<icon icon="grip-lines"/>
</span>
</div>
<span class="project-menu-title">{{ getProjectTitle(project) }}</span>
</BaseButton>
<BaseButton
v-if="project.id > 0"
class="favorite"
:class="{'is-favorite': project.isFavorite}"
@click="projectStore.toggleProjectFavorite(project)"
>
<icon :icon="project.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton>
<ProjectSettingsDropdown
v-if="project.id > 0"
class="menu-list-dropdown"
:project="project"
:level="level"
>
<template #trigger="{toggleOpen}">
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</template>
</ProjectSettingsDropdown>
<span class="list-setting-spacer" v-else></span>
</div>
<ProjectsNavigation
v-if="canNestDeeper && childProjectsOpen && canCollapse"
:model-value="childProjects"
:can-edit-order="true"
:can-collapse="canCollapse"
:level="level + 1"
/>
</li>
</template>
<script setup lang="ts">
import {computed, ref} from 'vue'
import {useProjectStore} from '@/stores/projects'
import {useBaseStore} from '@/stores/base'
import type {IProject} from '@/modelTypes/IProject'
import BaseButton from '@/components/base/BaseButton.vue'
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
import {getProjectTitle} from '@/helpers/getProjectTitle'
import ColorBubble from '@/components/misc/colorBubble.vue'
import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
import {canNestProjectDeeper} from '@/helpers/canNestProjectDeeper'
const props = withDefaults(defineProps<{
project: IProject,
isLoading?: boolean,
canCollapse?: boolean,
level?: number,
}>(), {
level: 0,
})
const projectStore = useProjectStore()
const baseStore = useBaseStore()
const currentProject = computed(() => baseStore.currentProject)
const childProjectsOpen = ref(true)
const childProjects = computed(() => {
if (!canNestDeeper.value) {
return []
}
return projectStore.getChildProjects(props.project.id)
.filter(p => !p.isArchived)
.sort((a, b) => a.position - b.position)
})
const canNestDeeper = computed(() => canNestProjectDeeper(props.level))
</script>
<style lang="scss" scoped>
.list-setting-spacer {
width: 5rem;
flex-shrink: 0;
}
.project-is-collapsed {
transform: rotate(-90deg);
}
.favorite {
transition: opacity $transition, color $transition;
opacity: 0;
&:hover,
&.is-favorite {
opacity: 1;
color: var(--warning);
}
}
.list-menu:hover > div > .favorite {
opacity: 1;
}
.list-menu:hover > div > a > .color-bubble-handle-wrapper.is-draggable > {
.saved-filter-icon,
.color-bubble {
opacity: 0;
}
}
.color-bubble-handle-wrapper {
position: relative;
width: 1rem;
height: 1rem;
display: flex;
align-items: center;
justify-content: flex-start;
margin-right: .25rem;
flex-shrink: 0;
.color-bubble, .icon {
transition: all $transition;
position: absolute;
width: 12px;
margin: 0 !important;
padding: 0 !important;
}
}
.project-menu-title {
overflow: hidden;
text-overflow: ellipsis;
}
.saved-filter-icon {
color: var(--grey-300) !important;
font-size: .75rem;
}
</style>

View File

@ -7,8 +7,9 @@
<MenuButton class="menu-button" /> <MenuButton class="menu-button" />
<div v-if="currentProject.id" class="project-title-wrapper"> <div v-if="currentProject?.id" class="project-title-wrapper">
<h1 class="project-title">{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }} <h1 class="project-title">
{{ currentProject.title === '' ? $t('misc.loading') : getProjectTitle(currentProject) }}
</h1> </h1>
<BaseButton :to="{ name: 'project.info', params: { projectId: currentProject.id } }" class="project-title-button"> <BaseButton :to="{ name: 'project.info', params: { projectId: currentProject.id } }" class="project-title-button">
@ -89,7 +90,7 @@ import { useAuthStore } from '@/stores/auth'
const baseStore = useBaseStore() const baseStore = useBaseStore()
const currentProject = computed(() => baseStore.currentProject) const currentProject = computed(() => baseStore.currentProject)
const background = computed(() => baseStore.background) const background = computed(() => baseStore.background)
const canWriteCurrentProject = computed(() => baseStore.currentProject.maxRight > Rights.READ) const canWriteCurrentProject = computed(() => baseStore.currentProject?.maxRight > Rights.READ)
const menuActive = computed(() => baseStore.menuActive) const menuActive = computed(() => baseStore.menuActive)
const authStore = useAuthStore() const authStore = useAuthStore()

View File

@ -12,9 +12,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import {ref} from 'vue' import {computed, ref} from 'vue'
import {useBaseStore} from '@/stores/base'
const updateAvailable = ref(false) const baseStore = useBaseStore()
const updateAvailable = computed(() => baseStore.updateAvailable)
const registration = ref(null) const registration = ref(null)
const refreshing = ref(false) const refreshing = ref(false)
@ -31,11 +34,11 @@ navigator?.serviceWorker?.addEventListener(
function showRefreshUI(e: Event) { function showRefreshUI(e: Event) {
console.log('recieved refresh event', e) console.log('recieved refresh event', e)
registration.value = e.detail registration.value = e.detail
updateAvailable.value = true baseStore.setUpdateAvailable(true)
} }
function refreshApp() { function refreshApp() {
updateAvailable.value = false baseStore.setUpdateAvailable(false)
if (!registration.value || !registration.value.waiting) { if (!registration.value || !registration.value.waiting) {
return return
} }
@ -65,7 +68,6 @@ function refreshApp() {
border-radius: $radius; border-radius: $radius;
font-size: .9rem; font-size: .9rem;
color: var(--grey-900); color: var(--grey-900);
} }
.update-notification__message { .update-notification__message {

View File

@ -69,6 +69,7 @@ import BaseButton from '@/components/base/BaseButton.vue'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useProjectStore} from '@/stores/projects'
import {useRouteWithModal} from '@/composables/useRouteWithModal' import {useRouteWithModal} from '@/composables/useRouteWithModal'
import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus' import {useRenewTokenOnFocus} from '@/composables/useRenewTokenOnFocus'
@ -94,14 +95,13 @@ watch(() => route.name as string, (routeName) => {
( (
[ [
'home', 'home',
'namespace.edit',
'teams.index', 'teams.index',
'teams.edit', 'teams.edit',
'tasks.range', 'tasks.range',
'labels.index', 'labels.index',
'migrate.start', 'migrate.start',
'migrate.wunderlist', 'migrate.wunderlist',
'namespaces.index', 'projects.index',
].includes(routeName) || ].includes(routeName) ||
routeName.startsWith('user.settings') routeName.startsWith('user.settings')
) )
@ -116,6 +116,9 @@ useRenewTokenOnFocus()
const labelStore = useLabelStore() const labelStore = useLabelStore()
labelStore.loadAllLabels() labelStore.loadAllLabels()
const projectStore = useProjectStore()
projectStore.loadProjects()
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -9,9 +9,9 @@
<Logo class="logo" v-if="logoVisible"/> <Logo class="logo" v-if="logoVisible"/>
<h1 <h1
:class="{'m-0': !logoVisible}" :class="{'m-0': !logoVisible}"
:style="{ 'opacity': currentProject.title === '' ? '0': '1' }" :style="{ 'opacity': currentProject?.title === '' ? '0': '1' }"
class="title"> class="title">
{{ currentProject.title === '' ? $t('misc.loading') : currentProject.title }} {{ currentProject?.title === '' ? $t('misc.loading') : currentProject?.title }}
</h1> </h1>
<div class="box has-text-left view"> <div class="box has-text-left view">
<router-view/> <router-view/>

View File

@ -1,10 +1,10 @@
<template> <template>
<aside :class="{'is-active': menuActive}" class="namespace-container"> <aside :class="{'is-active': baseStore.menuActive}" class="menu-container">
<nav class="menu top-menu"> <nav class="menu top-menu">
<router-link :to="{name: 'home'}" class="logo"> <router-link :to="{name: 'home'}" class="logo">
<Logo width="164" height="48"/> <Logo width="164" height="48"/>
</router-link> </router-link>
<ul class="menu-list"> <menu class="menu-list other-menu-items">
<li> <li>
<router-link :to="{ name: 'home'}" v-shortcut="'g o'"> <router-link :to="{ name: 'home'}" v-shortcut="'g o'">
<span class="menu-item-icon icon"> <span class="menu-item-icon icon">
@ -22,11 +22,11 @@
</router-link> </router-link>
</li> </li>
<li> <li>
<router-link :to="{ name: 'namespaces.index'}" v-shortcut="'g n'"> <router-link :to="{ name: 'projects.index'}" v-shortcut="'g p'">
<span class="menu-item-icon icon"> <span class="menu-item-icon icon">
<icon icon="layer-group"/> <icon icon="layer-group"/>
</span> </span>
{{ $t('namespace.title') }} {{ $t('project.projects') }}
</router-link> </router-link>
</li> </li>
<li> <li>
@ -45,238 +45,55 @@
{{ $t('team.title') }} {{ $t('team.title') }}
</router-link> </router-link>
</li> </li>
</ul> </menu>
</nav> </nav>
<nav class="menu namespaces-lists loader-container is-loading-small" :class="{'is-loading': loading}"> <Loading
<template v-for="(n, nk) in namespaces" :key="n.id"> v-if="projectStore.isLoading"
<div class="namespace-title" :class="{'has-menu': n.id > 0}"> variant="small"
<BaseButton />
@click="toggleProjects(n.id)" <template v-else>
class="menu-label" <nav class="menu" v-if="favoriteProjects">
v-tooltip="namespaceTitles[nk]" <ProjectsNavigation
> :model-value="favoriteProjects"
<ColorBubble :can-edit-order="false"
v-if="n.hexColor !== ''" :can-collapse="false"
:color="n.hexColor" />
class="mr-1" </nav>
/>
<span class="name">{{ namespaceTitles[nk] }}</span> <nav class="menu">
<div <ProjectsNavigation
class="icon menu-item-icon is-small toggle-lists-icon pl-2" :model-value="projects"
:class="{'active': typeof projectsVisible[n.id] !== 'undefined' ? projectsVisible[n.id] : true}" :can-edit-order="true"
> :can-collapse="true"
<icon icon="chevron-down"/> :level="1"
</div> />
<span class="count" :class="{'ml-2 mr-0': n.id > 0}"> </nav>
({{ namespaceProjectsCount[nk] }}) </template>
</span>
</BaseButton>
<namespace-settings-dropdown class="menu-list-dropdown" :namespace="n" v-if="n.id > 0"/>
</div>
<!--
NOTE: a v-model / computed setter is not possible, since the updateActiveProjects function
triggered by the change needs to have access to the current namespace
-->
<draggable
v-if="projectsVisible[n.id] ?? true"
v-bind="dragOptions"
:modelValue="activeProjects[nk]"
@update:modelValue="(projects) => updateActiveProjects(n, projects)"
group="namespace-lists"
@start="() => drag = true"
@end="saveListPosition"
handle=".handle"
:disabled="n.id < 0 || undefined"
tag="ul"
item-key="id"
:data-namespace-id="n.id"
:data-namespace-index="nk"
:component-data="{
type: 'transition-group',
name: !drag ? 'flip-list' : null,
class: [
'menu-list can-be-hidden',
{ 'dragging-disabled': n.id < 0 }
]
}"
>
<template #item="{element: l}">
<li
class="list-menu loader-container is-loading-small"
:class="{'is-loading': projectUpdating[l.id]}"
>
<BaseButton
:to="{ name: 'project.index', params: { projectId: l.id} }"
class="list-menu-link"
:class="{'router-link-exact-active': currentProject.id === l.id}"
>
<span class="icon menu-item-icon handle">
<icon icon="grip-lines"/>
</span>
<ColorBubble
v-if="l.hexColor !== ''"
:color="l.hexColor"
class="mr-1"
/>
<span class="list-menu-title">{{ getProjectTitle(l) }}</span>
</BaseButton>
<BaseButton
v-if="l.id > 0"
class="favorite"
:class="{'is-favorite': l.isFavorite}"
@click="projectStore.toggleProjectFavorite(l)"
>
<icon :icon="l.isFavorite ? 'star' : ['far', 'star']"/>
</BaseButton>
<ProjectSettingsDropdown class="menu-list-dropdown" :project="l" v-if="l.id > 0">
<template #trigger="{toggleOpen}">
<BaseButton class="menu-list-dropdown-trigger" @click="toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</template>
</ProjectSettingsDropdown>
<span class="list-setting-spacer" v-else></span>
</li>
</template>
</draggable>
</template>
</nav>
<PoweredByLink/> <PoweredByLink/>
</aside> </aside>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed, onBeforeMount} from 'vue' import {computed} from 'vue'
import draggable from 'zhyswan-vuedraggable'
import type {SortableEvent} from 'sortablejs'
import BaseButton from '@/components/base/BaseButton.vue'
import ProjectSettingsDropdown from '@/components/project/project-settings-dropdown.vue'
import NamespaceSettingsDropdown from '@/components/namespace/namespace-settings-dropdown.vue'
import PoweredByLink from '@/components/home/PoweredByLink.vue' import PoweredByLink from '@/components/home/PoweredByLink.vue'
import Logo from '@/components/home/Logo.vue' import Logo from '@/components/home/Logo.vue'
import Loading from '@/components/misc/loading.vue'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {getNamespaceTitle} from '@/helpers/getNamespaceTitle'
import {getProjectTitle} from '@/helpers/getProjectTitle'
import type {IProject} from '@/modelTypes/IProject'
import type {INamespace} from '@/modelTypes/INamespace'
import ColorBubble from '@/components/misc/colorBubble.vue'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces' import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
const drag = ref(false)
const dragOptions = {
animation: 100,
ghostClass: 'ghost',
}
const baseStore = useBaseStore() const baseStore = useBaseStore()
const namespaceStore = useNamespaceStore()
const currentProject = computed(() => baseStore.currentProject)
const menuActive = computed(() => baseStore.menuActive)
const loading = computed(() => namespaceStore.isLoading)
const namespaces = computed(() => {
return namespaceStore.namespaces.filter(n => !n.isArchived)
})
const activeProjects = computed(() => {
return namespaces.value.map(({projects}) => {
return projects?.filter(item => {
return typeof item !== 'undefined' && !item.isArchived
})
})
})
const namespaceTitles = computed(() => {
return namespaces.value.map((namespace) => getNamespaceTitle(namespace))
})
const namespaceProjectsCount = computed(() => {
return namespaces.value.map((_, index) => activeProjects.value[index]?.length ?? 0)
})
const projectStore = useProjectStore() const projectStore = useProjectStore()
function toggleProjects(namespaceId: INamespace['id']) { const projects = computed(() => projectStore.notArchivedRootProjects)
projectsVisible.value[namespaceId] = !projectsVisible.value[namespaceId] const favoriteProjects = computed(() => projectStore.favoriteProjects)
}
const projectsVisible = ref<{ [id: INamespace['id']]: boolean }>({})
// FIXME: async action will be unfinished when component mounts
onBeforeMount(async () => {
const namespaces = await namespaceStore.loadNamespaces()
namespaces.forEach(n => {
if (typeof projectsVisible.value[n.id] === 'undefined') {
projectsVisible.value[n.id] = true
}
})
})
function updateActiveProjects(namespace: INamespace, activeProjects: IProject[]) {
// This is a bit hacky: since we do have to filter out the archived items from the list
// for vue draggable updating it is not as simple as replacing it.
// To work around this, we merge the active projects with the archived ones. Doing so breaks the order
// because now all archived projects are sorted after the active ones. This is fine because they are sorted
// later when showing them anyway, and it makes the merging happening here a lot easier.
const projects = [
...activeProjects,
...namespace.projects.filter(l => l.isArchived),
]
namespaceStore.setNamespaceById({
...namespace,
projects,
})
}
const projectUpdating = ref<{ [id: INamespace['id']]: boolean }>({})
async function saveListPosition(e: SortableEvent) {
if (!e.newIndex && e.newIndex !== 0) return
const namespaceId = parseInt(e.to.dataset.namespaceId as string)
const newNamespaceIndex = parseInt(e.to.dataset.namespaceIndex as string)
const projectsActive = activeProjects.value[newNamespaceIndex]
// If the project was dragged to the last position, Safari will report e.newIndex as the size of the projectsActive
// array instead of using the position. Because the index is wrong in that case, dragging the project will fail.
// To work around that we're explicitly checking that case here and decrease the index.
const newIndex = e.newIndex === projectsActive.length ? e.newIndex - 1 : e.newIndex
const project = projectsActive[newIndex]
const projectBefore = projectsActive[newIndex - 1] ?? null
const projectAfter = projectsActive[newIndex + 1] ?? null
projectUpdating.value[project.id] = true
const position = calculateItemPosition(
projectBefore !== null ? projectBefore.position : null,
projectAfter !== null ? projectAfter.position : null,
)
try {
// create a copy of the project in order to not violate pinia manipulation
await projectStore.updateProject({
...project,
position,
namespaceId,
})
} finally {
projectUpdating.value[project.id] = false
}
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
$navbar-padding: 2rem;
$vikunja-nav-background: var(--site-background);
$vikunja-nav-color: var(--grey-700);
$vikunja-nav-selected-width: 0.4rem;
.logo { .logo {
display: block; display: block;
@ -289,10 +106,10 @@ $vikunja-nav-selected-width: 0.4rem;
} }
} }
.namespace-container { .menu-container {
background: $vikunja-nav-background; background: var(--site-background);
color: $vikunja-nav-color; color: $vikunja-nav-color;
padding: 0 0 1rem; padding: 1rem 0;
transition: transform $transition-duration ease-in; transition: transform $transition-duration ease-in;
position: fixed; position: fixed;
top: $navbar-height; top: $navbar-height;
@ -314,252 +131,24 @@ $vikunja-nav-selected-width: 0.4rem;
} }
} }
// these are general menu styles .top-menu .menu-list {
// should be in own components li {
.menu {
.menu-label,
.menu-list .list-menu-link,
.menu-list a {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
.color-bubble {
height: 12px;
flex: 0 0 12px;
}
}
.menu-list {
li {
height: 44px;
display: flex;
align-items: center;
&:hover {
background: var(--white);
}
.menu-list-dropdown {
opacity: 1;
transition: $transition;
}
@media(hover: hover) and (pointer: fine) {
.menu-list-dropdown {
opacity: 0;
}
&:hover .menu-list-dropdown {
opacity: 1;
}
}
}
.menu-item-icon {
color: var(--grey-400);
}
.menu-list-dropdown-trigger {
display: flex;
padding: 0.5rem;
}
.flip-list-move {
transition: transform $transition-duration;
}
.ghost {
background: var(--grey-200);
* {
opacity: 0;
}
}
a:hover {
background: transparent;
}
.list-menu-link,
li > a {
color: $vikunja-nav-color;
padding: 0.75rem .5rem 0.75rem ($navbar-padding * 1.5 - 1.75rem);
transition: all 0.2s ease;
border-radius: 0;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
width: 100%;
border-left: $vikunja-nav-selected-width solid transparent;
&:hover {
border-left: $vikunja-nav-selected-width solid var(--primary);
}
&.router-link-exact-active {
color: var(--primary);
border-left: $vikunja-nav-selected-width solid var(--primary);
}
.icon {
height: 1rem;
vertical-align: middle;
padding-right: 0.5rem;
}
&.router-link-exact-active .icon:not(.handle) {
color: var(--primary);
}
.handle {
opacity: 0;
transition: opacity $transition;
margin-right: .25rem;
}
&:hover .handle {
opacity: 1;
}
}
&:not(.dragging-disabled) .handle {
cursor: grab;
}
}
}
.top-menu {
margin-top: math.div($navbar-padding, 2);
.menu-list {
li {
font-weight: 600;
font-family: $vikunja-font;
}
.list-menu-link,
li > a {
padding-left: 2rem;
display: inline-block;
.icon {
padding-bottom: .25rem;
}
}
}
}
.namespaces-lists {
padding-top: math.div($navbar-padding, 2);
.menu-label {
font-size: 1rem;
font-weight: 700;
font-weight: bold;
font-family: $vikunja-font;
color: $vikunja-nav-color;
font-weight: 600; font-weight: 600;
min-height: 2.5rem; font-family: $vikunja-font;
padding-top: 0;
padding-left: $navbar-padding;
overflow: hidden;
margin-bottom: 0;
flex: 1 1 auto;
.name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: auto;
}
.count {
color: var(--grey-500);
margin-right: .5rem;
// align brackets with number
font-feature-settings: "case";
}
} }
.favorite { .list-menu-link,
margin-left: .25rem; li > a {
transition: opacity $transition, color $transition; padding-left: 2rem;
opacity: 1; display: inline-block;
&.is-favorite { .icon {
color: var(--warning); padding-bottom: .25rem;
opacity: 1;
} }
} }
@media(hover: hover) and (pointer: fine) {
.list-menu .favorite {
opacity: 0;
}
.list-menu:hover .favorite,
.favorite.is-favorite {
opacity: 1;
}
}
.list-menu-title {
overflow: hidden;
text-overflow: ellipsis;
width: 100%;
}
.color-bubble {
width: 14px;
height: 14px;
flex-basis: auto;
}
.is-archived {
min-width: 85px;
}
} }
.namespace-title { .menu + .menu {
display: flex; padding-top: math.div($navbar-padding, 2);
align-items: center;
justify-content: space-between;
color: $vikunja-nav-color;
padding: 0 .25rem;
.toggle-lists-icon {
svg {
transition: all $transition;
transform: rotate(90deg);
opacity: 1;
}
&.active svg {
transform: rotate(0deg);
opacity: 0;
}
}
&:hover .toggle-lists-icon svg {
opacity: 1;
}
&:not(.has-menu) .toggle-lists-icon {
padding-right: 1rem;
}
}
.list-setting-spacer {
width: 2.5rem;
flex-shrink: 0;
}
.namespaces-list.loader-container.is-loading {
min-height: calc(100vh - #{$navbar-height + 1.5rem + 1rem + 1.5rem});
} }
</style> </style>

View File

@ -1,63 +0,0 @@
<template>
<multiselect
v-model="selectedNamespaces"
:search-results="foundNamespaces"
:loading="namespaceService.loading"
:multiple="true"
:placeholder="$t('namespace.search')"
label="namespace"
@search="findNamespaces"
/>
</template>
<script setup lang="ts">
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
import Multiselect from '@/components/input/multiselect.vue'
import type {INamespace} from '@/modelTypes/INamespace'
import NamespaceService from '@/services/namespace'
import {includesById} from '@/helpers/utils'
const props = defineProps({
modelValue: {
type: Array as PropType<INamespace[]>,
default: () => [],
},
})
const emit = defineEmits<{
(e: 'update:modelValue', value: INamespace[]): void
}>()
const namespaces = ref<INamespace[]>([])
watchEffect(() => {
namespaces.value = props.modelValue
})
const selectedNamespaces = computed({
get() {
return namespaces.value
},
set: (value) => {
namespaces.value = value
emit('update:modelValue', value)
},
})
const namespaceService = shallowReactive(new NamespaceService())
const foundNamespaces = ref<INamespace[]>([])
async function findNamespaces(query: string) {
if (query === '') {
foundNamespaces.value = []
return
}
const response = await namespaceService.getAll({}, {s: query}) as INamespace[]
// Filter selected items from the results
foundNamespaces.value = response.filter(({id}) => !includesById(namespaces.value, id))
}
</script>

View File

@ -0,0 +1,26 @@
<template>
<BaseButton class="simple-button">
<slot/>
</BaseButton>
</template>
<script lang="ts" setup>
import BaseButton from '@/components/base/BaseButton.vue'
</script>
<style lang="scss" scoped>
.simple-button {
color: var(--text);
padding: .25rem .5rem;
transition: background-color $transition;
border-radius: $radius;
display: block;
margin: .1rem 0;
width: 100%;
text-align: left;
&:hover {
background: var(--white);
}
}
</style>

View File

@ -1,78 +1,15 @@
<template> <template>
<div class="datepicker"> <div class="datepicker">
<BaseButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined"> <SimpleButton @click.stop="toggleDatePopup" class="show" :disabled="disabled || undefined">
{{ date === null ? chooseDateLabel : formatDateShort(date) }} {{ date === null ? chooseDateLabel : formatDateShort(date) }}
</BaseButton> </SimpleButton>
<CustomTransition name="fade"> <CustomTransition name="fade">
<div v-if="show" class="datepicker-popup" ref="datepickerPopup"> <div v-if="show" class="datepicker-popup" ref="datepickerPopup">
<BaseButton <DatepickerInline
v-if="(new Date()).getHours() < 21" v-model="date"
class="datepicker__quick-select-date" @update:model-value="updateData"
@click.stop="setDate('today')"
>
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
<span class="text">
<span>{{ $t('input.datepicker.today') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('tomorrow')"
>
<span class="icon"><icon :icon="['far', 'sun']"/></span>
<span class="text">
<span>{{ $t('input.datepicker.tomorrow') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('nextMonday')"
>
<span class="icon"><icon icon="coffee"/></span>
<span class="text">
<span>{{ $t('input.datepicker.nextMonday') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('thisWeekend')"
>
<span class="icon"><icon icon="cocktail"/></span>
<span class="text">
<span>{{ $t('input.datepicker.thisWeekend') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('laterThisWeek')"
>
<span class="icon"><icon icon="chess-knight"/></span>
<span class="text">
<span>{{ $t('input.datepicker.laterThisWeek') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('nextWeek')"
>
<span class="icon"><icon icon="forward"/></span>
<span class="text">
<span>{{ $t('input.datepicker.nextWeek') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
</span>
</BaseButton>
<flat-pickr
:config="flatPickerConfig"
class="input"
v-model="flatPickrDate"
/> />
<x-button <x-button
@ -89,19 +26,15 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, onMounted, onBeforeUnmount, toRef, watch, computed, type PropType} from 'vue' import {ref, onMounted, onBeforeUnmount, toRef, watch, type PropType} from 'vue'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import BaseButton from '@/components/base/BaseButton.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue' import CustomTransition from '@/components/misc/CustomTransition.vue'
import DatepickerInline from '@/components/input/datepickerInline.vue'
import SimpleButton from '@/components/input/SimpleButton.vue'
import {formatDate, formatDateShort} from '@/helpers/time/formatDate' import {formatDateShort} from '@/helpers/time/formatDate'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside' import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {createDateFromString} from '@/helpers/time/createDateFromString' import {createDateFromString} from '@/helpers/time/createDateFromString'
import {useAuthStore} from '@/stores/auth'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
const props = defineProps({ const props = defineProps({
@ -125,8 +58,6 @@ const props = defineProps({
const emit = defineEmits(['update:modelValue', 'close', 'close-on-change']) const emit = defineEmits(['update:modelValue', 'close', 'close-on-change'])
const {t} = useI18n({useScope: 'global'})
const date = ref<Date | null>() const date = ref<Date | null>()
const show = ref(false) const show = ref(false)
const changed = ref(false) const changed = ref(false)
@ -141,37 +72,6 @@ watch(
{immediate: true}, {immediate: true},
) )
const authStore = useAuthStore()
const weekStart = computed(() => authStore.settings.weekStart)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: weekStart.value,
},
}))
// Since flatpickr dates are strings, we need to convert them to native date objects.
// To make that work, we need a separate variable since flatpickr does not have a change event.
const flatPickrDate = computed({
set(newValue: string | Date) {
date.value = createDateFromString(newValue)
updateData()
},
get() {
if (!date.value) {
return ''
}
return formatDate(date.value, 'yyy-LL-dd H:mm')
},
})
function setDateValue(dateString: string | Date | null) { function setDateValue(dateString: string | Date | null) {
if (dateString === null) { if (dateString === null) {
date.value = null date.value = null
@ -212,29 +112,6 @@ function close() {
} }
}, 200) }, 200)
} }
function setDate(dateString: string) {
if (date.value === null) {
date.value = new Date()
}
const interval = calculateDayInterval(dateString)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0)
newDate.setSeconds(0)
date.value = newDate
flatPickrDate.value = newDate
updateData()
}
function getWeekdayFromStringInterval(dateString: string) {
const interval = calculateDayInterval(dateString)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
return formatDate(newDate, 'E')
}
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -257,42 +134,6 @@ function getWeekdayFromStringInterval(dateString: string) {
} }
} }
.datepicker__quick-select-date {
display: flex;
align-items: center;
padding: 0 .5rem;
width: 100%;
height: 2.25rem;
color: var(--text);
transition: all $transition;
&:first-child {
border-radius: $radius $radius 0 0;
}
&:hover {
background: var(--grey-100);
}
.text {
width: 100%;
font-size: .85rem;
display: flex;
justify-content: space-between;
padding-right: .25rem;
.weekday {
color: var(--text-light);
text-transform: capitalize;
}
}
.icon {
width: 2rem;
text-align: center;
}
}
.datepicker__close-button { .datepicker__close-button {
margin: 1rem; margin: 1rem;
width: calc(100% - 2rem); width: calc(100% - 2rem);

View File

@ -0,0 +1,228 @@
<template>
<BaseButton
v-if="(new Date()).getHours() < 21"
class="datepicker__quick-select-date"
@click.stop="setDate('today')"
>
<span class="icon"><icon :icon="['far', 'calendar-alt']"/></span>
<span class="text">
<span>{{ $t('input.datepicker.today') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('today') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('tomorrow')"
>
<span class="icon"><icon :icon="['far', 'sun']"/></span>
<span class="text">
<span>{{ $t('input.datepicker.tomorrow') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('tomorrow') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('nextMonday')"
>
<span class="icon"><icon icon="coffee"/></span>
<span class="text">
<span>{{ $t('input.datepicker.nextMonday') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('nextMonday') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('thisWeekend')"
>
<span class="icon"><icon icon="cocktail"/></span>
<span class="text">
<span>{{ $t('input.datepicker.thisWeekend') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('thisWeekend') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('laterThisWeek')"
>
<span class="icon"><icon icon="chess-knight"/></span>
<span class="text">
<span>{{ $t('input.datepicker.laterThisWeek') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('laterThisWeek') }}</span>
</span>
</BaseButton>
<BaseButton
class="datepicker__quick-select-date"
@click.stop="setDate('nextWeek')"
>
<span class="icon"><icon icon="forward"/></span>
<span class="text">
<span>{{ $t('input.datepicker.nextWeek') }}</span>
<span class="weekday">{{ getWeekdayFromStringInterval('nextWeek') }}</span>
</span>
</BaseButton>
<div class="flatpickr-container">
<flat-pickr
:config="flatPickerConfig"
v-model="flatPickrDate"
/>
</div>
</template>
<script lang="ts" setup>
import {ref, toRef, watch, computed, type PropType} from 'vue'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
import BaseButton from '@/components/base/BaseButton.vue'
import {formatDate} from '@/helpers/time/formatDate'
import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {createDateFromString} from '@/helpers/time/createDateFromString'
import {useAuthStore} from '@/stores/auth'
import {useI18n} from 'vue-i18n'
const props = defineProps({
modelValue: {
type: [Date, null, String] as PropType<Date | null | string>,
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
default: null,
},
})
const emit = defineEmits(['update:modelValue', 'close-on-change'])
const {t} = useI18n({useScope: 'global'})
const date = ref<Date | null>()
const changed = ref(false)
const modelValue = toRef(props, 'modelValue')
watch(
modelValue,
setDateValue,
{immediate: true},
)
const authStore = useAuthStore()
const weekStart = computed(() => authStore.settings.weekStart)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
dateFormat: 'Y-m-d H:i',
enableTime: true,
time_24hr: true,
inline: true,
locale: {
firstDayOfWeek: weekStart.value,
},
}))
// Since flatpickr dates are strings, we need to convert them to native date objects.
// To make that work, we need a separate variable since flatpickr does not have a change event.
const flatPickrDate = computed({
set(newValue: string | Date | null) {
if (newValue === null) {
date.value = null
return
}
date.value = createDateFromString(newValue)
updateData()
},
get() {
if (!date.value) {
return ''
}
return formatDate(date.value, 'yyy-LL-dd H:mm')
},
})
function setDateValue(dateString: string | Date | null) {
if (dateString === null) {
date.value = null
return
}
date.value = createDateFromString(dateString)
}
function updateData() {
changed.value = true
emit('update:modelValue', date.value)
}
function setDate(dateString: string) {
if (date.value === null) {
date.value = new Date()
}
const interval = calculateDayInterval(dateString)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
newDate.setHours(calculateNearestHours(newDate))
newDate.setMinutes(0)
newDate.setSeconds(0)
date.value = newDate
flatPickrDate.value = newDate
updateData()
}
function getWeekdayFromStringInterval(dateString: string) {
const interval = calculateDayInterval(dateString)
const newDate = new Date()
newDate.setDate(newDate.getDate() + interval)
return formatDate(newDate, 'E')
}
</script>
<style lang="scss" scoped>
.datepicker__quick-select-date {
display: flex;
align-items: center;
padding: 0 .5rem;
width: 100%;
height: 2.25rem;
color: var(--text);
transition: all $transition;
&:first-child {
border-radius: $radius $radius 0 0;
}
&:hover {
background: var(--grey-100);
}
.text {
width: 100%;
font-size: .85rem;
display: flex;
justify-content: space-between;
padding-right: .25rem;
.weekday {
color: var(--text-light);
text-transform: capitalize;
}
}
.icon {
width: 2rem;
text-align: center;
}
}
.flatpickr-container {
:deep(.flatpickr-calendar) {
margin: 0 auto 8px;
box-shadow: none;
}
:deep(.input) {
border: none;
}
}
</style>

View File

@ -4,7 +4,7 @@
<vue-easymde <vue-easymde
:configs="config" :configs="config"
@change="() => bubble()" @change="() => bubbleNow()"
@update:modelValue="handleInput" @update:modelValue="handleInput"
class="content" class="content"
v-if="isEditActive" v-if="isEditActive"
@ -35,7 +35,7 @@
</BaseButton> </BaseButton>
<BaseButton <BaseButton
v-else-if="isEditActive" v-else-if="isEditActive"
@click="toggleEdit" @click="bubbleSaveClick"
class="done-edit"> class="done-edit">
{{ $t('misc.save') }} {{ $t('misc.save') }}
</BaseButton> </BaseButton>
@ -56,7 +56,7 @@
</ul> </ul>
<x-button <x-button
v-else-if="isEditActive" v-else-if="isEditActive"
@click="toggleEdit" @click="bubbleSaveClick"
variant="secondary" variant="secondary"
:shadow="false" :shadow="false"
v-cy="'saveEditor'"> v-cy="'saveEditor'">
@ -84,8 +84,8 @@ import {createRandomID} from '@/helpers/randomId'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue' import ButtonLink from '@/components/misc/ButtonLink.vue'
import type { IAttachment } from '@/modelTypes/IAttachment' import type {IAttachment} from '@/modelTypes/IAttachment'
import type { ITask } from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -115,7 +115,7 @@ const props = defineProps({
default: true, default: true,
}, },
bottomActions: { bottomActions: {
type: Array, type: Array,
default: () => [], default: () => [],
}, },
emptyText: { emptyText: {
@ -134,10 +134,9 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue', 'save'])
const text = ref('') const text = ref('')
const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
const isEditActive = ref(false) const isEditActive = ref(false)
const isPreviewActive = ref(true) const isPreviewActive = ref(true)
@ -148,7 +147,7 @@ const preview = ref('')
const attachmentService = new AttachmentService() const attachmentService = new AttachmentService()
type CacheKey = `${ITask['id']}-${IAttachment['id']}` type CacheKey = `${ITask['id']}-${IAttachment['id']}`
const loadedAttachments = ref<{[key: CacheKey]: string}>({}) const loadedAttachments = ref<{ [key: CacheKey]: string }>({})
const config = ref(createEasyMDEConfig({ const config = ref(createEasyMDEConfig({
placeholder: props.placeholder, placeholder: props.placeholder,
uploadImage: props.uploadEnabled, uploadImage: props.uploadEnabled,
@ -175,7 +174,7 @@ watch(
if (oldVal === '' && text.value === modelValue.value) { if (oldVal === '' && text.value === modelValue.value) {
return return
} }
bubble() bubbleNow()
}, },
) )
@ -208,17 +207,11 @@ function handleInput(val: string) {
} }
text.value = val text.value = val
bubble(1000) bubbleNow()
} }
function bubble(timeout = 500) { function bubbleNow() {
if (changeTimeout.value !== null) { emit('update:modelValue', text.value)
clearTimeout(changeTimeout.value)
}
changeTimeout.value = setTimeout(() => {
emit('update:modelValue', text.value)
}, timeout)
} }
function replaceAt(str: string, index: number, replacement: string) { function replaceAt(str: string, index: number, replacement: string) {
@ -287,24 +280,26 @@ function handleCheckboxClick(e: Event) {
return return
} }
const projectPrefix = text.value.substring(index, index + 1) const projectPrefix = text.value.substring(index, index + 1)
console.debug({index, projectPrefix, checked, text: text.value}) console.debug({index, projectPrefix, checked, text: text.value})
text.value = replaceAt(text.value, index, `${projectPrefix} ${checked ? '[x]' : '[ ]'} `) text.value = replaceAt(text.value, index, `${projectPrefix} ${checked ? '[x]' : '[ ]'} `)
bubble() bubbleNow()
emit('save', text.value)
renderPreview() renderPreview()
} }
function toggleEdit() { function toggleEdit() {
if (isEditActive.value) { isPreviewActive.value = false
isPreviewActive.value = true isEditActive.value = true
isEditActive.value = false }
renderPreview()
bubble(0) // save instantly function bubbleSaveClick() {
} else { isPreviewActive.value = true
isPreviewActive.value = false isEditActive.value = false
isEditActive.value = true renderPreview()
} bubbleNow()
emit('save', text.value)
} }
</script> </script>

View File

@ -32,6 +32,8 @@
@keydown.down.exact.prevent="() => preSelect(0)" @keydown.down.exact.prevent="() => preSelect(0)"
ref="searchInput" ref="searchInput"
@focus="handleFocus" @focus="handleFocus"
:autocomplete="autocompleteEnabled ? undefined : 'off'"
:spellcheck="autocompleteEnabled ? undefined : 'false'"
/> />
</div> </div>
</div> </div>
@ -196,6 +198,13 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: true, default: true,
}, },
/**
* If false, the search input will get the autocomplete="off" attributes attached to it.
*/
autocompleteEnabled: {
type: Boolean,
default: true,
},
}) })
const emit = defineEmits<{ const emit = defineEmits<{

View File

@ -4,6 +4,7 @@ import {
faAngleRight, faAngleRight,
faArchive, faArchive,
faArrowLeft, faArrowLeft,
faArrowUpFromBracket,
faBars, faBars,
faBell, faBell,
faCalendar, faCalendar,
@ -56,7 +57,7 @@ import {
faTimes, faTimes,
faTrashAlt, faTrashAlt,
faUser, faUser,
faUsers, faUsers, faX,
} from '@fortawesome/free-solid-svg-icons' } from '@fortawesome/free-solid-svg-icons'
import { import {
faBellSlash, faBellSlash,
@ -67,10 +68,11 @@ import {
faStar, faStar,
faSun, faSun,
faTimesCircle, faTimesCircle,
faCircleQuestion,
} from '@fortawesome/free-regular-svg-icons' } from '@fortawesome/free-regular-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome' import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
import type { FontAwesomeIcon as FontAwesomeIconFixedTypes } from '@/types/vue-fontawesome' import type {FontAwesomeIcon as FontAwesomeIconFixedTypes} from '@/types/vue-fontawesome'
library.add(faAlignLeft) library.add(faAlignLeft)
library.add(faAngleRight) library.add(faAngleRight)
@ -86,6 +88,7 @@ library.add(faCheckDouble)
library.add(faChessKnight) library.add(faChessKnight)
library.add(faChevronDown) library.add(faChevronDown)
library.add(faCircleInfo) library.add(faCircleInfo)
library.add(faCircleQuestion)
library.add(faClock) library.add(faClock)
library.add(faCloudDownloadAlt) library.add(faCloudDownloadAlt)
library.add(faCloudUploadAlt) library.add(faCloudUploadAlt)
@ -137,6 +140,8 @@ library.add(faTimesCircle)
library.add(faTrashAlt) library.add(faTrashAlt)
library.add(faUser) library.add(faUser)
library.add(faUsers) library.add(faUsers)
library.add(faArrowUpFromBracket)
library.add(faX)
// overwriting the wrong types // overwriting the wrong types
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes

View File

@ -24,12 +24,12 @@
}" }"
> >
<div :class="{'content': hasContent}"> <div :class="{'content': hasContent}">
<slot /> <slot/>
</div> </div>
</div> </div>
<footer v-if="$slots.footer" class="card-footer"> <footer v-if="$slots.footer" class="card-footer">
<slot name="footer" /> <slot name="footer"/>
</footer> </footer>
</div> </div>
</template> </template>
@ -76,22 +76,27 @@ defineEmits(['close'])
<style lang="scss" scoped> <style lang="scss" scoped>
.card { .card {
background-color: var(--white); background-color: var(--white);
border-radius: $radius; border-radius: $radius;
margin-bottom: 1rem; margin-bottom: 1rem;
border: 1px solid var(--card-border-color); border: 1px solid var(--card-border-color);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
@media print {
box-shadow: none;
border: none;
}
} }
.card-header { .card-header {
box-shadow: none; box-shadow: none;
border-bottom: 1px solid var(--card-border-color); border-bottom: 1px solid var(--card-border-color);
border-radius: $radius $radius 0 0; border-radius: $radius $radius 0 0;
} }
.card-footer { .card-footer {
background-color: var(--grey-50); background-color: var(--grey-50);
border-top: 0; border-top: 0;
padding: var(--modal-card-head-padding); padding: var(--modal-card-head-padding);
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -44,8 +44,8 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
combination: 'then', combination: 'then',
}, },
{ {
title: 'keyboardShortcuts.navigation.namespaces', title: 'keyboardShortcuts.navigation.projects',
keys: ['g', 'n'], keys: ['g', 'p'],
combination: 'then', combination: 'then',
}, },
{ {
@ -140,6 +140,18 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
title: 'keyboardShortcuts.task.description', title: 'keyboardShortcuts.task.description',
keys: ['e'], keys: ['e'],
}, },
{
title: 'keyboardShortcuts.task.priority',
keys: ['p'],
},
{
title: 'keyboardShortcuts.task.delete',
keys: ['shift', 'delete'],
},
{
title: 'keyboardShortcuts.task.favorite',
keys: ['s'],
},
], ],
}, },
] ]

View File

@ -1,13 +1,21 @@
<template> <template>
<div class="loader-container is-loading"></div> <div class="loader-container is-loading" :class="{'is-small': variant === 'small'}"></div>
</template> </template>
<script lang="ts"> <script lang="ts">
export default { export default {
inheritAttrs: false, inheritAttrs: true,
} }
</script> </script>
<script lang="ts" setup>
const {
variant = 'default',
} = defineProps<{
variant?: 'default' | 'small'
}>()
</script>
<style scoped lang="scss"> <style scoped lang="scss">
.loader-container { .loader-container {
height: 100%; height: 100%;
@ -20,5 +28,18 @@ export default {
min-height: 50px; min-height: 50px;
min-width: 100px; min-width: 100px;
} }
&.is-small {
min-width: 100%;
height: 150px;
&.is-loading::after {
width: 3rem;
height: 3rem;
top: calc(50% - 1.5rem);
left: calc(50% - 1.5rem);
border-width: 3px;
}
}
} }
</style> </style>

View File

@ -8,7 +8,7 @@
}" }"
ref="popup" ref="popup"
> >
<slot name="content" :isOpen="open"/> <slot name="content" :isOpen="open" :toggle="toggle"/>
</div> </div>
</template> </template>
@ -23,11 +23,14 @@ const props = defineProps({
}, },
}) })
const emit = defineEmits(['close'])
const open = ref(false) const open = ref(false)
const popup = ref<HTMLElement | null>(null) const popup = ref<HTMLElement | null>(null)
function close() { function close() {
open.value = false open.value = false
emit('close')
} }
function toggle() { function toggle() {

View File

@ -47,7 +47,7 @@ import {success} from '@/message'
import type { IconProp } from '@fortawesome/fontawesome-svg-core' import type { IconProp } from '@fortawesome/fontawesome-svg-core'
const props = defineProps({ const props = defineProps({
entity: String, entity: String as ISubscription['entity'],
entityId: Number, entityId: Number,
isButton: { isButton: {
type: Boolean, type: Boolean,
@ -73,12 +73,6 @@ const {t} = useI18n({useScope: 'global'})
const tooltipText = computed(() => { const tooltipText = computed(() => {
if (disabled.value) { if (disabled.value) {
if (props.entity === 'project' && subscriptionEntity.value === 'namespace') {
return t('task.subscription.subscribedProjectThroughParentNamespace')
}
if (props.entity === 'task' && subscriptionEntity.value === 'namespace') {
return t('task.subscription.subscribedTaskThroughParentNamespace')
}
if (props.entity === 'task' && subscriptionEntity.value === 'project') { if (props.entity === 'task' && subscriptionEntity.value === 'project') {
return t('task.subscription.subscribedTaskThroughParentProject') return t('task.subscription.subscribedTaskThroughParentProject')
} }
@ -87,10 +81,6 @@ const tooltipText = computed(() => {
} }
switch (props.entity) { switch (props.entity) {
case 'namespace':
return props.modelValue !== null ?
t('task.subscription.subscribedNamespace') :
t('task.subscription.notSubscribedNamespace')
case 'project': case 'project':
return props.modelValue !== null ? return props.modelValue !== null ?
t('task.subscription.subscribedProject') : t('task.subscription.subscribedProject') :
@ -130,9 +120,6 @@ async function subscribe() {
let message = '' let message = ''
switch (props.entity) { switch (props.entity) {
case 'namespace':
message = t('task.subscription.subscribeSuccessNamespace')
break
case 'project': case 'project':
message = t('task.subscription.subscribeSuccessProject') message = t('task.subscription.subscribeSuccessProject')
break break
@ -153,9 +140,6 @@ async function unsubscribe() {
let message = '' let message = ''
switch (props.entity) { switch (props.entity) {
case 'namespace':
message = t('task.subscription.unsubscribeSuccessNamespace')
break
case 'project': case 'project':
message = t('task.subscription.unsubscribeSuccessProject') message = t('task.subscription.unsubscribeSuccessProject')
break break

View File

@ -48,10 +48,11 @@ const displayName = computed(() => getDisplayName(props.user))
<style lang="scss" scoped> <style lang="scss" scoped>
.user { .user {
margin: .5rem; display: flex;
justify-items: center;
&.is-inline { &.is-inline {
display: inline; display: inline-flex;
} }
} }

View File

@ -1,103 +0,0 @@
<template>
<dropdown>
<template #trigger="triggerProps">
<slot name="trigger" v-bind="triggerProps">
<BaseButton class="dropdown-trigger" @click="triggerProps.toggleOpen">
<icon icon="ellipsis-h" class="icon"/>
</BaseButton>
</slot>
</template>
<template v-if="namespace.isArchived">
<dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
icon="archive"
>
{{ $t('menu.unarchive') }}
</dropdown-item>
</template>
<template v-else>
<dropdown-item
:to="{ name: 'namespace.settings.edit', params: { id: namespace.id } }"
icon="pen"
>
{{ $t('menu.edit') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.share', params: { namespaceId: namespace.id } }"
icon="share-alt"
>
{{ $t('menu.share') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'project.create', params: { namespaceId: namespace.id } }"
icon="plus"
>
{{ $t('menu.newProject') }}
</dropdown-item>
<dropdown-item
:to="{ name: 'namespace.settings.archive', params: { id: namespace.id } }"
icon="archive"
>
{{ $t('menu.archive') }}
</dropdown-item>
<Subscription
class="has-no-shadow"
:is-button="false"
entity="namespace"
:entity-id="namespace.id"
:model-value="subscription"
@update:model-value="setSubscriptionInStore"
type="dropdown"
/>
<dropdown-item
:to="{ name: 'namespace.settings.delete', params: { id: namespace.id } }"
icon="trash-alt"
class="has-text-danger"
>
{{ $t('menu.delete') }}
</dropdown-item>
</template>
</dropdown>
</template>
<script setup lang="ts">
import {ref, onMounted, type PropType} from 'vue'
import BaseButton from '@/components/base/BaseButton.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import Subscription from '@/components/misc/subscription.vue'
import type {INamespace} from '@/modelTypes/INamespace'
import type {ISubscription} from '@/modelTypes/ISubscription'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({
namespace: {
type: Object as PropType<INamespace>,
required: true,
},
})
const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null)
onMounted(() => {
subscription.value = props.namespace.subscription
})
function setSubscriptionInStore(sub: ISubscription) {
subscription.value = sub
namespaceStore.setNamespaceById({
...props.namespace,
subscription: sub,
})
}
</script>
<style scoped lang="scss">
.dropdown-trigger {
padding: 0.5rem;
display: flex;
}
</style>

View File

@ -20,7 +20,8 @@
:user="n.notification.doer" :user="n.notification.doer"
:show-username="false" :show-username="false"
:avatar-size="16" :avatar-size="16"
v-if="n.notification.doer"/> v-if="n.notification.doer"
/>
<div class="detail"> <div class="detail">
<div> <div>
<span class="has-text-weight-bold mr-1" v-if="n.notification.doer"> <span class="has-text-weight-bold mr-1" v-if="n.notification.doer">
@ -145,12 +146,13 @@ function to(n, index) {
.trigger-button { .trigger-button {
width: 100%; width: 100%;
position: relative;
} }
.unread-indicator { .unread-indicator {
position: absolute; position: absolute;
top: .75rem; top: 1rem;
right: 1.15rem; right: .5rem;
width: .75rem; width: .75rem;
height: .75rem; height: .75rem;

View File

@ -1,9 +1,13 @@
<template> <template>
<div <div
:class="{ 'is-loading': projectService.loading, 'is-archived': currentProject.isArchived}" :class="{ 'is-loading': projectService.loading, 'is-archived': currentProject?.isArchived}"
class="loader-container" class="loader-container"
> >
<div class="switch-view-container"> <h1 class="project-title-print">
{{ getProjectTitle(currentProject) }}
</h1>
<div class="switch-view-container d-print-none">
<div class="switch-view"> <div class="switch-view">
<BaseButton <BaseButton
v-shortcut="'g l'" v-shortcut="'g l'"
@ -45,8 +49,8 @@
<slot name="header" /> <slot name="header" />
</div> </div>
<CustomTransition name="fade"> <CustomTransition name="fade">
<Message variant="warning" v-if="currentProject.isArchived" class="mb-4"> <Message variant="warning" v-if="currentProject?.isArchived" class="mb-4">
{{ $t('project.archived') }} {{ $t('project.archivedMessage') }}
</Message> </Message>
</CustomTransition> </CustomTransition>
@ -98,7 +102,7 @@ const currentProject = computed(() => {
maxRight: null, maxRight: null,
} : baseStore.currentProject } : baseStore.currentProject
}) })
useTitle(() => currentProject.value.id ? getProjectTitle(currentProject.value) : '') useTitle(() => currentProject.value?.id ? getProjectTitle(currentProject.value) : '')
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before. // watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
// This resulted in loading and setting the project multiple times, even when navigating away from it. // This resulted in loading and setting the project multiple times, even when navigating away from it.
@ -118,7 +122,7 @@ watch(
( (
projectIdToLoad === loadedProjectId.value || projectIdToLoad === loadedProjectId.value ||
typeof projectIdToLoad === 'undefined' || typeof projectIdToLoad === 'undefined' ||
projectIdToLoad === currentProject.value.id projectIdToLoad === currentProject.value?.id
) )
&& typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null && typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null
) { ) {
@ -130,8 +134,8 @@ watch(
// Set the current project to the one we're about to load so that the title is already shown at the top // Set the current project to the one we're about to load so that the title is already shown at the top
loadedProjectId.value = 0 loadedProjectId.value = 0
const projectFromStore = projectStore.getProjectById(projectData.id) const projectFromStore = projectStore.projects[projectData.id]
if (projectFromStore !== null) { if (projectFromStore) {
baseStore.setBackground(null) baseStore.setBackground(null)
baseStore.setBlurHash(null) baseStore.setBlurHash(null)
baseStore.handleSetCurrentProject({project: projectFromStore}) baseStore.handleSetCurrentProject({project: projectFromStore})
@ -197,4 +201,15 @@ watch(
.is-archived .notification.is-warning { .is-archived .notification.is-warning {
margin-bottom: 1rem; margin-bottom: 1rem;
} }
.project-title-print {
display: none;
font-size: 1.75rem;
text-align: center;
margin-bottom: .5rem;
@media print {
display: block;
}
}
</style> </style>

View File

@ -15,9 +15,14 @@
:class="{'is-visible': background}" :class="{'is-visible': background}"
:style="{'background-image': background !== null ? `url(${background})` : undefined}" :style="{'background-image': background !== null ? `url(${background})` : undefined}"
/> />
<span v-if="project.isArchived" class="is-archived" >{{ $t('namespace.archived') }}</span> <span v-if="project.isArchived" class="is-archived" >{{ $t('project.archived') }}</span>
<div class="project-title" aria-hidden="true">{{ project.title }}</div> <div class="project-title" aria-hidden="true">
<span v-if="project.id < -1" class="saved-filter-icon icon">
<icon icon="filter"/>
</span>
{{ project.title }}
</div>
<BaseButton <BaseButton
class="project-button" class="project-button"
:aria-label="project.title" :aria-label="project.title"
@ -179,4 +184,9 @@ const projectStore = useProjectStore()
opacity: 1; opacity: 1;
} }
} }
.saved-filter-icon {
color: var(--grey-300);
font-size: .75em;
}
</style> </style>

View File

@ -147,6 +147,7 @@
<label class="label">{{ $t('task.attributes.labels') }}</label> <label class="label">{{ $t('task.attributes.labels') }}</label>
<div class="control labels-list"> <div class="control labels-list">
<edit-labels <edit-labels
:creatable="false"
v-model="entities.labels" v-model="entities.labels"
@update:model-value="changeLabelFilter" @update:model-value="changeLabelFilter"
/> />
@ -165,16 +166,6 @@
/> />
</div> </div>
</div> </div>
<div class="field">
<label class="label">{{ $t('namespace.namespaces') }}</label>
<div class="control">
<SelectNamespace
v-model="entities.namespace"
@select="changeMultiselectFilter('namespace', 'namespace')"
@remove="changeMultiselectFilter('namespace', 'namespace')"
/>
</div>
</div>
</template> </template>
</card> </card>
</template> </template>
@ -189,7 +180,6 @@ import {camelCase} from 'camel-case'
import type {ILabel} from '@/modelTypes/ILabel' import type {ILabel} from '@/modelTypes/ILabel'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import type {INamespace} from '@/modelTypes/INamespace'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
@ -201,7 +191,6 @@ import EditLabels from '@/components/tasks/partials/editLabels.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue' import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import SelectUser from '@/components/input/SelectUser.vue' import SelectUser from '@/components/input/SelectUser.vue'
import SelectProject from '@/components/input/SelectProject.vue' import SelectProject from '@/components/input/SelectProject.vue'
import SelectNamespace from '@/components/input/SelectNamespace.vue'
import {parseDateOrString} from '@/helpers/time/parseDateOrString' import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {dateIsValid, formatISO} from '@/helpers/time/formatDate' import {dateIsValid, formatISO} from '@/helpers/time/formatDate'
@ -209,7 +198,6 @@ import {objectToSnakeCase} from '@/helpers/case'
import UserService from '@/services/user' import UserService from '@/services/user'
import ProjectService from '@/services/project' import ProjectService from '@/services/project'
import NamespaceService from '@/services/namespace'
// FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS // FIXME: do not use this here for now. instead create new version from DEFAULT_PARAMS
import {getDefaultParams} from '@/composables/useTaskList' import {getDefaultParams} from '@/composables/useTaskList'
@ -240,7 +228,6 @@ const DEFAULT_FILTERS = {
assignees: '', assignees: '',
labels: '', labels: '',
project_id: '', project_id: '',
namespace: '',
} as const } as const
const props = defineProps({ const props = defineProps({
@ -265,23 +252,20 @@ const filters = ref({...DEFAULT_FILTERS})
const services = { const services = {
users: shallowReactive(new UserService()), users: shallowReactive(new UserService()),
projects: shallowReactive(new ProjectService()), projects: shallowReactive(new ProjectService()),
namespace: shallowReactive(new NamespaceService()),
} }
interface Entities { interface Entities {
users: IUser[] users: IUser[]
labels: ILabel[] labels: ILabel[]
projects: IProject[] projects: IProject[]
namespace: INamespace[]
} }
type EntityType = 'users' | 'labels' | 'projects' | 'namespace' type EntityType = 'users' | 'labels' | 'projects'
const entities: Entities = reactive({ const entities: Entities = reactive({
users: [], users: [],
labels: [], labels: [],
projects: [], projects: [],
namespace: [],
}) })
onMounted(() => { onMounted(() => {
@ -328,7 +312,6 @@ function prepareFilters() {
prepareDate('reminders') prepareDate('reminders')
prepareRelatedObjectFilter('users', 'assignees') prepareRelatedObjectFilter('users', 'assignees')
prepareRelatedObjectFilter('projects', 'project_id') prepareRelatedObjectFilter('projects', 'project_id')
prepareRelatedObjectFilter('namespace')
prepareSingleValue('labels') prepareSingleValue('labels')

View File

@ -72,6 +72,13 @@
@update:model-value="setSubscriptionInStore" @update:model-value="setSubscriptionInStore"
type="dropdown" type="dropdown"
/> />
<dropdown-item
v-if="level < 2"
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
icon="layer-group"
>
{{ $t('menu.createProject') }}
</dropdown-item>
<dropdown-item <dropdown-item
:to="{ name: 'project.settings.delete', params: { projectId: project.id } }" :to="{ name: 'project.settings.delete', params: { projectId: project.id } }"
icon="trash-alt" icon="trash-alt"
@ -96,17 +103,18 @@ import type {ISubscription} from '@/modelTypes/ISubscription'
import {isSavedFilter} from '@/services/savedFilter' import {isSavedFilter} from '@/services/savedFilter'
import {useConfigStore} from '@/stores/config' import {useConfigStore} from '@/stores/config'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
const props = defineProps({ const props = defineProps({
project: { project: {
type: Object as PropType<IProject>, type: Object as PropType<IProject>,
required: true, required: true,
}, },
level: {
type: Number,
},
}) })
const projectStore = useProjectStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore()
const subscription = ref<ISubscription | null>(null) const subscription = ref<ISubscription | null>(null)
watchEffect(() => { watchEffect(() => {
subscription.value = props.project.subscription ?? null subscription.value = props.project.subscription ?? null
@ -122,6 +130,5 @@ function setSubscriptionInStore(sub: ISubscription) {
subscription: sub, subscription: sub,
} }
projectStore.setProject(updatedProject) projectStore.setProject(updatedProject)
namespaceStore.setProjectInNamespaceById(updatedProject)
} }
</script> </script>

View File

@ -61,7 +61,6 @@ import {useRouter} from 'vue-router'
import TaskService from '@/services/task' import TaskService from '@/services/task'
import TeamService from '@/services/team' import TeamService from '@/services/team'
import NamespaceModel from '@/models/namespace'
import TeamModel from '@/models/team' import TeamModel from '@/models/team'
import ProjectModel from '@/models/project' import ProjectModel from '@/models/project'
@ -70,18 +69,16 @@ import QuickAddMagic from '@/components/tasks/partials/quick-add-magic.vue'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
import {useLabelStore} from '@/stores/labels' import {useLabelStore} from '@/stores/labels'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {useAuthStore} from '@/stores/auth'
import {getHistory} from '@/modules/projectHistory' import {getHistory} from '@/modules/projectHistory'
import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText' import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {success} from '@/message' import {success} from '@/message'
import type {ITeam} from '@/modelTypes/ITeam' import type {ITeam} from '@/modelTypes/ITeam'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import type {INamespace} from '@/modelTypes/INamespace'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -89,9 +86,9 @@ const router = useRouter()
const baseStore = useBaseStore() const baseStore = useBaseStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore()
const labelStore = useLabelStore() const labelStore = useLabelStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const authStore = useAuthStore()
type DoAction<Type = any> = { type: ACTION_TYPE } & Type type DoAction<Type = any> = { type: ACTION_TYPE } & Type
@ -105,7 +102,6 @@ enum ACTION_TYPE {
enum COMMAND_TYPE { enum COMMAND_TYPE {
NEW_TASK = 'newTask', NEW_TASK = 'newTask',
NEW_PROJECT = 'newProject', NEW_PROJECT = 'newProject',
NEW_NAMESPACE = 'newNamespace',
NEW_TEAM = 'newTeam', NEW_TEAM = 'newTeam',
} }
@ -147,24 +143,15 @@ const foundProjects = computed(() => {
return [] return []
} }
const ncache: { [id: ProjectModel['id']]: INamespace } = {}
const history = getHistory() const history = getHistory()
const allProjects = [ const allProjects = [
...new Set([ ...new Set([
...history.map((l) => projectStore.getProjectById(l.id)), ...history.map((l) => projectStore.projects[l.id]),
...projectStore.searchProject(project), ...projectStore.searchProject(project),
]), ]),
] ]
return allProjects.filter((l) => { return allProjects.filter(l => Boolean(l))
if (typeof l === 'undefined' || l === null) {
return false
}
if (typeof ncache[l.namespaceId] === 'undefined') {
ncache[l.namespaceId] = namespaceStore.getNamespaceById(l.namespaceId)
}
return !ncache[l.namespaceId].isArchived
})
}) })
// FIXME: use fuzzysearch // FIXME: use fuzzysearch
@ -205,7 +192,6 @@ const results = computed<Result[]>(() => {
const loading = computed(() => const loading = computed(() =>
taskService.loading || taskService.loading ||
namespaceStore.isLoading ||
projectStore.isLoading || projectStore.isLoading ||
teamService.loading, teamService.loading,
) )
@ -230,12 +216,6 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
placeholder: t('quickActions.newProject'), placeholder: t('quickActions.newProject'),
action: newProject, action: newProject,
}, },
newNamespace: {
type: COMMAND_TYPE.NEW_NAMESPACE,
title: t('quickActions.cmds.newNamespace'),
placeholder: t('quickActions.newNamespace'),
action: newNamespace,
},
newTeam: { newTeam: {
type: COMMAND_TYPE.NEW_TEAM, type: COMMAND_TYPE.NEW_TEAM,
title: t('quickActions.cmds.newTeam'), title: t('quickActions.cmds.newTeam'),
@ -252,7 +232,6 @@ const currentProject = computed(() => Object.keys(baseStore.currentProject).leng
) )
const hintText = computed(() => { const hintText = computed(() => {
let namespace
if (selectedCmd.value !== null && currentProject.value !== null) { if (selectedCmd.value !== null && currentProject.value !== null) {
switch (selectedCmd.value.type) { switch (selectedCmd.value.type) {
case COMMAND_TYPE.NEW_TASK: case COMMAND_TYPE.NEW_TASK:
@ -260,16 +239,11 @@ const hintText = computed(() => {
title: currentProject.value.title, title: currentProject.value.title,
}) })
case COMMAND_TYPE.NEW_PROJECT: case COMMAND_TYPE.NEW_PROJECT:
namespace = namespaceStore.getNamespaceById( return t('quickActions.createProject')
currentProject.value.namespaceId,
)
return t('quickActions.createProject', {
title: namespace?.title,
})
} }
} }
const prefixes = const prefixes =
PREFIXES[getQuickAddMagicMode()] ?? PREFIXES[PrefixMode.Default] PREFIXES[authStore.settings.frontendSettings.quickAddMagicMode] ?? PREFIXES[PrefixMode.Default]
return t('quickActions.hint', prefixes) return t('quickActions.hint', prefixes)
}) })
@ -278,11 +252,11 @@ const availableCmds = computed(() => {
if (currentProject.value !== null) { if (currentProject.value !== null) {
cmds.push(commands.value.newTask, commands.value.newProject) cmds.push(commands.value.newTask, commands.value.newProject)
} }
cmds.push(commands.value.newNamespace, commands.value.newTeam) cmds.push(commands.value.newTeam)
return cmds return cmds
}) })
const parsedQuery = computed(() => parseTaskText(query.value, getQuickAddMagicMode())) const parsedQuery = computed(() => parseTaskText(query.value, authStore.settings.frontendSettings.quickAddMagicMode))
const searchMode = computed(() => { const searchMode = computed(() => {
if (query.value === '') { if (query.value === '') {
@ -396,7 +370,7 @@ function searchTasks() {
const r = await taskService.getAll({}, params) as DoAction<ITask>[] const r = await taskService.getAll({}, params) as DoAction<ITask>[]
foundTasks.value = r.map((t) => { foundTasks.value = r.map((t) => {
t.type = ACTION_TYPE.TASK t.type = ACTION_TYPE.TASK
const project = projectStore.getProjectById(t.projectId) const project = projectStore.projects[t.projectId]
if (project !== null) { if (project !== null) {
t.title = `${t.title} (${project.title})` t.title = `${t.title} (${project.title})`
} }
@ -504,21 +478,10 @@ async function newProject() {
if (currentProject.value === null) { if (currentProject.value === null) {
return return
} }
const newProject = await projectStore.createProject(new ProjectModel({ await projectStore.createProject(new ProjectModel({
title: query.value, title: query.value,
namespaceId: currentProject.value.namespaceId,
})) }))
success({ message: t('project.create.createdSuccess')}) success({ message: t('project.create.createdSuccess')})
await router.push({
name: 'project.index',
params: { projectId: newProject.id },
})
}
async function newNamespace() {
const newNamespace = new NamespaceModel({ title: query.value })
await namespaceStore.createNamespace(newNamespace)
success({ message: t('namespace.create.success') })
} }
async function newTeam() { async function newTeam() {

View File

@ -139,10 +139,6 @@ import {ref, reactive, computed, shallowReactive, type Ref} from 'vue'
import type {PropType} from 'vue' import type {PropType} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import UserNamespaceService from '@/services/userNamespace'
import UserNamespaceModel from '@/models/userNamespace'
import type {IUserNamespace} from '@/modelTypes/IUserNamespace'
import UserProjectService from '@/services/userProject' import UserProjectService from '@/services/userProject'
import UserProjectModel from '@/models/userProject' import UserProjectModel from '@/models/userProject'
import type {IUserProject} from '@/modelTypes/IUserProject' import type {IUserProject} from '@/modelTypes/IUserProject'
@ -151,10 +147,6 @@ import UserService from '@/services/user'
import UserModel, { getDisplayName } from '@/models/user' import UserModel, { getDisplayName } from '@/models/user'
import type {IUser} from '@/modelTypes/IUser' import type {IUser} from '@/modelTypes/IUser'
import TeamNamespaceService from '@/services/teamNamespace'
import TeamNamespaceModel from '@/models/teamNamespace'
import type { ITeamNamespace } from '@/modelTypes/ITeamNamespace'
import TeamProjectService from '@/services/teamProject' import TeamProjectService from '@/services/teamProject'
import TeamProjectModel from '@/models/teamProject' import TeamProjectModel from '@/models/teamProject'
import type { ITeamProject } from '@/modelTypes/ITeamProject' import type { ITeamProject } from '@/modelTypes/ITeamProject'
@ -170,13 +162,15 @@ import Nothing from '@/components/misc/nothing.vue'
import {success} from '@/message' import {success} from '@/message'
import {useAuthStore} from '@/stores/auth' import {useAuthStore} from '@/stores/auth'
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
const props = defineProps({ const props = defineProps({
type: { type: {
type: String as PropType<'project' | 'namespace'>, type: String as PropType<'project'>,
default: '', default: '',
}, },
shareType: { shareType: {
type: String as PropType<'user' | 'team' | 'namespace'>, type: String as PropType<'user' | 'team'>,
default: '', default: '',
}, },
id: { id: {
@ -191,9 +185,9 @@ const props = defineProps({
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
// This user service is either a userNamespaceService or a userProjectService, depending on the type we are using // This user service is a userProjectService, depending on the type we are using
let stuffService: UserNamespaceService | UserProjectService | TeamProjectService | TeamNamespaceService let stuffService: UserProjectService | TeamProjectService
let stuffModel: IUserNamespace | IUserProject | ITeamProject | ITeamNamespace let stuffModel: IUserProject | ITeamProject
let searchService: UserService | TeamService let searchService: UserService | TeamService
let sharable: Ref<IUser | ITeam> let sharable: Ref<IUser | ITeam>
@ -231,10 +225,6 @@ const sharableName = computed(() => {
return t('project.list.title') return t('project.list.title')
} }
if (props.shareType === 'namespace') {
return t('namespace.namespace')
}
return '' return ''
}) })
@ -247,11 +237,6 @@ if (props.shareType === 'user') {
if (props.type === 'project') { if (props.type === 'project') {
stuffService = shallowReactive(new UserProjectService()) stuffService = shallowReactive(new UserProjectService())
stuffModel = reactive(new UserProjectModel({projectId: props.id})) stuffModel = reactive(new UserProjectModel({projectId: props.id}))
} else if (props.type === 'namespace') {
stuffService = shallowReactive(new UserNamespaceService())
stuffModel = reactive(new UserNamespaceModel({
namespaceId: props.id,
}))
} else { } else {
throw new Error('Unknown type: ' + props.type) throw new Error('Unknown type: ' + props.type)
} }
@ -264,11 +249,6 @@ if (props.shareType === 'user') {
if (props.type === 'project') { if (props.type === 'project') {
stuffService = shallowReactive(new TeamProjectService()) stuffService = shallowReactive(new TeamProjectService())
stuffModel = reactive(new TeamProjectModel({projectId: props.id})) stuffModel = reactive(new TeamProjectModel({projectId: props.id}))
} else if (props.type === 'namespace') {
stuffService = shallowReactive(new TeamNamespaceService())
stuffModel = reactive(new TeamNamespaceModel({
namespaceId: props.id,
}))
} else { } else {
throw new Error('Unknown type: ' + props.type) throw new Error('Unknown type: ' + props.type)
} }

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="task-add" ref="taskAdd"> <div class="task-add" ref="taskAdd">
<div class="add-task__field field is-grouped"> <div class="add-task__field field is-grouped">
<p class="control has-icons-left is-expanded"> <p class="control has-icons-left has-icons-right is-expanded">
<textarea <textarea
class="add-task-textarea input" class="add-task-textarea input"
:class="{'textarea-empty': newTaskTitle === ''}" :class="{'textarea-empty': newTaskTitle === ''}"
@ -16,6 +16,7 @@
<span class="icon is-small is-left"> <span class="icon is-small is-left">
<icon icon="tasks"/> <icon icon="tasks"/>
</span> </span>
<quick-add-magic :highlight-hint-icon="taskAddHovered"/>
</p> </p>
<p class="control"> <p class="control">
<x-button <x-button
@ -32,11 +33,10 @@
</x-button> </x-button>
</p> </p>
</div> </div>
<Expandable :open="errorMessage !== '' || taskAddFocused || taskAddHovered && debouncedTaskAddHovered"> <Expandable :open="errorMessage !== ''">
<p class="pt-3 mt-0 help is-danger" v-if="errorMessage !== ''"> <p class="pt-3 mt-0 help is-danger" v-if="errorMessage !== ''">
{{ errorMessage }} {{ errorMessage }}
</p> </p>
<quick-add-magic v-else class="quick-add-magic" />
</Expandable> </Expandable>
</div> </div>
</template> </template>
@ -44,7 +44,7 @@
<script setup lang="ts"> <script setup lang="ts">
import {computed, ref} from 'vue' import {computed, ref} from 'vue'
import {useI18n} from 'vue-i18n' import {useI18n} from 'vue-i18n'
import {refDebounced, useElementHover, useFocusWithin} from '@vueuse/core' import {useElementHover} from '@vueuse/core'
import {RELATION_KIND} from '@/types/IRelationKind' import {RELATION_KIND} from '@/types/IRelationKind'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
@ -77,8 +77,6 @@ const {t} = useI18n({useScope: 'global'})
const authStore = useAuthStore() const authStore = useAuthStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const taskAdd = ref<HTMLTextAreaElement | null>(null)
// enable only if we don't have a modal // enable only if we don't have a modal
// onStartTyping(() => { // onStartTyping(() => {
// if (newTaskInput.value === null || document.activeElement === newTaskInput.value) { // if (newTaskInput.value === null || document.activeElement === newTaskInput.value) {
@ -87,10 +85,8 @@ const taskAdd = ref<HTMLTextAreaElement | null>(null)
// newTaskInput.value.focus() // newTaskInput.value.focus()
// }) // })
const { focused: taskAddFocused } = useFocusWithin(taskAdd) const taskAdd = ref<HTMLTextAreaElement | null>(null)
const taskAddHovered = useElementHover(taskAdd) const taskAddHovered = useElementHover(taskAdd)
const debouncedTaskAddHovered = refDebounced(taskAddHovered, 500)
const errorMessage = ref('') const errorMessage = ref('')
@ -120,12 +116,12 @@ async function addTask() {
// This allows us to find the tasks with the title they had before being parsed // This allows us to find the tasks with the title they had before being parsed
// by quick add magic. // by quick add magic.
const createdTasks: { [key: ITask['title']]: ITask } = {} const createdTasks: { [key: ITask['title']]: ITask } = {}
const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value) const tasksToCreate = parseSubtasksViaIndention(newTaskTitle.value, authStore.settings.frontendSettings.quickAddMagicMode)
// We ensure all labels exist prior to passing them down to the create task method // We ensure all labels exist prior to passing them down to the create task method
// In the store it will only ever see one task at a time so there's no way to reliably // In the store it will only ever see one task at a time so there's no way to reliably
// check if a new label was created before (because everything happens async). // check if a new label was created before (because everything happens async).
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title) ?? []) const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title, authStore.settings.frontendSettings.quickAddMagicMode) ?? [])
await taskStore.ensureLabelsExist(allLabels.flat()) await taskStore.ensureLabelsExist(allLabels.flat())
const newTasks = tasksToCreate.map(async ({title, project}) => { const newTasks = tasksToCreate.map(async ({title, project}) => {
@ -244,7 +240,14 @@ defineExpose({
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.quick-add-magic { .control.has-icons-left .icon,
padding-top: 0.75rem; .control.has-icons-right .icon {
transition: all $transition;
}
</style>
<style>
button.show-helper-text {
right: 0;
} }
</style> </style>

View File

@ -74,9 +74,13 @@
@update:model-value=" @update:model-value="
() => { () => {
toggleEdit(c) toggleEdit(c)
editComment() editCommentWithDelay()
} }
" "
@save="() => {
toggleEdit(c)
editComment()
}"
:bottom-actions="actions[c.id]" :bottom-actions="actions[c.id]"
:show-save="true" :show-save="true"
/> />
@ -279,10 +283,26 @@ function toggleDelete(commentId: ITaskComment['id']) {
commentToDelete.id = commentId commentToDelete.id = commentId
} }
const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
async function editCommentWithDelay() {
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
changeTimeout.value = setTimeout(async () => {
await editComment()
}, 5000)
}
async function editComment() { async function editComment() {
if (commentEdit.comment === '') { if (commentEdit.comment === '') {
return return
} }
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
saving.value = commentEdit.id saving.value = commentEdit.id

View File

@ -25,7 +25,8 @@
:show-save="true" :show-save="true"
edit-shortcut="e" edit-shortcut="e"
v-model="task.description" v-model="task.description"
@update:model-value="save" @update:model-value="saveWithDelay"
@save="save"
/> />
</div> </div>
</template> </template>
@ -40,7 +41,6 @@ import type {ITask} from '@/modelTypes/ITask'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import TaskModel from '@/models/task' import TaskModel from '@/models/task'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
type: Object as PropType<ITask>, type: Object as PropType<ITask>,
@ -74,7 +74,23 @@ watch(
{immediate: true}, {immediate: true},
) )
const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
async function saveWithDelay() {
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
changeTimeout.value = setTimeout(async () => {
await save()
}, 5000)
}
async function save() { async function save() {
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
saving.value = true saving.value = true
try { try {

View File

@ -9,15 +9,19 @@
label="name" label="name"
:select-placeholder="$t('task.assignee.selectPlaceholder')" :select-placeholder="$t('task.assignee.selectPlaceholder')"
v-model="assignees" v-model="assignees"
:autocomplete-enabled="false"
> >
<template #tag="{item: user}"> <template #tag="{item: user}">
<span class="assignee"> <span class="assignee">
<user :avatar-size="32" :show-username="false" :user="user"/> <user :avatar-size="32" :show-username="false" :user="user" class="m-2"/>
<BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled"> <BaseButton @click="removeAssignee(user)" class="remove-assignee" v-if="!disabled">
<icon icon="times"/> <icon icon="times"/>
</BaseButton> </BaseButton>
</span> </span>
</template> </template>
<template #searchResult="{option: user}">
<user :avatar-size="24" :show-username="true" :user="user"/>
</template>
</Multiselect> </Multiselect>
</template> </template>
@ -104,11 +108,6 @@ async function removeAssignee(user: IUser) {
} }
async function findUser(query: string) { async function findUser(query: string) {
if (query === '') {
foundUsers.value = []
return
}
const response = await projectUserService.getAll({projectId: props.projectId}, {s: query}) as IUser[] const response = await projectUserService.getAll({projectId: props.projectId}, {s: query}) as IUser[]
// Filter the results to not include users who are already assigned // Filter the results to not include users who are already assigned

View File

@ -7,7 +7,7 @@
:search-results="foundLabels" :search-results="foundLabels"
@select="addLabel" @select="addLabel"
label="title" label="title"
:creatable="true" :creatable="creatable"
@create="createAndAddLabel" @create="createAndAddLabel"
:create-placeholder="$t('task.label.createPlaceholder')" :create-placeholder="$t('task.label.createPlaceholder')"
v-model="labels" v-model="labels"
@ -65,6 +65,10 @@ const props = defineProps({
disabled: { disabled: {
default: false, default: false,
}, },
creatable: {
type: Boolean,
default: true,
},
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])

View File

@ -11,8 +11,10 @@
@search="findProjects" @search="findProjects"
> >
<template #searchResult="{option}"> <template #searchResult="{option}">
<span class="project-namespace-title search-result">{{ namespace((option as IProject).namespaceId) }} ></span> <span class="has-text-grey" v-if="projectStore.getAncestors(option).length > 1">
{{ (option as IProject).title }} {{ projectStore.getAncestors(option).filter(p => p.id !== option.id).map(p => getProjectTitle(p)).join(' &gt; ') }} &gt;
</span>
{{ getProjectTitle(option) }}
</template> </template>
</Multiselect> </Multiselect>
</template> </template>
@ -20,13 +22,11 @@
<script lang="ts" setup> <script lang="ts" setup>
import {reactive, ref, watch} from 'vue' import {reactive, ref, watch} from 'vue'
import type {PropType} from 'vue' import type {PropType} from 'vue'
import {useI18n} from 'vue-i18n'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
import type {INamespace} from '@/modelTypes/INamespace'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces' import {getProjectTitle} from '@/helpers/getProjectTitle'
import ProjectModel from '@/models/project' import ProjectModel from '@/models/project'
@ -40,8 +40,6 @@ const props = defineProps({
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const {t} = useI18n({useScope: 'global'})
const project: IProject = reactive(new ProjectModel()) const project: IProject = reactive(new ProjectModel())
watch( watch(
@ -54,7 +52,6 @@ watch(
) )
const projectStore = useProjectStore() const projectStore = useProjectStore()
const namespaceStore = useNamespaceStore()
const foundProjects = ref<IProject[]>([]) const foundProjects = ref<IProject[]>([])
function findProjects(query: string) { function findProjects(query: string) {
if (query === '') { if (query === '') {
@ -70,17 +67,4 @@ function select(l: IProject | null) {
Object.assign(project, l) Object.assign(project, l)
emit('update:modelValue', project) emit('update:modelValue', project)
} }
function namespace(namespaceId: INamespace['id']) {
const namespace = namespaceStore.getNamespaceById(namespaceId)
return namespace !== null
? namespace.title
: t('project.shared')
}
</script> </script>
<style lang="scss" scoped>
.project-namespace-title {
color: var(--grey-500);
}
</style>

View File

@ -1,9 +1,14 @@
<template> <template>
<div v-if="mode !== 'disabled' && prefixes !== undefined"> <template v-if="mode !== 'disabled' && prefixes !== undefined">
<p class="help has-text-grey"> <BaseButton
{{ $t('task.quickAddMagic.hint') }}. @click="() => visible = true"
<ButtonLink @click="() => visible = true">{{ $t('task.quickAddMagic.what') }}</ButtonLink> class="icon is-small show-helper-text"
</p> v-tooltip="$t('task.quickAddMagic.hint')"
:aria-label="$t('task.quickAddMagic.hint')"
:class="{'is-highlighted': highlightHintIcon}"
>
<icon :icon="['far', 'circle-question']"/>
</BaseButton>
<modal <modal
:enabled="visible" :enabled="visible"
@close="() => visible = false" @close="() => visible = false"
@ -69,7 +74,7 @@
<li>17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})</li> <li>17th ({{ $t('task.quickAddMagic.dateNth', {day: '17'}) }})</li>
</ul> </ul>
<p>{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}</p> <p>{{ $t('task.quickAddMagic.dateTime', {time: 'at 17:00', timePM: '5pm'}) }}</p>
<h3>{{ $t('task.quickAddMagic.repeats') }}</h3> <h3>{{ $t('task.quickAddMagic.repeats') }}</h3>
<p>{{ $t('task.quickAddMagic.repeatsDescription', {suffix: 'every {amount} {type}'}) }}</p> <p>{{ $t('task.quickAddMagic.repeatsDescription', {suffix: 'every {amount} {type}'}) }}</p>
<p>{{ $t('misc.forExample') }}</p> <p>{{ $t('misc.forExample') }}</p>
@ -86,19 +91,36 @@
</ul> </ul>
</card> </card>
</modal> </modal>
</div> </template>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {ref, computed} from 'vue' import {ref, computed} from 'vue'
import ButtonLink from '@/components/misc/ButtonLink.vue' import BaseButton from '@/components/base/BaseButton.vue'
import {getQuickAddMagicMode} from '@/helpers/quickAddMagicMode'
import {PREFIXES} from '@/modules/parseTaskText' import {PREFIXES} from '@/modules/parseTaskText'
import {useAuthStore} from '@/stores/auth'
const authStore = useAuthStore()
const visible = ref(false) const visible = ref(false)
const mode = ref(getQuickAddMagicMode()) const mode = computed(() => authStore.settings.frontendSettings.quickAddMagicMode)
defineProps<{
highlightHintIcon: boolean,
}>()
const prefixes = computed(() => PREFIXES[mode.value]) const prefixes = computed(() => PREFIXES[mode.value])
</script> </script>
<style lang="scss" scoped>
.show-helper-text {
// Bulma adds pointer-events: none to the icon so we need to override it back here.
pointer-events: auto !important;
}
.is-highlighted {
color: inherit !important;
}
</style>

View File

@ -46,11 +46,6 @@
class="different-project" class="different-project"
v-if="task.projectId !== projectId" v-if="task.projectId !== projectId"
> >
<span
v-if="task.differentNamespace !== null"
v-tooltip="$t('task.relation.differentNamespace')">
{{ task.differentNamespace }} >
</span>
<span <span
v-if="task.differentProject !== null" v-if="task.differentProject !== null"
v-tooltip="$t('task.relation.differentProject')"> v-tooltip="$t('task.relation.differentProject')">
@ -101,11 +96,6 @@
class="different-project" class="different-project"
v-if="t.projectId !== projectId" v-if="t.projectId !== projectId"
> >
<span
v-if="t.differentNamespace !== null"
v-tooltip="$t('task.relation.differentNamespace')">
{{ t.differentNamespace }} >
</span>
<span <span
v-if="t.differentProject !== null" v-if="t.differentProject !== null"
v-tooltip="$t('task.relation.differentProject')"> v-tooltip="$t('task.relation.differentProject')">
@ -168,10 +158,9 @@ import BaseButton from '@/components/base/BaseButton.vue'
import Multiselect from '@/components/input/multiselect.vue' import Multiselect from '@/components/input/multiselect.vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue' import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import {useNamespaceStore} from '@/stores/namespaces'
import {error, success} from '@/message' import {error, success} from '@/message'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
import {useProjectStore} from '@/stores/projects'
const props = defineProps({ const props = defineProps({
taskId: { taskId: {
@ -196,7 +185,7 @@ const props = defineProps({
}) })
const taskStore = useTaskStore() const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore() const projectStore = useProjectStore()
const route = useRoute() const route = useRoute()
const {t} = useI18n({useScope: 'global'}) const {t} = useI18n({useScope: 'global'})
@ -230,26 +219,15 @@ async function findTasks(newQuery: string) {
foundTasks.value = await taskService.getAll({}, {s: newQuery}) foundTasks.value = await taskService.getAll({}, {s: newQuery})
} }
const getProjectAndNamespaceById = (projectId: number) => namespaceStore.getProjectAndNamespaceById(projectId, true)
const namespace = computed(() => getProjectAndNamespaceById(props.projectId)?.namespace)
function mapRelatedTasks(tasks: ITask[]) { function mapRelatedTasks(tasks: ITask[]) {
return tasks.map(task => { return tasks.map(task => {
// by doing this here once we can save a lot of duplicate calls in the template // by doing this here once we can save a lot of duplicate calls in the template
const { const project = projectStore.projects[task.ProjectId]
project,
namespace: taskNamespace,
} = getProjectAndNamespaceById(task.projectId) || {project: null, namespace: null}
return { return {
...task, ...task,
differentNamespace:
(taskNamespace !== null &&
taskNamespace.id !== namespace.value.id &&
taskNamespace?.title) || null,
differentProject: differentProject:
(project !== null && (project &&
task.projectId !== props.projectId && task.projectId !== props.projectId &&
project?.title) || null, project?.title) || null,
} }
@ -442,5 +420,6 @@ async function toggleTaskDone(task: ITask) {
.task-done-checkbox { .task-done-checkbox {
padding: 0; padding: 0;
height: 18px; // The exact height of the checkbox in the container height: 18px; // The exact height of the checkbox in the container
margin-right: .75rem;
} }
</style> </style>

View File

@ -0,0 +1,277 @@
<template>
<div>
<Popup @close="showFormSwitch = null">
<template #trigger="{toggle}">
<SimpleButton
v-tooltip="reminder.reminder && reminder.relativeTo !== null ? formatDateShort(reminder.reminder) : null"
@click.prevent.stop="toggle()"
>
{{ reminderText }}
</SimpleButton>
</template>
<template #content="{isOpen, toggle}">
<Card class="reminder-options-popup" :class="{'is-open': isOpen}" :padding="false">
<div class="options" v-if="activeForm === null">
<SimpleButton
v-for="(p, k) in presets"
:key="k"
class="option-button"
:class="{'currently-active': p.relativePeriod === modelValue?.relativePeriod && modelValue?.relativeTo === p.relativeTo}"
@click="setReminderFromPreset(p, toggle)"
>
{{ formatReminder(p) }}
</SimpleButton>
<SimpleButton
@click="showFormSwitch = 'relative'"
class="option-button"
:class="{'currently-active': typeof modelValue !== 'undefined' && modelValue?.relativeTo !== null && presets.find(p => p.relativePeriod === modelValue?.relativePeriod && modelValue?.relativeTo === p.relativeTo) === undefined}"
>
{{ $t('task.reminder.custom') }}
</SimpleButton>
<SimpleButton
@click="showFormSwitch = 'absolute'"
class="option-button"
:class="{'currently-active': modelValue?.relativeTo === null}"
>
{{ $t('task.reminder.dateAndTime') }}
</SimpleButton>
</div>
<ReminderPeriod
v-if="activeForm === 'relative'"
v-model="reminder"
@update:modelValue="updateDataAndMaybeClose(toggle)"
/>
<DatepickerInline
v-if="activeForm === 'absolute'"
v-model="reminderDate"
@update:modelValue="setReminderDate"
/>
<x-button
v-if="showFormSwitch !== null"
class="reminder__close-button"
:shadow="false"
@click="toggle"
>
{{ $t('misc.confirm') }}
</x-button>
</Card>
</template>
</Popup>
</div>
</template>
<script setup lang="ts">
import {computed, ref, watch} from 'vue'
import {toRef} from '@vueuse/core'
import {SECONDS_A_DAY, SECONDS_A_HOUR} from '@/constants/date'
import {IReminderPeriodRelativeTo, REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import {useI18n} from 'vue-i18n'
import {PeriodUnit, secondsToPeriod} from '@/helpers/time/period'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import {formatDateShort} from '@/helpers/time/formatDate'
import DatepickerInline from '@/components/input/datepickerInline.vue'
import ReminderPeriod from '@/components/tasks/partials/reminder-period.vue'
import Popup from '@/components/misc/popup.vue'
import TaskReminderModel from '@/models/taskReminder'
import Card from '@/components/misc/card.vue'
import SimpleButton from '@/components/input/SimpleButton.vue'
const {t} = useI18n({useScope: 'global'})
const props = withDefaults(defineProps<{
modelValue?: ITaskReminder,
disabled?: boolean,
clearAfterUpdate?: boolean,
defaultRelativeTo?: null | IReminderPeriodRelativeTo,
}>(), {
disabled: false,
clearAfterUpdate: false,
defaultRelativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE,
})
const emit = defineEmits(['update:modelValue'])
const reminder = ref<ITaskReminder>(new TaskReminderModel())
const presets = computed<TaskReminderModel[]>(() => [
{reminder: null, relativePeriod: 0, relativeTo: props.defaultRelativeTo},
{reminder: null, relativePeriod: -2 * SECONDS_A_HOUR, relativeTo: props.defaultRelativeTo},
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY, relativeTo: props.defaultRelativeTo},
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 3, relativeTo: props.defaultRelativeTo},
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 7, relativeTo: props.defaultRelativeTo},
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 30, relativeTo: props.defaultRelativeTo},
])
const reminderDate = ref(null)
type availableForms = null | 'relative' | 'absolute'
const showFormSwitch = ref<availableForms>(null)
const activeForm = computed<availableForms>(() => {
if (props.defaultRelativeTo === null) {
return 'absolute'
}
return showFormSwitch.value
})
const reminderText = computed(() => {
if (reminder.value.relativeTo !== null) {
return formatReminder(reminder.value)
}
if (reminder.value.reminder !== null) {
return formatDateShort(reminder.value.reminder)
}
return t('task.addReminder')
})
const modelValue = toRef(props, 'modelValue')
watch(
modelValue,
(newReminder) => {
reminder.value = newReminder || new TaskReminderModel()
},
{immediate: true},
)
function updateData() {
emit('update:modelValue', reminder.value)
if (props.clearAfterUpdate) {
reminder.value = new TaskReminderModel()
}
}
function setReminderDate() {
reminder.value.reminder = reminderDate.value === null
? null
: new Date(reminderDate.value)
reminder.value.relativeTo = null
reminder.value.relativePeriod = 0
updateData()
}
function setReminderFromPreset(preset, toggle) {
reminder.value = preset
updateData()
toggle()
}
function updateDataAndMaybeClose(toggle) {
updateData()
if (props.clearAfterUpdate) {
toggle()
}
}
function formatReminder(reminder: TaskReminderModel) {
const period = secondsToPeriod(reminder.relativePeriod)
if (period.amount === 0) {
switch (reminder.relativeTo) {
case REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE:
return t('task.reminder.onDueDate')
case REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE:
return t('task.reminder.onStartDate')
case REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE:
return t('task.reminder.onEndDate')
}
}
const amountAbs = Math.abs(period.amount)
let relativeTo = ''
switch (reminder.relativeTo) {
case REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE:
relativeTo = t('task.attributes.dueDate')
break
case REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE:
relativeTo = t('task.attributes.startDate')
break
case REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE:
relativeTo = t('task.attributes.endDate')
break
}
if (reminder.relativePeriod <= 0) {
return t('task.reminder.before', {
amount: amountAbs,
unit: translateUnit(amountAbs, period.unit),
type: relativeTo,
})
}
return t('task.reminder.after', {
amount: amountAbs,
unit: translateUnit(amountAbs, period.unit),
type: relativeTo,
})
}
function translateUnit(amount: number, unit: PeriodUnit): string {
switch (unit) {
case 'seconds':
return t('time.units.seconds', amount)
case 'minutes':
return t('time.units.minutes', amount)
case 'hours':
return t('time.units.hours', amount)
case 'days':
return t('time.units.days', amount)
case 'weeks':
return t('time.units.weeks', amount)
case 'years':
return t('time.units.years', amount)
}
}
</script>
<style lang="scss" scoped>
.options {
display: flex;
flex-direction: column;
align-items: flex-start;
}
:deep(.popup) {
top: unset;
}
.reminder-options-popup {
width: 310px;
z-index: 99;
@media screen and (max-width: ($tablet)) {
width: calc(100vw - 5rem);
}
.option-button {
font-size: .85rem;
border-radius: 0;
padding: .5rem;
margin: 0;
&:hover {
background: var(--grey-100);
}
}
}
.reminder__close-button {
margin: .5rem;
width: calc(100% - 1rem);
}
.currently-active {
color: var(--primary);
}
</style>

View File

@ -0,0 +1,131 @@
<template>
<div
class="reminder-period control"
>
<input
class="input"
v-model.number="period.duration"
type="number"
min="0"
@change="updateData"
/>
<div class="select">
<select v-model="period.durationUnit" @change="updateData">
<option value="minutes">{{ $t('time.units.minutes', period.duration) }}</option>
<option value="hours">{{ $t('time.units.hours', period.duration) }}</option>
<option value="days">{{ $t('time.units.days', period.duration) }}</option>
<option value="weeks">{{ $t('time.units.weeks', period.duration) }}</option>
</select>
</div>
<div class="select">
<select v-model.number="period.sign" @change="updateData">
<option value="-1">
{{ $t('task.reminder.beforeShort') }}
</option>
<option value="1">
{{ $t('task.reminder.afterShort') }}
</option>
</select>
</div>
<div class="select">
<select v-model="period.relativeTo" @change="updateData">
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE">
{{ $t('task.attributes.dueDate') }}
</option>
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE">
{{ $t('task.attributes.startDate') }}
</option>
<option :value="REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE">
{{ $t('task.attributes.endDate') }}
</option>
</select>
</div>
</div>
</template>
<script setup lang="ts">
import {ref, watch, type PropType} from 'vue'
import {toRef} from '@vueuse/core'
import {periodToSeconds, PeriodUnit, secondsToPeriod} from '@/helpers/time/period'
import TaskReminderModel from '@/models/taskReminder'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES, type IReminderPeriodRelativeTo} from '@/types/IReminderPeriodRelativeTo'
const props = defineProps({
modelValue: {
type: Object as PropType<ITaskReminder>,
required: false,
},
disabled: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['update:modelValue'])
const reminder = ref<ITaskReminder>(new TaskReminderModel())
interface PeriodInput {
duration: number,
durationUnit: PeriodUnit,
relativeTo: IReminderPeriodRelativeTo,
sign: -1 | 1,
}
const period = ref<PeriodInput>({
duration: 0,
durationUnit: 'hours',
relativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE,
sign: -1,
})
const modelValue = toRef(props, 'modelValue')
watch(
modelValue,
(value) => {
const p = secondsToPeriod(value?.relativePeriod)
period.value.durationUnit = p.unit
period.value.duration = Math.abs(p.amount)
period.value.relativeTo = value?.relativeTo || REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE
},
{immediate: true},
)
watch(
() => period.value.duration,
value => {
if (value < 0) {
period.value.duration = value * -1
}
},
)
function updateData() {
reminder.value.relativePeriod = period.value.sign * periodToSeconds(Math.abs(period.value.duration), period.value.durationUnit)
reminder.value.relativeTo = period.value.relativeTo
reminder.value.reminder = null
emit('update:modelValue', reminder.value)
}
</script>
<style lang="scss" scoped>
.reminder-period {
display: flex;
flex-direction: column;
gap: .25rem;
padding: .5rem .5rem 0;
.input, .select select {
width: 100% !important;
height: auto;
}
}
</style>

View File

@ -0,0 +1,26 @@
<script setup lang="ts">
import reminders from './reminders.vue'
import {ref} from 'vue'
import ReminderDetail from '@/components/tasks/partials/reminder-detail.vue'
const reminderNow = ref({reminder: new Date(), relativePeriod: 0, relativeTo: null } )
const relativeReminder = ref({reminder: null, relativePeriod: 1, relativeTo: 'due_date' } )
const newReminder = ref(null)
</script>
<template>
<Story>
<Variant title="Default">
<reminders/>
</Variant>
<Variant title="Reminder Detail with fixed date">
<reminder-detail v-model="reminderNow"/>
</Variant>
<Variant title="Reminder Detail with relative date">
<reminder-detail v-model="relativeReminder"/>
</Variant>
<Variant title="New Reminder Detail">
<reminder-detail v-model="newReminder"/>
</Variant>
</Story>
</template>

View File

@ -3,104 +3,96 @@
<div <div
v-for="(r, index) in reminders" v-for="(r, index) in reminders"
:key="index" :key="index"
:class="{ 'overdue': r < new Date()}" :class="{ 'overdue': r.reminder < new Date() }"
class="reminder-input" class="reminder-input"
> >
<Datepicker <ReminderDetail
v-model="reminders[index]" class="reminder-detail"
:disabled="disabled" :disabled="disabled"
@close-on-change="() => addReminderDate(index)" v-model="reminders[index]"
@update:model-value="updateData"
:default-relative-to="defaultRelativeTo"
/> />
<BaseButton @click="removeReminderByIndex(index)" v-if="!disabled" class="remove"> <BaseButton
<icon icon="times"></icon> v-if="!disabled"
@click="removeReminderByIndex(index)"
class="remove"
>
<icon icon="times"/>
</BaseButton> </BaseButton>
</div> </div>
<div class="reminder-input" v-if="!disabled">
<Datepicker <ReminderDetail
v-model="newReminder" :disabled="disabled"
@close-on-change="() => addReminderDate()" @update:modelValue="addNewReminder"
:choose-date-label="$t('task.addReminder')" :clear-after-update="true"
/> :default-relative-to="defaultRelativeTo"
</div> />
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import {type PropType, ref, onMounted, watch} from 'vue' import {ref, watch, computed} from 'vue'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import BaseButton from '@/components/base/BaseButton.vue' import BaseButton from '@/components/base/BaseButton.vue'
import Datepicker from '@/components/input/datepicker.vue' import ReminderDetail from '@/components/tasks/partials/reminder-detail.vue'
import type {ITask} from '@/modelTypes/ITask'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
type Reminder = Date | string const props = withDefaults(defineProps<{
modelValue: ITask,
disabled?: boolean,
}>(), {
const props = defineProps({ modelValue: [],
modelValue: { disabled: false,
type: Array as PropType<Reminder[]>,
default: () => [],
validator(prop) {
// This allows arrays of Dates and strings
if (!(prop instanceof Array)) {
return false
}
const isDate = (e: unknown) => e instanceof Date
const isString = (e: unknown) => typeof e === 'string'
for (const e of prop) {
if (!isDate(e) && !isString(e)) {
return false
}
}
return true
},
},
disabled: {
type: Boolean,
default: false,
},
}) })
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const reminders = ref<Reminder[]>([]) const reminders = ref<ITaskReminder[]>([])
onMounted(() => {
reminders.value = [...props.modelValue]
})
watch( watch(
() => props.modelValue, () => props.modelValue.reminders,
(newVal) => { (newVal) => {
for (const i in newVal) {
if (typeof newVal[i] === 'string') {
newVal[i] = new Date(newVal[i])
}
}
reminders.value = newVal reminders.value = newVal
}, },
{immediate: true, deep: true}, // deep watcher so that we get the resolved date after updating the task
) )
const defaultRelativeTo = computed(() => {
if (typeof props.modelValue === 'undefined') {
return null
}
if (props.modelValue?.dueDate) {
return REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE
}
if (props.modelValue.dueDate === null && props.modelValue.startDate !== null) {
return REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE
}
if (props.modelValue.dueDate === null && props.modelValue.startDate === null && props.modelValue.endDate !== null) {
return REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE
}
return null
})
function updateData() { function updateData() {
emit('update:modelValue', reminders.value) emit('update:modelValue', {
...props.modelValue,
reminders: reminders.value,
})
} }
const newReminder = ref(null) function addNewReminder(newReminder: ITaskReminder) {
function addReminderDate(index : number | null = null) { if (newReminder === null) {
// New Date
if (index === null) {
if (newReminder.value === null) {
return
}
reminders.value.push(new Date(newReminder.value))
newReminder.value = null
} else if(reminders.value[index] === null) {
return return
} }
reminders.value.push(newReminder)
updateData() updateData()
} }
@ -111,23 +103,27 @@ function removeReminderByIndex(index: number) {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
.reminders { .reminder-input {
.reminder-input { display: flex;
display: flex; align-items: center;
align-items: center;
&.overdue :deep(.datepicker .show) { &.overdue :deep(.datepicker .show) {
color: var(--danger); color: var(--danger);
} }
&:last-child { &::last-child {
margin-bottom: 0.75rem; margin-bottom: 0.75rem;
} }
}
.remove { .reminder-detail {
color: var(--danger); width: 100%;
padding-left: .5rem; }
}
} .remove {
color: var(--danger);
vertical-align: top;
padding-left: .5rem;
line-height: 1;
} }
</style> </style>

View File

@ -70,6 +70,7 @@ import {error} from '@/message'
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode' import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
import type {IRepeatAfter} from '@/types/IRepeatAfter' import type {IRepeatAfter} from '@/types/IRepeatAfter'
import type {ITask} from '@/modelTypes/ITask' import type {ITask} from '@/modelTypes/ITask'
import TaskModel from '@/models/task'
const props = defineProps({ const props = defineProps({
modelValue: { modelValue: {
@ -87,7 +88,7 @@ const {t} = useI18n({useScope: 'global'})
const emit = defineEmits(['update:modelValue']) const emit = defineEmits(['update:modelValue'])
const task = ref<ITask>() const task = ref<ITask>(new TaskModel())
const repeatAfter = reactive({ const repeatAfter = reactive({
amount: 0, amount: 0,
type: '', type: '',
@ -95,7 +96,7 @@ const repeatAfter = reactive({
watch( watch(
() => props.modelValue, () => props.modelValue,
(value) => { (value: ITask) => {
task.value = value task.value = value
if (typeof value.repeatAfter !== 'undefined') { if (typeof value.repeatAfter !== 'undefined') {
Object.assign(repeatAfter, value.repeatAfter) Object.assign(repeatAfter, value.repeatAfter)
@ -105,11 +106,14 @@ watch(
) )
function updateData() { function updateData() {
if (!task.value || task.value.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT && repeatAfter.amount === 0) { if (!task.value ||
(task.value.repeatMode === TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT && repeatAfter.amount === 0) ||
(task.value.repeatMode === TASK_REPEAT_MODES.REPEAT_MODE_FROM_CURRENT_DATE && repeatAfter.amount === 0)
) {
return return
} }
if (repeatAfter.amount < 0) { if (task.value.repeatMode === TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT && repeatAfter.amount < 0) {
error({message: t('task.repeat.invalidAmount')}) error({message: t('task.repeat.invalidAmount')})
return return
} }

View File

@ -7,19 +7,19 @@
/> />
<ColorBubble <ColorBubble
v-if="showProjectColor && projectColor !== '' && currentProject.id !== task.projectId" v-if="showProjectColor && projectColor !== '' && currentProject?.id !== task.projectId"
:color="projectColor" :color="projectColor"
class="mr-1" class="mr-1"
/> />
<router-link <router-link
:to="taskDetailRoute" :to="taskDetailRoute"
:class="{ 'done': task.done, 'show-project': showProject && project !== null}" :class="{ 'done': task.done, 'show-project': showProject && project}"
class="tasktext" class="tasktext"
> >
<span> <span>
<router-link <router-link
v-if="showProject && project !== null" v-if="showProject && typeof project !== 'undefined'"
:to="{ name: 'project.list', params: { projectId: task.projectId } }" :to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-project" class="task-project"
:class="{'mr-2': task.hexColor !== ''}" :class="{'mr-2': task.hexColor !== ''}"
@ -34,7 +34,7 @@
/> />
<!-- Show any parent tasks to make it clear this task is a sub task of something --> <!-- Show any parent tasks to make it clear this task is a sub task of something -->
<span class="parent-tasks" v-if="typeof task.relatedTasks.parenttask !== 'undefined'"> <span class="parent-tasks" v-if="typeof task.relatedTasks?.parenttask !== 'undefined'">
<template v-for="(pt, i) in task.relatedTasks.parenttask"> <template v-for="(pt, i) in task.relatedTasks.parenttask">
{{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">,&nbsp;</template> {{ pt.title }}<template v-if="(i + 1) < task.relatedTasks.parenttask.length">,&nbsp;</template>
</template> </template>
@ -56,6 +56,7 @@
:key="task.id + 'assignee' + a.id + i" :key="task.id + 'assignee' + a.id + i"
:show-username="false" :show-username="false"
:user="a" :user="a"
class="m-2"
/> />
<!-- FIXME: use popup --> <!-- FIXME: use popup -->
@ -104,7 +105,7 @@
</progress> </progress>
<router-link <router-link
v-if="!showProject && currentProject.id !== task.projectId && project !== null" v-if="!showProject && currentProject?.id !== task.projectId && project"
:to="{ name: 'project.list', params: { projectId: task.projectId } }" :to="{ name: 'project.list', params: { projectId: task.projectId } }"
class="task-project" class="task-project"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})" v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
@ -149,7 +150,6 @@ import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatD
import {success} from '@/message' import {success} from '@/message'
import {useProjectStore} from '@/stores/projects' import {useProjectStore} from '@/stores/projects'
import {useNamespaceStore} from '@/stores/namespaces'
import {useBaseStore} from '@/stores/base' import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks' import {useTaskStore} from '@/stores/tasks'
@ -209,10 +209,9 @@ onBeforeUnmount(() => {
const baseStore = useBaseStore() const baseStore = useBaseStore()
const projectStore = useProjectStore() const projectStore = useProjectStore()
const taskStore = useTaskStore() const taskStore = useTaskStore()
const namespaceStore = useNamespaceStore()
const project = computed(() => projectStore.getProjectById(task.value.projectId)) const project = computed(() => projectStore.projects[task.value.projectId])
const projectColor = computed(() => project.value !== null ? project.value.hexColor : '') const projectColor = computed(() => project.value ? project.value?.hexColor : '')
const currentProject = computed(() => { const currentProject = computed(() => {
return typeof baseStore.currentProject === 'undefined' ? { return typeof baseStore.currentProject === 'undefined' ? {
@ -257,10 +256,8 @@ function undoDone(checked: boolean) {
} }
async function toggleFavorite() { async function toggleFavorite() {
task.value.isFavorite = !task.value.isFavorite task.value = await taskStore.toggleFavorite(task.value)
task.value = await taskService.update(task.value)
emit('task-updated', task.value) emit('task-updated', task.value)
namespaceStore.loadNamespacesIfFavoritesDontExist()
} }
const deferDueDate = ref<typeof DeferTask | null>(null) const deferDueDate = ref<typeof DeferTask | null>(null)

View File

@ -1,8 +1,7 @@
import {computed, watch, readonly} from 'vue' import {computed, watch, readonly} from 'vue'
import {useStorage, createSharedComposable, usePreferredColorScheme, tryOnMounted} from '@vueuse/core' import {createSharedComposable, usePreferredColorScheme, tryOnMounted} from '@vueuse/core'
import type {BasicColorSchema} from '@vueuse/core' import type {BasicColorSchema} from '@vueuse/core'
import {useAuthStore} from '@/stores/auth'
const STORAGE_KEY = 'color-scheme'
const DEFAULT_COLOR_SCHEME_SETTING: BasicColorSchema = 'light' const DEFAULT_COLOR_SCHEME_SETTING: BasicColorSchema = 'light'
@ -17,7 +16,8 @@ const CLASS_LIGHT = 'light'
// - value is synced via `createSharedComposable` // - value is synced via `createSharedComposable`
// https://github.com/vueuse/vueuse/blob/main/packages/core/useDark/index.ts // https://github.com/vueuse/vueuse/blob/main/packages/core/useDark/index.ts
export const useColorScheme = createSharedComposable(() => { export const useColorScheme = createSharedComposable(() => {
const store = useStorage<BasicColorSchema>(STORAGE_KEY, DEFAULT_COLOR_SCHEME_SETTING) const authStore = useAuthStore()
const store = computed(() => authStore.settings.frontendSettings.colorSchema)
const preferredColorScheme = usePreferredColorScheme() const preferredColorScheme = usePreferredColorScheme()

View File

@ -1,19 +0,0 @@
import {ref, computed} from 'vue'
import {useNamespaceStore} from '@/stores/namespaces'
export function useNamespaceSearch() {
const query = ref('')
const namespaceStore = useNamespaceStore()
const namespaces = computed(() => namespaceStore.searchNamespace(query.value))
function findNamespaces(newQuery: string) {
query.value = newQuery
}
return {
namespaces,
findNamespaces,
}
}

View File

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

View File

@ -0,0 +1,7 @@
export function canNestProjectDeeper(level: number) {
if (level < 2) {
return true
}
return level >= 2 && window.PROJECT_INFINITE_NESTING_ENABLED
}

View File

@ -1,15 +0,0 @@
import {i18n} from '@/i18n'
import type {INamespace} from '@/modelTypes/INamespace'
export const getNamespaceTitle = (n: INamespace) => {
if (n.id === -1) {
return i18n.global.t('namespace.pseudo.sharedProjects.title')
}
if (n.id === -2) {
return i18n.global.t('namespace.pseudo.favorites.title')
}
if (n.id === -3) {
return i18n.global.t('namespace.pseudo.savedFilters.title')
}
return n.title
}

View File

@ -1,9 +1,14 @@
import {i18n} from '@/i18n' import {i18n} from '@/i18n'
import type {IProject} from '@/modelTypes/IProject' import type {IProject} from '@/modelTypes/IProject'
export function getProjectTitle(l: IProject) { export function getProjectTitle(project: IProject) {
if (l.id === -1) { if (project.id === -1) {
return i18n.global.t('project.pseudo.favorites.title') return i18n.global.t('project.pseudo.favorites.title')
} }
return l.title
if (project.title === 'Inbox') {
return i18n.global.t('project.inboxTitle')
}
return project.title
} }

View File

@ -1,5 +1,6 @@
import {describe, it, expect} from 'vitest' import {describe, expect, it} from 'vitest'
import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention' import {parseSubtasksViaIndention} from '@/helpers/parseSubtasksViaIndention'
import {PrefixMode} from '@/modules/parseTaskText'
describe('Parse Subtasks via Relation', () => { describe('Parse Subtasks via Relation', () => {
it('Should not return a parent for a single task', () => { it('Should not return a parent for a single task', () => {
@ -10,7 +11,7 @@ describe('Parse Subtasks via Relation', () => {
}) })
it('Should not return a parent for multiple tasks without indention', () => { it('Should not return a parent for multiple tasks without indention', () => {
const tasks = parseSubtasksViaIndention(`task one const tasks = parseSubtasksViaIndention(`task one
task two`) task two`, PrefixMode.Default)
expect(tasks).to.have.length(2) expect(tasks).to.have.length(2)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -18,7 +19,7 @@ task two`)
}) })
it('Should return a parent for two tasks with indention', () => { it('Should return a parent for two tasks with indention', () => {
const tasks = parseSubtasksViaIndention(`parent task const tasks = parseSubtasksViaIndention(`parent task
sub task`) sub task`, PrefixMode.Default)
expect(tasks).to.have.length(2) expect(tasks).to.have.length(2)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -29,7 +30,7 @@ task two`)
it('Should return a parent for multiple subtasks', () => { it('Should return a parent for multiple subtasks', () => {
const tasks = parseSubtasksViaIndention(`parent task const tasks = parseSubtasksViaIndention(`parent task
sub task one sub task one
sub task two`) sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(3) expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -42,7 +43,7 @@ task two`)
it('Should work with multiple indention levels', () => { it('Should work with multiple indention levels', () => {
const tasks = parseSubtasksViaIndention(`parent task const tasks = parseSubtasksViaIndention(`parent task
sub task sub task
sub sub task`) sub sub task`, PrefixMode.Default)
expect(tasks).to.have.length(3) expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -56,7 +57,7 @@ task two`)
const tasks = parseSubtasksViaIndention(`parent task const tasks = parseSubtasksViaIndention(`parent task
sub task sub task
sub sub task one sub sub task one
sub sub task two`) sub sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(4) expect(tasks).to.have.length(4)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -73,7 +74,7 @@ task two`)
sub task sub task
sub sub task one sub sub task one
sub sub sub task sub sub sub task
sub sub task two`) sub sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(5) expect(tasks).to.have.length(5)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -90,7 +91,7 @@ task two`)
it('Should return a parent for multiple subtasks with special stuff', () => { it('Should return a parent for multiple subtasks with special stuff', () => {
const tasks = parseSubtasksViaIndention(`* parent task const tasks = parseSubtasksViaIndention(`* parent task
* sub task one * sub task one
sub task two`) sub task two`, PrefixMode.Default)
expect(tasks).to.have.length(3) expect(tasks).to.have.length(3)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -101,7 +102,7 @@ task two`)
expect(tasks[2].parent).to.eq('parent task') expect(tasks[2].parent).to.eq('parent task')
}) })
it('Should not break when the first line is indented', () => { it('Should not break when the first line is indented', () => {
const tasks = parseSubtasksViaIndention(' single task') const tasks = parseSubtasksViaIndention(' single task', PrefixMode.Default)
expect(tasks).to.have.length(1) expect(tasks).to.have.length(1)
expect(tasks[0].parent).toBeNull() expect(tasks[0].parent).toBeNull()
@ -110,7 +111,7 @@ task two`)
const tasks = parseSubtasksViaIndention( const tasks = parseSubtasksViaIndention(
`parent task +list `parent task +list
sub task 1 sub task 1
sub task 2`) sub task 2`, PrefixMode.Default)
expect(tasks).to.have.length(3) expect(tasks).to.have.length(3)
expect(tasks[0].project).to.eq('list') expect(tasks[0].project).to.eq('list')

View File

@ -1,4 +1,4 @@
import {getProjectFromPrefix} from '@/modules/parseTaskText' import {getProjectFromPrefix, PrefixMode} from '@/modules/parseTaskText'
export interface TaskWithParent { export interface TaskWithParent {
title: string, title: string,
@ -16,7 +16,7 @@ const spaceRegex = /^ */
* @param taskTitles should be multiple lines of task tiles with indention to declare their parent/subtask * @param taskTitles should be multiple lines of task tiles with indention to declare their parent/subtask
* relation between each other. * relation between each other.
*/ */
export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[] { export function parseSubtasksViaIndention(taskTitles: string, prefixMode: PrefixMode): TaskWithParent[] {
const titles = taskTitles.split(/[\r\n]+/) const titles = taskTitles.split(/[\r\n]+/)
return titles.map((title, index) => { return titles.map((title, index) => {
@ -26,7 +26,7 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
project: null, project: null,
} }
task.project = getProjectFromPrefix(task.title) task.project = getProjectFromPrefix(task.title, prefixMode)
if (index === 0) { if (index === 0) {
return task return task
@ -49,7 +49,7 @@ export function parseSubtasksViaIndention(taskTitles: string): TaskWithParent[]
task.parent = task.parent.replace(spaceRegex, '') task.parent = task.parent.replace(spaceRegex, '')
if (task.project === null) { if (task.project === null) {
// This allows to specify a project once for the parent task and inherit it to all subtasks // This allows to specify a project once for the parent task and inherit it to all subtasks
task.project = getProjectFromPrefix(task.parent) task.project = getProjectFromPrefix(task.parent, prefixMode)
} }
} }

View File

@ -2,15 +2,6 @@ import popSoundFile from '@/assets/audio/pop.mp3'
export const playSoundWhenDoneKey = 'playSoundWhenTaskDone' export const playSoundWhenDoneKey = 'playSoundWhenTaskDone'
export function playPop() {
const enabled = localStorage.getItem(playSoundWhenDoneKey) === 'true'
if (!enabled) {
return
}
playPopSound()
}
export function playPopSound() { export function playPopSound() {
const popSound = new Audio(popSoundFile) const popSound = new Audio(popSoundFile)
popSound.play() popSound.play()

View File

@ -1,21 +0,0 @@
import {PrefixMode} from '@/modules/parseTaskText'
const key = 'quickAddMagicMode'
export const setQuickAddMagicMode = (mode: PrefixMode) => {
localStorage.setItem(key, mode)
}
export const getQuickAddMagicMode = (): PrefixMode => {
const mode = localStorage.getItem(key)
switch (mode) {
case null:
case PrefixMode.Default:
return PrefixMode.Default
case PrefixMode.Todoist:
return PrefixMode.Todoist
}
return PrefixMode.Disabled
}

View File

@ -129,7 +129,7 @@ const addTimeToDate = (text: string, date: Date, previousMatch: string | null):
} }
export const getDateFromText = (text: string, now: Date = new Date()) => { export const getDateFromText = (text: string, now: Date = new Date()) => {
const fullDateRegex = / ([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig const fullDateRegex = /(^| )([0-9][0-9]?\/[0-9][0-9]?\/[0-9][0-9]([0-9][0-9])?|[0-9][0-9][0-9][0-9]\/[0-9][0-9]?\/[0-9][0-9]?|[0-9][0-9][0-9][0-9]-[0-9][0-9]?-[0-9][0-9]?)/ig
// 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021 // 1. Try parsing the text as a "usual" date, like 2021-06-24 or 06/24/2021
let results: string[] | null = fullDateRegex.exec(text) let results: string[] | null = fullDateRegex.exec(text)
@ -138,7 +138,7 @@ export const getDateFromText = (text: string, now: Date = new Date()) => {
let containsYear = true let containsYear = true
if (result === null) { if (result === null) {
// 2. Try parsing the date as something like "jan 21" or "21 jan" // 2. Try parsing the date as something like "jan 21" or "21 jan"
const monthRegex = new RegExp(` (${monthsRegexGroup} [0-9][0-9]?|[0-9][0-9]? ${monthsRegexGroup})`, 'ig') const monthRegex = new RegExp(`(^| )(${monthsRegexGroup} [0-9][0-9]?|[0-9][0-9]? ${monthsRegexGroup})`, 'ig')
results = monthRegex.exec(text) results = monthRegex.exec(text)
result = results === null ? null : `${results[0]} ${now.getFullYear()}`.trim() result = results === null ? null : `${results[0]} ${now.getFullYear()}`.trim()
foundText = results === null ? '' : results[0].trim() foundText = results === null ? '' : results[0].trim()
@ -146,7 +146,7 @@ export const getDateFromText = (text: string, now: Date = new Date()) => {
if (result === null) { if (result === null) {
// 3. Try parsing the date as "27/01" or "01/27" // 3. Try parsing the date as "27/01" or "01/27"
const monthNumericRegex = / ([0-9][0-9]?\/[0-9][0-9]?)/ig const monthNumericRegex = /(^| )([0-9][0-9]?\/[0-9][0-9]?)/ig
results = monthNumericRegex.exec(text) results = monthNumericRegex.exec(text)
// Put the year before or after the date, depending on what works // Put the year before or after the date, depending on what works
@ -299,7 +299,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
} }
const getDayFromText = (text: string) => { const getDayFromText = (text: string) => {
const matcher = /($| )(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)($| )/ig const matcher = /(^| )(([1-2][0-9])|(3[01])|(0?[1-9]))(st|nd|rd|th|\.)($| )/ig
const results = matcher.exec(text) const results = matcher.exec(text)
if (results === null) { if (results === null) {
return { return {

View File

@ -0,0 +1,50 @@
import {
SECONDS_A_DAY,
SECONDS_A_HOUR,
SECONDS_A_MINUTE,
SECONDS_A_MONTH,
SECONDS_A_WEEK,
SECONDS_A_YEAR,
} from '@/constants/date'
export type PeriodUnit = 'seconds' | 'minutes' | 'hours' | 'days' | 'weeks' | 'months' | 'years'
/**
* Convert time period given as seconds to days, hour, minutes, seconds
*/
export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: number } {
if (seconds % SECONDS_A_DAY === 0) {
if (seconds % SECONDS_A_WEEK === 0) {
return {unit: 'weeks', amount: seconds / SECONDS_A_WEEK}
} else if (seconds % SECONDS_A_MONTH === 0) {
return {unit: 'days', amount: seconds / SECONDS_A_MONTH * 30}
} else if (seconds % SECONDS_A_YEAR === 0) {
return {unit: 'years', amount: seconds / SECONDS_A_YEAR}
} else {
return {unit: 'days', amount: seconds / SECONDS_A_DAY}
}
}
return {
unit: 'hours',
amount: seconds / SECONDS_A_HOUR,
}
}
/**
* Convert time period of days, hour, minutes, seconds to duration in seconds
*/
export function periodToSeconds(period: number, unit: PeriodUnit): number {
switch (unit) {
case 'minutes':
return period * SECONDS_A_MINUTE
case 'hours':
return period * SECONDS_A_HOUR
case 'days':
return period * SECONDS_A_DAY
case 'weeks':
return period * SECONDS_A_WEEK
}
return 0
}

View File

@ -32,7 +32,7 @@ export const i18n = createI18n({
} as Record<SupportedLocale, any>, } as Record<SupportedLocale, any>,
}) })
export async function setLanguage(lang: SupportedLocale = getCurrentLanguage()): Promise<SupportedLocale | undefined> { export async function setLanguage(lang: SupportedLocale): Promise<SupportedLocale | undefined> {
if (!lang) { if (!lang) {
throw new Error() throw new Error()
} }
@ -53,12 +53,7 @@ export async function setLanguage(lang: SupportedLocale = getCurrentLanguage()):
return lang return lang
} }
export function getCurrentLanguage(): SupportedLocale { export function getBrowserLanguage(): SupportedLocale {
const savedLanguage = localStorage.getItem('language') as SupportedLocale | null
if (savedLanguage !== null) {
return savedLanguage
}
const browserLanguage = navigator.language const browserLanguage = navigator.language
const language = Object.keys(SUPPORTED_LOCALES).find(langKey => { const language = Object.keys(SUPPORTED_LOCALES).find(langKey => {
@ -67,8 +62,3 @@ export function getCurrentLanguage(): SupportedLocale {
return language || DEFAULT_LANGUAGE return language || DEFAULT_LANGUAGE
} }
export async function saveLanguage(lang: SupportedLocale) {
localStorage.setItem('language', lang)
await setLanguage()
}

View File

@ -5,10 +5,9 @@
"welcomeDay": "Hi {username}!", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"project": { "project": {
"newText": "You can create a new project for your new tasks:", "importText": "Import your projects and tasks from other services into Vikunja:",
"new": "New project",
"importText": "Or import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
}, },
@ -78,8 +77,8 @@
"savedSuccess": "The settings were successfully updated.", "savedSuccess": "The settings were successfully updated.",
"emailReminders": "Send me reminders for tasks via Email", "emailReminders": "Send me reminders for tasks via Email",
"overdueReminders": "Send me a summary of my undone overdue tasks every day", "overdueReminders": "Send me a summary of my undone overdue tasks every day",
"discoverableByName": "Let other users find me when they search for my name", "discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
"discoverableByEmail": "Let other users find me when they search for my full email", "discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
"playSoundWhenDone": "Play a sound when marking tasks as done", "playSoundWhenDone": "Play a sound when marking tasks as done",
"weekStart": "Week starts on", "weekStart": "Week starts on",
"weekStartSunday": "Sunday", "weekStartSunday": "Sunday",
@ -143,7 +142,7 @@
}, },
"deletion": { "deletion": {
"title": "Delete your Vikunja Account", "title": "Delete your Vikunja Account",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.", "text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
"text2": "To proceed, please enter your password. You will receive an email with further instructions.", "text2": "To proceed, please enter your password. You will receive an email with further instructions.",
"confirm": "Delete my account", "confirm": "Delete my account",
"requestSuccess": "The request was successful. You'll receive an email with further instructions.", "requestSuccess": "The request was successful. You'll receive an email with further instructions.",
@ -157,7 +156,7 @@
}, },
"export": { "export": {
"title": "Export your Vikunja data", "title": "Export your Vikunja data",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", "description": "You can request a copy of all your Vikunja data. This includes Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"descriptionPasswordRequired": "Please enter your password to proceed:", "descriptionPasswordRequired": "Please enter your password to proceed:",
"request": "Request a copy of my Vikunja Data", "request": "Request a copy of my Vikunja Data",
"success": "You've successfully requested your Vikunja Data! We will send you an email once it's ready to download.", "success": "You've successfully requested your Vikunja Data! We will send you an email once it's ready to download.",
@ -165,14 +164,18 @@
} }
}, },
"project": { "project": {
"archived": "This project is archived. It is not possible to create new or edit tasks for it.", "archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archived",
"showArchived": "Show Archived",
"title": "Project Title", "title": "Project Title",
"color": "Color", "color": "Color",
"projects": "Projects", "projects": "Projects",
"parent": "Parent Project",
"search": "Type to search for a project…", "search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this project", "searchSelect": "Click or press enter to select this project",
"shared": "Shared Projects", "shared": "Shared Projects",
"noDescriptionAvailable": "No project description is available.", "noDescriptionAvailable": "No project description is available.",
"inboxTitle": "Inbox",
"create": { "create": {
"header": "New project", "header": "New project",
"titlePlaceholder": "The project's title goes here…", "titlePlaceholder": "The project's title goes here…",
@ -210,7 +213,7 @@
"duplicate": { "duplicate": {
"title": "Duplicate this project", "title": "Duplicate this project",
"label": "Duplicate", "label": "Duplicate",
"text": "Select a namespace which should hold the duplicated project:", "text": "Select a parent project which should hold the duplicated project:",
"success": "The project was successfully duplicated." "success": "The project was successfully duplicated."
}, },
"edit": { "edit": {
@ -238,7 +241,7 @@
"namePlaceholder": "e.g. Lorem Ipsum", "namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.", "nameExplanation": "All actions done by this link share will show up with the name.",
"password": "Password (optional)", "password": "Password (optional)",
"passwordExplanation": "When authenticating, the user will be required to enter this password.", "passwordExplanation": "When signing in, the user will be required to enter this password.",
"noName": "No name set", "noName": "No name set",
"remove": "Remove a link share", "remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!", "removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
@ -321,67 +324,6 @@
} }
} }
}, },
"namespace": {
"title": "Namespaces & Projects",
"namespace": "Namespace",
"showArchived": "Show Archived",
"noneAvailable": "You don't have any namespaces right now.",
"unarchive": "Un-Archive",
"archived": "Archived",
"noProjects": "This namespace does not contain any projects.",
"createProject": "Create a new project in this namespace.",
"namespaces": "Namespaces",
"search": "Type to search for a namespace…",
"create": {
"title": "New namespace",
"titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
"tooltip": "What's a namespace?",
"success": "The namespace was successfully created."
},
"archive": {
"titleArchive": "Archive \"{namespace}\"",
"titleUnarchive": "Un-Archive \"{namespace}\"",
"archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
"unarchiveText": "You will be able to create new projects or edit it.",
"success": "The namespace was successfully archived.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new projects or edit it."
},
"delete": {
"title": "Delete \"{namespace}\"",
"text1": "Are you sure you want to delete this namespace and all of its contents?",
"text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
"success": "The namespace was successfully deleted."
},
"edit": {
"title": "Edit \"{namespace}\"",
"success": "The namespace was successfully updated."
},
"share": {
"title": "Share \"{namespace}\""
},
"attributes": {
"title": "Namespace Title",
"titlePlaceholder": "The namespace title goes here…",
"description": "Description",
"descriptionPlaceholder": "The namespaces description goes here…",
"color": "Color",
"archived": "Is Archived",
"isArchived": "This namespace is archived"
},
"pseudo": {
"sharedProjects": {
"title": "Shared Projects"
},
"favorites": {
"title": "Favorites"
},
"savedFilters": {
"title": "Filters"
}
}
},
"filters": { "filters": {
"title": "Filters", "title": "Filters",
"clear": "Clear Filters", "clear": "Clear Filters",
@ -403,7 +345,7 @@
}, },
"create": { "create": {
"title": "New Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual project 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 project which is computed from a set of filters each time it is accessed.",
"action": "Create new saved filter", "action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter." "titleRequired": "Please provide a title for the filter."
}, },
@ -529,7 +471,7 @@
"code": "Code", "code": "Code",
"quote": "Quote", "quote": "Quote",
"unorderedList": "Unordered List", "unorderedList": "Unordered List",
"orderedList ": "Ordered List", "orderedList": "Ordered List",
"cleanBlock": "Clean Block", "cleanBlock": "Clean Block",
"link": "Link", "link": "Link",
"image": "Image", "image": "Image",
@ -566,14 +508,14 @@
"canuse": "You can use date math to filter for relative dates.", "canuse": "You can use date math to filter for relative dates.",
"learnhow": "Check out how it works", "learnhow": "Check out how it works",
"title": "Date Math", "title": "Date Math",
"intro": "Date Math allows you to specify relative dates which are resolved on the fly by Vikunja when applying the filter.", "intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
"expression": "Each Date Math expression starts with an anchor date, which can either be {0}, or a date string ending with {1}. This anchor date can optionally be followed by one or more maths expressions.", "expression": "Each Date Math expression starts with an anchor date, which can either be {0}, or a date string ending with {1}. This anchor date can optionally be followed by one or more maths expressions.",
"similar": "These expressions are similar to the ones provided by {0} and {1}.", "similar": "These expressions are similar to the ones provided by {0} and {1}.",
"add1Day": "Add one day", "add1Day": "Add one day",
"minus1Day": "Subtract one day", "minus1Day": "Subtract one day",
"roundDay": "Round down to the nearest day", "roundDay": "Round down to the nearest day",
"supportedUnits": "Supported time units are:", "supportedUnits": "Supported time units",
"someExamples": "Some examples of time expressions:", "someExamples": "Examples of time expressions",
"units": { "units": {
"seconds": "Seconds", "seconds": "Seconds",
"minutes": "Minutes", "minutes": "Minutes",
@ -674,19 +616,13 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.", "subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.", "subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.", "notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.", "subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.", "notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessProject": "You are now subscribed to this project", "subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project", "unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "You are now subscribed to this task", "subscribeSuccessTask": "You are now subscribed to this task",
@ -763,7 +699,6 @@
"searchPlaceholder": "Type search for a new task to add as related…", "searchPlaceholder": "Type search for a new task to add as related…",
"createPlaceholder": "Add this as new related task", "createPlaceholder": "Add this as new related task",
"differentProject": "This task belongs to a different project.", "differentProject": "This task belongs to a different project.",
"differentNamespace": "This task belongs to a different namespace.",
"noneYet": "No task relations yet.", "noneYet": "No task relations yet.",
"delete": "Delete Task Relation", "delete": "Delete Task Relation",
"deleteText1": "Are you sure you want to delete this task relation?", "deleteText1": "Are you sure you want to delete this task relation?",
@ -783,6 +718,17 @@
"copiedto": "Copied To | Copied To" "copiedto": "Copied To | Copied To"
} }
}, },
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
},
"repeat": { "repeat": {
"everyDay": "Every Day", "everyDay": "Every Day",
"everyWeek": "Every Week", "everyWeek": "Every Week",
@ -800,8 +746,7 @@
"invalidAmount": "Please enter more than 0." "invalidAmount": "Please enter more than 0."
}, },
"quickAddMagic": { "quickAddMagic": {
"hint": "You can use Quick Add Magic", "hint": "Use magic prefixes to define due dates, assignees and other task properties.",
"what": "What?",
"title": "Quick Add Magic", "title": "Quick Add Magic",
"intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.", "intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.",
"multiple": "You can use this multiple times.", "multiple": "You can use this multiple times.",
@ -848,19 +793,19 @@
"delete": { "delete": {
"header": "Delete the team", "header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?", "text1": "Are you sure you want to delete this team and all of its members?",
"text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!", "text2": "All team members will lose access to projects shared with this team. This CANNOT BE UNDONE!",
"success": "The team was successfully deleted." "success": "The team was successfully deleted."
}, },
"deleteUser": { "deleteUser": {
"header": "Remove a user from the team", "header": "Remove a user from the team",
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
}, },
"leave": { "leave": {
"title": "Leave team", "title": "Leave team",
"text1": "Are you sure you want to leave this team?", "text1": "Are you sure you want to leave this team?",
"text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.", "text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team." "success": "You have successfully left the team."
} }
}, },
@ -894,7 +839,10 @@
"color": "Change the color of this task", "color": "Change the color of this task",
"move": "Move this task to another project", "move": "Move this task to another project",
"reminder": "Manage reminders of this task", "reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description" "description": "Toggle editing of the task description",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
}, },
"project": { "project": {
"title": "Project Views", "title": "Project Views",
@ -907,9 +855,9 @@
"title": "Navigation", "title": "Navigation",
"overview": "Navigate to overview", "overview": "Navigate to overview",
"upcoming": "Navigate to upcoming tasks", "upcoming": "Navigate to upcoming tasks",
"namespaces": "Navigate to namespaces & projects",
"labels": "Navigate to labels", "labels": "Navigate to labels",
"teams": "Navigate to teams" "teams": "Navigate to teams",
"projects": "Navigate to projects"
} }
}, },
"update": { "update": {
@ -924,7 +872,8 @@
"unarchive": "Un-Archive", "unarchive": "Un-Archive",
"setBackground": "Set background", "setBackground": "Set background",
"share": "Share", "share": "Share",
"newProject": "New project" "newProject": "New project",
"createProject": "Create project"
}, },
"apiConfig": { "apiConfig": {
"url": "Vikunja URL", "url": "Vikunja URL",
@ -943,7 +892,7 @@
"notification": { "notification": {
"title": "Notifications", "title": "Notifications",
"none": "You don't have any notifications. Have a nice day!", "none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen." "explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
}, },
"quickActions": { "quickActions": {
"commands": "Commands", "commands": "Commands",
@ -954,14 +903,12 @@
"teams": "Teams", "teams": "Teams",
"newProject": "Enter the title of the new project…", "newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…", "newTask": "Enter the title of the new task…",
"newNamespace": "Enter the title of the new namespace…",
"newTeam": "Enter the name of the new team…", "newTeam": "Enter the name of the new team…",
"createTask": "Create a task in the current project ({title})", "createTask": "Create a task in the current project ({title})",
"createProject": "Create a project in the current namespace ({title})", "createProject": "Create a project",
"cmds": { "cmds": {
"newTask": "New task", "newTask": "New task",
"newProject": "New project", "newProject": "New project",
"newNamespace": "New namespace",
"newTeam": "New team" "newTeam": "New team"
} }
}, },
@ -1017,16 +964,9 @@
"4017": "Invalid task filter comparator.", "4017": "Invalid task filter comparator.",
"4018": "Invalid task filter concatenator.", "4018": "Invalid task filter concatenator.",
"4019": "Invalid task filter value.", "4019": "Invalid task filter value.",
"5001": "The namespace does not exist.",
"5003": "You do not have access to the specified namespace.",
"5006": "The namespace name cannot be empty.",
"5009": "You need to have namespace read access to perform that action.",
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be empty.", "6001": "The team name cannot be empty.",
"6002": "The team does not exist.", "6002": "The team does not exist.",
"6004": "The team already has access to that namespace or project.", "6004": "The team already has access to that project.",
"6005": "The user is already a member of that team.", "6005": "The user is already a member of that team.",
"6006": "Cannot delete the last team member.", "6006": "Cannot delete the last team member.",
"6007": "The team does not have access to the project to perform that action.", "6007": "The team does not have access to the project to perform that action.",
@ -1052,5 +992,16 @@
"title": "About", "title": "About",
"frontendVersion": "Frontend Version: {version}", "frontendVersion": "Frontend Version: {version}",
"apiVersion": "API Version: {version}" "apiVersion": "API Version: {version}"
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
}
} }
} }

View File

@ -5,10 +5,9 @@
"welcomeDay": "Ahoj {username}!", "welcomeDay": "Ahoj {username}!",
"welcomeEvening": "Dobrý večer {username}!", "welcomeEvening": "Dobrý večer {username}!",
"lastViewed": "Naposledy zobrazeno", "lastViewed": "Naposledy zobrazeno",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"project": { "project": {
"newText": "You can create a new project for your new tasks:", "importText": "Import your projects and tasks from other services into Vikunja:",
"new": "New project",
"importText": "Or import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
}, },
@ -78,8 +77,8 @@
"savedSuccess": "Nastavení bylo úspěšně aktualizováno.", "savedSuccess": "Nastavení bylo úspěšně aktualizováno.",
"emailReminders": "Posílat mi připomenutí pro úkoly e-mailem", "emailReminders": "Posílat mi připomenutí pro úkoly e-mailem",
"overdueReminders": "Pošlete mi každý den shrnutí mých zpožděných úkolů", "overdueReminders": "Pošlete mi každý den shrnutí mých zpožděných úkolů",
"discoverableByName": "Nechat ostatní uživatele mě najít podle jména", "discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
"discoverableByEmail": "Nechat ostatní uživatele mě najít podle e-mailu", "discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
"playSoundWhenDone": "Přehrát zvuk při označení úkolů jako hotovo", "playSoundWhenDone": "Přehrát zvuk při označení úkolů jako hotovo",
"weekStart": "Začátek týdne", "weekStart": "Začátek týdne",
"weekStartSunday": "Neděle", "weekStartSunday": "Neděle",
@ -143,7 +142,7 @@
}, },
"deletion": { "deletion": {
"title": "Smazat svůj účet", "title": "Smazat svůj účet",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.", "text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
"text2": "Chcete-li pokračovat, zadejte své heslo. Obdržíte e-mail s dalšími pokyny.", "text2": "Chcete-li pokračovat, zadejte své heslo. Obdržíte e-mail s dalšími pokyny.",
"confirm": "Smazat můj účet", "confirm": "Smazat můj účet",
"requestSuccess": "Požadavek byl úspěšný. Obdržíte e-mail s dalšími pokyny.", "requestSuccess": "Požadavek byl úspěšný. Obdržíte e-mail s dalšími pokyny.",
@ -157,7 +156,7 @@
}, },
"export": { "export": {
"title": "Exportovat data účtu", "title": "Exportovat data účtu",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", "description": "You can request a copy of all your Vikunja data. This includes Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"descriptionPasswordRequired": "Pokračujte zadáním vašeho hesla:", "descriptionPasswordRequired": "Pokračujte zadáním vašeho hesla:",
"request": "Požádat o kopii mých dat", "request": "Požádat o kopii mých dat",
"success": "Úspěšně jste požádali o svá data! Jakmile budou připravena ke stažení, pošleme Vám e-mail.", "success": "Úspěšně jste požádali o svá data! Jakmile budou připravena ke stažení, pošleme Vám e-mail.",
@ -165,14 +164,18 @@
} }
}, },
"project": { "project": {
"archived": "This project is archived. It is not possible to create new or edit tasks for it.", "archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archived",
"showArchived": "Show Archived",
"title": "Project Title", "title": "Project Title",
"color": "Color", "color": "Color",
"projects": "Projects", "projects": "Projects",
"parent": "Parent Project",
"search": "Type to search for a project…", "search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this project", "searchSelect": "Click or press enter to select this project",
"shared": "Shared Projects", "shared": "Shared Projects",
"noDescriptionAvailable": "No project description is available.", "noDescriptionAvailable": "No project description is available.",
"inboxTitle": "Inbox",
"create": { "create": {
"header": "New project", "header": "New project",
"titlePlaceholder": "The project's title goes here…", "titlePlaceholder": "The project's title goes here…",
@ -210,7 +213,7 @@
"duplicate": { "duplicate": {
"title": "Duplicate this project", "title": "Duplicate this project",
"label": "Duplicate", "label": "Duplicate",
"text": "Select a namespace which should hold the duplicated project:", "text": "Select a parent project which should hold the duplicated project:",
"success": "The project was successfully duplicated." "success": "The project was successfully duplicated."
}, },
"edit": { "edit": {
@ -238,7 +241,7 @@
"namePlaceholder": "e.g. Lorem Ipsum", "namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.", "nameExplanation": "All actions done by this link share will show up with the name.",
"password": "Password (optional)", "password": "Password (optional)",
"passwordExplanation": "When authenticating, the user will be required to enter this password.", "passwordExplanation": "When signing in, the user will be required to enter this password.",
"noName": "No name set", "noName": "No name set",
"remove": "Remove a link share", "remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!", "removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
@ -321,67 +324,6 @@
} }
} }
}, },
"namespace": {
"title": "Namespaces & Projects",
"namespace": "Prostor",
"showArchived": "Zobrazit archivované",
"noneAvailable": "Momentálně nemáte žádné prostory.",
"unarchive": "Obnovit archiv",
"archived": "Archivováno",
"noProjects": "This namespace does not contain any projects.",
"createProject": "Create a new project in this namespace.",
"namespaces": "Prostory",
"search": "Začni psát pro vyhledání prostoru…",
"create": {
"title": "Nový prostor",
"titleRequired": "Uveďte prosím název.",
"explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
"tooltip": "Co je prostor?",
"success": "Prostor byl úspěšně vytvořen."
},
"archive": {
"titleArchive": "Archivovat \"{namespace}\"",
"titleUnarchive": "Odarchivovat \"{namespace}\"",
"archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
"unarchiveText": "You will be able to create new projects or edit it.",
"success": "Prostor byl úspěšně archivován.",
"unarchiveSuccess": "Jmenný prostor byl úspěšně obnoven.",
"description": "If a namespace is archived, you cannot create new projects or edit it."
},
"delete": {
"title": "Smazat \"{namespace}\"",
"text1": "Opravdu chcete odstranit tento prostor a všechen jeho obsah?",
"text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
"success": "Prostor byl úspěšně smazán."
},
"edit": {
"title": "Upravit \"{namespace}\"",
"success": "Prostor byl úspěšně aktualizován."
},
"share": {
"title": "Sdílet \"{namespace}\""
},
"attributes": {
"title": "Název prostoru",
"titlePlaceholder": "Název seznamu přijde sem…",
"description": "Popis",
"descriptionPlaceholder": "Popis seznamu přijde sem…",
"color": "Barva",
"archived": "Archivováno",
"isArchived": "Tento prostor je archivován"
},
"pseudo": {
"sharedProjects": {
"title": "Shared Projects"
},
"favorites": {
"title": "Oblíbené"
},
"savedFilters": {
"title": "Filtry"
}
}
},
"filters": { "filters": {
"title": "Filtry", "title": "Filtry",
"clear": "Vymazat filtry", "clear": "Vymazat filtry",
@ -403,7 +345,7 @@
}, },
"create": { "create": {
"title": "Nový uložený filtr", "title": "Nový uložený filtr",
"description": "A saved filter is a virtual project 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 project which is computed from a set of filters each time it is accessed.",
"action": "Vytvořit uložený filtr", "action": "Vytvořit uložený filtr",
"titleRequired": "Please provide a title for the filter." "titleRequired": "Please provide a title for the filter."
}, },
@ -529,7 +471,7 @@
"code": "Kód", "code": "Kód",
"quote": "Citace", "quote": "Citace",
"unorderedList": "Seznam s odrážkami", "unorderedList": "Seznam s odrážkami",
"orderedList ": "Ordered List", "orderedList": "Ordered List",
"cleanBlock": "Čistý blok", "cleanBlock": "Čistý blok",
"link": "Odkaz", "link": "Odkaz",
"image": "Obrázek", "image": "Obrázek",
@ -566,14 +508,14 @@
"canuse": "Můžete použít vzorec pro filtrování podle relativních datumů.", "canuse": "Můžete použít vzorec pro filtrování podle relativních datumů.",
"learnhow": "Podívejte se, jak to funguje", "learnhow": "Podívejte se, jak to funguje",
"title": "Datumový vzorec", "title": "Datumový vzorec",
"intro": "Datumový vzorec umožňuje určit relativní data, která jsou při použití filtru vyřešena za běhu Vikunjou.", "intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
"expression": "Každý datumový matematický výraz začíná datem ukotvení, které může být buď {0}, nebo datový řetězec končící {1}. Po tomto ukotvení může volitelně následovat jeden nebo více matematických výrazů.", "expression": "Každý datumový matematický výraz začíná datem ukotvení, které může být buď {0}, nebo datový řetězec končící {1}. Po tomto ukotvení může volitelně následovat jeden nebo více matematických výrazů.",
"similar": "Tyto výrazy jsou podobné výrazům poskytnutým {0} a {1}.", "similar": "Tyto výrazy jsou podobné výrazům poskytnutým {0} a {1}.",
"add1Day": "Přidat jeden den", "add1Day": "Přidat jeden den",
"minus1Day": "Odečíst jeden den", "minus1Day": "Odečíst jeden den",
"roundDay": "Zaokrouhlit dolů na nejbližší den", "roundDay": "Zaokrouhlit dolů na nejbližší den",
"supportedUnits": "Podporované časové jednotky jsou:", "supportedUnits": "Supported time units",
"someExamples": "Některé příklady časových výrazů:", "someExamples": "Examples of time expressions",
"units": { "units": {
"seconds": "Sekundy", "seconds": "Sekundy",
"minutes": "Minuty", "minutes": "Minuty",
@ -674,19 +616,13 @@
"updated": "Aktualizováno" "updated": "Aktualizováno"
}, },
"subscription": { "subscription": {
"subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.",
"subscribedTaskThroughParentNamespace": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto úkolu prostřednictvím jeho prostoru.",
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.", "subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedNamespace": "Nyní jste přihlášeni k odběru tohoto prostoru a budete dostávat oznámení o změnách.",
"notSubscribedNamespace": "Nejste přihlášeni k odběru tohoto prostoru, takže nebudete dostávat upozornění na změny.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.", "subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.", "notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "Nyní jste přihlášeni k odběru tohoto úkolu a budete dostávat oznámení o změnách.", "subscribedTask": "Nyní jste přihlášeni k odběru tohoto úkolu a budete dostávat oznámení o změnách.",
"notSubscribedTask": "Nejste přihlášeni k odběru tohoto úkolu, takže nebudete dostávat upozornění na změny.", "notSubscribedTask": "Nejste přihlášeni k odběru tohoto úkolu, takže nebudete dostávat upozornění na změny.",
"subscribe": "Odebírat", "subscribe": "Odebírat",
"unsubscribe": "Odhlásit odběr", "unsubscribe": "Odhlásit odběr",
"subscribeSuccessNamespace": "Nyní jste přihlášeni k tomuto prostoru",
"unsubscribeSuccessNamespace": "Nyní jste odhlášeni od tohoto prostoru",
"subscribeSuccessProject": "You are now subscribed to this project", "subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project", "unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "Nyní jste přihlášeni k tomuto úkolu", "subscribeSuccessTask": "Nyní jste přihlášeni k tomuto úkolu",
@ -763,7 +699,6 @@
"searchPlaceholder": "Hledejte nový úkol, který chcete přidat jako související…", "searchPlaceholder": "Hledejte nový úkol, který chcete přidat jako související…",
"createPlaceholder": "Přidat toto jako nový související úkol", "createPlaceholder": "Přidat toto jako nový související úkol",
"differentProject": "This task belongs to a different project.", "differentProject": "This task belongs to a different project.",
"differentNamespace": "Tento úkol patří do jiného prostoru.",
"noneYet": "Zatím žádné vztahy mezi úkoly.", "noneYet": "Zatím žádné vztahy mezi úkoly.",
"delete": "Odstranit vztah k úloze", "delete": "Odstranit vztah k úloze",
"deleteText1": "Jste si jisti, že chcete odstranit tento vztah úkolu?", "deleteText1": "Jste si jisti, že chcete odstranit tento vztah úkolu?",
@ -783,6 +718,17 @@
"copiedto": "Zkopírováno do | Zkopírováno do" "copiedto": "Zkopírováno do | Zkopírováno do"
} }
}, },
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
},
"repeat": { "repeat": {
"everyDay": "Každý den", "everyDay": "Každý den",
"everyWeek": "Každý týden", "everyWeek": "Každý týden",
@ -800,8 +746,7 @@
"invalidAmount": "Zadejte prosím více než 0." "invalidAmount": "Zadejte prosím více než 0."
}, },
"quickAddMagic": { "quickAddMagic": {
"hint": "Můžeš použít Kouzelné rychlé přidání", "hint": "Use magic prefixes to define due dates, assignees and other task properties.",
"what": "Co?",
"title": "Kouzelné rychlé přidání", "title": "Kouzelné rychlé přidání",
"intro": "Při vytváření úkolu můžete použít speciální klíčová slova pro přímé přidání atributů k nově vytvořenému úkolu. To umožňuje přidat běžně používané atributy k úkolům mnohem rychleji.", "intro": "Při vytváření úkolu můžete použít speciální klíčová slova pro přímé přidání atributů k nově vytvořenému úkolu. To umožňuje přidat běžně používané atributy k úkolům mnohem rychleji.",
"multiple": "Toto můžete použít několikrát.", "multiple": "Toto můžete použít několikrát.",
@ -848,19 +793,19 @@
"delete": { "delete": {
"header": "Smazat tým", "header": "Smazat tým",
"text1": "Jste si jisti, že chcete smazat tento tým a všechny jeho členy?", "text1": "Jste si jisti, že chcete smazat tento tým a všechny jeho členy?",
"text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!", "text2": "All team members will lose access to projects shared with this team. This CANNOT BE UNDONE!",
"success": "Tým byl úspěšně smazán." "success": "Tým byl úspěšně smazán."
}, },
"deleteUser": { "deleteUser": {
"header": "Odebrat uživatele z týmu", "header": "Odebrat uživatele z týmu",
"text1": "Opravdu chcete odebrat tohoto uživatele z týmu?", "text1": "Opravdu chcete odebrat tohoto uživatele z týmu?",
"text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
"success": "Uživatel byl úspěšně odstraněn z týmu." "success": "Uživatel byl úspěšně odstraněn z týmu."
}, },
"leave": { "leave": {
"title": "Opustit tým", "title": "Opustit tým",
"text1": "Opravdu chcete opustit tento tým?", "text1": "Opravdu chcete opustit tento tým?",
"text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.", "text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "Úspěšně jste opustili tým." "success": "Úspěšně jste opustili tým."
} }
}, },
@ -894,7 +839,10 @@
"color": "Změnit barvu tohoto úkolu", "color": "Změnit barvu tohoto úkolu",
"move": "Move this task to another project", "move": "Move this task to another project",
"reminder": "Spravovat připomenutí této úlohy", "reminder": "Spravovat připomenutí této úlohy",
"description": "Přepnout úpravy popisu úkolu" "description": "Přepnout úpravy popisu úkolu",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
}, },
"project": { "project": {
"title": "Project Views", "title": "Project Views",
@ -907,9 +855,9 @@
"title": "Navigace", "title": "Navigace",
"overview": "Přejít na přehled", "overview": "Přejít na přehled",
"upcoming": "Přejít na nadcházející úkoly", "upcoming": "Přejít na nadcházející úkoly",
"namespaces": "Navigate to namespaces & projects",
"labels": "Přejít na štítky", "labels": "Přejít na štítky",
"teams": "Přejít na týmy" "teams": "Přejít na týmy",
"projects": "Navigate to projects"
} }
}, },
"update": { "update": {
@ -924,7 +872,8 @@
"unarchive": "Zrušit archivaci", "unarchive": "Zrušit archivaci",
"setBackground": "Nastavit pozadí", "setBackground": "Nastavit pozadí",
"share": "Sdílet", "share": "Sdílet",
"newProject": "New project" "newProject": "New project",
"createProject": "Create project"
}, },
"apiConfig": { "apiConfig": {
"url": "Vikunja URL", "url": "Vikunja URL",
@ -943,7 +892,7 @@
"notification": { "notification": {
"title": "Oznámení", "title": "Oznámení",
"none": "Nemáte žádná oznámení. Mějte příjemný den!", "none": "Nemáte žádná oznámení. Mějte příjemný den!",
"explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen." "explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
}, },
"quickActions": { "quickActions": {
"commands": "Příkazy", "commands": "Příkazy",
@ -954,14 +903,12 @@
"teams": "Týmy", "teams": "Týmy",
"newProject": "Enter the title of the new project…", "newProject": "Enter the title of the new project…",
"newTask": "Zadejte název nového úkolu…", "newTask": "Zadejte název nového úkolu…",
"newNamespace": "Zadejte název nového prostoru…",
"newTeam": "Zadejte název nového týmu…", "newTeam": "Zadejte název nového týmu…",
"createTask": "Create a task in the current project ({title})", "createTask": "Create a task in the current project ({title})",
"createProject": "Create a project in the current namespace ({title})", "createProject": "Create a project",
"cmds": { "cmds": {
"newTask": "Nový úkol", "newTask": "Nový úkol",
"newProject": "New project", "newProject": "New project",
"newNamespace": "Nový prostor",
"newTeam": "Nový tým" "newTeam": "Nový tým"
} }
}, },
@ -1017,16 +964,9 @@
"4017": "Neplatný komparátor filtru úkolů.", "4017": "Neplatný komparátor filtru úkolů.",
"4018": "Neplatné zřetězení filtru úkolů.", "4018": "Neplatné zřetězení filtru úkolů.",
"4019": "Neplatná hodnota filtru úkolů.", "4019": "Neplatná hodnota filtru úkolů.",
"5001": "Prostor neexistuje.",
"5003": "Nemáte přístup ke zvolenému prostoru.",
"5006": "Název prostoru nemůže být prázdný.",
"5009": "Pro provedení této akce musíte mít k prostoru přístup ke čtení.",
"5010": "Tento tým nemá k tomuto prostoru přístup.",
"5011": "Tento uživatel již má přístup k tomuto prostoru.",
"5012": "Prostor je archivován, a proto je přístupný pouze pro čtení.",
"6001": "Název týmu nemůže být prázdný.", "6001": "Název týmu nemůže být prázdný.",
"6002": "Tým neexistuje.", "6002": "Tým neexistuje.",
"6004": "The team already has access to that namespace or project.", "6004": "The team already has access to that project.",
"6005": "Uživatel je již členem tohoto týmu.", "6005": "Uživatel je již členem tohoto týmu.",
"6006": "Nelze odstranit posledního člena týmu.", "6006": "Nelze odstranit posledního člena týmu.",
"6007": "The team does not have access to the project to perform that action.", "6007": "The team does not have access to the project to perform that action.",
@ -1052,5 +992,16 @@
"title": "O aplikaci", "title": "O aplikaci",
"frontendVersion": "Verze frontendu: {version}", "frontendVersion": "Verze frontendu: {version}",
"apiVersion": "Verze API: {version}" "apiVersion": "Verze API: {version}"
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
}
} }
} }

View File

@ -5,10 +5,9 @@
"welcomeDay": "Hej {username}!", "welcomeDay": "Hej {username}!",
"welcomeEvening": "Godaften {username}!", "welcomeEvening": "Godaften {username}!",
"lastViewed": "Sidst vist", "lastViewed": "Sidst vist",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"project": { "project": {
"newText": "You can create a new project for your new tasks:", "importText": "Import your projects and tasks from other services into Vikunja:",
"new": "New project",
"importText": "Or import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
}, },
@ -78,8 +77,8 @@
"savedSuccess": "Indstillingerne er gemt.", "savedSuccess": "Indstillingerne er gemt.",
"emailReminders": "Send mig påmindelser for opgaver via e-mail", "emailReminders": "Send mig påmindelser for opgaver via e-mail",
"overdueReminders": "Send mig en oversigt over mine ufærdige opgaver hver dag", "overdueReminders": "Send mig en oversigt over mine ufærdige opgaver hver dag",
"discoverableByName": "Lad andre brugere finde mig, når de søger efter mit navn", "discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
"discoverableByEmail": "Lad andre brugere finde mig, når de søger efter min fulde e-mail", "discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
"playSoundWhenDone": "Afspil en lyd, når du markerer opgaver som udført", "playSoundWhenDone": "Afspil en lyd, når du markerer opgaver som udført",
"weekStart": "Ugen starter på en", "weekStart": "Ugen starter på en",
"weekStartSunday": "Søndag", "weekStartSunday": "Søndag",
@ -143,7 +142,7 @@
}, },
"deletion": { "deletion": {
"title": "Slet din Vikunja konto", "title": "Slet din Vikunja konto",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.", "text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
"text2": "For at fortsætte, skal du indtaste din adgangskode. Du vil modtage en e-mail med yderligere instruktioner.", "text2": "For at fortsætte, skal du indtaste din adgangskode. Du vil modtage en e-mail med yderligere instruktioner.",
"confirm": "Slet min konto", "confirm": "Slet min konto",
"requestSuccess": "Anmodningen blev gennemført. Du vil modtage en e-mail med yderligere instruktioner.", "requestSuccess": "Anmodningen blev gennemført. Du vil modtage en e-mail med yderligere instruktioner.",
@ -157,7 +156,7 @@
}, },
"export": { "export": {
"title": "Eksporter dine Vikunja-data", "title": "Eksporter dine Vikunja-data",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", "description": "You can request a copy of all your Vikunja data. This includes Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"descriptionPasswordRequired": "Indtast venligst din adgangskode for at fortsætte:", "descriptionPasswordRequired": "Indtast venligst din adgangskode for at fortsætte:",
"request": "Anmod om en kopi af mine Vikunja-data", "request": "Anmod om en kopi af mine Vikunja-data",
"success": "Du har anmodet om dine Vikunja-data! Vi sender dig en e-mail, når den er klar til hentning.", "success": "Du har anmodet om dine Vikunja-data! Vi sender dig en e-mail, når den er klar til hentning.",
@ -165,14 +164,18 @@
} }
}, },
"project": { "project": {
"archived": "This project is archived. It is not possible to create new or edit tasks for it.", "archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archived",
"showArchived": "Show Archived",
"title": "Project Title", "title": "Project Title",
"color": "Color", "color": "Color",
"projects": "Projects", "projects": "Projects",
"parent": "Parent Project",
"search": "Type to search for a project…", "search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this project", "searchSelect": "Click or press enter to select this project",
"shared": "Shared Projects", "shared": "Shared Projects",
"noDescriptionAvailable": "No project description is available.", "noDescriptionAvailable": "No project description is available.",
"inboxTitle": "Inbox",
"create": { "create": {
"header": "New project", "header": "New project",
"titlePlaceholder": "The project's title goes here…", "titlePlaceholder": "The project's title goes here…",
@ -210,7 +213,7 @@
"duplicate": { "duplicate": {
"title": "Duplicate this project", "title": "Duplicate this project",
"label": "Duplicate", "label": "Duplicate",
"text": "Select a namespace which should hold the duplicated project:", "text": "Select a parent project which should hold the duplicated project:",
"success": "The project was successfully duplicated." "success": "The project was successfully duplicated."
}, },
"edit": { "edit": {
@ -238,7 +241,7 @@
"namePlaceholder": "e.g. Lorem Ipsum", "namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.", "nameExplanation": "All actions done by this link share will show up with the name.",
"password": "Password (optional)", "password": "Password (optional)",
"passwordExplanation": "When authenticating, the user will be required to enter this password.", "passwordExplanation": "When signing in, the user will be required to enter this password.",
"noName": "No name set", "noName": "No name set",
"remove": "Remove a link share", "remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!", "removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
@ -321,67 +324,6 @@
} }
} }
}, },
"namespace": {
"title": "Namespaces & Projects",
"namespace": "Navneområde",
"showArchived": "Vis arkiverede",
"noneAvailable": "Du har ingen navneområder lige nu.",
"unarchive": "Tilbagekald",
"archived": "Arkiveret",
"noProjects": "This namespace does not contain any projects.",
"createProject": "Create a new project in this namespace.",
"namespaces": "Navneområder",
"search": "Skriv for at søge efter et navneområde…",
"create": {
"title": "Nyt navneområde",
"titleRequired": "Angiv venligst en titel.",
"explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
"tooltip": "Hvad er et navneområde?",
"success": "Navneområdet blev oprettet."
},
"archive": {
"titleArchive": "Arkiver \"{namespace}\"",
"titleUnarchive": "Fjern arkivering \"{namespace}\"",
"archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
"unarchiveText": "You will be able to create new projects or edit it.",
"success": "Navneområdet blev arkiveret.",
"unarchiveSuccess": "Navneområdet blev tilbagekaldt.",
"description": "If a namespace is archived, you cannot create new projects or edit it."
},
"delete": {
"title": "Slet \"{namespace}\"",
"text1": "Er du sikker på, at du vil slette dette navneområde og alt dets indhold?",
"text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
"success": "Navneområdet blev slettet."
},
"edit": {
"title": "Rediger \"{namespace}\"",
"success": "Navneområdet blev opdateret."
},
"share": {
"title": "Del \"{namespace}\""
},
"attributes": {
"title": "Navneområde Titel",
"titlePlaceholder": "Navneområdets titel skrives her…",
"description": "Beskrivelse",
"descriptionPlaceholder": "Navneområdets beskrivelse skrives her…",
"color": "Farve",
"archived": "Er Arkiveret",
"isArchived": "Dette navneområde er arkiveret"
},
"pseudo": {
"sharedProjects": {
"title": "Shared Projects"
},
"favorites": {
"title": "Favoritter"
},
"savedFilters": {
"title": "Filtre"
}
}
},
"filters": { "filters": {
"title": "Filtre", "title": "Filtre",
"clear": "Ryd Filtre", "clear": "Ryd Filtre",
@ -403,7 +345,7 @@
}, },
"create": { "create": {
"title": "Nyt Gemt Filter", "title": "Nyt Gemt Filter",
"description": "A saved filter is a virtual project 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 project which is computed from a set of filters each time it is accessed.",
"action": "Opret nyt gemt filter", "action": "Opret nyt gemt filter",
"titleRequired": "Please provide a title for the filter." "titleRequired": "Please provide a title for the filter."
}, },
@ -529,7 +471,7 @@
"code": "Kode", "code": "Kode",
"quote": "Citat", "quote": "Citat",
"unorderedList": "Usorteret liste", "unorderedList": "Usorteret liste",
"orderedList ": "Ordered List", "orderedList": "Ordered List",
"cleanBlock": "Ryd Blok", "cleanBlock": "Ryd Blok",
"link": "Link", "link": "Link",
"image": "Billede", "image": "Billede",
@ -566,14 +508,14 @@
"canuse": "Du kan bruge datomatematik til at filtrere for relative datoer.", "canuse": "Du kan bruge datomatematik til at filtrere for relative datoer.",
"learnhow": "Se hvordan det virker", "learnhow": "Se hvordan det virker",
"title": "Datomatematik", "title": "Datomatematik",
"intro": "Dato Matematik giver dig mulighed for at angive relative datoer, som er løst løbende af Vikunja, når du anvender filteret.", "intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
"expression": "Hver Datomatematik udtryk starter med en ankerdato, som enten kan være {0} eller en datostreng, der slutter med {1}. Denneanker dato kan eventuelt efterfølges af en eller flere matematik udtryk.", "expression": "Hver Datomatematik udtryk starter med en ankerdato, som enten kan være {0} eller en datostreng, der slutter med {1}. Denneanker dato kan eventuelt efterfølges af en eller flere matematik udtryk.",
"similar": "Disse udtryk ligner dem fra {0} og {1}.", "similar": "Disse udtryk ligner dem fra {0} og {1}.",
"add1Day": "Læg en dag til", "add1Day": "Læg en dag til",
"minus1Day": "Træk en dag fra", "minus1Day": "Træk en dag fra",
"roundDay": "Rund ned til nærmeste dag", "roundDay": "Rund ned til nærmeste dag",
"supportedUnits": "Understøttede tidsenheder er:", "supportedUnits": "Supported time units",
"someExamples": "Eksempler på tidsudtryk:", "someExamples": "Examples of time expressions",
"units": { "units": {
"seconds": "Sekunder", "seconds": "Sekunder",
"minutes": "Minutter", "minutes": "Minutter",
@ -674,19 +616,13 @@
"updated": "Opdateret" "updated": "Opdateret"
}, },
"subscription": { "subscription": {
"subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.",
"subscribedTaskThroughParentNamespace": "Du kan ikke afmelde dig her, fordi du abonnerer på denne opgave gennem dens navneområde.",
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.", "subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedNamespace": "Du abonnerer i øjeblikket på dette navneområde og vil modtage notifikationer om ændringer.",
"notSubscribedNamespace": "Du abonnerer ikke på dette navneområde og modtager ikke notifikationer om ændringer.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.", "subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.", "notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "Du abonnerer på denne opgave og vil modtage notifikationer om ændringer.", "subscribedTask": "Du abonnerer på denne opgave og vil modtage notifikationer om ændringer.",
"notSubscribedTask": "Du abonnerer ikke på denne opgave og modtager ikke notifikationer om ændringer.", "notSubscribedTask": "Du abonnerer ikke på denne opgave og modtager ikke notifikationer om ændringer.",
"subscribe": "Abonner", "subscribe": "Abonner",
"unsubscribe": "Afmeld", "unsubscribe": "Afmeld",
"subscribeSuccessNamespace": "Du abonnerer nu på dette navneområde",
"unsubscribeSuccessNamespace": "Du er nu afmeldt dette navneområde",
"subscribeSuccessProject": "You are now subscribed to this project", "subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project", "unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "Du abonnerer nu på denne opgave", "subscribeSuccessTask": "Du abonnerer nu på denne opgave",
@ -763,7 +699,6 @@
"searchPlaceholder": "Indtast søgning efter en ny opgave der tilføjes som relateret…", "searchPlaceholder": "Indtast søgning efter en ny opgave der tilføjes som relateret…",
"createPlaceholder": "Tilføj dette som en ny relateret opgave", "createPlaceholder": "Tilføj dette som en ny relateret opgave",
"differentProject": "This task belongs to a different project.", "differentProject": "This task belongs to a different project.",
"differentNamespace": "Denne opgave hører til et andet navneområde.",
"noneYet": "Ingen opgaverelationer endnu.", "noneYet": "Ingen opgaverelationer endnu.",
"delete": "Slet Opgaverelation", "delete": "Slet Opgaverelation",
"deleteText1": "Er du sikker på, at du vil slette denne opgaverelation?", "deleteText1": "Er du sikker på, at du vil slette denne opgaverelation?",
@ -783,6 +718,17 @@
"copiedto": "Kopieret Til | Kopieret Til" "copiedto": "Kopieret Til | Kopieret Til"
} }
}, },
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
},
"repeat": { "repeat": {
"everyDay": "Hver Dag", "everyDay": "Hver Dag",
"everyWeek": "Hver Uge", "everyWeek": "Hver Uge",
@ -800,8 +746,7 @@
"invalidAmount": "Angiv venligst mere end 0." "invalidAmount": "Angiv venligst mere end 0."
}, },
"quickAddMagic": { "quickAddMagic": {
"hint": "Du kan bruge Hurtigtilføjelsesmagi", "hint": "Use magic prefixes to define due dates, assignees and other task properties.",
"what": "Hvad?",
"title": "Hurtigtilføjelsemagi", "title": "Hurtigtilføjelsemagi",
"intro": "Når du opretter en opgave, kan du bruge specielle søgeord til direkte at tilføje attributter til den nyoprettede opgave. Dette giver muligheden for at tilføje almindeligt anvendte attributter til opgaver meget hurtigere.", "intro": "Når du opretter en opgave, kan du bruge specielle søgeord til direkte at tilføje attributter til den nyoprettede opgave. Dette giver muligheden for at tilføje almindeligt anvendte attributter til opgaver meget hurtigere.",
"multiple": "Du kan bruge dette flere gange.", "multiple": "Du kan bruge dette flere gange.",
@ -848,19 +793,19 @@
"delete": { "delete": {
"header": "Slet holdet", "header": "Slet holdet",
"text1": "Er du sikker på du vil slette dette hold og alle dets medlemmer?", "text1": "Er du sikker på du vil slette dette hold og alle dets medlemmer?",
"text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!", "text2": "All team members will lose access to projects shared with this team. This CANNOT BE UNDONE!",
"success": "Holdet blev slettet." "success": "Holdet blev slettet."
}, },
"deleteUser": { "deleteUser": {
"header": "Fjern en bruger fra holdet", "header": "Fjern en bruger fra holdet",
"text1": "Er du sikker på du vil fjerne denne bruger fra holdet?", "text1": "Er du sikker på du vil fjerne denne bruger fra holdet?",
"text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
"success": "Brugeren blev fjernet fra holdet." "success": "Brugeren blev fjernet fra holdet."
}, },
"leave": { "leave": {
"title": "Forlad hold", "title": "Forlad hold",
"text1": "Er du sikker på du vil forlade dette hold?", "text1": "Er du sikker på du vil forlade dette hold?",
"text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.", "text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "Du har forladt holdet." "success": "Du har forladt holdet."
} }
}, },
@ -894,7 +839,10 @@
"color": "Skift farven på denne opgave", "color": "Skift farven på denne opgave",
"move": "Move this task to another project", "move": "Move this task to another project",
"reminder": "Administrer påmindelser om denne opgave", "reminder": "Administrer påmindelser om denne opgave",
"description": "Slå redigering af opgavebeskrivelse til/fra" "description": "Slå redigering af opgavebeskrivelse til/fra",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
}, },
"project": { "project": {
"title": "Project Views", "title": "Project Views",
@ -907,9 +855,9 @@
"title": "Navigation", "title": "Navigation",
"overview": "Gå til oversigt", "overview": "Gå til oversigt",
"upcoming": "Gå til kommende opgaver", "upcoming": "Gå til kommende opgaver",
"namespaces": "Navigate to namespaces & projects",
"labels": "Naviger til etiketter", "labels": "Naviger til etiketter",
"teams": "Naviger til hold" "teams": "Naviger til hold",
"projects": "Navigate to projects"
} }
}, },
"update": { "update": {
@ -924,7 +872,8 @@
"unarchive": "Tilbagekald", "unarchive": "Tilbagekald",
"setBackground": "Indstil baggrund", "setBackground": "Indstil baggrund",
"share": "Del", "share": "Del",
"newProject": "New project" "newProject": "New project",
"createProject": "Create project"
}, },
"apiConfig": { "apiConfig": {
"url": "Vikunja URL", "url": "Vikunja URL",
@ -943,7 +892,7 @@
"notification": { "notification": {
"title": "Notifikationer", "title": "Notifikationer",
"none": "Du har ingen notifikationer. Hav en dejlig dag!", "none": "Du har ingen notifikationer. Hav en dejlig dag!",
"explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen." "explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
}, },
"quickActions": { "quickActions": {
"commands": "Kommandoer", "commands": "Kommandoer",
@ -954,14 +903,12 @@
"teams": "Hold", "teams": "Hold",
"newProject": "Enter the title of the new project…", "newProject": "Enter the title of the new project…",
"newTask": "Indtast titlen på den nye opgave…", "newTask": "Indtast titlen på den nye opgave…",
"newNamespace": "Indtast titlen på det nye navneområde…",
"newTeam": "Indtast navnet på det nye hold…", "newTeam": "Indtast navnet på det nye hold…",
"createTask": "Create a task in the current project ({title})", "createTask": "Create a task in the current project ({title})",
"createProject": "Create a project in the current namespace ({title})", "createProject": "Create a project",
"cmds": { "cmds": {
"newTask": "Ny Opgave", "newTask": "Ny Opgave",
"newProject": "New project", "newProject": "New project",
"newNamespace": "Nyt navneområde",
"newTeam": "Nyt hold" "newTeam": "Nyt hold"
} }
}, },
@ -1017,16 +964,9 @@
"4017": "Ugyldig komparator til opgavefilter.", "4017": "Ugyldig komparator til opgavefilter.",
"4018": "Ugyldig sammenkædning til opgavefilter.", "4018": "Ugyldig sammenkædning til opgavefilter.",
"4019": "Ugyldig værdi til opgavefilter.", "4019": "Ugyldig værdi til opgavefilter.",
"5001": "Navneområdet findes ikke.",
"5003": "Du har ikke adgang til det angivne navneområde.",
"5006": "Navneområdets navn må ikke være tomt.",
"5009": "Du skal have navneområde læseadgang for at udføre denne handling.",
"5010": "Dette hold har ikke adgang til dette navneområde.",
"5011": "Denne bruger har allerede adgang til dette navneområde.",
"5012": "Navneområdet er arkiveret og kan derfor kun læses.",
"6001": "Holdnavnet må ikke være tomt.", "6001": "Holdnavnet må ikke være tomt.",
"6002": "Holdet findes ikke.", "6002": "Holdet findes ikke.",
"6004": "The team already has access to that namespace or project.", "6004": "The team already has access to that project.",
"6005": "Brugeren er allerede medlem af holdet.", "6005": "Brugeren er allerede medlem af holdet.",
"6006": "Kan ikke slette det sidste holdmedlem.", "6006": "Kan ikke slette det sidste holdmedlem.",
"6007": "The team does not have access to the project to perform that action.", "6007": "The team does not have access to the project to perform that action.",
@ -1052,5 +992,16 @@
"title": "Om", "title": "Om",
"frontendVersion": "Frontend Version: {version}", "frontendVersion": "Frontend Version: {version}",
"apiVersion": "API Version: {version}" "apiVersion": "API Version: {version}"
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
}
} }
} }

View File

@ -5,11 +5,10 @@
"welcomeDay": "Hallo {username}!", "welcomeDay": "Hallo {username}!",
"welcomeEvening": "Guten Abend, {username}!", "welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zuletzt angesehen", "lastViewed": "Zuletzt angesehen",
"addToHomeScreen": "Füge diese App deinem Startbildschirm hinzu, um schneller darauf zuzugreifen und das Erlebnis zu verbessern.",
"project": { "project": {
"newText": "Du kannst ein neues Projekt für deine neuen Aufgaben erstellen:", "importText": "Importiere deine Projekte und Aufgaben aus anderen Diensten in Vikunja:",
"new": "New project", "import": "Importiere deine Daten in Vikunja"
"importText": "Or import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
} }
}, },
"404": { "404": {
@ -78,14 +77,14 @@
"savedSuccess": "Die Einstellungen wurden erfolgreich aktualisiert.", "savedSuccess": "Die Einstellungen wurden erfolgreich aktualisiert.",
"emailReminders": "Erinnerungen an Aufgaben per E-Mail senden", "emailReminders": "Erinnerungen an Aufgaben per E-Mail senden",
"overdueReminders": "Sende mir jeden Tag eine Zusammenfassung meiner überfälligen Aufgaben", "overdueReminders": "Sende mir jeden Tag eine Zusammenfassung meiner überfälligen Aufgaben",
"discoverableByName": "Andere können mich finden, wenn sie nach meinem Namen suchen", "discoverableByName": "Erlaube anderen Benutzer:innen, mich als Mitglied zu Teams oder Projekten hinzuzufügen, wenn sie nach meinem Namen suchen",
"discoverableByEmail": "Andere können mich finden, wenn sie nach meiner kompletten E-Mail-Adresse suchen", "discoverableByEmail": "Erlaube anderen Benutzer:innen, mich als Mitglied zu Teams oder Projekten hinzuzufügen, wenn sie nach meiner vollständigen E-Mail Adresse suchen",
"playSoundWhenDone": "Einen Ton abspielen, wenn Aufgaben als erledigt markiert werden", "playSoundWhenDone": "Einen Ton abspielen, wenn Aufgaben als erledigt markiert werden",
"weekStart": "Woche beginnt am", "weekStart": "Woche beginnt am",
"weekStartSunday": "Sonntag", "weekStartSunday": "Sonntag",
"weekStartMonday": "Montag", "weekStartMonday": "Montag",
"language": "Sprache", "language": "Sprache",
"defaultProject": "Default Project", "defaultProject": "Standard-Projekt",
"timezone": "Zeitzone", "timezone": "Zeitzone",
"overdueTasksRemindersTime": "Zeit der E-Mail-Zusammenfassung der überfälligen Aufgaben" "overdueTasksRemindersTime": "Zeit der E-Mail-Zusammenfassung der überfälligen Aufgaben"
}, },
@ -143,7 +142,7 @@
}, },
"deletion": { "deletion": {
"title": "Lösche deinen Vikunja-Account", "title": "Lösche deinen Vikunja-Account",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.", "text1": "Das Löschen deines Accounts ist dauerhaft und unwiderruflich. Alle Projekte, Aufgaben und zugehörige Daten werden gelöscht.",
"text2": "Zum Fortfahren gib bitte dein Passwort ein. Du erhältst eine E-Mail mit weiteren Anweisungen.", "text2": "Zum Fortfahren gib bitte dein Passwort ein. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"confirm": "Meinen Account löschen", "confirm": "Meinen Account löschen",
"requestSuccess": "Die Anfrage war erfolgreich. Du erhältst eine E-Mail mit weiteren Anweisungen.", "requestSuccess": "Die Anfrage war erfolgreich. Du erhältst eine E-Mail mit weiteren Anweisungen.",
@ -157,7 +156,7 @@
}, },
"export": { "export": {
"title": "Exportiere deine Vikunja-Daten", "title": "Exportiere deine Vikunja-Daten",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", "description": "Du kannst eine Kopie deiner Daten bei Vikunja anfordern. Dazu gehören Projekte, Aufgaben und alles, was damit zusammenhängt. Du kannst diese Daten dann in jeder Vikunja-Instanz über die Migrationsfunktion importieren.",
"descriptionPasswordRequired": "Bitte gib dein Passwort ein, um fortzufahren:", "descriptionPasswordRequired": "Bitte gib dein Passwort ein, um fortzufahren:",
"request": "Eine Kopie meiner Vikunja Daten anfordern", "request": "Eine Kopie meiner Vikunja Daten anfordern",
"success": "Du hast deine Daten bei Vikunja erfolgreich angefordert! Wir schicken dir eine E-Mail, sobald sie zum Download bereitstehen.", "success": "Du hast deine Daten bei Vikunja erfolgreich angefordert! Wir schicken dir eine E-Mail, sobald sie zum Download bereitstehen.",
@ -165,220 +164,163 @@
} }
}, },
"project": { "project": {
"archived": "This project is archived. It is not possible to create new or edit tasks for it.", "archivedMessage": "Dieses Projekt ist archiviert. Es ist nicht möglich, neue Aufgaben zu erstellen oder es zu bearbeiten.",
"title": "Project Title", "archived": "Archiviert",
"color": "Color", "showArchived": "Archivierte anzeigen",
"projects": "Projects", "title": "Projekttitel",
"search": "Type to search for a project…", "color": "Farbe",
"searchSelect": "Click or press enter to select this project", "projects": "Projekte",
"shared": "Shared Projects", "parent": "Übergeordnetes Projekt",
"noDescriptionAvailable": "No project description is available.", "search": "Tippe, um nach einem Projekt zu suchen…",
"searchSelect": "Klicke oder drücke die Eingabetaste, um dieses Projekt auszuwählen",
"shared": "Geteilte Projekte",
"noDescriptionAvailable": "Keine Projektbeschreibung verfügbar.",
"inboxTitle": "Eingang",
"create": { "create": {
"header": "New project", "header": "Neues Projekt",
"titlePlaceholder": "The project's title goes here…", "titlePlaceholder": "Der Titel des Projekts kommt hier hin…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Bitte gebe einen Titel an.",
"createdSuccess": "The project was successfully created.", "createdSuccess": "Das Projekt wurde erfolgreich erstellt.",
"addProjectRequired": "Please specify a project or set a default project in the settings." "addProjectRequired": "Bitte gebe ein Projekt an oder lege ein Standard-Projekt in den Einstellungen fest."
}, },
"archive": { "archive": {
"title": "Archive \"{project}\"", "title": "„{project}“ archivieren",
"archive": "Archive this project", "archive": "Dieses Projekt archivieren",
"unarchive": "Un-Archive this project", "unarchive": "Archivierung dieses Projekts aufheben",
"unarchiveText": "You will be able to create new tasks or edit it.", "unarchiveText": "Du wirst neue Aufgaben erstellen oder sie bearbeiten können.",
"archiveText": "You won't be able to edit this project or create new tasks until you un-archive it.", "archiveText": "Du kannst dieses Projekt nicht bearbeiten oder neue Aufgaben erstellen, bis du die Archivierung aufhebst.",
"success": "The project was successfully archived." "success": "Das Projekt wurde erfolgreich archiviert."
}, },
"background": { "background": {
"title": "Set project background", "title": "Projekthintergrund festlegen",
"remove": "Remove Background", "remove": "Hintergrund entfernen",
"upload": "Choose a background from your pc", "upload": "Wähle einen Hintergrund von deinem Computer",
"searchPlaceholder": "Search for a background…", "searchPlaceholder": "Nach einem Hintergrund suchen…",
"poweredByUnsplash": "Powered by Unsplash", "poweredByUnsplash": "Powered by Unsplash",
"loadMore": "Load more photos", "loadMore": "Weitere Bilder laden",
"success": "The background has been set successfully!", "success": "Der Hintergrund wurde erfolgreich eingestellt!",
"removeSuccess": "The background has been removed successfully!" "removeSuccess": "Der Hintergrund wurde erfolgreich entfernt!"
}, },
"delete": { "delete": {
"title": "Delete \"{project}\"", "title": "„{project}“ löschen",
"header": "Delete this project", "header": "Dieses Projekt löschen",
"text1": "Are you sure you want to delete this project and all of its contents?", "text1": "Bist du sicher, dass du dieses Projekt und alle seine Inhalte löschen willst?",
"text2": "This includes all tasks and CANNOT BE UNDONE!", "text2": "Dies umfasst alle Aufgaben und kann NICHT rückgängig gemacht werden!",
"success": "The project was successfully deleted.", "success": "Das Projekt wurde erfolgreich gelöscht.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.", "tasksToDelete": "Dies löscht unwiderruflich ca. {count} Aufgaben.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." "noTasksToDelete": "Dieses Projekt enthält keine Aufgaben, es kann sicher gelöscht werden."
}, },
"duplicate": { "duplicate": {
"title": "Duplicate this project", "title": "Dupliziere dieses Projekt",
"label": "Duplicate", "label": "Duplizieren",
"text": "Select a namespace which should hold the duplicated project:", "text": "Wähle ein übergeordnetes Projekt aus, welches das duplizierte Projekt enthalten soll:",
"success": "The project was successfully duplicated." "success": "Das Projekt wurde erfolgreich dupliziert."
}, },
"edit": { "edit": {
"header": "Edit This Project", "header": "Dieses Projekt bearbeiten",
"title": "Edit \"{project}\"", "title": "„{project}“ bearbeiten",
"titlePlaceholder": "The project title goes here…", "titlePlaceholder": "Der Titel des Projekts kommt hier hin…",
"identifierTooltip": "The project identifier can be used to uniquely identify a task across projects. You can set it to empty to disable it.", "identifierTooltip": "Der Projektbezeichner kann zur eindeutigen Identifizierung einer Aufgabe über mehrere Projekte hinweg verwendet werden. Du kannst ihn auf leer setzen, um ihn zu deaktivieren.",
"identifier": "Project Identifier", "identifier": "Projektbezeichner",
"identifierPlaceholder": "The project identifier goes here…", "identifierPlaceholder": "Der Projektbezeichner kommt hierhin…",
"description": "Description", "description": "Beschreibung",
"descriptionPlaceholder": "The projects description goes here…", "descriptionPlaceholder": "Projektbeschreibung eingeben…",
"color": "Color", "color": "Farbe",
"success": "The project was successfully updated." "success": "Das Projekt wurde erfolgreich aktualisiert."
}, },
"share": { "share": {
"header": "Share this project", "header": "Projekt teilen",
"title": "Share \"{project}\"", "title": "„{project}“ teilen",
"share": "Share", "share": "Teilen",
"links": { "links": {
"title": "Share Links", "title": "Linkfreigaben",
"what": "What is a share link?", "what": "Was ist eine Linkfreigabe?",
"explanation": "Share Links allow you to easily share a project with other users who don't have an account on Vikunja.", "explanation": "Mit Linkfreigaben kannst Projekt du Listen mit Benutzer:innen ohne Vikunja-Account teilen.",
"create": "Create a new link share", "create": "Erstelle ein neue Linkfreigabe",
"name": "Name (optional)", "name": "Name (optional)",
"namePlaceholder": "e.g. Lorem Ipsum", "namePlaceholder": "z.B. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.", "nameExplanation": "Alle Aktionen, die mit dieser Linkfreigabe durchgeführt werden, werden mit diesem Namen angezeigt.",
"password": "Password (optional)", "password": "Passwort (optional)",
"passwordExplanation": "When authenticating, the user will be required to enter this password.", "passwordExplanation": "Bei der Authentifizierung wird der:die Benutzer:in aufgefordert, dieses Passwort einzugeben.",
"noName": "No name set", "noName": "Kein Name festgelegt",
"remove": "Remove a link share", "remove": "Linkfreigabe entfernen",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!", "removeText": "Bist du sicher, dass du diese Linkfreigabe unwiderruflich löschen möchtest? Über die Linkfreigabe ist danach der Zugriff auf dieses Projekt nicht mehr möglich!",
"createSuccess": "The link share was successfully created.", "createSuccess": "Die Linkfreigabe wurde erfolgreich erstellt.",
"deleteSuccess": "The link share was successfully deleted", "deleteSuccess": "Die Linkfreigabe wurde erfolgreich gelöscht",
"view": "View", "view": "Ansicht",
"sharedBy": "Shared by {0}" "sharedBy": "Von {0} geteilt"
}, },
"userTeam": { "userTeam": {
"typeUser": "user | users", "typeUser": "Benutzer:in | Benutzer:innen",
"typeTeam": "team | teams", "typeTeam": "Team | Teams",
"shared": "Shared with these {type}", "shared": "Geteilt mit diesen {type}",
"you": "You", "you": "Du",
"notShared": "Not shared with any {type} yet.", "notShared": "Noch nicht mit einem {type} geteilt.",
"removeHeader": "Remove a {type} from the {sharable}", "removeHeader": "Einen {type} von {sharable} entfernen",
"removeText": "Are you sure you want to remove this {sharable} from the {type}? This cannot be undone!", "removeText": "Diesen {sharable} von {type} entfernen? Dies kann nicht rückgängig gemacht werden!",
"removeSuccess": "The {sharable} was successfully removed from the {type}.", "removeSuccess": "{sharable} wurde erfolgreich von {type} entfernt.",
"addedSuccess": "The {type} was successfully added.", "addedSuccess": "{type} wurde erfolgreich hinzugefügt.",
"updatedSuccess": "The {type} was successfully added." "updatedSuccess": "{type} wurde erfolgreich hinzugefügt."
}, },
"right": { "right": {
"title": "Permission", "title": "Berechtigung",
"read": "Read only", "read": "Nur Leserechte",
"readWrite": "Read & write", "readWrite": "Lesen & Schreiben",
"admin": "Admin" "admin": "Admin"
}, },
"attributes": { "attributes": {
"link": "Link", "link": "Link",
"delete": "Delete" "delete": "Löschen"
} }
}, },
"list": { "list": {
"title": "List", "title": "Liste",
"add": "Add", "add": "Hinzufügen",
"addPlaceholder": "Add a new task…", "addPlaceholder": "Neue Aufgabe hinzufügen…",
"empty": "This project is currently empty.", "empty": "Dieses Project ist derzeit leer.",
"newTaskCta": "Create a new task.", "newTaskCta": "Eine neue Aufgabe erstellen.",
"editTask": "Edit Task" "editTask": "Aufgabe bearbeiten"
}, },
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Aufgaben anzeigen, für die keine Daten festgelegt sind",
"size": "Size", "size": "Größe",
"default": "Default", "default": "Standard",
"month": "Month", "month": "Monat",
"day": "Day", "day": "Tag",
"hour": "Hour", "hour": "Stunde",
"range": "Date Range", "range": "Zeitraum",
"noDates": "This task has no dates set." "noDates": "Diese Aufgabe hat keine Daten definiert."
}, },
"table": { "table": {
"title": "Table", "title": "Tabelle",
"columns": "Columns" "columns": "Spalten"
}, },
"kanban": { "kanban": {
"title": "Kanban", "title": "Kanban",
"limit": "Limit: {limit}", "limit": "Limit: {limit}",
"noLimit": "Not Set", "noLimit": "Nicht gesetzt",
"doneBucket": "Done bucket", "doneBucket": "Erledigt Spalte",
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.", "doneBucketHint": "Alle Aufgaben, die in diese Spalte verschoben werden, werden automatisch als erledigt markiert.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.", "doneBucketHintExtended": "Alle Aufgaben, die in die Erledigt Spalte verschoben wurden, werden automatisch als erledigt markiert. Aufgaben, die in einer anderen Spalte als Erledigt markiert wurden, werden auch in diese Spalte verschoben.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.", "doneBucketSavedSuccess": "Erledigt Spalte gespeichert.",
"deleteLast": "You cannot remove the last bucket.", "deleteLast": "Du kannst die letzte Spalte nicht entfernen.",
"addTaskPlaceholder": "Enter the new task title…", "addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
"addTask": "Add a task", "addTask": "Eine Aufgabe hinzufügen",
"addAnotherTask": "Add another task", "addAnotherTask": "Weitere Aufgabe hinzufügen",
"addBucket": "Create a new bucket", "addBucket": "Eine neue Spalte erstellen",
"addBucketPlaceholder": "Enter the new bucket title…", "addBucketPlaceholder": "Gebe einen Spaltentitel ein…",
"deleteHeaderBucket": "Delete the bucket", "deleteHeaderBucket": "Spalte löschen",
"deleteBucketText1": "Are you sure you want to delete this bucket?", "deleteBucketText1": "Bist du sicher, dass du diese Spalte löschen möchtest?",
"deleteBucketText2": "This will not delete any tasks but move them into the default bucket.", "deleteBucketText2": "Dies löscht keine Aufgaben, sondern verschiebt sie in die Standardspalte.",
"deleteBucketSuccess": "The bucket has been deleted successfully.", "deleteBucketSuccess": "Die Spalte wurde erfolgreich gelöscht.",
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.", "bucketTitleSavedSuccess": "Der Spaltenname wurde erfolgreich gespeichert.",
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.", "bucketLimitSavedSuccess": "Das Spaltenlimit wurde erfolgreich gespeichert.",
"collapse": "Collapse this bucket" "collapse": "Spalte einklappen"
}, },
"pseudo": { "pseudo": {
"favorites": {
"title": "Favorites"
}
}
},
"namespace": {
"title": "Namespaces & Projects",
"namespace": "Namespace",
"showArchived": "Archivierte anzeigen",
"noneAvailable": "Du hast momentan keine Namespaces.",
"unarchive": "Archivierung aufheben",
"archived": "Archiviert",
"noProjects": "This namespace does not contain any projects.",
"createProject": "Create a new project in this namespace.",
"namespaces": "Namespaces",
"search": "Beginne zu schreiben, um einen Namespace zu suchen…",
"create": {
"title": "Neuer Namespace",
"titleRequired": "Bitte gebe einen Titel an.",
"explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
"tooltip": "Was ist ein Namespace?",
"success": "Der Namespace wurde erfolgreich erstellt."
},
"archive": {
"titleArchive": "„{namespace}“ archivieren",
"titleUnarchive": "Archivierung von \"{namespace}\" aufheben",
"archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
"unarchiveText": "You will be able to create new projects or edit it.",
"success": "Der Namespace wurde erfolgreich archiviert.",
"unarchiveSuccess": "Der Namespace wurde erfolgreich wiederhergestellt.",
"description": "If a namespace is archived, you cannot create new projects or edit it."
},
"delete": {
"title": "„{namespace}“ löschen",
"text1": "Diesen Namespace mit sämtlichem Inhalt löschen?",
"text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
"success": "Der Namespace wurde erfolgreich gelöscht."
},
"edit": {
"title": "„{namespace}“ bearbeiten",
"success": "Der Namespace wurde erfolgreich aktualisiert."
},
"share": {
"title": "„{namespace}“ teilen"
},
"attributes": {
"title": "Namespace Titel",
"titlePlaceholder": "Titel des Namespace angeben…",
"description": "Beschreibung",
"descriptionPlaceholder": "Beschreibung für den Namespace eingeben…",
"color": "Farbe",
"archived": "Ist archiviert",
"isArchived": "Dieser Namespace ist archiviert"
},
"pseudo": {
"sharedProjects": {
"title": "Shared Projects"
},
"favorites": { "favorites": {
"title": "Favoriten" "title": "Favoriten"
},
"savedFilters": {
"title": "Filter"
} }
} }
}, },
@ -403,7 +345,7 @@
}, },
"create": { "create": {
"title": "Neuer gespeicherter Filter", "title": "Neuer gespeicherter Filter",
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "Ein gespeicherter Filter ist ein virtuelles Projekt, das bei jedem Zugriff aus einem Satz von Filtern errechnet wird.",
"action": "Neuen gespeicherten Filter erstellen", "action": "Neuen gespeicherten Filter erstellen",
"titleRequired": "Bitte gib den Titel für den Filter an." "titleRequired": "Bitte gib den Titel für den Filter an."
}, },
@ -435,7 +377,7 @@
"label": { "label": {
"title": "Labels", "title": "Labels",
"manage": "Label verwalten", "manage": "Label verwalten",
"description": "Click on a label to edit it. You can edit all labels you created, you can use all labels which are associated with a task to whose project you have access.", "description": "Klicke auf ein Label um es zu editieren. Du kannst alle Labels, welche du erstellt hast, editieren. Du kannst alle Labels, welche mit einer Aufgabe verknüpft sind, auf die du Zugriff hast, benutzen.",
"newCTA": "Du hast momentan keine Labels.", "newCTA": "Du hast momentan keine Labels.",
"search": "Beginne zu schreiben, um nach einem Label zu suchen…", "search": "Beginne zu schreiben, um nach einem Label zu suchen…",
"create": { "create": {
@ -460,7 +402,7 @@
}, },
"sharing": { "sharing": {
"authenticating": "Authentifizierung …", "authenticating": "Authentifizierung …",
"passwordRequired": "This shared project requires a password. Please enter it below:", "passwordRequired": "Dieses geteilte Projekt benötigt ein Passwort. Bitte gebe es unten ein:",
"error": "Es ist ein Fehler aufgetreten.", "error": "Es ist ein Fehler aufgetreten.",
"invalidPassword": "Das Passwort ist ungültig." "invalidPassword": "Das Passwort ist ungültig."
}, },
@ -529,7 +471,7 @@
"code": "Code", "code": "Code",
"quote": "Zitat", "quote": "Zitat",
"unorderedList": "Ungeordnete Liste", "unorderedList": "Ungeordnete Liste",
"orderedList ": "Ordered List", "orderedList": "Geordnete Liste",
"cleanBlock": "Formatierung löschen", "cleanBlock": "Formatierung löschen",
"link": "Link", "link": "Link",
"image": "Bild", "image": "Bild",
@ -566,14 +508,14 @@
"canuse": "Du kannst Datumsberechnung verwenden, um nach relativen Daten zu filtern.", "canuse": "Du kannst Datumsberechnung verwenden, um nach relativen Daten zu filtern.",
"learnhow": "Sieh dir an, wie es funktioniert", "learnhow": "Sieh dir an, wie es funktioniert",
"title": "Datumsberechnung", "title": "Datumsberechnung",
"intro": "Die Datumsberechnung erlaubt es, relative Daten anzugeben, die bei der Anwendung des Filters von Vikunja aufgelöst werden.", "intro": "Du kannst relative Daten angeben, die bei der Anwendung des Filters von Vikunja aufgelöst werden.",
"expression": "Jeder Ausdruck der Datumsberechnung beginnt mit einem Datumswert, welcher entweder {0} sein kann oder mit {1} endet. Auf diesen Datumswert kann optional ein oder mehrere mathematische Ausdrücke folgen.", "expression": "Jeder Ausdruck der Datumsberechnung beginnt mit einem Datumswert, welcher entweder {0} sein kann oder mit {1} endet. Auf diesen Datumswert kann optional ein oder mehrere mathematische Ausdrücke folgen.",
"similar": "Diese Ausdrücke ähneln denen von {0} und {1}.", "similar": "Diese Ausdrücke ähneln denen von {0} und {1}.",
"add1Day": "Einen Tag hinzufügen", "add1Day": "Einen Tag hinzufügen",
"minus1Day": "Einen Tag abziehen", "minus1Day": "Einen Tag abziehen",
"roundDay": "Auf den nächsten Tag abrunden", "roundDay": "Auf den nächsten Tag abrunden",
"supportedUnits": "Unterstützte Zeiteinheiten sind:", "supportedUnits": "Unterstützte Zeiteinheiten",
"someExamples": "Einige Beispiele für Zeitausdrücke:", "someExamples": "Beispiele für Zeitausdrücke",
"units": { "units": {
"seconds": "Sekunden", "seconds": "Sekunden",
"minutes": "Minuten", "minutes": "Minuten",
@ -619,7 +561,7 @@
"chooseDueDate": "Klicke hier, um ein Fälligkeitsdatum zu setzen", "chooseDueDate": "Klicke hier, um ein Fälligkeitsdatum zu setzen",
"chooseStartDate": "Klicke hier, um ein Startdatum zu setzen", "chooseStartDate": "Klicke hier, um ein Startdatum zu setzen",
"chooseEndDate": "Klicke hier, um ein Enddatum zu setzen", "chooseEndDate": "Klicke hier, um ein Enddatum zu setzen",
"move": "Move task to a different project", "move": "Aufgabe in ein anderes Projekt verschieben",
"done": "Als erledigt markieren!", "done": "Als erledigt markieren!",
"undone": "Als nicht erledigt markieren", "undone": "Als nicht erledigt markieren",
"created": "Erstellt {0} von {1}", "created": "Erstellt {0} von {1}",
@ -627,7 +569,7 @@
"doneAt": "Erledigt {0}", "doneAt": "Erledigt {0}",
"updateSuccess": "Die Aufgabe wurde erfolgreich gespeichert.", "updateSuccess": "Die Aufgabe wurde erfolgreich gespeichert.",
"deleteSuccess": "Die Aufgabe wurde erfolgreich gelöscht.", "deleteSuccess": "Die Aufgabe wurde erfolgreich gelöscht.",
"belongsToProject": "This task belongs to project '{project}'", "belongsToProject": "Diese Aufgabe gehört zum Projekt „{project}“",
"due": "Fällig {at}", "due": "Fällig {at}",
"closePopup": "Popup schließen", "closePopup": "Popup schließen",
"delete": { "delete": {
@ -647,7 +589,7 @@
"percentDone": "Fortschritt einstellen", "percentDone": "Fortschritt einstellen",
"attachments": "Anhänge hinzufügen", "attachments": "Anhänge hinzufügen",
"relatedTasks": "Beziehung hinzufügen", "relatedTasks": "Beziehung hinzufügen",
"moveProject": "Move", "moveProject": "Verschieben",
"color": "Farbe setzen", "color": "Farbe setzen",
"delete": "Löschen", "delete": "Löschen",
"favorite": "Zu Favoriten hinzufügen", "favorite": "Zu Favoriten hinzufügen",
@ -674,21 +616,15 @@
"updated": "Aktualisiert" "updated": "Aktualisiert"
}, },
"subscription": { "subscription": {
"subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.", "subscribedTaskThroughParentProject": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihr Projekt abonniert hast.",
"subscribedTaskThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihren Namespace abonniert hast.", "subscribedProject": "Du hast dieses Projekt abonniert und erhältst Benachrichtigungen über Änderungen.",
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.", "notSubscribedProject": "Du hast dieses Projekt nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedNamespace": "Du hast diesen Namespace abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedNamespace": "Du hast diesen Namespace nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "Du hast diese Aufgabe abonniert und erhältst Benachrichtigungen über Änderungen.", "subscribedTask": "Du hast diese Aufgabe abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedTask": "Du hast diese Aufgabe nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.", "notSubscribedTask": "Du hast diese Aufgabe nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribe": "Abonnieren", "subscribe": "Abonnieren",
"unsubscribe": "Abbestellen", "unsubscribe": "Abbestellen",
"subscribeSuccessNamespace": "Du hast diesen Namespace jetzt abonniert", "subscribeSuccessProject": "Du hast dieses Projekt jetzt abonniert",
"unsubscribeSuccessNamespace": "Du hast diesen Namespace jetzt nicht mehr abonniert", "unsubscribeSuccessProject": "Du hast dieses Projekt jetzt nicht mehr abonniert",
"subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "Du hast diese Aufgabe jetzt abonniert", "subscribeSuccessTask": "Du hast diese Aufgabe jetzt abonniert",
"unsubscribeSuccessTask": "Du hast diese Aufgabe jetzt nicht mehr abonniert" "unsubscribeSuccessTask": "Du hast diese Aufgabe jetzt nicht mehr abonniert"
}, },
@ -762,8 +698,7 @@
"new": "Neue Aufgabenbeziehung", "new": "Neue Aufgabenbeziehung",
"searchPlaceholder": "Beginne zu schreiben, um eine Aufgabe zu suchen, die als Beziehung hinzugefügt werden soll…", "searchPlaceholder": "Beginne zu schreiben, um eine Aufgabe zu suchen, die als Beziehung hinzugefügt werden soll…",
"createPlaceholder": "Füge diese Aufgabe als neue Aufgabenbeziehung hinzu", "createPlaceholder": "Füge diese Aufgabe als neue Aufgabenbeziehung hinzu",
"differentProject": "This task belongs to a different project.", "differentProject": "Diese Aufgabe gehört zu einem anderen Projekt.",
"differentNamespace": "Diese Aufgabe gehört zu einem anderen Namespace.",
"noneYet": "Keine Aufgabenbeziehung vorhanden.", "noneYet": "Keine Aufgabenbeziehung vorhanden.",
"delete": "Aufgabenbeziehung entfernen", "delete": "Aufgabenbeziehung entfernen",
"deleteText1": "Willst du diese Aufgabenbeziehung wirklich entfernen?", "deleteText1": "Willst du diese Aufgabenbeziehung wirklich entfernen?",
@ -783,6 +718,17 @@
"copiedto": "Kopiert nach | Kopiert nach" "copiedto": "Kopiert nach | Kopiert nach"
} }
}, },
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
},
"repeat": { "repeat": {
"everyDay": "Jeden Tag", "everyDay": "Jeden Tag",
"everyWeek": "Jede Woche", "everyWeek": "Jede Woche",
@ -800,8 +746,7 @@
"invalidAmount": "Bitte mehr als 0 eingeben." "invalidAmount": "Bitte mehr als 0 eingeben."
}, },
"quickAddMagic": { "quickAddMagic": {
"hint": "Du kannst Quick Add Magic verwenden", "hint": "Verwende magische Präfixe, um Fälligkeitsdaten, Zuweisungen und andere Aufgabeneigenschaften zu definieren.",
"what": "Was?",
"title": "Quick Add Magic", "title": "Quick Add Magic",
"intro": "Beim Erstellen einer Aufgabe kannst du spezielle Schlüsselwörter verwenden, um Attribute direkt zu der neu erstellten Aufgabe hinzuzufügen. Dadurch können häufig verwendete Attribute schneller zu Aufgaben hinzugefügt werden.", "intro": "Beim Erstellen einer Aufgabe kannst du spezielle Schlüsselwörter verwenden, um Attribute direkt zu der neu erstellten Aufgabe hinzuzufügen. Dadurch können häufig verwendete Attribute schneller zu Aufgaben hinzugefügt werden.",
"multiple": "Du kannst das mehrmals benutzen.", "multiple": "Du kannst das mehrmals benutzen.",
@ -812,10 +757,10 @@
"priority1": "Um die Priorität einer Aufgabe zu setzen, gibt eine Zahl zwischen 1 und 5 mit einem vorangestellten {prefix} ein.", "priority1": "Um die Priorität einer Aufgabe zu setzen, gibt eine Zahl zwischen 1 und 5 mit einem vorangestellten {prefix} ein.",
"priority2": "Je höher die Zahl, desto höher die Priorität.", "priority2": "Je höher die Zahl, desto höher die Priorität.",
"assignees": "Um die Aufgabe direkt jemandem zuzuweisen, füge vor dem Anmeldenamen der Person ein {prefix} Zeichen ein.", "assignees": "Um die Aufgabe direkt jemandem zuzuweisen, füge vor dem Anmeldenamen der Person ein {prefix} Zeichen ein.",
"project1": "To set a project for the task to appear in, enter its name prefixed with {prefix}.", "project1": "Um ein Projekt für die Aufgabe festzulegen, gib seinen Namen mit einem vorangestellten {prefix} ein.",
"project2": "This will return an error if the project does not exist.", "project2": "Dies gibt einen Fehler zurück, wenn das Projekt nicht existiert.",
"project3": "To use spaces, simply add a \" or ' around the project name.", "project3": "Um Leerzeichen zu verwenden, füge einfach ein \" oder ' um den Namen des Projekts hinzu.",
"project4": "For example: {prefix}\"Project with spaces\".", "project4": "Zum Beispiel: {prefix}\"Projekt mit Leerzeichen\".",
"dateAndTime": "Datum und Uhrzeit", "dateAndTime": "Datum und Uhrzeit",
"date": "Jedes Datum wird als Enddatum der neuen Aufgabe verwendet. Du kannst Daten in jedem dieser Formate verwenden:", "date": "Jedes Datum wird als Enddatum der neuen Aufgabe verwendet. Du kannst Daten in jedem dieser Formate verwenden:",
"dateWeekday": "jeder Wochentag, wird das nächste Datum mit diesem Tag verwenden", "dateWeekday": "jeder Wochentag, wird das nächste Datum mit diesem Tag verwenden",
@ -848,19 +793,19 @@
"delete": { "delete": {
"header": "Team löschen", "header": "Team löschen",
"text1": "Bist du sicher, dass du dieses Team und alle seine Mitglieder löschen willst?", "text1": "Bist du sicher, dass du dieses Team und alle seine Mitglieder löschen willst?",
"text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!", "text2": "Alle Teammitglieder verlieren den Zugriff auf Projekte, die mit diesem Team geteilt sind. Dies KANN NICHT rückgängig gemacht werden!",
"success": "Das Team wurde erfolgreich gelöscht." "success": "Das Team wurde erfolgreich gelöscht."
}, },
"deleteUser": { "deleteUser": {
"header": "Benutzer:innen aus dem Team entfernen", "header": "Benutzer:innen aus dem Team entfernen",
"text1": "Bist du sicher, dass du diese:n Benutzer:in aus dem Team entfernen willst?", "text1": "Bist du sicher, dass du diese:n Benutzer:in aus dem Team entfernen willst?",
"text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Projekte, auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!",
"success": "Der:die Benutzer:in wurde erfolgreich aus dem Team gelöscht." "success": "Der:die Benutzer:in wurde erfolgreich aus dem Team gelöscht."
}, },
"leave": { "leave": {
"title": "Team verlassen", "title": "Team verlassen",
"text1": "Bist du sicher, dass du dieses Team verlassen willst?", "text1": "Bist du sicher, dass du dieses Team verlassen willst?",
"text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.", "text2": "Du wirst Zugriff auf alle Projekte verlieren, auf die dieses Team Zugriff hat. Wenn du deine Meinung änderst, musst du durch einen Team-Admin wieder hinzugefügt werden.",
"success": "Du hast das Team erfolgreich verlassen." "success": "Du hast das Team erfolgreich verlassen."
} }
}, },
@ -892,24 +837,27 @@
"attachment": "Einen Anhang dieser Aufgabe hinzufügen", "attachment": "Einen Anhang dieser Aufgabe hinzufügen",
"related": "Ändere die Abhängigen Aufgaben dieser Aufgabe", "related": "Ändere die Abhängigen Aufgaben dieser Aufgabe",
"color": "Die Farbe dieser Aufgabe ändern", "color": "Die Farbe dieser Aufgabe ändern",
"move": "Move this task to another project", "move": "Aufgabe in ein anderes Projekt verschieben",
"reminder": "Erinnerungen für diese Aufgabe verwalten", "reminder": "Erinnerungen für diese Aufgabe verwalten",
"description": "Aufgabenbeschreibung bearbeiten" "description": "Aufgabenbeschreibung bearbeiten",
"delete": "Diese Aufgabe löschen",
"priority": "Die Priorität dieser Aufgabe ändern",
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen"
}, },
"project": { "project": {
"title": "Project Views", "title": "Projektansichten",
"switchToListView": "Switch to list view", "switchToListView": "Zu Listenansicht wechseln",
"switchToGanttView": "Switch to gantt view", "switchToGanttView": "Zur Ganttansicht wechseln",
"switchToKanbanView": "Switch to kanban view", "switchToKanbanView": "Zur Kanbanansicht wechseln",
"switchToTableView": "Switch to table view" "switchToTableView": "Zur Tabellenansicht wechseln"
}, },
"navigation": { "navigation": {
"title": "Navigation", "title": "Navigation",
"overview": "Die Startseite aufrufen", "overview": "Die Startseite aufrufen",
"upcoming": "Anstehende Aufgaben aufrufen", "upcoming": "Anstehende Aufgaben aufrufen",
"namespaces": "Navigate to namespaces & projects",
"labels": "Labels aufrufen", "labels": "Labels aufrufen",
"teams": "Teams aufrufen" "teams": "Teams aufrufen",
"projects": "Projekte aufrufen"
} }
}, },
"update": { "update": {
@ -924,7 +872,8 @@
"unarchive": "Archivierung aufheben", "unarchive": "Archivierung aufheben",
"setBackground": "Hintergrund einstellen", "setBackground": "Hintergrund einstellen",
"share": "Teilen", "share": "Teilen",
"newProject": "New project" "newProject": "Neues Projekt",
"createProject": "Projekt erstellen"
}, },
"apiConfig": { "apiConfig": {
"url": "Vikunja-URL", "url": "Vikunja-URL",
@ -943,25 +892,23 @@
"notification": { "notification": {
"title": "Benachrichtigungen", "title": "Benachrichtigungen",
"none": "Du hast keine Benachrichtigungen. Einen schönen Tag noch!", "none": "Du hast keine Benachrichtigungen. Einen schönen Tag noch!",
"explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen." "explainer": "Benachrichtigungen werden hier angezeigt, wenn Aktionen für Projekte oder Aufgaben, die du abonniert hast, ausgeführt werden."
}, },
"quickActions": { "quickActions": {
"commands": "Befehle", "commands": "Befehle",
"placeholder": "Gib einen Befehl oder eine Suche ein …", "placeholder": "Gib einen Befehl oder eine Suche ein …",
"hint": "You can use {project} to limit the search to a project. Combine {project} or {label} (labels) with a search query to search for a task with these labels or on that project. Use {assignee} to only search for teams.", "hint": "Du kannst {project} verwenden, um die Suche auf ein Projekt zu beschränken. Kombiniere {project} oder {label} (Labels) mit einer Suchabfrage, um eine Aufgabe mit diesen Labels oder auf diesem Projekt zu suchen. Verwende {assignee}, um nur nach Teams zu suchen.",
"tasks": "Aufgaben", "tasks": "Aufgaben",
"projects": "Projects", "projects": "Projekte",
"teams": "Teams", "teams": "Teams",
"newProject": "Enter the title of the new project…", "newProject": "Gib den Titel des neuen Projekts ein…",
"newTask": "Gib den Titel der neuen Aufgabe ein …", "newTask": "Gib den Titel der neuen Aufgabe ein …",
"newNamespace": "Gib den Titel des neuen Namespaces ein…",
"newTeam": "Gib den Namen des neuen Teams ein …", "newTeam": "Gib den Namen des neuen Teams ein …",
"createTask": "Create a task in the current project ({title})", "createTask": "Eine Aufgabe im aktuellen Projekt erstellen ({title})",
"createProject": "Create a project in the current namespace ({title})", "createProject": "Projekt erstellen",
"cmds": { "cmds": {
"newTask": "Neue Aufgabe", "newTask": "Neue Aufgabe",
"newProject": "New project", "newProject": "Neues Projekt",
"newNamespace": "Neuer Namespace",
"newTeam": "Neues Team" "newTeam": "Neues Team"
} }
}, },
@ -992,15 +939,15 @@
"1018": "Die Avatareinstellungen sind falsch.", "1018": "Die Avatareinstellungen sind falsch.",
"2001": "Die ID kann nicht leer oder 0 sein.", "2001": "Die ID kann nicht leer oder 0 sein.",
"2002": "Ein Teil der Anfragedaten ist ungültig.", "2002": "Ein Teil der Anfragedaten ist ungültig.",
"3001": "The project does not exist.", "3001": "Das Projekt ist nicht vorhanden.",
"3004": "You need to have read permissions on that project to perform that action.", "3004": "Um das zu machen, benötigst du eine Leseberechtigung für dieses Projekt.",
"3005": "The project title cannot be empty.", "3005": "Der Projekttitel darf nicht leer sein.",
"3006": "The project share does not exist.", "3006": "Diese Linkfreigabe existiert nicht.",
"3007": "A project with this identifier already exists.", "3007": "Ein Projekt mit diesem Bezeichner existiert bereits.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.", "3008": "Dieses Projekt ist archiviert und kann deshalb nur gelesen werden. Dies gilt auch für alle Aufgaben, die mit diesem Projekt verbunden sind.",
"4001": "The project task text cannot be empty.", "4001": "Der Aufgabentitel kann nicht leer sein.",
"4002": "The project task does not exist.", "4002": "Diese Aufgabe existiert nicht.",
"4003": "All bulk editing tasks must belong to the same project.", "4003": "Alle Massenbearbeitungen an Aufgaben müssen zum selben Projekt gehören.",
"4004": "Es benötigt mindestens einen Task, um eine Massenänderung durchzuführen.", "4004": "Es benötigt mindestens einen Task, um eine Massenänderung durchzuführen.",
"4005": "Du hast keine Berechtigungen, um diese Aufgabe anzuzeigen.", "4005": "Du hast keine Berechtigungen, um diese Aufgabe anzuzeigen.",
"4006": "Du kannst die übergeordnete Aufgabe nicht auf sich selbst referenzieren.", "4006": "Du kannst die übergeordnete Aufgabe nicht auf sich selbst referenzieren.",
@ -1017,30 +964,23 @@
"4017": "Ungültiger Aufgabenfilter (Vergleichskriterium).", "4017": "Ungültiger Aufgabenfilter (Vergleichskriterium).",
"4018": "Ungültige Verkettung von Aufgabenfiltern.", "4018": "Ungültige Verkettung von Aufgabenfiltern.",
"4019": "Ungültiger Aufgabenfilter (Wert).", "4019": "Ungültiger Aufgabenfilter (Wert).",
"5001": "Dieser Namespace existiert nicht.",
"5003": "Du hast keinen Zugriff auf den Namespace.",
"5006": "Der Namespace Titel kann nicht leer sein.",
"5009": "Du benötigst Leserechte in diesem Namespace, um diese Aktion durchzuführen.",
"5010": "Dieses Team hat keinen Zugriff auf diesen Namespace.",
"5011": "Diese:r Benutzer:in hat bereits Zugriff auf diesen Namespace.",
"5012": "Dieser Namespace ist archiviert und kann deshalb nur gelesen werden.",
"6001": "Der Teamname kann nicht leer sein.", "6001": "Der Teamname kann nicht leer sein.",
"6002": "Das Team existiert nicht.", "6002": "Das Team existiert nicht.",
"6004": "The team already has access to that namespace or project.", "6004": "Das Team hat bereits Zugriff auf dieses Projekt.",
"6005": "Diese:r Benutzer:in ist bereits dem Team beigetreten.", "6005": "Diese:r Benutzer:in ist bereits dem Team beigetreten.",
"6006": "Du kannst den:die letzten Benutzer:in dieses Teams nicht löschen.", "6006": "Du kannst den:die letzten Benutzer:in dieses Teams nicht löschen.",
"6007": "The team does not have access to the project to perform that action.", "6007": "Das Team hat keine Berechtigungen auf diesem Projekt, um das durchzuführen.",
"7002": "The user already has access to that project.", "7002": "Der:die Benutzer:in hat bereits Zugriff auf dieses Projekt",
"7003": "You do not have access to that project.", "7003": "Du hast keinen Zugriff auf dieses Projekt.",
"8001": "Dieses Label existiert bereits auf dieser Aufgabe.", "8001": "Dieses Label existiert bereits auf dieser Aufgabe.",
"8002": "Das Label existiert nicht.", "8002": "Das Label existiert nicht.",
"8003": "Du hast keinen Zugriff auf dieses Label.", "8003": "Du hast keinen Zugriff auf dieses Label.",
"9001": "Das Recht ist ungültig.", "9001": "Das Recht ist ungültig.",
"10001": "Diese Spalte existiert nicht.", "10001": "Diese Spalte existiert nicht.",
"10002": "The bucket does not belong to that project.", "10002": "Diese Spalte gehört nicht zu diesem Projekt.",
"10003": "You cannot remove the last bucket on a project.", "10003": "Du kannst die letze Spalte in einem Projekt nicht entfernen.",
"10004": "Du kannst die Aufgabe nicht in diese Spalte legen, da sie schon die maximale Anzahl an Aufgaben enthält.", "10004": "Du kannst die Aufgabe nicht in diese Spalte legen, da sie schon die maximale Anzahl an Aufgaben enthält.",
"10005": "There can be only one done bucket per project.", "10005": "Es kann nur eine Erledigt-Spalte pro Projekt geben.",
"11001": "Der gespeicherte Filter existiert nicht.", "11001": "Der gespeicherte Filter existiert nicht.",
"11002": "Gespeicherte Ansichten sind für Linkfreigaben nicht verfügbar.", "11002": "Gespeicherte Ansichten sind für Linkfreigaben nicht verfügbar.",
"12001": "Der Abonnement-Typ ist ungültig.", "12001": "Der Abonnement-Typ ist ungültig.",
@ -1052,5 +992,16 @@
"title": "Über", "title": "Über",
"frontendVersion": "Frontend-Version: {version}", "frontendVersion": "Frontend-Version: {version}",
"apiVersion": "API-Version: {version}" "apiVersion": "API-Version: {version}"
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
}
} }
} }

View File

@ -5,11 +5,10 @@
"welcomeDay": "Hallo {username}!", "welcomeDay": "Hallo {username}!",
"welcomeEvening": "Guten Abend, {username}!", "welcomeEvening": "Guten Abend, {username}!",
"lastViewed": "Zletscht ahglueget", "lastViewed": "Zletscht ahglueget",
"addToHomeScreen": "Füge diese App deinem Startbildschirm hinzu, um schneller darauf zuzugreifen und das Erlebnis zu verbessern.",
"project": { "project": {
"newText": "Du kannst ein neues Projekt für deine neuen Aufgaben erstellen:", "importText": "Importiere deine Projekte und Aufgaben aus anderen Diensten in Vikunja:",
"new": "New project", "import": "Importiere deine Daten in Vikunja"
"importText": "Or import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja"
} }
}, },
"404": { "404": {
@ -78,14 +77,14 @@
"savedSuccess": "Die Iihstellige sind erfolgriich aktualisiert wordä.", "savedSuccess": "Die Iihstellige sind erfolgriich aktualisiert wordä.",
"emailReminders": "Schick mir e Errinnerig für Uufgabe per E-Mail", "emailReminders": "Schick mir e Errinnerig für Uufgabe per E-Mail",
"overdueReminders": "Sende mir jeden Tag eine Zusammenfassung meiner überfälligen Aufgaben", "overdueReminders": "Sende mir jeden Tag eine Zusammenfassung meiner überfälligen Aufgaben",
"discoverableByName": "Anderi Lüüt chend mi findä, wenn si nach miim Name sueched", "discoverableByName": "Erlaube anderen Benutzer:innen, mich als Mitglied zu Teams oder Projekten hinzuzufügen, wenn sie nach meinem Namen suchen",
"discoverableByEmail": "Anderi Benutzer chend mich finde, wenns mini voll E-Mail Adressä sueched", "discoverableByEmail": "Erlaube anderen Benutzer:innen, mich als Mitglied zu Teams oder Projekten hinzuzufügen, wenn sie nach meiner vollständigen E-Mail Adresse suchen",
"playSoundWhenDone": "Spil es Tönli ab, wenn en Task als fertig markiert wird", "playSoundWhenDone": "Spil es Tönli ab, wenn en Task als fertig markiert wird",
"weekStart": "D'Wuche fangt ah am", "weekStart": "D'Wuche fangt ah am",
"weekStartSunday": "Sunntig", "weekStartSunday": "Sunntig",
"weekStartMonday": "Määntig", "weekStartMonday": "Määntig",
"language": "Sproch", "language": "Sproch",
"defaultProject": "Default Project", "defaultProject": "Standard-Projekt",
"timezone": "Zeitzone", "timezone": "Zeitzone",
"overdueTasksRemindersTime": "Zeit der E-Mail-Zusammenfassung der überfälligen Aufgaben" "overdueTasksRemindersTime": "Zeit der E-Mail-Zusammenfassung der überfälligen Aufgaben"
}, },
@ -143,7 +142,7 @@
}, },
"deletion": { "deletion": {
"title": "Lösche deinen Vikunja-Account", "title": "Lösche deinen Vikunja-Account",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.", "text1": "Das Löschen deines Accounts ist dauerhaft und unwiderruflich. Alle Projekte, Aufgaben und zugehörige Daten werden gelöscht.",
"text2": "Zum Fortfahren gib bitte dein Passwort ein. Du erhältst eine E-Mail mit weiteren Anweisungen.", "text2": "Zum Fortfahren gib bitte dein Passwort ein. Du erhältst eine E-Mail mit weiteren Anweisungen.",
"confirm": "Meinen Account löschen", "confirm": "Meinen Account löschen",
"requestSuccess": "Die Anfrage war erfolgreich. Du erhältst eine E-Mail mit weiteren Anweisungen.", "requestSuccess": "Die Anfrage war erfolgreich. Du erhältst eine E-Mail mit weiteren Anweisungen.",
@ -157,7 +156,7 @@
}, },
"export": { "export": {
"title": "Exportiere deine Vikunja-Daten", "title": "Exportiere deine Vikunja-Daten",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", "description": "Du kannst eine Kopie deiner Daten bei Vikunja anfordern. Dazu gehören Projekte, Aufgaben und alles, was damit zusammenhängt. Du kannst diese Daten dann in jeder Vikunja-Instanz über die Migrationsfunktion importieren.",
"descriptionPasswordRequired": "Bitte gib dein Passwort ein, um fortzufahren:", "descriptionPasswordRequired": "Bitte gib dein Passwort ein, um fortzufahren:",
"request": "Eine Kopie meiner Vikunja Daten anfordern", "request": "Eine Kopie meiner Vikunja Daten anfordern",
"success": "Du hast deine Daten bei Vikunja erfolgreich angefordert! Wir schicken dir eine E-Mail, sobald sie zum Download bereitstehen.", "success": "Du hast deine Daten bei Vikunja erfolgreich angefordert! Wir schicken dir eine E-Mail, sobald sie zum Download bereitstehen.",
@ -165,220 +164,163 @@
} }
}, },
"project": { "project": {
"archived": "This project is archived. It is not possible to create new or edit tasks for it.", "archivedMessage": "Dieses Projekt ist archiviert. Es ist nicht möglich, neue Aufgaben zu erstellen oder es zu bearbeiten.",
"title": "Project Title", "archived": "Archiviert",
"color": "Color", "showArchived": "Archivierte anzeigen",
"projects": "Projects", "title": "Projekttitel",
"search": "Type to search for a project…", "color": "Farbe",
"searchSelect": "Click or press enter to select this project", "projects": "Projekte",
"shared": "Shared Projects", "parent": "Übergeordnetes Projekt",
"noDescriptionAvailable": "No project description is available.", "search": "Tippe, um nach einem Projekt zu suchen…",
"searchSelect": "Klicke oder drücke die Eingabetaste, um dieses Projekt auszuwählen",
"shared": "Geteilte Projekte",
"noDescriptionAvailable": "Keine Projektbeschreibung verfügbar.",
"inboxTitle": "Eingang",
"create": { "create": {
"header": "New project", "header": "Neues Projekt",
"titlePlaceholder": "The project's title goes here…", "titlePlaceholder": "Der Titel des Projekts kommt hier hin…",
"addTitleRequired": "Please specify a title.", "addTitleRequired": "Bitte gebe einen Titel an.",
"createdSuccess": "The project was successfully created.", "createdSuccess": "Das Projekt wurde erfolgreich erstellt.",
"addProjectRequired": "Please specify a project or set a default project in the settings." "addProjectRequired": "Bitte gebe ein Projekt an oder lege ein Standard-Projekt in den Einstellungen fest."
}, },
"archive": { "archive": {
"title": "Archive \"{project}\"", "title": "„{project}“ archivieren",
"archive": "Archive this project", "archive": "Dieses Projekt archivieren",
"unarchive": "Un-Archive this project", "unarchive": "Archivierung dieses Projekts aufheben",
"unarchiveText": "You will be able to create new tasks or edit it.", "unarchiveText": "Du wirst neue Aufgaben erstellen oder sie bearbeiten können.",
"archiveText": "You won't be able to edit this project or create new tasks until you un-archive it.", "archiveText": "Du kannst dieses Projekt nicht bearbeiten oder neue Aufgaben erstellen, bis du die Archivierung aufhebst.",
"success": "The project was successfully archived." "success": "Das Projekt wurde erfolgreich archiviert."
}, },
"background": { "background": {
"title": "Set project background", "title": "Projekthintergrund festlegen",
"remove": "Remove Background", "remove": "Hintergrund entfernen",
"upload": "Choose a background from your pc", "upload": "Wähle einen Hintergrund von deinem Computer",
"searchPlaceholder": "Search for a background…", "searchPlaceholder": "Nach einem Hintergrund suchen…",
"poweredByUnsplash": "Powered by Unsplash", "poweredByUnsplash": "Powered by Unsplash",
"loadMore": "Load more photos", "loadMore": "Weitere Bilder laden",
"success": "The background has been set successfully!", "success": "Der Hintergrund wurde erfolgreich eingestellt!",
"removeSuccess": "The background has been removed successfully!" "removeSuccess": "Der Hintergrund wurde erfolgreich entfernt!"
}, },
"delete": { "delete": {
"title": "Delete \"{project}\"", "title": "„{project}“ löschen",
"header": "Delete this project", "header": "Dieses Projekt löschen",
"text1": "Are you sure you want to delete this project and all of its contents?", "text1": "Bist du sicher, dass du dieses Projekt und alle seine Inhalte löschen willst?",
"text2": "This includes all tasks and CANNOT BE UNDONE!", "text2": "Dies umfasst alle Aufgaben und kann NICHT rückgängig gemacht werden!",
"success": "The project was successfully deleted.", "success": "Das Projekt wurde erfolgreich gelöscht.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.", "tasksToDelete": "Dies löscht unwiderruflich ca. {count} Aufgaben.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete." "noTasksToDelete": "Dieses Projekt enthält keine Aufgaben, es kann sicher gelöscht werden."
}, },
"duplicate": { "duplicate": {
"title": "Duplicate this project", "title": "Dupliziere dieses Projekt",
"label": "Duplicate", "label": "Duplizieren",
"text": "Select a namespace which should hold the duplicated project:", "text": "Wähle ein übergeordnetes Projekt aus, welches das duplizierte Projekt enthalten soll:",
"success": "The project was successfully duplicated." "success": "Das Projekt wurde erfolgreich dupliziert."
}, },
"edit": { "edit": {
"header": "Edit This Project", "header": "Dieses Projekt bearbeiten",
"title": "Edit \"{project}\"", "title": "„{project}“ bearbeiten",
"titlePlaceholder": "The project title goes here…", "titlePlaceholder": "Der Titel des Projekts kommt hier hin…",
"identifierTooltip": "The project identifier can be used to uniquely identify a task across projects. You can set it to empty to disable it.", "identifierTooltip": "Der Projektbezeichner kann zur eindeutigen Identifizierung einer Aufgabe über mehrere Projekte hinweg verwendet werden. Du kannst ihn auf leer setzen, um ihn zu deaktivieren.",
"identifier": "Project Identifier", "identifier": "Projektbezeichner",
"identifierPlaceholder": "The project identifier goes here…", "identifierPlaceholder": "Der Projektbezeichner kommt hierhin…",
"description": "Description", "description": "Beschreibung",
"descriptionPlaceholder": "The projects description goes here…", "descriptionPlaceholder": "Projektbeschreibung eingeben…",
"color": "Color", "color": "Farbe",
"success": "The project was successfully updated." "success": "Das Projekt wurde erfolgreich aktualisiert."
}, },
"share": { "share": {
"header": "Share this project", "header": "Projekt teilen",
"title": "Share \"{project}\"", "title": "„{project}“ teilen",
"share": "Share", "share": "Teilen",
"links": { "links": {
"title": "Share Links", "title": "Linkfreigaben",
"what": "What is a share link?", "what": "Was ist eine Linkfreigabe?",
"explanation": "Share Links allow you to easily share a project with other users who don't have an account on Vikunja.", "explanation": "Mit Linkfreigaben kannst Projekt du Listen mit Benutzer:innen ohne Vikunja-Account teilen.",
"create": "Create a new link share", "create": "Erstelle ein neue Linkfreigabe",
"name": "Name (optional)", "name": "Name (optional)",
"namePlaceholder": "e.g. Lorem Ipsum", "namePlaceholder": "z.B. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.", "nameExplanation": "Alle Aktionen, die mit dieser Linkfreigabe durchgeführt werden, werden mit diesem Namen angezeigt.",
"password": "Password (optional)", "password": "Passwort (optional)",
"passwordExplanation": "When authenticating, the user will be required to enter this password.", "passwordExplanation": "Bei der Authentifizierung wird der:die Benutzer:in aufgefordert, dieses Passwort einzugeben.",
"noName": "No name set", "noName": "Kein Name festgelegt",
"remove": "Remove a link share", "remove": "Linkfreigabe entfernen",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!", "removeText": "Bist du sicher, dass du diese Linkfreigabe unwiderruflich löschen möchtest? Über die Linkfreigabe ist danach der Zugriff auf dieses Projekt nicht mehr möglich!",
"createSuccess": "The link share was successfully created.", "createSuccess": "Die Linkfreigabe wurde erfolgreich erstellt.",
"deleteSuccess": "The link share was successfully deleted", "deleteSuccess": "Die Linkfreigabe wurde erfolgreich gelöscht",
"view": "View", "view": "Ansicht",
"sharedBy": "Shared by {0}" "sharedBy": "Von {0} geteilt"
}, },
"userTeam": { "userTeam": {
"typeUser": "user | users", "typeUser": "Benutzer:in | Benutzer:innen",
"typeTeam": "team | teams", "typeTeam": "Team | Teams",
"shared": "Shared with these {type}", "shared": "Geteilt mit diesen {type}",
"you": "You", "you": "Du",
"notShared": "Not shared with any {type} yet.", "notShared": "Noch nicht mit einem {type} geteilt.",
"removeHeader": "Remove a {type} from the {sharable}", "removeHeader": "Einen {type} von {sharable} entfernen",
"removeText": "Are you sure you want to remove this {sharable} from the {type}? This cannot be undone!", "removeText": "Diesen {sharable} von {type} entfernen? Dies kann nicht rückgängig gemacht werden!",
"removeSuccess": "The {sharable} was successfully removed from the {type}.", "removeSuccess": "{sharable} wurde erfolgreich von {type} entfernt.",
"addedSuccess": "The {type} was successfully added.", "addedSuccess": "{type} wurde erfolgreich hinzugefügt.",
"updatedSuccess": "The {type} was successfully added." "updatedSuccess": "{type} wurde erfolgreich hinzugefügt."
}, },
"right": { "right": {
"title": "Permission", "title": "Berechtigung",
"read": "Read only", "read": "Nur Leserechte",
"readWrite": "Read & write", "readWrite": "Lesen & Schreiben",
"admin": "Admin" "admin": "Admin"
}, },
"attributes": { "attributes": {
"link": "Link", "link": "Link",
"delete": "Delete" "delete": "Löschen"
} }
}, },
"list": { "list": {
"title": "List", "title": "Liste",
"add": "Add", "add": "Hinzufügen",
"addPlaceholder": "Add a new task…", "addPlaceholder": "Neue Aufgabe hinzufügen…",
"empty": "This project is currently empty.", "empty": "Dieses Project ist derzeit leer.",
"newTaskCta": "Create a new task.", "newTaskCta": "Eine neue Aufgabe erstellen.",
"editTask": "Edit Task" "editTask": "Aufgabe bearbeiten"
}, },
"gantt": { "gantt": {
"title": "Gantt", "title": "Gantt",
"showTasksWithoutDates": "Show tasks which don't have dates set", "showTasksWithoutDates": "Aufgaben anzeigen, für die keine Daten festgelegt sind",
"size": "Size", "size": "Größe",
"default": "Default", "default": "Standard",
"month": "Month", "month": "Monat",
"day": "Day", "day": "Tag",
"hour": "Hour", "hour": "Stunde",
"range": "Date Range", "range": "Zeitraum",
"noDates": "This task has no dates set." "noDates": "Diese Aufgabe hat keine Daten definiert."
}, },
"table": { "table": {
"title": "Table", "title": "Tabelle",
"columns": "Columns" "columns": "Spalten"
}, },
"kanban": { "kanban": {
"title": "Kanban", "title": "Kanban",
"limit": "Limit: {limit}", "limit": "Limit: {limit}",
"noLimit": "Not Set", "noLimit": "Nicht gesetzt",
"doneBucket": "Done bucket", "doneBucket": "Erledigt Spalte",
"doneBucketHint": "All tasks moved into this bucket will automatically marked as done.", "doneBucketHint": "Alle Aufgaben, die in diese Spalte verschoben werden, werden automatisch als erledigt markiert.",
"doneBucketHintExtended": "All tasks moved into the done bucket will be marked as done automatically. All tasks marked as done from elsewhere will be moved as well.", "doneBucketHintExtended": "Alle Aufgaben, die in die Erledigt Spalte verschoben wurden, werden automatisch als erledigt markiert. Aufgaben, die in einer anderen Spalte als Erledigt markiert wurden, werden auch in diese Spalte verschoben.",
"doneBucketSavedSuccess": "The done bucket has been saved successfully.", "doneBucketSavedSuccess": "Erledigt Spalte gespeichert.",
"deleteLast": "You cannot remove the last bucket.", "deleteLast": "Du kannst die letzte Spalte nicht entfernen.",
"addTaskPlaceholder": "Enter the new task title…", "addTaskPlaceholder": "Gebe einen Aufgabentitel ein …",
"addTask": "Add a task", "addTask": "Eine Aufgabe hinzufügen",
"addAnotherTask": "Add another task", "addAnotherTask": "Weitere Aufgabe hinzufügen",
"addBucket": "Create a new bucket", "addBucket": "Eine neue Spalte erstellen",
"addBucketPlaceholder": "Enter the new bucket title…", "addBucketPlaceholder": "Gebe einen Spaltentitel ein…",
"deleteHeaderBucket": "Delete the bucket", "deleteHeaderBucket": "Spalte löschen",
"deleteBucketText1": "Are you sure you want to delete this bucket?", "deleteBucketText1": "Bist du sicher, dass du diese Spalte löschen möchtest?",
"deleteBucketText2": "This will not delete any tasks but move them into the default bucket.", "deleteBucketText2": "Dies löscht keine Aufgaben, sondern verschiebt sie in die Standardspalte.",
"deleteBucketSuccess": "The bucket has been deleted successfully.", "deleteBucketSuccess": "Die Spalte wurde erfolgreich gelöscht.",
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.", "bucketTitleSavedSuccess": "Der Spaltenname wurde erfolgreich gespeichert.",
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.", "bucketLimitSavedSuccess": "Das Spaltenlimit wurde erfolgreich gespeichert.",
"collapse": "Collapse this bucket" "collapse": "Spalte einklappen"
}, },
"pseudo": { "pseudo": {
"favorites": { "favorites": {
"title": "Favorites" "title": "Favoriten"
}
}
},
"namespace": {
"title": "Namespaces & Projects",
"namespace": "Namensruum",
"showArchived": "Archivierti aahzeige",
"noneAvailable": "Du hesch momentan kein Namensruuim.",
"unarchive": "Ent-archiviere",
"archived": "Archiviert",
"noProjects": "This namespace does not contain any projects.",
"createProject": "Create a new project in this namespace.",
"namespaces": "Namensrüüm",
"search": "Schriib, um nachemne Namensruum z'sueche…",
"create": {
"title": "Neuer Namespace",
"titleRequired": "Bitte gib en Titl ah.",
"explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
"tooltip": "Was isch en Namensruum?",
"success": "Namensruum erstellt."
},
"archive": {
"titleArchive": "\"{namespace}\" archiviere",
"titleUnarchive": "\"{namespace}\" ent-archiviere",
"archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
"unarchiveText": "You will be able to create new projects or edit it.",
"success": "De Namensruum isch erfolgriich archiviert worde.",
"unarchiveSuccess": "Der Namespace wurde erfolgreich wiederhergestellt.",
"description": "If a namespace is archived, you cannot create new projects or edit it."
},
"delete": {
"title": "\"{namespace}\" chüble",
"text1": "Bisch du dir sicher, dass du de Namensruum und all ihren Inhalt chüble wetsch?",
"text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
"success": "Namensruum g'chüblet."
},
"edit": {
"title": "\"{namespace}\" bearbeite",
"success": "Namensruum aktualisiert."
},
"share": {
"title": "\"{namespace}\" teile"
},
"attributes": {
"title": "Namensruumtitl",
"titlePlaceholder": "De Namensruumtitl chunt da ahne…",
"description": "Beschriibig",
"descriptionPlaceholder": "D'Namensruum Beschriibig chunt da ahne…",
"color": "Farb",
"archived": "Isch archiviert",
"isArchived": "De Namensruum isch archiviert"
},
"pseudo": {
"sharedProjects": {
"title": "Shared Projects"
},
"favorites": {
"title": "Favorite"
},
"savedFilters": {
"title": "Filter"
} }
} }
}, },
@ -403,7 +345,7 @@
}, },
"create": { "create": {
"title": "Neuer gespeicherter Filter", "title": "Neuer gespeicherter Filter",
"description": "A saved filter is a virtual project which is computed from a set of filters each time it is accessed. Once created, it will appear in a special namespace.", "description": "Ein gespeicherter Filter ist ein virtuelles Projekt, das bei jedem Zugriff aus einem Satz von Filtern errechnet wird.",
"action": "Neue gspeicherete Filter erstelle", "action": "Neue gspeicherete Filter erstelle",
"titleRequired": "Bitte gib den Titel für den Filter an." "titleRequired": "Bitte gib den Titel für den Filter an."
}, },
@ -435,7 +377,7 @@
"label": { "label": {
"title": "Labels", "title": "Labels",
"manage": "Label migriere", "manage": "Label migriere",
"description": "Click on a label to edit it. You can edit all labels you created, you can use all labels which are associated with a task to whose project you have access.", "description": "Klicke auf ein Label um es zu editieren. Du kannst alle Labels, welche du erstellt hast, editieren. Du kannst alle Labels, welche mit einer Aufgabe verknüpft sind, auf die du Zugriff hast, benutzen.",
"newCTA": "Du hesch momentan kei Labels.", "newCTA": "Du hesch momentan kei Labels.",
"search": "Schriib, um nachemne Label z'sueche…", "search": "Schriib, um nachemne Label z'sueche…",
"create": { "create": {
@ -460,7 +402,7 @@
}, },
"sharing": { "sharing": {
"authenticating": "Authentifiziere…", "authenticating": "Authentifiziere…",
"passwordRequired": "This shared project requires a password. Please enter it below:", "passwordRequired": "Dieses geteilte Projekt benötigt ein Passwort. Bitte gebe es unten ein:",
"error": "Het en Fähler geh. :(", "error": "Het en Fähler geh. :(",
"invalidPassword": "Da Passwort isch ungültig." "invalidPassword": "Da Passwort isch ungültig."
}, },
@ -529,7 +471,7 @@
"code": "Code", "code": "Code",
"quote": "Zitaat", "quote": "Zitaat",
"unorderedList": "Ungordnedi Listä", "unorderedList": "Ungordnedi Listä",
"orderedList ": "Ordered List", "orderedList": "Geordnete Liste",
"cleanBlock": "Formatierig Lösche", "cleanBlock": "Formatierig Lösche",
"link": "Link", "link": "Link",
"image": "Bild", "image": "Bild",
@ -566,14 +508,14 @@
"canuse": "Du kannst Datumsberechnung verwenden, um nach relativen Daten zu filtern.", "canuse": "Du kannst Datumsberechnung verwenden, um nach relativen Daten zu filtern.",
"learnhow": "Sieh dir an, wie es funktioniert", "learnhow": "Sieh dir an, wie es funktioniert",
"title": "Datumsberechnung", "title": "Datumsberechnung",
"intro": "Die Datumsberechnung erlaubt es, relative Daten anzugeben, die bei der Anwendung des Filters von Vikunja aufgelöst werden.", "intro": "Du kannst relative Daten angeben, die bei der Anwendung des Filters von Vikunja aufgelöst werden.",
"expression": "Jeder Ausdruck der Datumsberechnung beginnt mit einem Datumswert, welcher entweder {0} sein kann oder mit {1} endet. Auf diesen Datumswert kann optional ein oder mehrere mathematische Ausdrücke folgen.", "expression": "Jeder Ausdruck der Datumsberechnung beginnt mit einem Datumswert, welcher entweder {0} sein kann oder mit {1} endet. Auf diesen Datumswert kann optional ein oder mehrere mathematische Ausdrücke folgen.",
"similar": "Diese Ausdrücke ähneln denen von {0} und {1}.", "similar": "Diese Ausdrücke ähneln denen von {0} und {1}.",
"add1Day": "Einen Tag hinzufügen", "add1Day": "Einen Tag hinzufügen",
"minus1Day": "Einen Tag abziehen", "minus1Day": "Einen Tag abziehen",
"roundDay": "Auf den nächsten Tag abrunden", "roundDay": "Auf den nächsten Tag abrunden",
"supportedUnits": "Unterstützte Zeiteinheiten sind:", "supportedUnits": "Unterstützte Zeiteinheiten",
"someExamples": "Einige Beispiele für Zeitausdrücke:", "someExamples": "Beispiele für Zeitausdrücke",
"units": { "units": {
"seconds": "Sekunden", "seconds": "Sekunden",
"minutes": "Minuten", "minutes": "Minuten",
@ -619,7 +561,7 @@
"chooseDueDate": "Druck da, um es Fälligkeitsdatum z'setze", "chooseDueDate": "Druck da, um es Fälligkeitsdatum z'setze",
"chooseStartDate": "Druck dah, um es Startdatum z'setze", "chooseStartDate": "Druck dah, um es Startdatum z'setze",
"chooseEndDate": "Druck da, um es Enddatum z'setze", "chooseEndDate": "Druck da, um es Enddatum z'setze",
"move": "Move task to a different project", "move": "Aufgabe in ein anderes Projekt verschieben",
"done": "Als erledigt markieren!", "done": "Als erledigt markieren!",
"undone": "Als unerledigt markierä", "undone": "Als unerledigt markierä",
"created": "Erstellt am {0} vo {1}", "created": "Erstellt am {0} vo {1}",
@ -627,7 +569,7 @@
"doneAt": "{0} erledigt", "doneAt": "{0} erledigt",
"updateSuccess": "Die Uufgab isch erfolgriich g'speichered wore.", "updateSuccess": "Die Uufgab isch erfolgriich g'speichered wore.",
"deleteSuccess": "Die Uufgab isch erfolgriich g'chüblet wore.", "deleteSuccess": "Die Uufgab isch erfolgriich g'chüblet wore.",
"belongsToProject": "This task belongs to project '{project}'", "belongsToProject": "Diese Aufgabe gehört zum Projekt „{project}“",
"due": "Fällig bis {at}", "due": "Fällig bis {at}",
"closePopup": "Popup schließen", "closePopup": "Popup schließen",
"delete": { "delete": {
@ -647,7 +589,7 @@
"percentDone": "Fortschritt einstellen", "percentDone": "Fortschritt einstellen",
"attachments": "Anhänge hinzufügen", "attachments": "Anhänge hinzufügen",
"relatedTasks": "Beziehung hinzufügen", "relatedTasks": "Beziehung hinzufügen",
"moveProject": "Move", "moveProject": "Verschieben",
"color": "Farbe setzen", "color": "Farbe setzen",
"delete": "Löschen", "delete": "Löschen",
"favorite": "Zu Favoriten hinzufügen", "favorite": "Zu Favoriten hinzufügen",
@ -674,21 +616,15 @@
"updated": "Aktualisiert" "updated": "Aktualisiert"
}, },
"subscription": { "subscription": {
"subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.", "subscribedTaskThroughParentProject": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihr Projekt abonniert hast.",
"subscribedTaskThroughParentNamespace": "Du kannst hier nicht de-abonnieren, da du diese Aufgabe über ihren Namespace abonniert hast.", "subscribedProject": "Du hast dieses Projekt abonniert und erhältst Benachrichtigungen über Änderungen.",
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.", "notSubscribedProject": "Du hast dieses Projekt nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedNamespace": "Du hast diesen Namespace abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedNamespace": "Du hast diesen Namespace nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "Du hast diese Aufgabe abonniert und erhältst Benachrichtigungen über Änderungen.", "subscribedTask": "Du hast diese Aufgabe abonniert und erhältst Benachrichtigungen über Änderungen.",
"notSubscribedTask": "Du hast diese Aufgabe nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.", "notSubscribedTask": "Du hast diese Aufgabe nicht abonniert und erhältst keine Benachrichtigungen über Änderungen.",
"subscribe": "Abooniere", "subscribe": "Abooniere",
"unsubscribe": "Deabonniere", "unsubscribe": "Deabonniere",
"subscribeSuccessNamespace": "Du hast diesen Namespace jetzt abonniert", "subscribeSuccessProject": "Du hast dieses Projekt jetzt abonniert",
"unsubscribeSuccessNamespace": "Du hast diesen Namespace jetzt nicht mehr abonniert", "unsubscribeSuccessProject": "Du hast dieses Projekt jetzt nicht mehr abonniert",
"subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "Du hast diese Aufgabe jetzt abonniert", "subscribeSuccessTask": "Du hast diese Aufgabe jetzt abonniert",
"unsubscribeSuccessTask": "Du hast diese Aufgabe jetzt nicht mehr abonniert" "unsubscribeSuccessTask": "Du hast diese Aufgabe jetzt nicht mehr abonniert"
}, },
@ -762,8 +698,7 @@
"new": "Neui Uufgabe Beziehig", "new": "Neui Uufgabe Beziehig",
"searchPlaceholder": "Schriib, um e neui Uufgab als Zueghörigkeit hinzuezfüege…", "searchPlaceholder": "Schriib, um e neui Uufgab als Zueghörigkeit hinzuezfüege…",
"createPlaceholder": "Das als en neui Zueghörigkeit hinzuefüege", "createPlaceholder": "Das als en neui Zueghörigkeit hinzuefüege",
"differentProject": "This task belongs to a different project.", "differentProject": "Diese Aufgabe gehört zu einem anderen Projekt.",
"differentNamespace": "Diese Aufgabe gehört zu einem anderen Namespace.",
"noneYet": "S'git kei Uufgabe Beziehige.", "noneYet": "S'git kei Uufgabe Beziehige.",
"delete": "Uufgabe Beziehig chüble", "delete": "Uufgabe Beziehig chüble",
"deleteText1": "Bisch du dir sicher, dass du die Zueghörigkeit chüblä wetsch?", "deleteText1": "Bisch du dir sicher, dass du die Zueghörigkeit chüblä wetsch?",
@ -783,6 +718,17 @@
"copiedto": "Kopiert nach | Kopiert nach" "copiedto": "Kopiert nach | Kopiert nach"
} }
}, },
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
},
"repeat": { "repeat": {
"everyDay": "Jedä Tag", "everyDay": "Jedä Tag",
"everyWeek": "Jedi Wuche", "everyWeek": "Jedi Wuche",
@ -800,8 +746,7 @@
"invalidAmount": "Bitte mehr als 0 eingeben." "invalidAmount": "Bitte mehr als 0 eingeben."
}, },
"quickAddMagic": { "quickAddMagic": {
"hint": "Du chasch Quick Add Magic verwendä", "hint": "Verwende magische Präfixe, um Fälligkeitsdaten, Zuweisungen und andere Aufgabeneigenschaften zu definieren.",
"what": "Was?",
"title": "Quick Add Magic", "title": "Quick Add Magic",
"intro": "Bim erstelle vonere Uufgab, chasch du spezielli Schlüsselwörter verwende, umm Attribute direkt zu dere Uufgab hinzuezfüege. Das Erlaubts, um pblichi Attribute schneller zu Uufgabe hinzuezfüege.", "intro": "Bim erstelle vonere Uufgab, chasch du spezielli Schlüsselwörter verwende, umm Attribute direkt zu dere Uufgab hinzuezfüege. Das Erlaubts, um pblichi Attribute schneller zu Uufgabe hinzuezfüege.",
"multiple": "Du chasch da mehrmals mache.", "multiple": "Du chasch da mehrmals mache.",
@ -812,10 +757,10 @@
"priority1": "Um e Task Priorität z'setze: füeg e nummere zwüsched 1 und 5, mit em {prefix} als Prefix iih.", "priority1": "Um e Task Priorität z'setze: füeg e nummere zwüsched 1 und 5, mit em {prefix} als Prefix iih.",
"priority2": "Je höher d'nummere, desto höher d'Priorität.", "priority2": "Je höher d'nummere, desto höher d'Priorität.",
"assignees": "Um die Aufgabe direkt jemandem zuzuweisen, füge vor dem Anmeldenamen der Person ein {prefix} Zeichen ein.", "assignees": "Um die Aufgabe direkt jemandem zuzuweisen, füge vor dem Anmeldenamen der Person ein {prefix} Zeichen ein.",
"project1": "To set a project for the task to appear in, enter its name prefixed with {prefix}.", "project1": "Um ein Projekt für die Aufgabe festzulegen, gib seinen Namen mit einem vorangestellten {prefix} ein.",
"project2": "This will return an error if the project does not exist.", "project2": "Dies gibt einen Fehler zurück, wenn das Projekt nicht existiert.",
"project3": "To use spaces, simply add a \" or ' around the project name.", "project3": "Um Leerzeichen zu verwenden, füge einfach ein \" oder ' um den Namen des Projekts hinzu.",
"project4": "For example: {prefix}\"Project with spaces\".", "project4": "Zum Beispiel: {prefix}\"Projekt mit Leerzeichen\".",
"dateAndTime": "Datum und Ziit", "dateAndTime": "Datum und Ziit",
"date": "Jedes Datum wird als Abgabedatum für di neu Uufgab gnoh. Du chasch Date i de folgende Format verwende:", "date": "Jedes Datum wird als Abgabedatum für di neu Uufgab gnoh. Du chasch Date i de folgende Format verwende:",
"dateWeekday": "jede Wuchetaag wird nimmt s'negste Datum mit dem Datum", "dateWeekday": "jede Wuchetaag wird nimmt s'negste Datum mit dem Datum",
@ -848,19 +793,19 @@
"delete": { "delete": {
"header": "Das Team chüble", "header": "Das Team chüble",
"text1": "Bischder sicher, dasst wetsch da Team mit allne Mitglieder lösche?", "text1": "Bischder sicher, dasst wetsch da Team mit allne Mitglieder lösche?",
"text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!", "text2": "Alle Teammitglieder verlieren den Zugriff auf Projekte, die mit diesem Team geteilt sind. Dies KANN NICHT rückgängig gemacht werden!",
"success": "Da Team isch erfolgriich g'chüblet wore." "success": "Da Team isch erfolgriich g'chüblet wore."
}, },
"deleteUser": { "deleteUser": {
"header": "Benutzer usem Team entferne", "header": "Benutzer usem Team entferne",
"text1": "Bisch du dir sicher, dass du de Benutzer usm Team werfe wetsch?", "text1": "Bisch du dir sicher, dass du de Benutzer usm Team werfe wetsch?",
"text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "Diese:r Benutzer:in verliert den Zugriff auf alle Projekte, auf die dieses Team Zugriff hat. Dies kann nicht rückgängig gemacht werden!",
"success": "Benutzer erfolgriich usegworfe." "success": "Benutzer erfolgriich usegworfe."
}, },
"leave": { "leave": {
"title": "Team verlassen", "title": "Team verlassen",
"text1": "Bist du sicher, dass du dieses Team verlassen willst?", "text1": "Bist du sicher, dass du dieses Team verlassen willst?",
"text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.", "text2": "Du wirst Zugriff auf alle Projekte verlieren, auf die dieses Team Zugriff hat. Wenn du deine Meinung änderst, musst du durch einen Team-Admin wieder hinzugefügt werden.",
"success": "Du hast das Team erfolgreich verlassen." "success": "Du hast das Team erfolgreich verlassen."
} }
}, },
@ -892,24 +837,27 @@
"attachment": "En Aahang dere Uufgab hinzuefüege", "attachment": "En Aahang dere Uufgab hinzuefüege",
"related": "Beziehige vo dere Uufgab bearbeite", "related": "Beziehige vo dere Uufgab bearbeite",
"color": "Die Farbe dieser Aufgabe ändern", "color": "Die Farbe dieser Aufgabe ändern",
"move": "Move this task to another project", "move": "Aufgabe in ein anderes Projekt verschieben",
"reminder": "Erinnerungen für diese Aufgabe verwalten", "reminder": "Erinnerungen für diese Aufgabe verwalten",
"description": "Aufgabenbeschreibung bearbeiten" "description": "Aufgabenbeschreibung bearbeiten",
"delete": "Diese Aufgabe löschen",
"priority": "Die Priorität dieser Aufgabe ändern",
"favorite": "Diese Aufgabe zum Favoriten machen / von Favoriten entfernen"
}, },
"project": { "project": {
"title": "Project Views", "title": "Projektansichten",
"switchToListView": "Switch to list view", "switchToListView": "Zu Listenansicht wechseln",
"switchToGanttView": "Switch to gantt view", "switchToGanttView": "Zur Ganttansicht wechseln",
"switchToKanbanView": "Switch to kanban view", "switchToKanbanView": "Zur Kanbanansicht wechseln",
"switchToTableView": "Switch to table view" "switchToTableView": "Zur Tabellenansicht wechseln"
}, },
"navigation": { "navigation": {
"title": "Navigation", "title": "Navigation",
"overview": "Die Startseite aufrufen", "overview": "Die Startseite aufrufen",
"upcoming": "Anstehende Aufgaben aufrufen", "upcoming": "Anstehende Aufgaben aufrufen",
"namespaces": "Navigate to namespaces & projects",
"labels": "Labels aufrufen", "labels": "Labels aufrufen",
"teams": "Teams aufrufen" "teams": "Teams aufrufen",
"projects": "Projekte aufrufen"
} }
}, },
"update": { "update": {
@ -924,7 +872,8 @@
"unarchive": "Ent-archiviere", "unarchive": "Ent-archiviere",
"setBackground": "Hintergrund iihstelle", "setBackground": "Hintergrund iihstelle",
"share": "Teilä", "share": "Teilä",
"newProject": "New project" "newProject": "Neues Projekt",
"createProject": "Projekt erstellen"
}, },
"apiConfig": { "apiConfig": {
"url": "Vikunja URL", "url": "Vikunja URL",
@ -943,25 +892,23 @@
"notification": { "notification": {
"title": "Benachrichtigunge", "title": "Benachrichtigunge",
"none": "Du hesch kei neui Benachrichtunge. Heb e schös Tägli!", "none": "Du hesch kei neui Benachrichtunge. Heb e schös Tägli!",
"explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen." "explainer": "Benachrichtigungen werden hier angezeigt, wenn Aktionen für Projekte oder Aufgaben, die du abonniert hast, ausgeführt werden."
}, },
"quickActions": { "quickActions": {
"commands": "Befehl", "commands": "Befehl",
"placeholder": "Schriib en Befehl oder suech…", "placeholder": "Schriib en Befehl oder suech…",
"hint": "You can use {project} to limit the search to a project. Combine {project} or {label} (labels) with a search query to search for a task with these labels or on that project. Use {assignee} to only search for teams.", "hint": "Du kannst {project} verwenden, um die Suche auf ein Projekt zu beschränken. Kombiniere {project} oder {label} (Labels) mit einer Suchabfrage, um eine Aufgabe mit diesen Labels oder auf diesem Projekt zu suchen. Verwende {assignee}, um nur nach Teams zu suchen.",
"tasks": "Uufgabe", "tasks": "Uufgabe",
"projects": "Projects", "projects": "Projekte",
"teams": "Teams", "teams": "Teams",
"newProject": "Enter the title of the new project…", "newProject": "Gib den Titel des neuen Projekts ein…",
"newTask": "Gib en Titl für die neu Uufgab iih…", "newTask": "Gib en Titl für die neu Uufgab iih…",
"newNamespace": "Gib en Titl für de neu Namensruum iih…",
"newTeam": "Gib en Name für da neui Team iih…", "newTeam": "Gib en Name für da neui Team iih…",
"createTask": "Create a task in the current project ({title})", "createTask": "Eine Aufgabe im aktuellen Projekt erstellen ({title})",
"createProject": "Create a project in the current namespace ({title})", "createProject": "Projekt erstellen",
"cmds": { "cmds": {
"newTask": "Neui Uufgab", "newTask": "Neui Uufgab",
"newProject": "New project", "newProject": "Neues Projekt",
"newNamespace": "Neue Namensruum",
"newTeam": "Neus Team" "newTeam": "Neus Team"
} }
}, },
@ -992,15 +939,15 @@
"1018": "Die Benutzer Profilbild Iihstellige sind nid gültig.", "1018": "Die Benutzer Profilbild Iihstellige sind nid gültig.",
"2001": "ID chann nid leer oder 0 sii.", "2001": "ID chann nid leer oder 0 sii.",
"2002": "Ebbis vo de Ahfragedate isch ungültig.", "2002": "Ebbis vo de Ahfragedate isch ungültig.",
"3001": "The project does not exist.", "3001": "Das Projekt ist nicht vorhanden.",
"3004": "You need to have read permissions on that project to perform that action.", "3004": "Um das zu machen, benötigst du eine Leseberechtigung für dieses Projekt.",
"3005": "The project title cannot be empty.", "3005": "Der Projekttitel darf nicht leer sein.",
"3006": "The project share does not exist.", "3006": "Diese Linkfreigabe existiert nicht.",
"3007": "A project with this identifier already exists.", "3007": "Ein Projekt mit diesem Bezeichner existiert bereits.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.", "3008": "Dieses Projekt ist archiviert und kann deshalb nur gelesen werden. Dies gilt auch für alle Aufgaben, die mit diesem Projekt verbunden sind.",
"4001": "The project task text cannot be empty.", "4001": "Der Aufgabentitel kann nicht leer sein.",
"4002": "The project task does not exist.", "4002": "Diese Aufgabe existiert nicht.",
"4003": "All bulk editing tasks must belong to the same project.", "4003": "Alle Massenbearbeitungen an Aufgaben müssen zum selben Projekt gehören.",
"4004": "Es bruucht mindestens ei Uufgab, um e Masseänderig durezfüehre.", "4004": "Es bruucht mindestens ei Uufgab, um e Masseänderig durezfüehre.",
"4005": "Du hesch kei Berechtigung, um die Uufgab ahzzeige.", "4005": "Du hesch kei Berechtigung, um die Uufgab ahzzeige.",
"4006": "Du chasch kei übergordneti Uufgab uf sich selbst refferenziere.", "4006": "Du chasch kei übergordneti Uufgab uf sich selbst refferenziere.",
@ -1017,30 +964,23 @@
"4017": "Ungültige Uufgabefilter vergliich.", "4017": "Ungültige Uufgabefilter vergliich.",
"4018": "Ungültige Verkettung von Aufgabenfiltern.", "4018": "Ungültige Verkettung von Aufgabenfiltern.",
"4019": "Ungültigi Uufgabe Filter Wert.", "4019": "Ungültigi Uufgabe Filter Wert.",
"5001": "De Namensruum existiert nid.",
"5003": "Du hesch kei Zuegriff zu dem Namensruum.",
"5006": "De Namensruum Name cha nid leer sii.",
"5009": "Du bruuchsch Läsezuegriff uf de Namensruum, um das durezfüehre.",
"5010": "Da Team hett kei zuegriff uf de Namensruum.",
"5011": "De Benutzer hett bereits zuegriff uf de Namensruum.",
"5012": "De Namensruum isch momentan schriibgschützt weil er archiviert isch.",
"6001": "Der Teamname kann nicht leer sein.", "6001": "Der Teamname kann nicht leer sein.",
"6002": "Da Team giz nid.", "6002": "Da Team giz nid.",
"6004": "The team already has access to that namespace or project.", "6004": "Das Team hat bereits Zugriff auf dieses Projekt.",
"6005": "De Benutzer isch scho bi dem Team.", "6005": "De Benutzer isch scho bi dem Team.",
"6006": "Du chasch nid de letschti Benutzer vom Team lösche.", "6006": "Du chasch nid de letschti Benutzer vom Team lösche.",
"6007": "The team does not have access to the project to perform that action.", "6007": "Das Team hat keine Berechtigungen auf diesem Projekt, um das durchzuführen.",
"7002": "The user already has access to that project.", "7002": "Der:die Benutzer:in hat bereits Zugriff auf dieses Projekt",
"7003": "You do not have access to that project.", "7003": "Du hast keinen Zugriff auf dieses Projekt.",
"8001": "Da Label existiert scho für die Uufgab.", "8001": "Da Label existiert scho für die Uufgab.",
"8002": "Das Label giz nid.", "8002": "Das Label giz nid.",
"8003": "Du hesch kei Zuegriff uf da Label.", "8003": "Du hesch kei Zuegriff uf da Label.",
"9001": "Die Berechtigung isch ungültig.", "9001": "Die Berechtigung isch ungültig.",
"10001": "De Chübl gits nid.", "10001": "De Chübl gits nid.",
"10002": "The bucket does not belong to that project.", "10002": "Diese Spalte gehört nicht zu diesem Projekt.",
"10003": "You cannot remove the last bucket on a project.", "10003": "Du kannst die letze Spalte in einem Projekt nicht entfernen.",
"10004": "Du chasch die Uufgab nid dem Chübl zue wiise, weil er d'Limite für Uufgabe erreicht het.", "10004": "Du chasch die Uufgab nid dem Chübl zue wiise, weil er d'Limite für Uufgabe erreicht het.",
"10005": "There can be only one done bucket per project.", "10005": "Es kann nur eine Erledigt-Spalte pro Projekt geben.",
"11001": "De g'speicheret Filter giz nid.", "11001": "De g'speicheret Filter giz nid.",
"11002": "G'speichereti Filter chend nid Teilt werde.", "11002": "G'speichereti Filter chend nid Teilt werde.",
"12001": "De Abonnement Entitätstyp isch ungültig.", "12001": "De Abonnement Entitätstyp isch ungültig.",
@ -1052,5 +992,16 @@
"title": "Über", "title": "Über",
"frontendVersion": "Frontend Version: {version}", "frontendVersion": "Frontend Version: {version}",
"apiVersion": "API Version: {version}" "apiVersion": "API Version: {version}"
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
}
} }
} }

View File

@ -5,10 +5,9 @@
"welcomeDay": "Hi {username}!", "welcomeDay": "Hi {username}!",
"welcomeEvening": "Good Evening {username}!", "welcomeEvening": "Good Evening {username}!",
"lastViewed": "Last viewed", "lastViewed": "Last viewed",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"project": { "project": {
"newText": "You can create a new project for your new tasks:", "importText": "Import your projects and tasks from other services into Vikunja:",
"new": "New project",
"importText": "Or import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
}, },
@ -78,8 +77,8 @@
"savedSuccess": "The settings were successfully updated.", "savedSuccess": "The settings were successfully updated.",
"emailReminders": "Send me reminders for tasks via Email", "emailReminders": "Send me reminders for tasks via Email",
"overdueReminders": "Send me a summary of my undone overdue tasks every day", "overdueReminders": "Send me a summary of my undone overdue tasks every day",
"discoverableByName": "Let other users find me when they search for my name", "discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
"discoverableByEmail": "Let other users find me when they search for my full email", "discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
"playSoundWhenDone": "Play a sound when marking tasks as done", "playSoundWhenDone": "Play a sound when marking tasks as done",
"weekStart": "Week starts on", "weekStart": "Week starts on",
"weekStartSunday": "Sunday", "weekStartSunday": "Sunday",
@ -143,7 +142,7 @@
}, },
"deletion": { "deletion": {
"title": "Delete your Vikunja Account", "title": "Delete your Vikunja Account",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.", "text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
"text2": "To proceed, please enter your password. You will receive an email with further instructions.", "text2": "To proceed, please enter your password. You will receive an email with further instructions.",
"confirm": "Delete my account", "confirm": "Delete my account",
"requestSuccess": "The request was successful. You'll receive an email with further instructions.", "requestSuccess": "The request was successful. You'll receive an email with further instructions.",
@ -157,7 +156,7 @@
}, },
"export": { "export": {
"title": "Export your Vikunja data", "title": "Export your Vikunja data",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", "description": "You can request a copy of all your Vikunja data. This includes Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"descriptionPasswordRequired": "Please enter your password to proceed:", "descriptionPasswordRequired": "Please enter your password to proceed:",
"request": "Request a copy of my Vikunja Data", "request": "Request a copy of my Vikunja Data",
"success": "You've successfully requested your Vikunja Data! We will send you an email once it's ready to download.", "success": "You've successfully requested your Vikunja Data! We will send you an email once it's ready to download.",
@ -165,14 +164,18 @@
} }
}, },
"project": { "project": {
"archived": "This project is archived. It is not possible to create new or edit tasks for it.", "archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archived",
"showArchived": "Show Archived",
"title": "Project Title", "title": "Project Title",
"color": "Color", "color": "Color",
"projects": "Projects", "projects": "Projects",
"parent": "Parent Project",
"search": "Type to search for a project…", "search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this project", "searchSelect": "Click or press enter to select this project",
"shared": "Shared Projects", "shared": "Shared Projects",
"noDescriptionAvailable": "No project description is available.", "noDescriptionAvailable": "No project description is available.",
"inboxTitle": "Inbox",
"create": { "create": {
"header": "New project", "header": "New project",
"titlePlaceholder": "The project's title goes here…", "titlePlaceholder": "The project's title goes here…",
@ -210,7 +213,7 @@
"duplicate": { "duplicate": {
"title": "Duplicate this project", "title": "Duplicate this project",
"label": "Duplicate", "label": "Duplicate",
"text": "Select a namespace which should hold the duplicated project:", "text": "Select a parent project which should hold the duplicated project:",
"success": "The project was successfully duplicated." "success": "The project was successfully duplicated."
}, },
"edit": { "edit": {
@ -238,7 +241,7 @@
"namePlaceholder": "e.g. Lorem Ipsum", "namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.", "nameExplanation": "All actions done by this link share will show up with the name.",
"password": "Password (optional)", "password": "Password (optional)",
"passwordExplanation": "When authenticating, the user will be required to enter this password.", "passwordExplanation": "When signing in, the user will be required to enter this password.",
"noName": "No name set", "noName": "No name set",
"remove": "Remove a link share", "remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!", "removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
@ -321,67 +324,6 @@
} }
} }
}, },
"namespace": {
"title": "Namespaces & Projects",
"namespace": "Namespace",
"showArchived": "Show Archived",
"noneAvailable": "You don't have any namespaces right now.",
"unarchive": "Un-Archive",
"archived": "Archived",
"noProjects": "This namespace does not contain any projects.",
"createProject": "Create a new project in this namespace.",
"namespaces": "Namespaces",
"search": "Type to search for a namespace…",
"create": {
"title": "New namespace",
"titleRequired": "Please specify a title.",
"explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
"tooltip": "What's a namespace?",
"success": "The namespace was successfully created."
},
"archive": {
"titleArchive": "Archive \"{namespace}\"",
"titleUnarchive": "Un-Archive \"{namespace}\"",
"archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
"unarchiveText": "You will be able to create new projects or edit it.",
"success": "The namespace was successfully archived.",
"unarchiveSuccess": "The namespace was successfully un-archived.",
"description": "If a namespace is archived, you cannot create new projects or edit it."
},
"delete": {
"title": "Delete \"{namespace}\"",
"text1": "Are you sure you want to delete this namespace and all of its contents?",
"text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
"success": "The namespace was successfully deleted."
},
"edit": {
"title": "Edit \"{namespace}\"",
"success": "The namespace was successfully updated."
},
"share": {
"title": "Share \"{namespace}\""
},
"attributes": {
"title": "Namespace Title",
"titlePlaceholder": "The namespace title goes here…",
"description": "Description",
"descriptionPlaceholder": "The namespaces description goes here…",
"color": "Color",
"archived": "Is Archived",
"isArchived": "This namespace is archived"
},
"pseudo": {
"sharedProjects": {
"title": "Shared Projects"
},
"favorites": {
"title": "Favorites"
},
"savedFilters": {
"title": "Filters"
}
}
},
"filters": { "filters": {
"title": "Filters", "title": "Filters",
"clear": "Clear Filters", "clear": "Clear Filters",
@ -403,7 +345,7 @@
}, },
"create": { "create": {
"title": "New Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual project 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 project which is computed from a set of filters each time it is accessed.",
"action": "Create new saved filter", "action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter." "titleRequired": "Please provide a title for the filter."
}, },
@ -529,7 +471,7 @@
"code": "Code", "code": "Code",
"quote": "Quote", "quote": "Quote",
"unorderedList": "Unordered List", "unorderedList": "Unordered List",
"orderedList ": "Ordered List", "orderedList": "Ordered List",
"cleanBlock": "Clean Block", "cleanBlock": "Clean Block",
"link": "Link", "link": "Link",
"image": "Image", "image": "Image",
@ -569,14 +511,14 @@
"canuse": "You can use date math to filter for relative dates.", "canuse": "You can use date math to filter for relative dates.",
"learnhow": "Check out how it works", "learnhow": "Check out how it works",
"title": "Date Math", "title": "Date Math",
"intro": "Date Math allows you to specify relative dates which are resolved on the fly by Vikunja when applying the filter.", "intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
"expression": "Each Date Math expression starts with an anchor date, which can either be {0}, or a date string ending with {1}. This anchor date can optionally be followed by one or more maths expressions.", "expression": "Each Date Math expression starts with an anchor date, which can either be {0}, or a date string ending with {1}. This anchor date can optionally be followed by one or more maths expressions.",
"similar": "These expressions are similar to the ones provided by {0} and {1}.", "similar": "These expressions are similar to the ones provided by {0} and {1}.",
"add1Day": "Add one day", "add1Day": "Add one day",
"minus1Day": "Subtract one day", "minus1Day": "Subtract one day",
"roundDay": "Round down to the nearest day", "roundDay": "Round down to the nearest day",
"supportedUnits": "Supported time units are:", "supportedUnits": "Supported time units",
"someExamples": "Some examples of time expressions:", "someExamples": "Examples of time expressions",
"units": { "units": {
"seconds": "Seconds", "seconds": "Seconds",
"minutes": "Minutes", "minutes": "Minutes",
@ -677,19 +619,13 @@
"updated": "Updated" "updated": "Updated"
}, },
"subscription": { "subscription": {
"subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.",
"subscribedTaskThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this task through its namespace.",
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.", "subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedNamespace": "You are currently subscribed to this namespace and will receive notifications for changes.",
"notSubscribedNamespace": "You are not subscribed to this namespace and won't receive notifications for changes.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.", "subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.", "notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.", "subscribedTask": "You are currently subscribed to this task and will receive notifications for changes.",
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.", "notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
"subscribe": "Subscribe", "subscribe": "Subscribe",
"unsubscribe": "Unsubscribe", "unsubscribe": "Unsubscribe",
"subscribeSuccessNamespace": "You are now subscribed to this namespace",
"unsubscribeSuccessNamespace": "You are now unsubscribed to this namespace",
"subscribeSuccessProject": "You are now subscribed to this project", "subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project", "unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "You are now subscribed to this task", "subscribeSuccessTask": "You are now subscribed to this task",
@ -766,7 +702,6 @@
"searchPlaceholder": "Type search for a new task to add as related…", "searchPlaceholder": "Type search for a new task to add as related…",
"createPlaceholder": "Add this as new related task", "createPlaceholder": "Add this as new related task",
"differentProject": "This task belongs to a different project.", "differentProject": "This task belongs to a different project.",
"differentNamespace": "This task belongs to a different namespace.",
"noneYet": "No task relations yet.", "noneYet": "No task relations yet.",
"delete": "Delete Task Relation", "delete": "Delete Task Relation",
"deleteText1": "Are you sure you want to delete this task relation?", "deleteText1": "Are you sure you want to delete this task relation?",
@ -786,6 +721,17 @@
"copiedto": "Copied To | Copied To" "copiedto": "Copied To | Copied To"
} }
}, },
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
},
"repeat": { "repeat": {
"everyDay": "Every Day", "everyDay": "Every Day",
"everyWeek": "Every Week", "everyWeek": "Every Week",
@ -803,8 +749,7 @@
"invalidAmount": "Please enter more than 0." "invalidAmount": "Please enter more than 0."
}, },
"quickAddMagic": { "quickAddMagic": {
"hint": "You can use Quick Add Magic", "hint": "Use magic prefixes to define due dates, assignees and other task properties.",
"what": "What?",
"title": "Quick Add Magic", "title": "Quick Add Magic",
"intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.", "intro": "When creating a task, you can use special keywords to directly add attributes to the newly created task. This allows to add commonly used attributes to tasks much faster.",
"multiple": "You can use this multiple times.", "multiple": "You can use this multiple times.",
@ -851,19 +796,19 @@
"delete": { "delete": {
"header": "Delete the team", "header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?", "text1": "Are you sure you want to delete this team and all of its members?",
"text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!", "text2": "All team members will lose access to projects shared with this team. This CANNOT BE UNDONE!",
"success": "The team was successfully deleted." "success": "The team was successfully deleted."
}, },
"deleteUser": { "deleteUser": {
"header": "Remove a user from the team", "header": "Remove a user from the team",
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
"success": "The user was successfully deleted from the team." "success": "The user was successfully deleted from the team."
}, },
"leave": { "leave": {
"title": "Leave team", "title": "Leave team",
"text1": "Are you sure you want to leave this team?", "text1": "Are you sure you want to leave this team?",
"text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.", "text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "You have successfully left the team." "success": "You have successfully left the team."
} }
}, },
@ -897,7 +842,10 @@
"color": "Change the color of this task", "color": "Change the color of this task",
"move": "Move this task to another project", "move": "Move this task to another project",
"reminder": "Manage reminders of this task", "reminder": "Manage reminders of this task",
"description": "Toggle editing of the task description" "description": "Toggle editing of the task description",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
}, },
"project": { "project": {
"title": "Project Views", "title": "Project Views",
@ -910,9 +858,9 @@
"title": "Navigation", "title": "Navigation",
"overview": "Navigate to overview", "overview": "Navigate to overview",
"upcoming": "Navigate to upcoming tasks", "upcoming": "Navigate to upcoming tasks",
"namespaces": "Navigate to namespaces & projects",
"labels": "Navigate to labels", "labels": "Navigate to labels",
"teams": "Navigate to teams" "teams": "Navigate to teams",
"projects": "Navigate to projects"
} }
}, },
"update": { "update": {
@ -927,7 +875,8 @@
"unarchive": "Un-Archive", "unarchive": "Un-Archive",
"setBackground": "Set background", "setBackground": "Set background",
"share": "Share", "share": "Share",
"newProject": "New project" "newProject": "New project",
"createProject": "Create project"
}, },
"apiConfig": { "apiConfig": {
"url": "Vikunja URL", "url": "Vikunja URL",
@ -946,7 +895,7 @@
"notification": { "notification": {
"title": "Notifications", "title": "Notifications",
"none": "You don't have any notifications. Have a nice day!", "none": "You don't have any notifications. Have a nice day!",
"explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen." "explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
}, },
"quickActions": { "quickActions": {
"commands": "Commands", "commands": "Commands",
@ -957,14 +906,12 @@
"teams": "Teams", "teams": "Teams",
"newProject": "Enter the title of the new project…", "newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…", "newTask": "Enter the title of the new task…",
"newNamespace": "Enter the title of the new namespace…",
"newTeam": "Enter the name of the new team…", "newTeam": "Enter the name of the new team…",
"createTask": "Create a task in the current project ({title})", "createTask": "Create a task in the current project ({title})",
"createProject": "Create a project in the current namespace ({title})", "createProject": "Create a project",
"cmds": { "cmds": {
"newTask": "New task", "newTask": "New task",
"newProject": "New project", "newProject": "New project",
"newNamespace": "New namespace",
"newTeam": "New team" "newTeam": "New team"
} }
}, },
@ -1020,16 +967,9 @@
"4017": "Invalid task filter comparator.", "4017": "Invalid task filter comparator.",
"4018": "Invalid task filter concatenator.", "4018": "Invalid task filter concatenator.",
"4019": "Invalid task filter value.", "4019": "Invalid task filter value.",
"5001": "The namespace does not exist.",
"5003": "You do not have access to the specified namespace.",
"5006": "The namespace name cannot be empty.",
"5009": "You need to have namespace read access to perform that action.",
"5010": "This team does not have access to that namespace.",
"5011": "This user has already access to that namespace.",
"5012": "The namespace is archived and can therefore only be accessed read only.",
"6001": "The team name cannot be empty.", "6001": "The team name cannot be empty.",
"6002": "The team does not exist.", "6002": "The team does not exist.",
"6004": "The team already has access to that namespace or project.", "6004": "The team already has access to that project.",
"6005": "The user is already a member of that team.", "6005": "The user is already a member of that team.",
"6006": "Cannot delete the last team member.", "6006": "Cannot delete the last team member.",
"6007": "The team does not have access to the project to perform that action.", "6007": "The team does not have access to the project to perform that action.",
@ -1055,5 +995,16 @@
"title": "About", "title": "About",
"frontendVersion": "Frontend Version: {version}", "frontendVersion": "Frontend Version: {version}",
"apiVersion": "API Version: {version}" "apiVersion": "API Version: {version}"
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
}
} }
} }

1007
src/i18n/lang/eo-UY.json Normal file

File diff suppressed because it is too large Load Diff

View File

@ -5,10 +5,9 @@
"welcomeDay": "¡Hola {username}!", "welcomeDay": "¡Hola {username}!",
"welcomeEvening": "¡Buenas Tardes {username}!", "welcomeEvening": "¡Buenas Tardes {username}!",
"lastViewed": "Visto por última vez", "lastViewed": "Visto por última vez",
"addToHomeScreen": "Add this app to your home screen for faster access and improved experience.",
"project": { "project": {
"newText": "You can create a new project for your new tasks:", "importText": "Import your projects and tasks from other services into Vikunja:",
"new": "New project",
"importText": "Or import your projects and tasks from other services into Vikunja:",
"import": "Import your data into Vikunja" "import": "Import your data into Vikunja"
} }
}, },
@ -78,8 +77,8 @@
"savedSuccess": "Ajustes actualizados con éxito.", "savedSuccess": "Ajustes actualizados con éxito.",
"emailReminders": "Enviarme recordatorios de tareas vía Correo Electrónico", "emailReminders": "Enviarme recordatorios de tareas vía Correo Electrónico",
"overdueReminders": "Enviarme un resumen de mis tareas pendientes todos los días", "overdueReminders": "Enviarme un resumen de mis tareas pendientes todos los días",
"discoverableByName": "Permitir que otros usuarios me encuentren cuando busquen mi nombre", "discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
"discoverableByEmail": "Permitir que otros usuarios me encuentren cuando busquen mi correo completo", "discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
"playSoundWhenDone": "Reproducir un sonido cuando marque tareas como completadas", "playSoundWhenDone": "Reproducir un sonido cuando marque tareas como completadas",
"weekStart": "La semana empieza en", "weekStart": "La semana empieza en",
"weekStartSunday": "Domingo", "weekStartSunday": "Domingo",
@ -143,7 +142,7 @@
}, },
"deletion": { "deletion": {
"title": "Eliminar tu Cuenta de Vikunja", "title": "Eliminar tu Cuenta de Vikunja",
"text1": "The deletion of your account is permanent and cannot be undone. We will delete all your namespaces, projects, tasks and everything associated with it.", "text1": "The deletion of your account is permanent and cannot be undone. We will delete all your projects, tasks and everything associated with it.",
"text2": "Para continuar, por favor, introduce tu contraseña. Recibirás un correo electrónico con más instrucciones.", "text2": "Para continuar, por favor, introduce tu contraseña. Recibirás un correo electrónico con más instrucciones.",
"confirm": "Eliminar mi cuenta", "confirm": "Eliminar mi cuenta",
"requestSuccess": "La solicitud ha sido exitosa. Recibirás un correo electrónico con más instrucciones.", "requestSuccess": "La solicitud ha sido exitosa. Recibirás un correo electrónico con más instrucciones.",
@ -157,7 +156,7 @@
}, },
"export": { "export": {
"title": "Exportar tus datos de Vikunja", "title": "Exportar tus datos de Vikunja",
"description": "You can request a copy of all your Vikunja data. This include Namespaces, Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.", "description": "You can request a copy of all your Vikunja data. This includes Projects, Tasks and everything associated to them. You can import this data in any Vikunja instance through the migration function.",
"descriptionPasswordRequired": "Por favor, introduce tu contraseña para continuar:", "descriptionPasswordRequired": "Por favor, introduce tu contraseña para continuar:",
"request": "Solicitar una copia de mis datos de Vikunja", "request": "Solicitar una copia de mis datos de Vikunja",
"success": "Tu petición de datos de Vikunja ha sido procesada correctamente. Te enviaremos un correo una vez esté lista para descargar.", "success": "Tu petición de datos de Vikunja ha sido procesada correctamente. Te enviaremos un correo una vez esté lista para descargar.",
@ -165,14 +164,18 @@
} }
}, },
"project": { "project": {
"archived": "This project is archived. It is not possible to create new or edit tasks for it.", "archivedMessage": "This project is archived. It is not possible to create new or edit tasks for it.",
"archived": "Archived",
"showArchived": "Show Archived",
"title": "Project Title", "title": "Project Title",
"color": "Color", "color": "Color",
"projects": "Projects", "projects": "Projects",
"parent": "Parent Project",
"search": "Type to search for a project…", "search": "Type to search for a project…",
"searchSelect": "Click or press enter to select this project", "searchSelect": "Click or press enter to select this project",
"shared": "Shared Projects", "shared": "Shared Projects",
"noDescriptionAvailable": "No project description is available.", "noDescriptionAvailable": "No project description is available.",
"inboxTitle": "Inbox",
"create": { "create": {
"header": "New project", "header": "New project",
"titlePlaceholder": "The project's title goes here…", "titlePlaceholder": "The project's title goes here…",
@ -210,7 +213,7 @@
"duplicate": { "duplicate": {
"title": "Duplicate this project", "title": "Duplicate this project",
"label": "Duplicate", "label": "Duplicate",
"text": "Select a namespace which should hold the duplicated project:", "text": "Select a parent project which should hold the duplicated project:",
"success": "The project was successfully duplicated." "success": "The project was successfully duplicated."
}, },
"edit": { "edit": {
@ -238,7 +241,7 @@
"namePlaceholder": "e.g. Lorem Ipsum", "namePlaceholder": "e.g. Lorem Ipsum",
"nameExplanation": "All actions done by this link share will show up with the name.", "nameExplanation": "All actions done by this link share will show up with the name.",
"password": "Password (optional)", "password": "Password (optional)",
"passwordExplanation": "When authenticating, the user will be required to enter this password.", "passwordExplanation": "When signing in, the user will be required to enter this password.",
"noName": "No name set", "noName": "No name set",
"remove": "Remove a link share", "remove": "Remove a link share",
"removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!", "removeText": "Are you sure you want to remove this link share? It will no longer be possible to access this project with this link share. This cannot be undone!",
@ -321,67 +324,6 @@
} }
} }
}, },
"namespace": {
"title": "Namespaces & Projects",
"namespace": "Proyecto",
"showArchived": "Mostrar Archivados",
"noneAvailable": "No tienes ningún proyecto en este momento.",
"unarchive": "Desarchivar",
"archived": "Archivado",
"noProjects": "This namespace does not contain any projects.",
"createProject": "Create a new project in this namespace.",
"namespaces": "Proyectos",
"search": "Escribe para buscar un proyecto…",
"create": {
"title": "Nuevo proyecto",
"titleRequired": "Por favor, especifica un título.",
"explanation": "A namespace is a collection of projects you can share and use to organize your projects with. In fact, every project belongs to a namespace.",
"tooltip": "¿Qué es un proyecto?",
"success": "El proyecto se ha creado correctamente."
},
"archive": {
"titleArchive": "Archivar \"{namespace}\"",
"titleUnarchive": "Desarchivar \"{namespace}\"",
"archiveText": "You won't be able to edit this namespace or create new projects until you un-archive it. This will also archive all projects in this namespace.",
"unarchiveText": "You will be able to create new projects or edit it.",
"success": "El proyecto fue archivado con éxito.",
"unarchiveSuccess": "El proyecto se ha desarchivado con éxito.",
"description": "If a namespace is archived, you cannot create new projects or edit it."
},
"delete": {
"title": "Eliminar \"{namespace}\"",
"text1": "¿Estás seguro de que deseas eliminar este proyecto y todo su contenido?",
"text2": "This includes all projects and tasks and CANNOT BE UNDONE!",
"success": "El proyecto se ha eliminado con éxito."
},
"edit": {
"title": "Editar \"{namespace}\"",
"success": "El proyecto se actualizó con éxito."
},
"share": {
"title": "Compartir \"{namespace}\""
},
"attributes": {
"title": "Título del proyecto",
"titlePlaceholder": "El título del proyecto va aquí…",
"description": "Descripción",
"descriptionPlaceholder": "La descripción del proyecto va aquí…",
"color": "Color",
"archived": "Está archivado",
"isArchived": "Este proyecto está archivado"
},
"pseudo": {
"sharedProjects": {
"title": "Shared Projects"
},
"favorites": {
"title": "Favoritos"
},
"savedFilters": {
"title": "Filtros"
}
}
},
"filters": { "filters": {
"title": "Filtros", "title": "Filtros",
"clear": "Limpiar Filtros", "clear": "Limpiar Filtros",
@ -403,7 +345,7 @@
}, },
"create": { "create": {
"title": "New Saved Filter", "title": "New Saved Filter",
"description": "A saved filter is a virtual project 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 project which is computed from a set of filters each time it is accessed.",
"action": "Create new saved filter", "action": "Create new saved filter",
"titleRequired": "Please provide a title for the filter." "titleRequired": "Please provide a title for the filter."
}, },
@ -529,7 +471,7 @@
"code": "Código", "code": "Código",
"quote": "Cita", "quote": "Cita",
"unorderedList": "Lista no ordenada", "unorderedList": "Lista no ordenada",
"orderedList ": "Ordered List", "orderedList": "Ordered List",
"cleanBlock": "Borrar Bloque", "cleanBlock": "Borrar Bloque",
"link": "Enlace", "link": "Enlace",
"image": "Imagen", "image": "Imagen",
@ -566,14 +508,14 @@
"canuse": "Puedes usar ecuaciones para filtrar por fechas relacionadas.", "canuse": "Puedes usar ecuaciones para filtrar por fechas relacionadas.",
"learnhow": "Mira cómo funciona", "learnhow": "Mira cómo funciona",
"title": "Ecuaciones", "title": "Ecuaciones",
"intro": "Las Ecuaciones permiten determinar qué fechas relacionadas te mostrará Vikunja al aplicar este filtro.", "intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
"expression": "Cada expresión matemática empieza con una fecha ancla, que puede ser {0}, o una cadena de texto que acabe en {1}. Opcionalmente, esta fecha puede estar seguida de una o más expresiones.", "expression": "Cada expresión matemática empieza con una fecha ancla, que puede ser {0}, o una cadena de texto que acabe en {1}. Opcionalmente, esta fecha puede estar seguida de una o más expresiones.",
"similar": "Estas expresiones son similares a las definidas en {0} y {1}.", "similar": "Estas expresiones son similares a las definidas en {0} y {1}.",
"add1Day": "Añadir un día", "add1Day": "Añadir un día",
"minus1Day": "Subtract one day", "minus1Day": "Subtract one day",
"roundDay": "Round down to the nearest day", "roundDay": "Round down to the nearest day",
"supportedUnits": "Supported time units are:", "supportedUnits": "Supported time units",
"someExamples": "Some examples of time expressions:", "someExamples": "Examples of time expressions",
"units": { "units": {
"seconds": "Seconds", "seconds": "Seconds",
"minutes": "Minutes", "minutes": "Minutes",
@ -674,19 +616,13 @@
"updated": "Actualizado" "updated": "Actualizado"
}, },
"subscription": { "subscription": {
"subscribedProjectThroughParentNamespace": "You can't unsubscribe here because you are subscribed to this project through its namespace.",
"subscribedTaskThroughParentNamespace": "No puede cancelar la suscripción aquí porque está suscrito a esta tarea a través de su proyecto.",
"subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.", "subscribedTaskThroughParentProject": "You can't unsubscribe here because you are subscribed to this task through its project.",
"subscribedNamespace": "Actualmente está suscrito a este proyecto y recibirás notificaciones de cambios.",
"notSubscribedNamespace": "No está suscrito a este proyecto y no recibirá notificaciones de cambios.",
"subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.", "subscribedProject": "You are currently subscribed to this project and will receive notifications for changes.",
"notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.", "notSubscribedProject": "You are not subscribed to this project and won't receive notifications for changes.",
"subscribedTask": "Actualmente estás suscrito a esta tarea y recibirás notificaciones de cambios.", "subscribedTask": "Actualmente estás suscrito a esta tarea y recibirás notificaciones de cambios.",
"notSubscribedTask": "No estás suscrito a esta tarea y no recibirás notificaciones de cambios.", "notSubscribedTask": "No estás suscrito a esta tarea y no recibirás notificaciones de cambios.",
"subscribe": "Suscribirse", "subscribe": "Suscribirse",
"unsubscribe": "Desuscribirse", "unsubscribe": "Desuscribirse",
"subscribeSuccessNamespace": "Ahora está suscrito a este proyecto",
"unsubscribeSuccessNamespace": "Ya no está suscrito a este proyecto",
"subscribeSuccessProject": "You are now subscribed to this project", "subscribeSuccessProject": "You are now subscribed to this project",
"unsubscribeSuccessProject": "You are now unsubscribed to this project", "unsubscribeSuccessProject": "You are now unsubscribed to this project",
"subscribeSuccessTask": "You are now subscribed to this task", "subscribeSuccessTask": "You are now subscribed to this task",
@ -763,7 +699,6 @@
"searchPlaceholder": "Escriba para buscar una nueva tarea a añadir como relacionada…", "searchPlaceholder": "Escriba para buscar una nueva tarea a añadir como relacionada…",
"createPlaceholder": "Añadir esto como nueva tarea relacionada", "createPlaceholder": "Añadir esto como nueva tarea relacionada",
"differentProject": "This task belongs to a different project.", "differentProject": "This task belongs to a different project.",
"differentNamespace": "Esta tarea pertenece a un proyecto diferente.",
"noneYet": "Aún no hay tareas relacionadas.", "noneYet": "Aún no hay tareas relacionadas.",
"delete": "Eliminar Relación de Tarea", "delete": "Eliminar Relación de Tarea",
"deleteText1": "¿Está seguro que desea eliminar esta relación de la tarea?", "deleteText1": "¿Está seguro que desea eliminar esta relación de la tarea?",
@ -783,6 +718,17 @@
"copiedto": "Copiado A | Copiado A" "copiedto": "Copiado A | Copiado A"
} }
}, },
"reminder": {
"before": "{amount} {unit} before {type}",
"after": "{amount} {unit} after {type}",
"beforeShort": "before",
"afterShort": "after",
"onDueDate": "On the due date",
"onStartDate": "On the start date",
"onEndDate": "On the end date",
"custom": "Custom",
"dateAndTime": "Date and time"
},
"repeat": { "repeat": {
"everyDay": "Cada Día", "everyDay": "Cada Día",
"everyWeek": "Cada Semana", "everyWeek": "Cada Semana",
@ -800,8 +746,7 @@
"invalidAmount": "Por favor introduzca más de 0." "invalidAmount": "Por favor introduzca más de 0."
}, },
"quickAddMagic": { "quickAddMagic": {
"hint": "Puede usar Añadido Rápido Mágico", "hint": "Use magic prefixes to define due dates, assignees and other task properties.",
"what": "¿Qué?",
"title": "Añadido Rápido Mágico", "title": "Añadido Rápido Mágico",
"intro": "Al crear una tarea, puede utilizar palabras clave especiales para añadir directamente atributos a la tarea recién creada. Esto permite agregar atributos comúnmente usados en tareas mucho más rápido.", "intro": "Al crear una tarea, puede utilizar palabras clave especiales para añadir directamente atributos a la tarea recién creada. Esto permite agregar atributos comúnmente usados en tareas mucho más rápido.",
"multiple": "Puedes usar esto varias veces.", "multiple": "Puedes usar esto varias veces.",
@ -848,19 +793,19 @@
"delete": { "delete": {
"header": "Delete the team", "header": "Delete the team",
"text1": "Are you sure you want to delete this team and all of its members?", "text1": "Are you sure you want to delete this team and all of its members?",
"text2": "All team members will lose access to projects and namespaces shared with this team. This CANNOT BE UNDONE!", "text2": "All team members will lose access to projects shared with this team. This CANNOT BE UNDONE!",
"success": "El equipo fue eliminado con éxito." "success": "El equipo fue eliminado con éxito."
}, },
"deleteUser": { "deleteUser": {
"header": "Remove a user from the team", "header": "Remove a user from the team",
"text1": "Are you sure you want to remove this user from the team?", "text1": "Are you sure you want to remove this user from the team?",
"text2": "They will lose access to all projects and namespaces this team has access to. This CANNOT BE UNDONE!", "text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
"success": "El usuario fue quitado del equipo con éxito." "success": "El usuario fue quitado del equipo con éxito."
}, },
"leave": { "leave": {
"title": "Leave team", "title": "Leave team",
"text1": "Are you sure you want to leave this team?", "text1": "Are you sure you want to leave this team?",
"text2": "You will lose access to all projects and namespaces this team has access to. If you change your mind you'll need a team admin to add you again.", "text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
"success": "Has abandonado el equipo con éxito." "success": "Has abandonado el equipo con éxito."
} }
}, },
@ -894,7 +839,10 @@
"color": "Cambia el color de esta tarea", "color": "Cambia el color de esta tarea",
"move": "Move this task to another project", "move": "Move this task to another project",
"reminder": "Administrar recordatorios de esta tarea", "reminder": "Administrar recordatorios de esta tarea",
"description": "Editar la descripción de la tarea" "description": "Editar la descripción de la tarea",
"delete": "Delete this task",
"priority": "Change the priority of this task",
"favorite": "Mark this task as favorite / unfavorite"
}, },
"project": { "project": {
"title": "Project Views", "title": "Project Views",
@ -907,9 +855,9 @@
"title": "Secciones", "title": "Secciones",
"overview": "Ir a resumen", "overview": "Ir a resumen",
"upcoming": "Ir a tareas próximas", "upcoming": "Ir a tareas próximas",
"namespaces": "Navigate to namespaces & projects",
"labels": "Ir a etiquetas", "labels": "Ir a etiquetas",
"teams": "Ir a equipos" "teams": "Ir a equipos",
"projects": "Navigate to projects"
} }
}, },
"update": { "update": {
@ -924,7 +872,8 @@
"unarchive": "Desarchivar", "unarchive": "Desarchivar",
"setBackground": "Establecer fondo", "setBackground": "Establecer fondo",
"share": "Compartir", "share": "Compartir",
"newProject": "New project" "newProject": "New project",
"createProject": "Create project"
}, },
"apiConfig": { "apiConfig": {
"url": "URL de Vikunja", "url": "URL de Vikunja",
@ -943,7 +892,7 @@
"notification": { "notification": {
"title": "Notificaciones", "title": "Notificaciones",
"none": "No tienes notificaciones. ¡Que tengas un buen día!", "none": "No tienes notificaciones. ¡Que tengas un buen día!",
"explainer": "Notifications will appear here when actions on namespaces, projects or tasks you subscribed to happen." "explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen."
}, },
"quickActions": { "quickActions": {
"commands": "Commands", "commands": "Commands",
@ -954,14 +903,12 @@
"teams": "Teams", "teams": "Teams",
"newProject": "Enter the title of the new project…", "newProject": "Enter the title of the new project…",
"newTask": "Enter the title of the new task…", "newTask": "Enter the title of the new task…",
"newNamespace": "Enter the title of the new namespace…",
"newTeam": "Enter the name of the new team…", "newTeam": "Enter the name of the new team…",
"createTask": "Create a task in the current project ({title})", "createTask": "Create a task in the current project ({title})",
"createProject": "Create a project in the current namespace ({title})", "createProject": "Create a project",
"cmds": { "cmds": {
"newTask": "New task", "newTask": "New task",
"newProject": "New project", "newProject": "New project",
"newNamespace": "New namespace",
"newTeam": "New team" "newTeam": "New team"
} }
}, },
@ -1017,16 +964,9 @@
"4017": "Comparador de filtro de tarea inválido.", "4017": "Comparador de filtro de tarea inválido.",
"4018": "Concatenador de filtro de tarea inválido.", "4018": "Concatenador de filtro de tarea inválido.",
"4019": "Valor de filtro de tarea inválido.", "4019": "Valor de filtro de tarea inválido.",
"5001": "El proyecto no existe.",
"5003": "No tiene acceso al proyecto especificado.",
"5006": "El nombre del proyecto no puede estar vacío.",
"5009": "Necesita tener acceso de lectura al proyecto para realizar esa acción.",
"5010": "Este equipo no tiene acceso a ese proyecto.",
"5011": "Este usuario ya tiene acceso a ese proyecto.",
"5012": "El proyecto está archivado y por lo tanto solo podrá acceder en modo solo lectura.",
"6001": "El nombre del equipo no puede estar vacío.", "6001": "El nombre del equipo no puede estar vacío.",
"6002": "Este equipo no existe.", "6002": "Este equipo no existe.",
"6004": "The team already has access to that namespace or project.", "6004": "The team already has access to that project.",
"6005": "El usuario ya es miembro de ese equipo.", "6005": "El usuario ya es miembro de ese equipo.",
"6006": "No se puede quitar al último miembro del equipo.", "6006": "No se puede quitar al último miembro del equipo.",
"6007": "The team does not have access to the project to perform that action.", "6007": "The team does not have access to the project to perform that action.",
@ -1052,5 +992,16 @@
"title": "Acerca de", "title": "Acerca de",
"frontendVersion": "Frontend Version: {version}", "frontendVersion": "Frontend Version: {version}",
"apiVersion": "Versión de la API: {version}" "apiVersion": "Versión de la API: {version}"
},
"time": {
"units": {
"seconds": "second|seconds",
"minutes": "minute|minutes",
"hours": "hour|hours",
"days": "day|days",
"weeks": "week|weeks",
"months": "month|months",
"years": "year|years"
}
} }
} }

File diff suppressed because it is too large Load Diff

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