Compare commits

..

255 Commits

Author SHA1 Message Date
renovate a2e2cf32cb fix(deps): update module github.com/wneessen/go-mail to v0.4.1
continuous-integration/drone/pr Build is failing Details
2024-04-08 10:05:44 +00:00
renovate 61322d2e2e fix(deps): update module github.com/yuin/goldmark to v1.7.1
continuous-integration/drone/push Build is passing Details
2024-04-08 09:28:22 +00:00
renovate a41e248e5f fix(deps): update font awesome to v6.5.2
continuous-integration/drone/push Build is failing Details
2024-04-08 09:10:31 +00:00
kolaente 6e37934b61
chore(deps): update goreleaser/nfpm docker tag to v2.36.1
continuous-integration/drone/push Build is failing Details
2024-04-08 11:09:39 +02:00
renovate d64322bb7a fix(deps): update dependency @infectoone/vue-ganttastic to v2.3.2
continuous-integration/drone/push Build is failing Details
2024-04-08 08:58:21 +00:00
renovate fa3b657e7e chore(deps): update pnpm to v8.15.6
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-04-08 08:06:35 +00:00
Raymi306 1adaa73141 docs: fix build-from-sources docs mistake (#2251)
continuous-integration/drone/push Build is passing Details
While attempting to build on OpenBSD without having built the frontend, I ran into the following error:

`frontend/embed.go:21:12: pattern dist: no matching files found`

I saw in the docs to create a directory and touch a file, this resulted in a second error:

`frontend/embed.go:21:12: pattern dist: cannot embed directory dist: contains no embeddable files`

Creating the index.html file inside the new directory allowed me to build Vikunja

Reviewed-on: #2251
Co-authored-by: Raymi306 <raymi306@gmail.com>
Co-committed-by: Raymi306 <raymi306@gmail.com>
2024-04-08 07:48:12 +00:00
renovate 3e77e3043e chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-04-08 07:09:20 +00:00
kolaente d082c0399d
fix(test): visit one more project in project history test
continuous-integration/drone/push Build is passing Details
2024-04-07 22:36:09 +02:00
kolaente 0b9ef27d04
fix(migration): show correct message after starting a migration
continuous-integration/drone/push Build is failing Details
Related to https://github.com/go-vikunja/vikunja/issues/238
2024-04-07 15:11:59 +02:00
kolaente 7acd1a7e51
fix(project): remove child projects from state when deleting a project
continuous-integration/drone/push Build is failing Details
2024-04-07 15:03:18 +02:00
kolaente 8bee5aa806
fix(project): return the full project when setting a background
continuous-integration/drone/push Build is failing Details
Related to #2246
2024-04-07 14:53:57 +02:00
kolaente 6641cbebc2
fix(project): save the last 6 projects in history, show only 5 on desktop
continuous-integration/drone/push Build is failing Details
The project grid on the home page with the recently visited projects now contains an even number of projects which makes for a much nicer grid (because it's now uniform).
2024-04-07 14:34:18 +02:00
kolaente 5892622676
fix(notifications): rendering of plaintext mails
continuous-integration/drone/push Build is passing Details
2024-04-07 14:12:44 +02:00
kolaente 191a476823
fix(notifications): only sanitze html content in notifications, do not convert it to markdown
continuous-integration/drone/push Build is failing Details
Resolves https://community.vikunja.io/t/trello-import-html-mails/2197
2024-04-07 13:34:53 +02:00
renovate c146b72d64 chore(deps): update golangci/golangci-lint docker tag to v1.57.2 (#2225)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2225
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-04-07 11:09:14 +00:00
kolaente ca33c0b2bc
fix: drop bucket index before recreating it
continuous-integration/drone/push Build is failing Details
Resolves #2243
2024-04-07 12:50:42 +02:00
kolaente 4d78ae7fa8
chore(dev): move nix flake to top level, add api tooling
continuous-integration/drone/push Build is passing Details
2024-04-07 12:16:13 +02:00
kolaente c1d06c5e5a
fix(projects): do not return parent project id of parents where the user does not have access
continuous-integration/drone/push Build is failing Details
This caused the frontend to not show such projects, throwing errors in the process and sometimes made it hang.
2024-04-07 12:10:20 +02:00
kolaente f1c3ce5eeb
fix(projects): allow arbitrary nesting of new projects 2024-04-07 12:00:39 +02:00
kolaente 2f6b395334
feat(kanban): set task position to 0 (top) when it is moved into the done bucket automatically after marking it done
continuous-integration/drone/push Build is passing Details
2024-04-06 14:35:05 +02:00
kolaente 1cd5dd2b2f
fix: lint
continuous-integration/drone/push Build is passing Details
2024-04-06 14:12:08 +02:00
kolaente 521300613f
fix: update task in typesense when adding a label or assignee to them
continuous-integration/drone/push Build is failing Details
Resolves https://community.vikunja.io/t/typesense-only-works-if-i-re-index/2212
2024-04-06 14:04:04 +02:00
kolaente 037022e857
fix: do not try to fetch nonexistant bucket
continuous-integration/drone/push Build is failing Details
2024-04-06 13:55:11 +02:00
kolaente ec1ff80791
fix(kanban): save done and default bucket on the view and not on the project
The frontend was still trying to update the two in the project which won't work since they are now saved at the view level, not the project.
2024-04-06 13:32:54 +02:00
kolaente 7b8fab33a5
fix(kanban): Make sure all saved taskBucket positions are saved with their project view id
continuous-integration/drone/push Build is passing Details
When the tasks were migrated from belonging directly to a bucket to only belonging to a view, I forgot to add the view in that migration, resulting in task buckets where the view was 0. These entries were not deleted when a task was moved between buckets, but the new task bucket relation nevertheless inserted. This resulted in tasks showing up multiple times on the kanban board.

This change adds a new migration which adds the correct project view id (as derived from the bucket) and fixes the old migration as well.

Resolves https://community.vikunja.io/t/no-longer-able-to-properly-move-tasks-between-kanban-columns/2175
2024-04-06 13:04:36 +02:00
kolaente e0417c8bda
docs: add Korganizer to supported caldav clients
continuous-integration/drone/push Build is passing Details
2024-04-06 12:15:08 +02:00
kolaente 6fbd24d5f6
fix(filter): move spaces out of button to after the matched filter value to prevent removal of spaces
continuous-integration/drone/push Build is failing Details
2024-04-06 12:08:58 +02:00
kolaente e534a6a5bf
fix(modal): do not set p in modal card as flex
This fixes a bug where the description of a project or filter would be aligned right.
2024-04-06 12:08:58 +02:00
kolaente bf85cb0505
fix(filters): always show filter values in a readable color 2024-04-06 12:08:57 +02:00
kolaente 20e2314128
fix(filters): enclose values with a slash in them as strings so that date math values work
Previously, in a filter like "due_date = now/d", the / was parsed as the beginning of a comment, but as it did not contain the full value, this is an invalid comment, resulting in an error message.

Resolves https://community.vikunja.io/t/filter-setting-s/1791/12
2024-04-06 12:08:57 +02:00
kolaente 1ebb551864
fix(filters): make sure the same filter attribute is transformed in all instances
Resolves https://community.vikunja.io/t/filter-setting-s/1791/13
2024-04-06 12:08:57 +02:00
renovate 30c1a46ed4 fix(deps): update src.techknowlogick.com/xgo digest to e01c4fb
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-04-06 09:07:13 +00:00
kolaente 1910f69392
fix(test): correctly mock localstorage in unit tests
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-04-06 10:34:41 +02:00
renovate fe4a093825 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is failing Details
2024-04-06 00:07:33 +00:00
renovate 90055d063c chore(deps): update dev-dependencies (#2229)
continuous-integration/drone/push Build is passing Details
Reviewed-on: #2229
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-04-05 20:41:15 +00:00
waza-ari f0d695e789 fix(views): remove default filter from frontend, apply by default to new list views instead (#2240)
continuous-integration/drone/push Build is passing Details
Fixes #2234

Co-authored-by: Daniel Herrmann <daniel.herrmann1@gmail.com>
Reviewed-on: #2240
Reviewed-by: konrad <k@knt.li>
Co-authored-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
Co-committed-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
2024-04-02 13:49:38 +00:00
kolaente 95276ceebe
fix(reactions): do not enable reaction picker when the current user does not have write access
continuous-integration/drone/push Build is passing Details
2024-04-02 14:48:13 +02:00
kolaente 1558921f42
fix(test): use correct selector in Cypress test
continuous-integration/drone/push Build is failing Details
2024-04-02 14:31:15 +02:00
kolaente bf5088e546
fix(sharing): show user display name and avatar when displaying search results
continuous-integration/drone/push Build is failing Details
Resolves https://community.vikunja.io/t/autogenerated-username-using-google-openid/2183/12
2024-04-02 14:29:22 +02:00
kolaente 6f366d4907
feat(views): lint
continuous-integration/drone/push Build is failing Details
2024-04-02 14:04:17 +02:00
kolaente d7554d9e70
feat(views): hide view switcher when there is only one view 2024-04-02 14:02:59 +02:00
kolaente 8a72fe26f8
fix(views): refactor filter button slot in wrapper
Before this change, the filter button on the top right was positioned using absolute positioning and plenty of tricks, which were brittle and not really maintainable. Now, the buttons are positioned using flexbox, which should make this a lot more maintainable.
2024-04-02 14:02:31 +02:00
kolaente 13cab62d14
fix(views): transform view filter before and after loading it from the api
continuous-integration/drone/push Build is failing Details
Previously, the actual filter was kept as-is when sending it to the api, essentially creating an invalid filter. This change fixes this, transforming the filter before saving and after loading.

Resolves #2233
2024-04-02 13:20:17 +02:00
kolaente 81de986d8d
fix(gantt): correctly show day in chart 2024-04-02 12:53:14 +02:00
kolaente 915f677c2a
fix(views): correctly pass view id to wrapper when gantt view is active 2024-04-02 12:50:10 +02:00
kolaente 8a6e3d5bd7
fix(views): use correct assertion in test
continuous-integration/drone/push Build is passing Details
2024-04-02 12:42:07 +02:00
kolaente 81fe8391e4
fix(project): load full project after creating a project
continuous-integration/drone/push Build is failing Details
When a new project was created, it contained all details already. This led to duplicated views and overridden attributes in the response.

Resolves #2242
2024-03-29 19:28:17 +01:00
kolaente 89e37b88d9
fix(views): update all fields when updating a view
continuous-integration/drone/push Build is failing Details
Resolves #2241
2024-03-29 18:19:16 +01:00
kolaente cc6801c5b1
fix(filter): make sure highlight works for doneAt attribute
continuous-integration/drone/push Build is failing Details
2024-03-29 18:09:02 +01:00
kolaente 767b058915
fix(filter): add white background to filter input 2024-03-29 18:07:37 +01:00
kolaente 2c0d3f2885
fix(views): add bottom spacing 2024-03-29 18:05:30 +01:00
renovate fa170b9397 fix(deps): update sentry-javascript monorepo to v7.109.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-03-28 22:06:03 +00:00
kolaente a5fd6f834a
chore(deps): sign drone config
continuous-integration/drone/push Build is passing Details
2024-03-28 23:00:24 +01:00
renovate 8984e0e9f0 fix(deps): update module github.com/go-sql-driver/mysql to v1.8.1
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is failing Details
2024-03-28 21:06:31 +00:00
renovate 176c41dc40 fix(deps): update dependency express to v4.19.2
continuous-integration/drone/push Build is passing Details
2024-03-28 20:57:07 +00:00
waza-ari c4d3d99cd4 fix: pick first available view if currently configured view got deleted (#2235)
continuous-integration/drone/push Build is failing Details
Resolves #2232

Co-authored-by: Daniel Herrmann <daniel.herrmann1@gmail.com>
Reviewed-on: #2235
Reviewed-by: konrad <k@knt.li>
Co-authored-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
Co-committed-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
2024-03-28 20:55:52 +00:00
renovate 30b4ed6b23 fix(deps): update dependency dompurify to v3.0.11
continuous-integration/drone/push Build is failing Details
2024-03-28 20:53:00 +00:00
renovate aed92d1cd2 fix(deps): update sentry-javascript monorepo to v7.108.0
continuous-integration/drone/push Build is failing Details
2024-03-28 20:49:17 +00:00
Frederick [Bot] 2239d73797 chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-03-27 00:11:11 +00:00
Frederick [Bot] 98b833f61a chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-03-25 00:10:35 +00:00
Frederick [Bot] ecd002dca3 chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-03-21 00:05:22 +00:00
renovate 5e3616bda3 fix(deps): update dependency ufo to v1.5.3
continuous-integration/drone/push Build is passing Details
2024-03-20 16:32:22 +00:00
renovate 3e5ff77b91 fix(deps): update dependency express to v4.19.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-03-20 16:05:36 +00:00
kolaente 403db6adbf
fix(reminder): do not close the popup directly after changing the value
continuous-integration/drone/push Build is passing Details
Fixes https://github.com/go-vikunja/vikunja/issues/225
2024-03-20 11:58:29 +01:00
kolaente fd4312382e
fix(kanban): remove unused function
continuous-integration/drone/push Build is failing Details
2024-03-20 11:46:36 +01:00
kolaente 2dcf6c6861
fix(kanban): do not use the bucket id saved on the task
continuous-integration/drone/push Build was killed Details
2024-03-20 11:36:54 +01:00
kolaente 8f85af07ca
fix(task): clear timeout for description save when closing the task detail 2024-03-20 11:26:54 +01:00
kolaente 9f89fbe5a6
fix(tests): do not try to create tasks with bucket_id
continuous-integration/drone/push Build is failing Details
2024-03-20 10:54:37 +01:00
kolaente 6ad83c0685
chore: do not import message dynamically
continuous-integration/drone/push Build is failing Details
Since it was not done consistently, it would not get imported dynamically anyway. This fixes the compile warnings.
2024-03-20 10:52:59 +01:00
kolaente 97b7592e7c
fix(views): do not map bucket id from xorm
continuous-integration/drone/push Build was killed Details
2024-03-20 10:41:58 +01:00
kolaente 19c9cd9bc2
fix(docker): don't install cypress in docker image
continuous-integration/drone/push Build is passing Details
2024-03-20 09:38:28 +01:00
Frederick [Bot] e53fcd3367 [skip ci] Updated swagger docs 2024-03-20 08:35:16 +00:00
kolaente d635fd2dd3
fix(projects): remove done bucket id field from projects struct
continuous-integration/drone/push Build is passing Details
2024-03-20 09:21:40 +01:00
renovate b8584301a3 fix(deps): update dependency @infectoone/vue-ganttastic to v2.3.1
continuous-integration/drone/push Build is passing Details
2024-03-20 08:01:25 +00:00
renovate 2bb4c31f20 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-03-20 03:07:47 +00:00
Frederick [Bot] d22ebef0b3 chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-03-20 00:06:31 +00:00
Frederick [Bot] 68d8ed5a7a [skip ci] Updated swagger docs 2024-03-19 19:28:04 +00:00
konrad 7230db1603 feat: decouple views from projects (#2217)
continuous-integration/drone/push Build is passing Details
This PR decouples views from projects. On the surface, everything stays the same - by default, there are the same views as right now in main - List, Gantt, Table, Kanban. With this feature, it is possible to modify these or create new ones. That means you can remove views you never need or create multiple ones if you need different configurations.

Each view can have an optional filter to change what you see in the frontend on that view. For kanban, you can either set it to "manual" mode, where you can create buckets and move tasks around, or "filter" mode, where each bucket is the result of a filter (and you cannot move them around).

All positions and buckets are now tied to the view, not the project. This means you can (finally!) have views for saved filters.

Reviewed-on: #2217
2024-03-19 19:16:11 +00:00
kolaente 32e8a15f1f
fix(views): create default bucket
continuous-integration/drone/pr Build is passing Details
2024-03-19 19:53:46 +01:00
renovate f80ffcd541 fix(deps): update module github.com/coreos/go-oidc/v3 to v3.10.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-03-19 18:05:40 +00:00
kolaente 89ed71777e
fix(views): create bucket in test
continuous-integration/drone/pr Build is failing Details
2024-03-19 17:57:00 +01:00
kolaente 5b01710943
fix(views): intercept request
continuous-integration/drone/pr Build is failing Details
2024-03-19 17:42:59 +01:00
kolaente d7b40f393e
fix(views): redirect to project after authenticating with a link share 2024-03-19 17:38:33 +01:00
kolaente fee75e55a3
fix(views): stable assertion for bucket in tests 2024-03-19 17:27:03 +01:00
kolaente fa137b1ffc
fix(views): include order by fields in distinct clause when sorting by task position
continuous-integration/drone/pr Build is failing Details
2024-03-19 17:05:12 +01:00
kolaente e7d6ee2392
fix(views): update done status of recurring tasks 2024-03-19 17:04:39 +01:00
kolaente 62ff05695f
fix(views): kanban test assertions 2024-03-19 16:59:46 +01:00
kolaente 6f51b56589
fix: lint 2024-03-19 16:49:39 +01:00
kolaente 5e9edef3b3
fix: lint
continuous-integration/drone/pr Build is failing Details
2024-03-19 16:33:23 +01:00
kolaente f18fcc5569
fix(views): make task cypress tests work again
continuous-integration/drone/pr Build is failing Details
2024-03-19 15:27:31 +01:00
kolaente bc8b5da61d
fix(views): make overview cypress tests work again 2024-03-19 15:23:36 +01:00
kolaente b3a96ea251
fix(views): make link share cypress tests work again 2024-03-19 15:09:38 +01:00
kolaente b0ad087a36
fix(views): correctly save and retrieve last accessed project views 2024-03-19 14:57:16 +01:00
kolaente b65d05ec3d
fix(views): make table view cypress tests work again 2024-03-19 14:43:52 +01:00
kolaente 974c9cdd21
fix(views): always redirect to the first view when none was specified 2024-03-19 14:39:10 +01:00
kolaente 8206cc8767
fix(views): make list cypress tests work again 2024-03-19 14:38:52 +01:00
kolaente 53e57d524a
fix(views): make kanban cypress tests work again 2024-03-19 14:21:32 +01:00
kolaente 165d291cd5
fix(views): reset bucket when moving tasks between projects 2024-03-19 14:16:05 +01:00
kolaente e940db6d32
fix(views): return only tasks when the bucket id was already specified 2024-03-19 13:55:28 +01:00
kolaente 7c30b00668
fix(views): correctly pass project id when loading more tasks in kanban views 2024-03-19 13:55:05 +01:00
renovate 0ee8150e24 fix(deps): update dependency dompurify to v3.0.10
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-03-19 12:06:01 +00:00
kolaente cf9b2fa203
fix(views): tests for kanban and gantt views
continuous-integration/drone/pr Build is failing Details
2024-03-19 12:11:27 +01:00
renovate a9622fe03a chore(deps): update dev-dependencies
continuous-integration/drone/push Build is passing Details
2024-03-19 10:08:57 +00:00
renovate c67f0ae3cc fix(deps): update dependency @kyvg/vue3-notification to v3.2.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-03-19 09:07:58 +00:00
renovate 8e7828f71d fix(deps): update dependency ufo to v1.5.2
continuous-integration/drone/push Build is passing Details
2024-03-19 08:44:45 +00:00
renovate 5e60edd9ae chore(deps): update dependency happy-dom to v14
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-03-19 01:06:24 +00:00
kolaente 511c9aa824
fix(views): make tests for project history kind of work again
continuous-integration/drone/pr Build is failing Details
2024-03-19 00:47:52 +01:00
kolaente 4b903c4f48
fix(views): lint 2024-03-19 00:47:52 +01:00
kolaente 30b41bd143
fix(views): lint 2024-03-19 00:47:51 +01:00
kolaente f3cdd7d15f
fix(views): import 2024-03-19 00:47:51 +01:00
kolaente 8b90eb4a15
fix(views): integration tests 2024-03-19 00:47:51 +01:00
kolaente 803f58f402
fix(views): return correct error 2024-03-19 00:47:51 +01:00
kolaente b7b3169169
fix(views): count task buckets 2024-03-19 00:47:51 +01:00
kolaente 409f9a0cc6
fix(views): test assertions 2024-03-19 00:47:51 +01:00
kolaente 9075a45cb8
fix(views): update test fixtures for new structure 2024-03-19 00:47:51 +01:00
kolaente d4bdd2d4e8
fix(views): duplicate all views and related entities when duplicating a project 2024-03-19 00:47:51 +01:00
kolaente 9cc273d9bd
fix(views): move all tasks to the default bucket when deleting a bucket 2024-03-19 00:47:51 +01:00
kolaente 0f60a92873
fix(views): make kanban tests work again 2024-03-19 00:47:51 +01:00
kolaente bec9e3eb7d
fix(views): set current project after modifying views 2024-03-19 00:47:51 +01:00
kolaente 3f8c5a5feb
fix(views): set correct default view 2024-03-19 00:47:51 +01:00
kolaente 24fa3b206f
fix(views): create view 2024-03-19 00:47:50 +01:00
kolaente 434b1ea0e8
feat(views): crud in frontend 2024-03-19 00:47:50 +01:00
kolaente 433584813a
fix(views): view deletion 2024-03-19 00:47:50 +01:00
kolaente 3ec3bb76af
fix(views): make parsing work 2024-03-19 00:47:50 +01:00
kolaente 6e53bf4ebe
feat(filter): add unique id to filter input 2024-03-19 00:47:50 +01:00
kolaente b8ff7910b0
feat(filter): make filter input label configurable 2024-03-19 00:47:50 +01:00
kolaente f6485be9e2
chore(views): move actual project views into their own folder 2024-03-19 00:47:50 +01:00
kolaente 004f1e06bb
fix(views): do not return kanban tasks multiple times 2024-03-19 00:47:50 +01:00
kolaente 27cb6e3372
fix(views): make bucket edit work 2024-03-19 00:47:50 +01:00
kolaente 445f1c06fa
fix(views): make bucket creation work again 2024-03-19 00:47:50 +01:00
kolaente 4c1a53beed
chore(views): use view id instead of passing whole view object 2024-03-19 00:47:50 +01:00
kolaente 7368a51f18
fix(views): make setting task position in saved filters work 2024-03-19 00:47:49 +01:00
kolaente e1774cc49a
feat(views): show tasks on kanban board in saved filter 2024-03-19 00:47:49 +01:00
kolaente 61e27ae3eb
feat(views): create task bucket relation when creating a new bucket 2024-03-19 00:47:49 +01:00
kolaente 7f1788eba9
fix(views): get tasks in saved filter 2024-03-19 00:47:49 +01:00
kolaente 39c9928421
fix(views): do not load views async 2024-03-19 00:47:49 +01:00
kolaente 59ced554cd
chore(views): remove old view routes 2024-03-19 00:47:49 +01:00
kolaente bc34a33922
fix(views): move to new project view when moving tasks 2024-03-19 00:47:49 +01:00
kolaente 2dfb3a6379
fix(views): make no initial view work in the frontend 2024-03-19 00:47:49 +01:00
kolaente 337d289a39
chore: remove old saved views migration 2024-03-19 00:47:49 +01:00
kolaente 5451ddf58d
fix(views): return tasks directly or in buckets, no matter if accessing via user or link share 2024-03-19 00:47:49 +01:00
kolaente a3714c74fd
feat(views): load views when navigating with link share 2024-03-19 00:47:49 +01:00
kolaente 4170f5468f
feat(views): save task position in list view 2024-03-19 00:47:49 +01:00
kolaente f364f3bec8
feat(views): return position when retriving tasks 2024-03-19 00:47:48 +01:00
kolaente 786e67f692
feat(views): save task position 2024-03-19 00:47:48 +01:00
kolaente c36fdb9f5d
chore(views): add fixme 2024-03-19 00:47:48 +01:00
kolaente dee78be579
fix(views): return buckets when fetching tasks via kanban view 2024-03-19 00:47:48 +01:00
kolaente 398c9f1056
fix(views): return tasks in their buckets 2024-03-19 00:47:48 +01:00
kolaente ca0550acea
fix(views): fetch buckets through view 2024-03-19 00:47:48 +01:00
kolaente cb111df2b7
fix(views): make fetching tasks in kanban buckets through view actually work 2024-03-19 00:47:48 +01:00
kolaente df415f97a9
fix(views): make table view load tasks again 2024-03-19 00:47:48 +01:00
kolaente 86039b1dd2
fix(views): make gantt view load tasks again 2024-03-19 00:47:48 +01:00
kolaente 73e5483e87
fix(views): do not break filters when combining them with view filters 2024-03-19 00:47:48 +01:00
kolaente 6913334b17
fix(views): correctly fetch project when fetching tasks 2024-03-19 00:47:48 +01:00
kolaente 7866543198
feat(views): generate swagger docs 2024-03-19 00:47:48 +01:00
kolaente cf15cc6f12
feat(views): fetch tasks via view context when accessing them through views 2024-03-19 00:47:47 +01:00
kolaente ee6ea03506
feat(views): sort by position 2024-03-19 00:47:47 +01:00
kolaente 43f24661d7
feat(views): save view and position in Typesense 2024-03-19 00:47:47 +01:00
kolaente 5641da27f7
feat(views): save position in Typesense 2024-03-19 00:47:47 +01:00
kolaente 14353b24d7
feat(views): set default position 2024-03-19 00:47:47 +01:00
kolaente ca4e3e01c5
feat(views): recalculate all positions when updating 2024-03-19 00:47:47 +01:00
kolaente 8ce476491e
feat(views): only update the bucket when necessary 2024-03-19 00:47:47 +01:00
kolaente f2a0d69670
feat(views)!: make updating a bucket work again 2024-03-19 00:47:47 +01:00
kolaente a13276e28e
feat(views)!: decouple bucket <-> task relationship 2024-03-19 00:47:47 +01:00
kolaente 9cf84646a1
feat(views)!: move done and default bucket setting to view 2024-03-19 00:47:47 +01:00
kolaente 006f932dc4
feat(views)!: decouple bucket CRUD from projects 2024-03-19 00:47:47 +01:00
kolaente 0a3f45ab11
feat(views): decouple buckets from projects 2024-03-19 00:47:47 +01:00
kolaente d1d07f462c
feat(views): sort tasks by their position relative to the view they're in 2024-03-19 00:47:46 +01:00
kolaente 2502776460
feat(views)!: move task position handling to its own crud entity
BREAKING CHANGE: the position of tasks now can't be updated anymore via the task update endpoint. Instead, there is a new endpoint which takes the project view into account as well.
2024-03-19 00:47:46 +01:00
kolaente 238baf86f7
feat(views)!: return tasks in buckets by view
BREAKING CHANGE: tasks in their bucket are now only retrievable via their view. The /project/:id/buckets endpoint now only returns the buckets for that project, which is more in line with the other endpoints
2024-03-19 00:47:46 +01:00
kolaente 652bf4b4ed
feat(views): (un)marshal custom project view mode types 2024-03-19 00:47:46 +01:00
kolaente a9020e976d
feat(views): add bucket configuration mode 2024-03-19 00:47:46 +01:00
kolaente 38457aaca5
feat(views): use project id when fetching views 2024-03-19 00:47:46 +01:00
kolaente 98b7cc9254
feat(views): do not override filters in view 2024-03-19 00:47:46 +01:00
kolaente 4149ebed3a
feat(views): create default views when creating a filter 2024-03-19 00:47:46 +01:00
kolaente 2096fc5274
feat(views): return tasks in a view 2024-03-19 00:47:46 +01:00
kolaente e4b1a5d2db
feat(views): create default 4 default view for projects 2024-03-19 00:47:46 +01:00
kolaente 2fa3e2c2f5
feat(views): return views with their projects 2024-03-19 00:47:46 +01:00
kolaente ee228106fc
feat(views): add new default views for filters 2024-03-19 00:47:45 +01:00
kolaente b39c5580c2
feat(views): add crud handlers and routes for views 2024-03-19 00:47:45 +01:00
kolaente 6bdb33fb46
feat(views): add new model and migration 2024-03-19 00:47:45 +01:00
renovate 091e03a39d fix(deps): update module xorm.io/xorm to v1.3.9
continuous-integration/drone/push Build is passing Details
2024-03-18 09:23:11 +00:00
renovate 55c9403dda chore(deps): update pnpm to v8.15.5
continuous-integration/drone/push Build is passing Details
2024-03-18 09:08:09 +00:00
renovate 3f33f903b5 chore(deps): update dev-dependencies
continuous-integration/drone/push Build is failing Details
2024-03-18 09:05:33 +00:00
renovate 650c6cb339 fix(deps): update dependency date-fns to v3.6.0
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build encountered an error Details
2024-03-18 02:07:06 +00:00
kolaente 2fff9f1c59
fix(deps): update module github.com/adlio/trello to v1.11.0
continuous-integration/drone/push Build is passing Details
2024-03-17 21:44:20 +01:00
renovate 2cbd20a084 fix(deps): update dependency date-fns to v3.5.0
continuous-integration/drone/push Build is failing Details
2024-03-16 10:12:02 +00:00
renovate 15949adc2b fix(deps): update dependency ufo to v1.5.1
continuous-integration/drone/push Build is failing Details
2024-03-16 10:11:29 +00:00
renovate 11f2db0e9c chore(deps): update dev-dependencies
continuous-integration/drone/push Build is failing Details
2024-03-16 10:11:17 +00:00
Frederick [Bot] 87ebe85972 chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-03-16 00:05:25 +00:00
renovate 0cf11228cf fix(deps): update dependency vue-i18n to v9.10.2
continuous-integration/drone/push Build is failing Details
2024-03-15 18:16:11 +00:00
renovate 9d01b9105a fix(deps): update dependency ufo to v1.5.0
continuous-integration/drone/push Build is failing Details
2024-03-15 18:15:56 +00:00
renovate d6bc09b0cf fix(deps): update dependency axios to v1.6.8
continuous-integration/drone/pr Build is failing Details
continuous-integration/drone/push Build is failing Details
2024-03-15 17:07:21 +00:00
waza-ari be54a361fd docs: add details about supported and required OIDC claims (#2201)
continuous-integration/drone/push Build is passing Details
Again based on a [community question](https://community.vikunja.io/t/oidc-how-can-i-prevent-username-from-being-set-randomly-how-can-users-find-each-other/2138/2), it might make sense to add a few more details about the OIDC behaviour to the docs.

Co-authored-by: Daniel Herrmann <daniel.herrmann1@gmail.com>
Reviewed-on: #2201
Co-authored-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
Co-committed-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
2024-03-15 13:50:46 +00:00
renovate 725a04b93c fix(deps): update sentry-javascript monorepo to v7.107.0
continuous-integration/drone/push Build is failing Details
2024-03-15 07:50:09 +00:00
renovate 2add517d6e chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-03-15 00:06:35 +00:00
kolaente 96186250f4
fix(filters): clear autocomplete results when starting the next character
continuous-integration/drone/push Build is passing Details
2024-03-14 09:05:07 +01:00
kolaente 6cf3a578c0
fix(filters): correctly replace values when clicking on an autocomplete result
continuous-integration/drone/push Build is failing Details
Related #2194
2024-03-14 09:02:57 +01:00
kolaente c8b35d49ca
fix(filters): correctly return project from filter
continuous-integration/drone/push Build is passing Details
Related #2194
2024-03-14 08:49:02 +01:00
kolaente 161bb1b192
fix(filters): do not watch debounced 2024-03-14 08:40:03 +01:00
kolaente 3ab22d8e06
chore(deps): update google.golang.org/protobuf from 1.32.0 to 1.33.0
continuous-integration/drone/push Build is passing Details
2024-03-14 08:33:13 +01:00
renovate 273f5ddf59 chore(deps): update dev-dependencies
continuous-integration/drone/push Build is failing Details
2024-03-14 07:22:29 +00:00
Frederick [Bot] 88fdfb50b7 chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-03-14 00:06:47 +00:00
kolaente 07e84f2abf
fix(reminders): make debounce logic actually work
continuous-integration/drone/push Build is passing Details
2024-03-13 20:11:00 +01:00
kolaente d4605905d3
fix(filters): do not fire filter change immediately
continuous-integration/drone/push Build is failing Details
Related to #2194 (comment)
2024-03-13 19:58:24 +01:00
kolaente 8c826c44d2
fix(webhooks): fire webhooks set on parent projects as well
continuous-integration/drone/push Build is failing Details
2024-03-13 19:41:34 +01:00
kolaente 117079bbda
fix(sentry): do not send api errors to sentry
continuous-integration/drone/push Build is failing Details
2024-03-13 19:31:43 +01:00
kolaente f34577f293
fix(editor): do not use Tiptap to open links when clicking on them, use the browser native attributes instead
continuous-integration/drone/push Build is failing Details
It looks like links are opened twice, when the openOnClick option is enabled. That means they will get opened twice when clicking on them. Disabling that option will not fire the click handler and only rely on browser functionality to open links.

Resolves #2155
2024-03-13 19:23:02 +01:00
kolaente 8ff59d4649
fix(task): navigate back to project when the project was the last page in the history the user visited
continuous-integration/drone/push Build is failing Details
2024-03-13 19:11:49 +01:00
kolaente 7bf2664e55
fix(filters): persist filters in url
continuous-integration/drone/push Build is failing Details
This allows us to keep the filters when navigating back from a task or other url.
2024-03-13 19:03:23 +01:00
kolaente ccb708a56f
fix(reminders): emit reminder changes at the correct time (and make sure they are actually emitted)
continuous-integration/drone/push Build is failing Details
Resolves https://github.com/go-vikunja/vikunja/issues/225
2024-03-13 18:42:55 +01:00
kolaente 1de39b1cd1
fix(quick actions): do not allow creating a task when the current project is a saved filter
continuous-integration/drone/push Build is passing Details
Resolves https://community.vikunja.io/t/creating-task-on-saved-filter-page-doesnt-save/2127
2024-03-13 18:16:18 +01:00
kolaente b3caece256
fix(datepicker): emit date value changes as soon as they happen
continuous-integration/drone/push Build is passing Details
Flatpickr only returns a change event when the value in the input it's referring to changes. That means it will usually only trigger when the focus is moved out of the input field. This is fine most of the time. However, since we're displaying flatpickr in a popup, the whole html dom instance might get destroyed, before the change event had a chance to fire. In that case, it would not update the date value. To fix this, we're now listening on every change and bubble them up as soon as they happen.

Resolves https://community.vikunja.io/t/due-date-confirm-button-not-working/2104
2024-03-13 18:03:49 +01:00
kolaente a6edf1d325
feat(filters): make clear filters button less obvious
continuous-integration/drone/push Build is passing Details
2024-03-13 17:33:34 +01:00
kolaente fc4eed6eb4
fix(filters): lint 2024-03-13 17:21:20 +01:00
kolaente 15215b30a0
fix(filters): rework filter popup button
continuous-integration/drone/push Build is failing Details
2024-03-13 17:19:15 +01:00
kolaente 79577c14b7
fix(filters): set default filter value to only undone tasks
continuous-integration/drone/push Build is failing Details
2024-03-13 17:07:10 +01:00
kolaente 99c5524115
fix(editor): don't allow image upload when it's not possible to do it
continuous-integration/drone/push Build is failing Details
2024-03-13 16:59:57 +01:00
renovate 17e222edfd chore(deps): update dependency happy-dom to v13.8.2
continuous-integration/drone/push Build is passing Details
2024-03-13 07:44:23 +00:00
Frederick [Bot] fb5b2542a5 chore(i18n): update translations via Crowdin
continuous-integration/drone/push Build is passing Details
2024-03-13 00:05:52 +00:00
kolaente 5b2b7f7bdc
fix(kanban): reset done and default bucket when the bucket itself is deleted
continuous-integration/drone/push Build is passing Details
Resolves https://github.com/go-vikunja/vikunja/issues/234
2024-03-12 22:23:35 +01:00
kolaente e1c972d64d
fix(filters): replace project titles at the match position, not anywhere in the filter string
continuous-integration/drone/push Build is passing Details
This fixes a bug where the project title would not be replaced correctly in cases where the project title contained parts of the word "project".

Resolves #2194
2024-03-12 22:05:26 +01:00
kolaente cf6b476b7d
chore: cleanup leftover console.log
continuous-integration/drone/push Build is passing Details
2024-03-12 21:33:24 +01:00
kolaente eb4f880c64
fix(filter): do not show filter footer when creating a filter
continuous-integration/drone/push Build is failing Details
2024-03-12 21:30:59 +01:00
kolaente e44897e0d4
fix(filter): do not match join operator
Partial fix for #2194
2024-03-12 21:30:59 +01:00
renovate 0e2ad5dde6 fix(deps): pin dependency vuemoji-picker to 0.2.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-03-12 20:07:21 +00:00
Frederick [Bot] 792bf88dcf [skip ci] Updated swagger docs 2024-03-12 19:47:16 +00:00
kolaente a5c51d4b1e feat: emoji reactions for tasks and comments (#2196)
continuous-integration/drone/push Build is passing Details
This PR adds reactions for tasks and comments, similar to what you can do on Gitea, GitHub, Slack and plenty of other tools.

Reviewed-on: #2196
Co-authored-by: kolaente <k@knt.li>
Co-committed-by: kolaente <k@knt.li>
2024-03-12 19:25:58 +00:00
renovate b9c513f681 fix(deps): update sentry-javascript monorepo to v7.106.1
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-03-12 09:07:14 +00:00
renovate 40bdecfe0d fix(deps): update dependency date-fns to v3.4.0
continuous-integration/drone/push Build is passing Details
2024-03-12 08:56:15 +00:00
renovate da53c8e7ef chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details
2024-03-12 06:07:20 +00:00
kolaente 85fb8e3443
fix(filters): invalid filter range when converting dates to strings
continuous-integration/drone/push Build is passing Details
Resolves https://community.vikunja.io/t/my-vikunja-instance-creates-tasks-with-due-date-time-of-9am-for-tasks-with-the-word-today-word-in-it/2105/10
2024-03-11 23:28:35 +01:00
Frederick [Bot] 3f380e0d61 [skip ci] Updated swagger docs 2024-03-11 16:41:16 +00:00
kolaente 659de54db1
feat(kanban): do not remove focus from the input after creating a new bucket
continuous-integration/drone/push Build is passing Details
2024-03-11 17:29:28 +01:00
kolaente 49ab90fc19
fix: lint 2024-03-11 17:24:40 +01:00
kolaente 0910d5d2f2
chore(auth): refactor removing empty openid teams to cron job
continuous-integration/drone/push Build is failing Details
2024-03-11 17:20:05 +01:00
kolaente 09d5128050
fix(filters): don't escape valid escaped in queries
continuous-integration/drone/push Build is failing Details
2024-03-11 17:02:04 +01:00
kolaente e097721817
fix(tasks): use correct filter query when filtering 2024-03-11 16:39:27 +01:00
kolaente a66e26678e
feat(filters): pass timezone down when filtering with relative date math
Resolves https://community.vikunja.io/t/my-vikunja-instance-creates-tasks-with-due-date-time-of-9am-for-tasks-with-the-word-today-word-in-it/2105/8
2024-03-11 16:28:25 +01:00
kolaente 6fc3d1e98f
fix: lint
continuous-integration/drone/push Build is failing Details
2024-03-11 15:42:09 +01:00
kolaente dbfe162cd2
fix(filters): label highlighting and autocomplete fields now work with in operator
continuous-integration/drone/push Build was killed Details
Previously, when creating a filter query with the 'in' operator and multiple values, autocompletion and highlighting was not available. This change now implements a split for each value, seperated by a comma.
2024-03-11 15:41:06 +01:00
kolaente 0529f30e77
fix(filters): parse labels and projects correctly when using `in` filter operator 2024-03-11 15:16:39 +01:00
kolaente 3896c680d3
fix(filters): do not require string for in comparator
continuous-integration/drone/push Build is failing Details
2024-03-11 14:36:59 +01:00
kolaente 3b77fff4c9
fix(project): correctly show the number of tasks and projects when deleting a project
continuous-integration/drone/push Build is passing Details
2024-03-11 14:21:42 +01:00
renovate 12fbde8e84 chore(deps): update dev-dependencies
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is passing Details
2024-03-11 05:06:04 +00:00
waza-ari 6c98052176 fix(teams): fix duplicate teams being shown when new public team visibility feature is enabled (#2187)
continuous-integration/drone/push Build is passing Details
Due to the `INNER JOIN` on the `team_members` table and the new `OR` conditions allowing teams with the `isPublic` flag set to `true`, teams are returned multiple times. As we're only after the teams, a simple distinct query should fix the issue.

Co-authored-by: Daniel Herrmann <daniel.herrmann1@gmail.com>
Reviewed-on: #2187
Co-authored-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
Co-committed-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
2024-03-10 21:42:34 +00:00
kolaente 0057ac5836
fix(migration): only download uploaded attachments
continuous-integration/drone/push Build is passing Details
2024-03-10 18:41:37 +01:00
kolaente 22dcedcd7d
fix(filter): correctly replace project title in filter query
continuous-integration/drone/push Build is failing Details
Resolves https://community.vikunja.io/t/filter-option-to-exclude-a-tag-project-etc/1523/6
2024-03-10 18:32:15 +01:00
kolaente ca0de680ad
fix(migration): import card covers when migrating from Trello
continuous-integration/drone/push Build is passing Details
2024-03-10 16:30:06 +01:00
waza-ari 4bb1d5edfc fix(docs): openid docs whitespace formatting (#2186)
continuous-integration/drone/push Build is passing Details
Co-authored-by: Daniel Herrmann <daniel.herrmann1@gmail.com>
Reviewed-on: #2186
Reviewed-by: konrad <k@knt.li>
Co-authored-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
Co-committed-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
2024-03-10 14:43:04 +00:00
Elscrux 4b4a7f3c0a docs: fix broken link in migration docs (#2185)
continuous-integration/drone/push Build is passing Details
Seems like one link was broken, this attempts to fix that.

Co-authored-by: Elscrux <nickposer2102@gmail.com>
Reviewed-on: #2185
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Elscrux <elscrux@gmail.com>
Co-committed-by: Elscrux <elscrux@gmail.com>
2024-03-10 14:12:00 +00:00
waza-ari ffa82556e0 feat(teams): add public flags to teams to allow easier sharing with other teams (#2179)
continuous-integration/drone/push Build is failing Details
Resolves #2173
Co-authored-by: Daniel Herrmann <daniel.herrmann1@gmail.com>
Reviewed-on: #2179
Reviewed-by: konrad <k@knt.li>
Co-authored-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
Co-committed-by: waza-ari <daniel.herrmann@makerspace-darmstadt.de>
2024-03-10 14:04:32 +00:00
renovate d7fdefcead chore(deps): update golangci/golangci-lint docker tag to v1.56.2 (#2099)
continuous-integration/drone/push Build is failing Details
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: #2099
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-03-10 13:47:19 +00:00
Elscrux 1d5517b53a docs: add migrations setup doc (#2183)
continuous-integration/drone/push Build is failing Details
This should hopefully make the migration process for obvious, as discussed here https://community.vikunja.io/t/trello-import-issues/2110/7

Co-authored-by: Elscrux <nickposer2102@gmail.com>
Reviewed-on: #2183
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Elscrux <elscrux@gmail.com>
Co-committed-by: Elscrux <elscrux@gmail.com>
2024-03-10 13:32:27 +00:00
245 changed files with 14663 additions and 4505 deletions

View File

@ -139,7 +139,7 @@ steps:
event: [ push, tag, pull_request ]
- name: api-lint
image: golangci/golangci-lint:v1.55.2
image: golangci/golangci-lint:v1.57.2
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
@ -461,7 +461,6 @@ steps:
- cp -r dist-test dist-preview
# Override the default api url used for preview
- sed -i 's|http://localhost:3456|https://try.vikunja.io|g' dist-preview/index.html
- apk add --no-cache perl-utils
# create via:
# `shasum -a 384 ./scripts/deploy-preview-netlify.mjs > ./scripts/deploy-preview-netlify.mjs.sha384`
- shasum -a 384 -c ./scripts/deploy-preview-netlify.mjs.sha384
@ -717,7 +716,7 @@ steps:
# Build os packages and push it to our bucket
- name: build-os-packages-unstable
image: goreleaser/nfpm:v2.35.3
image: goreleaser/nfpm:v2.36.1
pull: always
commands:
- apk add git go
@ -733,7 +732,7 @@ steps:
depends_on: [ after-build-compress ]
- name: build-os-packages-version
image: goreleaser/nfpm:v2.35.3
image: goreleaser/nfpm:v2.36.1
pull: always
commands:
- apk add git go
@ -1401,6 +1400,6 @@ steps:
- failure
---
kind: signature
hmac: bd616ecf66fe95bd25c5f4bd73fc9ccfc20601a87a4f8dd4574d66393eacd077
hmac: 2c9cb0483fb346988188515f6423929f46eefb9e14eb26b0f312a0b694d5fe8c
...

1
.gitignore vendored
View File

@ -28,3 +28,4 @@ vendor/
os-packages/
mage_output_file.go
mage-static
.direnv/

View File

@ -18,6 +18,7 @@ linters:
- scopelint # Obsolete, using exportloopref instead
- durationcheck
- goconst
- musttag
presets:
- bugs
- unused
@ -99,3 +100,7 @@ issues:
- path: pkg/modules/migration/ticktick/ticktick_test.go
linters:
- testifylint
- path: pkg/migration/*
text: "parameter 'tx' seems to be unused, consider removing or renaming it as"
linters:
- revive

View File

@ -5,6 +5,7 @@ WORKDIR /build
ENV PNPM_CACHE_FOLDER .cache/pnpm/
ENV PUPPETEER_SKIP_DOWNLOAD true
ENV CYPRESS_INSTALL_BINARY 0
COPY frontend/ ./

View File

@ -62,6 +62,9 @@ service:
allowiconchanges: true
# Allow using a custom logo via external URL.
customlogourl: ''
# Enables the public team feature. If enabled, it is possible to configure teams to be public, which makes them
# discoverable when sharing a project, therefore not only showing teams the user is member of.
enablepublicteams: false
sentry:
# If set to true, enables anonymous error tracking of api errors via Sentry. This allows us to gather more

View File

@ -51,11 +51,11 @@
}
},
"devDependencies": {
"electron": "29.1.1",
"electron": "29.2.0",
"electron-builder": "24.13.3"
},
"dependencies": {
"connect-history-api-fallback": "2.0.0",
"express": "4.18.3"
"express": "4.19.2"
}
}

View File

@ -581,10 +581,10 @@ cookie-signature@1.0.6:
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
cookie@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
cookie@0.6.0:
version "0.6.0"
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.6.0.tgz#2798b04b071b0ecbff0dbb62a505a8efa4e19051"
integrity sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==
core-util-is@1.0.2:
version "1.0.2"
@ -769,10 +769,10 @@ electron-publish@24.13.1:
lazy-val "^1.0.5"
mime "^2.5.2"
electron@29.1.1:
version "29.1.1"
resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.1.tgz#e9cb11311324e4b43a3e73667cd2b65a30e8fa34"
integrity sha512-cXN15NgCi7MkzGo5/23ZQbii+0UfhmUiDjACunmzcUofYCjF42XhFbL7JZnwgI0qtBCCeJU8qZNZt9lU91gUFw==
electron@29.2.0:
version "29.2.0"
resolved "https://registry.yarnpkg.com/electron/-/electron-29.2.0.tgz#98e9d45dcebda124fb0bd1ff20fc509ec692101c"
integrity sha512-ALKrCN52RG4g9prx4DriXSPnY5WoiyRUCNp7zEVQuoiNOpHTNqMMpRidQAHzntV4hajF1LMWHVoBkwqIs1jHhg==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"
@ -830,17 +830,17 @@ etag@~1.8.1:
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
express@4.18.3:
version "4.18.3"
resolved "https://registry.yarnpkg.com/express/-/express-4.18.3.tgz#6870746f3ff904dee1819b82e4b51509afffb0d4"
integrity sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==
express@4.19.2:
version "4.19.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
integrity sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==
dependencies:
accepts "~1.3.8"
array-flatten "1.1.1"
body-parser "1.20.2"
content-disposition "0.5.4"
content-type "~1.0.4"
cookie "0.5.0"
cookie "0.6.0"
cookie-signature "1.0.6"
debug "2.6.9"
depd "2.0.0"

View File

@ -35,7 +35,7 @@ That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.21`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch index.html`.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch frontend/dist/index.html`.
4. Run `mage build` in the source of the main repo. This will build a binary in the root of the repo which will be able to run on your system.
### Build for different architectures

View File

@ -346,6 +346,17 @@ Full path: `service.customlogourl`
Environment path: `VIKUNJA_SERVICE_CUSTOMLOGOURL`
### enablepublicteams
discoverable when sharing a project, therefore not only showing teams the user is member of.
Default: `false`
Full path: `service.enablepublicteams`
Environment path: `VIKUNJA_SERVICE_ENABLEPUBLICTEAMS`
---
## sentry

View File

@ -0,0 +1,57 @@
---
date: "2023-03-09:00:00+02:00"
title: "Migration from third-party services"
draft: false
type: "doc"
menu:
sidebar:
parent: "setup"
weight: 5
---
# Migration from third-party services
There are several importers available for third-party services like Trello, Microsoft To Do or Todoist.
All available migration options can be found [here](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample#L218).
You can develop migrations for more services, see the [documentation]({{< ref "../development/migration.md">}}) for more info.
{{< table_of_contents >}}
## Trello
### Config Setup
Log into Trello and navigate to the [site](https://trello.com/app-key) to manage your API keys.
Save your `Personal Key` for later and add your Vikunja domain to the Allowed Origins list.
Create a `config.yml` file based on [default config file](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample) if you haven't already.
- Copy the [Trello options](https://kolaente.dev/vikunja/vikunja/src/branch/main/config.yml.sample#L233) from the default config file
- Set `enable` to true
- Set `key` to your [trello API key](https://trello.com/app-key)
- Replace `<frontend url>` in `redirecturl` with your url
### Config Loading
To load the config with Vikunja, see the [installation]({{< ref "install.md">}}) documentation for instructions to load the `config.yml` file and start Vikunja.
### Config Loading with Docker Compose
In case you are using Docker Compose you need to edit `docker-compose.yml` to load `config.yml`.
Mount the `config.yml` file into the Vikunja container, by adding this line to the volumes of the Vikunja container and replacing the `./path/to/config.yml` with the relative path from the `docker-compose.yml` to your `config.yml`.
```yaml
volumes:
- ./path/to/config.yml:/etc/vikunja/config.yml
```
After all the setup is done, start Vikunja as shown in the [Docker Compose setup]({{< ref "full-docker-example.md">}}).
### Start the Migration Process
Log in, and navigate to Settings > Import from other services. In the list of available third-party services, there should be a Trello icon now.
If not, ensure that you are properly loading your config file. Refer to the Vikunja log to see if the config file was loaded or not.
In case the config file was loaded, and there is no Trello icon, make sure your [config setup](#config-setup) is correct.
Click on Trello and on Get Started. This will redirect you to Trello where you need to allow Vikunja Migration to access your account. In case there is an error when being directed to Trello, make sure that your Vikunja domain is in your Trello Allowed Origins list.
Once this is done, you will be redirected to Vikunja which should tell you that the migration is in progress now. Note that this can take up to several hours depending on the amount of boards in your Trello account.

View File

@ -34,6 +34,22 @@ Claims in turn are assertions containing information about the token bearer, usu
**Scopes** are requested by the client when redirecting the end-user to the Authorization Server for authentication, and indirectly control which claims are included in the resulting tokens.
There's certain default scopes, but its also possible to define custom scopes, which are used by the feature assigning users to Teams automatically.
## Supported and required claims
Vikunja only requires a few claims to be present in the ID token to successfully authenticate the user.
Additional claims can be added though to customize behaviour during user creation.
The following table gives an overview about the claims supported by Vikunja. The scope column lists the scope that should request the claim according to the [OpenID Connect Standard](https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims). It omits the claims such as `sub` or `issuer` required by the `openid` scope, which must always be present.
| Claim | Type | Scope | Comment |
| ------|------|-------|---------|
| email | required | email | Sets the email address of the user. Taken from the `userinfo` endpoint if not present in ID token. User creation fails if claim not present and userinfo lookup fails. |
| name | optional | profile | Sets the display name of the user. Taken from the `userinfo` endpoint if not present in ID token. |
| preferred_username | optional | profile | Sets the username of the user. Taken from the `userinfo` endpoint if not present in ID token. If this also doesn't contain the claim, use the `nickname` claim from `userinfo` instead. If that one is not available either, the username is auto-generated by Vikunja. |
| vikunja_groups | optional | N/A | Can be used to automatically assign users to teams. See below for a more detailed explanation about the expected format and implementation examples. |
If one of the claims `email`, `name` or `preferred_username` is missing from the ID token, Vikunja will attempt to query the `userinfo` endpoint to obtain the information from there.
## Configuring OIDC Authentication
To achieve authentication via an external provider, it is required to (a) configure a confidential Client on your OAuth 2.0 provider and (b) configure Vikunja to authenticate against this provider.
@ -64,7 +80,7 @@ auth:
redirecturl: https://vikunja.mydomain.com/auth/openid/ <---- slash at the end is important
providers:
- name: <provider-name>
authurl: <auth-url> <----- Used for OIDC Discovery, usually the issuer
authurl: <auth-url> <----- Used for OIDC Discovery, usually the issuer
clientid: <vikunja client-id>
clientsecret: <vikunja client-secret>
scope: openid profile email
@ -99,7 +115,7 @@ It depends on the provider being used as well as the preferences of the administ
Typically you'd want to request an additional scope (e.g. `vikunja_scope`) which then triggers the identity provider to add the claim.
If the `vikunja_groups` is part of the **ID token**, Vikunja will start the procedure and import teams and team memberships.
The claim structure expexted by Vikunja is as follows:
The minimal claim structure expected by Vikunja is as follows:
```json
{
@ -116,6 +132,21 @@ The claim structure expexted by Vikunja is as follows:
}
```
It is also possible to pass the `description` and the `isPublic` flag as optional parameters. If not present, the description will be empty and project visibility defaults to false.
```json
{
"vikunja_groups": [
{
"name": "team 3",
"oidcID": 33349,
"description": "My Team Description",
"isPublic": true
},
]
}
```
For each team, you need to define a team `name` and an `oidcID`, where the `oidcID` can be any string with a length of less than 250 characters.
The `oidcID` is used to uniquely identify the team, so please make sure to keep this unique.

View File

@ -72,6 +72,7 @@ Vikunja **currently does not** support these properties:
* [Evolution](https://wiki.gnome.org/Apps/Evolution/)
* [OpenTasks](https://opentasks.app/) & [DAVx⁵](https://www.davx5.com/)
* [Tasks (Android)](https://tasks.org/)
* [Korganizer](https://apps.kde.org/korganizer/)
### Not working

View File

@ -55,19 +55,20 @@ This document describes the different errors Vikunja can return.
## Project
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|-------------------------------------------------------------------------------------------------------------------------------------|
| 3001 | 404 | The project does not exist. |
| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
| 3005 | 400 | The project title cannot be empty. |
| 3006 | 404 | The project share does not exist. |
| 3007 | 400 | A project with this identifier already exists. |
| ErrorCode | HTTP Status Code | Description |
|-----------|------------------|------------------------------------------------------------------------------------------------------------------------------------|
| 3001 | 404 | The project does not exist. |
| 3004 | 403 | The user needs to have read permissions on that project to perform that action. |
| 3005 | 400 | The project title cannot be empty. |
| 3006 | 404 | The project share does not exist. |
| 3007 | 400 | A project with this identifier already exists. |
| 3008 | 412 | The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project. |
| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". |
| 3010 | 412 | This project cannot be a child of itself. |
| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. |
| 3012 | 412 | This project cannot be deleted because a user has set it as their default project. |
| 3013 | 412 | This project cannot be archived because a user has set it as their default project. |
| 3009 | 412 | The project cannot belong to a dynamically generated parent project like "Favorites". |
| 3010 | 412 | This project cannot be a child of itself. |
| 3011 | 412 | This project cannot have a cyclic relationship to a parent project. |
| 3012 | 412 | This project cannot be deleted because a user has set it as their default project. |
| 3013 | 412 | This project cannot be archived because a user has set it as their default project. |
| 3014 | 404 | This project view does not exist. |
## Task
@ -97,6 +98,8 @@ This document describes the different errors Vikunja can return.
| 4022 | 400 | The task has a relative reminder which does not specify relative to what. |
| 4023 | 409 | Tried to create a task relation which would create a cycle. |
| 4024 | 400 | The provided filter expression is invalid. |
| 4025 | 400 | The reaction kind is invalid. |
| 4026 | 400 | You must provide a project view ID when sorting by position. |
## Team

View File

@ -46,7 +46,7 @@ The available operators for filtering include:
* `<`: Less than
* `<=`: Less than or equal to
* `like`: Matches a pattern (using wildcard `%`)
* `in`: Matches any value in a list
* `in`: Matches any value in a comma-seperated list of values
To combine multiple conditions, you can use the following logical operators:

View File

@ -2,11 +2,11 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1701336116,
"narHash": "sha256-kEmpezCR/FpITc6yMbAh4WrOCiT2zg5pSjnKrq51h5Y=",
"lastModified": 1712449641,
"narHash": "sha256-U9DDWMexN6o5Td2DznEgguh8TRIUnIl9levmit43GcI=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f5c27c6136db4d76c30e533c20517df6864c46ee",
"rev": "600b15aea1b36eeb43833a50b0e96579147099ff",
"type": "github"
},
"original": {

18
flake.nix Normal file
View File

@ -0,0 +1,18 @@
{
description = "Vikunja dev environment";
outputs = { self, nixpkgs }:
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
in {
defaultPackage.x86_64-linux =
pkgs.mkShell { buildInputs = with pkgs; [
# General tools
git-cliff
# Frontend tools
nodePackages.pnpm cypress
# API tools
go golangci-lint mage
];
};
};
}

1
frontend/.gitignore vendored
View File

@ -13,7 +13,6 @@ node_modules
/dist*
coverage
*.zip
.direnv/
# Test files
cypress/screenshots

View File

@ -1,15 +1,50 @@
import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {ProjectViewFactory} from "../../factories/project_view";
export function createDefaultViews(projectId) {
ProjectViewFactory.truncate()
const list = ProjectViewFactory.create(1, {
id: 1,
project_id: projectId,
view_kind: 0,
}, false)
const gantt = ProjectViewFactory.create(1, {
id: 2,
project_id: projectId,
view_kind: 1,
}, false)
const table = ProjectViewFactory.create(1, {
id: 3,
project_id: projectId,
view_kind: 2,
}, false)
const kanban = ProjectViewFactory.create(1, {
id: 4,
project_id: projectId,
view_kind: 3,
bucket_configuration_mode: 1,
}, false)
return [
list[0],
gantt[0],
table[0],
kanban[0],
]
}
export function createProjects() {
const projects = ProjectFactory.create(1, {
title: 'First Project'
})
TaskFactory.truncate()
projects.views = createDefaultViews(projects[0].id)
return projects
}
export function prepareProjects(setProjects = (...args: any[]) => {}) {
export function prepareProjects(setProjects = (...args: any[]) => {
}) {
beforeEach(() => {
const projects = createProjects()
setProjects(projects)

View File

@ -2,6 +2,7 @@ import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {ProjectFactory} from '../../factories/project'
import {prepareProjects} from './prepareProjects'
import {ProjectViewFactory} from '../../factories/project_view'
describe('Project History', () => {
createFakeUserAndLogin()
@ -11,24 +12,31 @@ describe('Project History', () => {
cy.intercept(Cypress.env('API_URL') + '/projects*').as('loadProjectArray')
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
const projects = ProjectFactory.create(6)
const projects = ProjectFactory.create(7)
ProjectViewFactory.truncate()
projects.forEach(p => ProjectViewFactory.create(1, {
id: p.id,
project_id: p.id,
}, false))
cy.visit('/')
cy.wait('@loadProjectArray')
cy.get('body')
.should('not.contain', 'Last viewed')
cy.visit(`/projects/${projects[0].id}`)
cy.visit(`/projects/${projects[0].id}/${projects[0].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[1].id}`)
cy.visit(`/projects/${projects[1].id}/${projects[1].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[2].id}`)
cy.visit(`/projects/${projects[2].id}/${projects[2].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[3].id}`)
cy.visit(`/projects/${projects[3].id}/${projects[3].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[4].id}`)
cy.visit(`/projects/${projects[4].id}/${projects[4].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[5].id}`)
cy.visit(`/projects/${projects[5].id}/${projects[5].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[6].id}/${projects[6].id}`)
cy.wait('@loadProject')
// cy.visit('/')
@ -46,5 +54,6 @@ describe('Project History', () => {
.should('contain', projects[3].title)
.should('contain', projects[4].title)
.should('contain', projects[5].title)
.should('contain', projects[6].title)
})
})

View File

@ -11,7 +11,7 @@ describe('Project View Gantt', () => {
it('Hides tasks with no dates', () => {
const tasks = TaskFactory.create(1)
cy.visit('/projects/1/gantt')
cy.visit('/projects/1/2')
cy.get('.g-gantt-rows-container')
.should('not.contain', tasks[0].title)
@ -25,7 +25,7 @@ describe('Project View Gantt', () => {
nextMonth.setDate(1)
nextMonth.setMonth(9)
cy.visit('/projects/1/gantt')
cy.visit('/projects/1/2')
cy.get('.g-timeunits-container')
.should('contain', format(now, 'MMMM'))
@ -38,7 +38,7 @@ describe('Project View Gantt', () => {
start_date: now.toISOString(),
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
})
cy.visit('/projects/1/gantt')
cy.visit('/projects/1/2')
cy.get('.g-gantt-rows-container')
.should('not.be.empty')
@ -50,7 +50,7 @@ describe('Project View Gantt', () => {
start_date: null,
end_date: null,
})
cy.visit('/projects/1/gantt')
cy.visit('/projects/1/2')
cy.get('.gantt-options .fancycheckbox')
.contains('Show tasks which don\'t have dates set')
@ -69,7 +69,7 @@ describe('Project View Gantt', () => {
start_date: now.toISOString(),
end_date: new Date(new Date(now).setDate(now.getDate() + 4)).toISOString(),
})
cy.visit('/projects/1/gantt')
cy.visit('/projects/1/2')
cy.get('.g-gantt-rows-container .g-gantt-row .g-gantt-row-bars-container div .g-gantt-bar')
.first()
@ -83,7 +83,7 @@ describe('Project View Gantt', () => {
const now = Date.UTC(2022, 10, 9)
cy.clock(now, ['Date'])
cy.visit('/projects/1/gantt')
cy.visit('/projects/1/2')
cy.get('.project-gantt .gantt-options .field .control input.input.form-control')
.click()
@ -99,7 +99,7 @@ describe('Project View Gantt', () => {
})
it('Should change the date range based on date query parameters', () => {
cy.visit('/projects/1/gantt?dateFrom=2022-09-25&dateTo=2022-11-05')
cy.visit('/projects/1/2?dateFrom=2022-09-25&dateTo=2022-11-05')
cy.get('.g-timeunits-container')
.should('contain', 'September 2022')
@ -115,7 +115,7 @@ describe('Project View Gantt', () => {
start_date: formatISO(now),
end_date: formatISO(now.setDate(now.getDate() + 4)),
})
cy.visit('/projects/1/gantt')
cy.visit('/projects/1/2')
cy.get('.gantt-container .g-gantt-chart .g-gantt-row-bars-container .g-gantt-bar')
.dblclick()

View File

@ -4,35 +4,64 @@ import {BucketFactory} from '../../factories/bucket'
import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
import {ProjectViewFactory} from "../../factories/project_view";
import {TaskBucketFactory} from "../../factories/task_buckets";
function createSingleTaskInBucket(count = 1, attrs = {}) {
const projects = ProjectFactory.create(1)
const buckets = BucketFactory.create(2, {
const views = ProjectViewFactory.create(1, {
id: 1,
project_id: projects[0].id,
view_kind: 3,
bucket_configuration_mode: 1,
})
const buckets = BucketFactory.create(2, {
project_view_id: views[0].id,
})
const tasks = TaskFactory.create(count, {
project_id: projects[0].id,
bucket_id: buckets[0].id,
...attrs,
})
return tasks[0]
TaskBucketFactory.create(1, {
task_id: tasks[0].id,
bucket_id: buckets[0].id,
project_view_id: views[0].id,
})
return {
task: tasks[0],
view: views[0],
project: projects[0],
}
}
function createTaskWithBuckets(buckets, count = 1) {
const data = TaskFactory.create(10, {
project_id: 1,
})
TaskBucketFactory.truncate()
data.forEach(t => TaskBucketFactory.create(count, {
task_id: t.id,
bucket_id: buckets[0].id,
project_view_id: buckets[0].project_view_id,
}, false))
return data
}
describe('Project View Kanban', () => {
createFakeUserAndLogin()
prepareProjects()
let buckets
beforeEach(() => {
buckets = BucketFactory.create(2)
buckets = BucketFactory.create(2, {
project_view_id: 4,
})
})
it('Shows all buckets with their tasks', () => {
const data = TaskFactory.create(10, {
project_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
const data = createTaskWithBuckets(buckets, 10)
cy.visit('/projects/1/4')
cy.get('.kanban .bucket .title')
.contains(buckets[0].title)
@ -46,11 +75,8 @@ describe('Project View Kanban', () => {
})
it('Can add a new task to a bucket', () => {
TaskFactory.create(2, {
project_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
createTaskWithBuckets(buckets, 2)
cy.visit('/projects/1/4')
cy.get('.kanban .bucket')
.contains(buckets[0].title)
@ -68,7 +94,7 @@ describe('Project View Kanban', () => {
})
it('Can create a new bucket', () => {
cy.visit('/projects/1/kanban')
cy.visit('/projects/1/4')
cy.get('.kanban .bucket.new-bucket .button')
.click()
@ -82,7 +108,7 @@ describe('Project View Kanban', () => {
})
it('Can set a bucket limit', () => {
cy.visit('/projects/1/kanban')
cy.visit('/projects/1/4')
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
.first()
@ -103,7 +129,7 @@ describe('Project View Kanban', () => {
})
it('Can rename a bucket', () => {
cy.visit('/projects/1/kanban')
cy.visit('/projects/1/4')
cy.get('.kanban .bucket .bucket-header .title')
.first()
@ -114,7 +140,7 @@ describe('Project View Kanban', () => {
})
it('Can delete a bucket', () => {
cy.visit('/projects/1/kanban')
cy.visit('/projects/1/4')
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-trigger')
.first()
@ -137,17 +163,14 @@ describe('Project View Kanban', () => {
})
it('Can drag tasks around', () => {
const tasks = TaskFactory.create(2, {
project_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
const tasks = createTaskWithBuckets(buckets, 2)
cy.visit('/projects/1/4')
cy.get('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
.first()
.drag('.kanban .bucket:nth-child(2) .tasks')
cy.get('.kanban .bucket:nth-child(2) .tasks')
.should('contain', tasks[0].title)
cy.get('.kanban .bucket:nth-child(1) .tasks')
@ -155,12 +178,8 @@ describe('Project View Kanban', () => {
})
it('Should navigate to the task when the task card is clicked', () => {
const tasks = TaskFactory.create(5, {
id: '{increment}',
project_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
const tasks = createTaskWithBuckets(buckets, 5)
cy.visit('/projects/1/4')
cy.get('.kanban .bucket .tasks .task')
.contains(tasks[0].title)
@ -168,28 +187,33 @@ describe('Project View Kanban', () => {
.click()
cy.url()
.should('contain', `/tasks/${tasks[0].id}`, { timeout: 1000 })
.should('contain', `/tasks/${tasks[0].id}`, {timeout: 1000})
})
it('Should remove a task from the kanban board when moving it to another project', () => {
const projects = ProjectFactory.create(2)
BucketFactory.create(2, {
const views = ProjectViewFactory.create(2, {
project_id: '{increment}',
view_kind: 3,
bucket_configuration_mode: 1,
})
BucketFactory.create(2)
const tasks = TaskFactory.create(5, {
id: '{increment}',
project_id: 1,
bucket_id: 1,
})
TaskBucketFactory.create(5, {
project_view_id: 1,
})
const task = tasks[0]
cy.visit('/projects/1/kanban')
cy.visit('/projects/1/'+views[0].id)
cy.get('.kanban .bucket .tasks .task')
.contains(task.title)
.should('be.visible')
.click()
cy.get('.task-view .action-buttons .button', { timeout: 3000 })
cy.get('.task-view .action-buttons .button', {timeout: 3000})
.contains('Move')
.click()
cy.get('.task-view .content.details .field .multiselect.control .input-wrapper input')
@ -201,27 +225,23 @@ describe('Project View Kanban', () => {
.first()
.click()
cy.get('.global-notification', { timeout: 1000 })
cy.get('.global-notification', {timeout: 1000})
.should('contain', 'Success')
cy.go('back')
cy.get('.kanban .bucket')
.should('not.contain', task.title)
})
it('Shows a button to filter the kanban board', () => {
const data = TaskFactory.create(10, {
project_id: 1,
bucket_id: 1,
})
cy.visit('/projects/1/kanban')
cy.visit('/projects/1/4')
cy.get('.project-kanban .filter-container .base-button')
.should('exist')
})
it('Should remove a task from the board when deleting it', () => {
const task = createSingleTaskInBucket(5)
cy.visit('/projects/1/kanban')
const {task, view} = createSingleTaskInBucket(5)
cy.visit(`/projects/1/${view.id}`)
cy.get('.kanban .bucket .tasks .task')
.contains(task.title)
@ -239,18 +259,18 @@ describe('Project View Kanban', () => {
cy.get('.global-notification')
.should('contain', 'Success')
cy.get('.kanban .bucket .tasks')
.should('not.contain', task.title)
})
it('Should show a task description icon if the task has a description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
const {task, view} = createSingleTaskInBucket(1, {
description: 'Lorem Ipsum',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.visit(`/projects/${task.project_id}/${view.id}`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
@ -258,12 +278,12 @@ describe('Project View Kanban', () => {
})
it('Should not show a task description icon if the task has an empty description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
const {task, view} = createSingleTaskInBucket(1, {
description: '',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.visit(`/projects/${task.project_id}/${view.id}`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
@ -271,15 +291,15 @@ describe('Project View Kanban', () => {
})
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/buckets**').as('loadTasks')
const task = createSingleTaskInBucket(1, {
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
const {task, view} = createSingleTaskInBucket(1, {
description: '<p></p>',
})
cy.visit(`/projects/${task.project_id}/kanban`)
cy.visit(`/projects/${task.project_id}/${view.id}`)
cy.wait('@loadTasks')
cy.get('.bucket .tasks .task .footer .icon svg')
.should('not.exist')
})
})
})

View File

@ -5,15 +5,16 @@ import {TaskFactory} from '../../factories/task'
import {UserFactory} from '../../factories/user'
import {ProjectFactory} from '../../factories/project'
import {prepareProjects} from './prepareProjects'
import {BucketFactory} from '../../factories/bucket'
describe('Project View Project', () => {
describe('Project View List', () => {
createFakeUserAndLogin()
prepareProjects()
it('Should be an empty project', () => {
cy.visit('/projects/1')
cy.url()
.should('contain', '/projects/1/list')
.should('contain', '/projects/1/1')
cy.get('.project-title')
.should('contain', 'First Project')
cy.get('.project-title-dropdown')
@ -24,6 +25,10 @@ describe('Project View Project', () => {
})
it('Should create a new task', () => {
BucketFactory.create(2, {
project_view_id: 4,
})
const newTaskTitle = 'New task'
cy.visit('/projects/1')
@ -38,7 +43,7 @@ describe('Project View Project', () => {
id: '{increment}',
project_id: 1,
})
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.get('.tasks .task .tasktext')
.contains(tasks[0].title)
@ -88,10 +93,10 @@ describe('Project View Project', () => {
title: i => `task${i}`,
project_id: 1,
})
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.get('.tasks')
.should('contain', tasks[1].title)
.should('contain', tasks[20].title)
cy.get('.tasks')
.should('not.contain', tasks[99].title)
@ -104,6 +109,6 @@ describe('Project View Project', () => {
cy.get('.tasks')
.should('contain', tasks[99].title)
cy.get('.tasks')
.should('not.contain', tasks[1].title)
.should('not.contain', tasks[20].title)
})
})

View File

@ -1,13 +1,15 @@
import {createFakeUserAndLogin} from '../../support/authenticateUser'
import {TaskFactory} from '../../factories/task'
import {prepareProjects} from './prepareProjects'
describe('Project View Table', () => {
createFakeUserAndLogin()
prepareProjects()
it('Should show a table with tasks', () => {
const tasks = TaskFactory.create(1)
cy.visit('/projects/1/table')
cy.visit('/projects/1/3')
cy.get('.project-table table.table')
.should('exist')
@ -17,9 +19,9 @@ describe('Project View Table', () => {
it('Should have working column switches', () => {
TaskFactory.create(1)
cy.visit('/projects/1/table')
cy.visit('/projects/1/3')
cy.get('.project-table .filter-container .items .button')
cy.get('.project-table .filter-container .button')
.contains('Columns')
.click()
cy.get('.project-table .filter-container .card.columns-filter .card-content .fancycheckbox')
@ -42,7 +44,7 @@ describe('Project View Table', () => {
id: '{increment}',
project_id: 1,
})
cy.visit('/projects/1/table')
cy.visit('/projects/1/3')
cy.get('.project-table table.table')
.contains(tasks[0].title)

View File

@ -33,14 +33,14 @@ describe('Projects', () => {
})
it('Should redirect to a specific project view after visited', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/*/buckets*').as('loadBuckets')
cy.visit('/projects/1/kanban')
cy.intercept(Cypress.env('API_URL') + '/projects/*/views/*/tasks**').as('loadBuckets')
cy.visit('/projects/1/4')
cy.url()
.should('contain', '/projects/1/kanban')
.should('contain', '/projects/1/4')
cy.wait('@loadBuckets')
cy.visit('/projects/1')
cy.url()
.should('contain', '/projects/1/kanban')
.should('contain', '/projects/1/4')
})
it('Should rename the project in all places', () => {

View File

@ -1,9 +1,9 @@
import {LinkShareFactory} from '../../factories/link_sharing'
import {ProjectFactory} from '../../factories/project'
import {TaskFactory} from '../../factories/task'
import {createProjects} from '../project/prepareProjects'
function prepareLinkShare() {
const projects = ProjectFactory.create(1)
const projects = createProjects()
const tasks = TaskFactory.create(10, {
project_id: projects[0].id
})
@ -32,13 +32,13 @@ describe('Link shares', () => {
cy.get('.tasks')
.should('contain', tasks[0].title)
cy.url().should('contain', `/projects/${project.id}/list#share-auth-token=${share.hash}`)
cy.url().should('contain', `/projects/${project.id}/1#share-auth-token=${share.hash}`)
})
it('Should work when directly viewing a project with share hash present', () => {
const {share, project, tasks} = prepareLinkShare()
cy.visit(`/projects/${project.id}/list#share-auth-token=${share.hash}`)
cy.visit(`/projects/${project.id}/1#share-auth-token=${share.hash}`)
cy.get('h1.title')
.should('contain', project.title)

View File

@ -5,11 +5,13 @@ import {seed} from '../../support/seed'
import {TaskFactory} from '../../factories/task'
import {BucketFactory} from '../../factories/bucket'
import {updateUserSettings} from '../../support/updateUserSettings'
import {createDefaultViews} from "../project/prepareProjects";
function seedTasks(numberOfTasks = 50, startDueDate = new Date()) {
const project = ProjectFactory.create()[0]
const views = createDefaultViews(project.id)
BucketFactory.create(1, {
project_id: project.id,
project_view_id: views[3].id,
})
const tasks = []
let dueDate = startDueDate
@ -60,7 +62,7 @@ describe('Home Page Task Overview', () => {
})
it('Should show a new task with a very soon due date at the top', () => {
const {tasks} = seedTasks()
const {tasks} = seedTasks(49)
const newTaskTitle = 'New Task'
cy.visit('/')
@ -71,9 +73,8 @@ describe('Home Page Task Overview', () => {
due_date: new Date().toISOString(),
}, false)
cy.visit(`/projects/${tasks[0].project_id}/list`)
cy.visit(`/projects/${tasks[0].project_id}/1`)
cy.get('.tasks .task')
.first()
.should('contain.text', newTaskTitle)
cy.visit('/')
cy.get('[data-cy="showTasks"] .card .task')
@ -88,7 +89,7 @@ describe('Home Page Task Overview', () => {
cy.visit('/')
cy.visit(`/projects/${tasks[0].project_id}/list`)
cy.visit(`/projects/${tasks[0].project_id}/1`)
cy.get('.task-add textarea')
.type(newTaskTitle+'{enter}')
cy.visit('/')

View File

@ -12,6 +12,7 @@ import {BucketFactory} from '../../factories/bucket'
import {TaskAttachmentFactory} from '../../factories/task_attachments'
import {TaskReminderFactory} from '../../factories/task_reminders'
import {createDefaultViews} from "../project/prepareProjects";
function addLabelToTaskAndVerify(labelTitle: string) {
cy.get('.task-view .action-buttons .button')
@ -53,15 +54,16 @@ describe('Task', () => {
beforeEach(() => {
// UserFactory.create(1)
projects = ProjectFactory.create(1)
const views = createDefaultViews(projects[0].id)
buckets = BucketFactory.create(1, {
project_id: projects[0].id,
project_view_id: views[3].id,
})
TaskFactory.truncate()
UserProjectFactory.truncate()
})
it('Should be created new', () => {
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.get('.input[placeholder="Add a new task…"')
.type('New Task')
cy.get('.button')
@ -75,7 +77,7 @@ describe('Task', () => {
it('Inserts new tasks at the top of the project', () => {
TaskFactory.create(1)
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.get('.project-is-empty-notice')
.should('not.exist')
cy.get('.input[placeholder="Add a new task…"')
@ -93,7 +95,7 @@ describe('Task', () => {
it('Marks a task as done', () => {
TaskFactory.create(1)
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.get('.tasks .task .fancycheckbox')
.first()
.click()
@ -104,7 +106,7 @@ describe('Task', () => {
it('Can add a task to favorites', () => {
TaskFactory.create(1)
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.get('.tasks .task .favorite')
.first()
.click()
@ -113,12 +115,12 @@ describe('Task', () => {
})
it('Should show a task description icon if the task has a description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: 'Lorem Ipsum',
})
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
@ -126,12 +128,12 @@ describe('Task', () => {
})
it('Should not show a task description icon if the task has an empty description', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: '',
})
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
@ -139,12 +141,12 @@ describe('Task', () => {
})
it('Should not show a task description icon if the task has a description containing only an empty p tag', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/1/tasks**').as('loadTasks')
cy.intercept(Cypress.env('API_URL') + '/projects/1/views/*/tasks**').as('loadTasks')
TaskFactory.create(1, {
description: '<p></p>',
})
cy.visit('/projects/1/list')
cy.visit('/projects/1/1')
cy.wait('@loadTasks')
cy.get('.tasks .task .project-task-icon')
@ -314,8 +316,9 @@ describe('Task', () => {
it('Can move a task to another project', () => {
const projects = ProjectFactory.create(2)
const views = createDefaultViews(projects[0].id)
BucketFactory.create(2, {
project_id: '{increment}',
project_view_id: views[3].id,
})
const tasks = TaskFactory.create(1, {
id: 1,
@ -464,12 +467,11 @@ describe('Task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
project_id: projects[0].id,
bucket_id: buckets[0].id,
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
cy.visit(`/projects/${projects[0].id}/kanban`)
cy.visit(`/projects/${projects[0].id}/4`)
cy.get('.bucket .task')
.contains(tasks[0].title)
@ -831,12 +833,11 @@ describe('Task', () => {
const tasks = TaskFactory.create(1, {
id: 1,
project_id: projects[0].id,
bucket_id: buckets[0].id,
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
cy.visit(`/projects/${projects[0].id}/kanban`)
cy.visit(`/projects/${projects[0].id}/4`)
cy.get('.bucket .task')
.contains(tasks[0].title)

View File

@ -10,7 +10,7 @@ export class BucketFactory extends Factory {
return {
id: '{increment}',
title: faker.lorem.words(3),
project_id: 1,
project_view_id: '{increment}',
created_by_id: 1,
created: now.toISOString(),
updated: now.toISOString(),

View File

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

View File

@ -14,7 +14,6 @@ export class TaskFactory extends Factory {
project_id: 1,
created_by_id: 1,
index: '{increment}',
position: '{increment}',
created: now.toISOString(),
updated: now.toISOString()
}

View File

@ -0,0 +1,13 @@
import {Factory} from '../support/factory'
export class TaskBucketFactory extends Factory {
static table = 'task_buckets'
static factory() {
return {
task_id: '{increment}',
bucket_id: '{increment}',
project_view_id: '{increment}',
}
}
}

View File

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

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.15.4",
"packageManager": "pnpm@8.15.6",
"keywords": [
"todo",
"productivity",
@ -50,16 +50,16 @@
"story:preview": "histoire preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-regular-svg-icons": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/vue-fontawesome": "3.0.6",
"@github/hotkey": "3.1.0",
"@infectoone/vue-ganttastic": "2.2.0",
"@infectoone/vue-ganttastic": "2.3.2",
"@intlify/unplugin-vue-i18n": "3.0.1",
"@kyvg/vue3-notification": "3.2.0",
"@sentry/tracing": "7.106.0",
"@sentry/vue": "7.106.0",
"@kyvg/vue3-notification": "3.2.1",
"@sentry/tracing": "7.109.0",
"@sentry/vue": "7.109.0",
"@tiptap/core": "2.2.4",
"@tiptap/extension-blockquote": "2.2.4",
"@tiptap/extension-bold": "2.2.4",
@ -97,13 +97,13 @@
"@types/lodash.clonedeep": "4.5.9",
"@vueuse/core": "10.9.0",
"@vueuse/router": "10.9.0",
"axios": "1.6.7",
"axios": "1.6.8",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"date-fns": "3.3.1",
"date-fns": "3.6.0",
"dayjs": "1.11.10",
"dompurify": "3.0.9",
"dompurify": "3.0.11",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
@ -117,12 +117,13 @@
"snake-case": "3.0.4",
"sortablejs": "1.15.2",
"tippy.js": "6.3.7",
"ufo": "1.4.0",
"ufo": "1.5.3",
"vue": "3.4.21",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "9.10.1",
"vue-i18n": "9.10.2",
"vue-router": "4.3.0",
"vuemoji-picker": "0.2.1",
"workbox-precaching": "7.0.0",
"zhyswan-vuedraggable": "4.1.3"
},
@ -131,60 +132,61 @@
"@cypress/vite-dev-server": "5.0.7",
"@cypress/vue": "6.0.0",
"@faker-js/faker": "8.4.1",
"@histoire/plugin-screenshot": "0.17.8",
"@histoire/plugin-vue": "0.17.12",
"@rushstack/eslint-patch": "1.7.2",
"@tsconfig/node18": "18.2.2",
"@histoire/plugin-screenshot": "0.17.15",
"@histoire/plugin-vue": "0.17.15",
"@rushstack/eslint-patch": "1.10.1",
"@tsconfig/node18": "18.2.4",
"@types/codemirror": "5.60.15",
"@types/dompurify": "3.0.5",
"@types/flexsearch": "0.7.6",
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.11.25",
"@types/node": "20.12.5",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.1.1",
"@typescript-eslint/parser": "7.1.1",
"@typescript-eslint/eslint-plugin": "7.5.0",
"@typescript-eslint/parser": "7.5.0",
"@vitejs/plugin-legacy": "5.3.2",
"@vitejs/plugin-vue": "5.0.4",
"@vue/eslint-config-typescript": "13.0.0",
"@vue/test-utils": "2.4.4",
"@vue/test-utils": "2.4.5",
"@vue/tsconfig": "0.5.1",
"autoprefixer": "10.4.18",
"autoprefixer": "10.4.19",
"browserslist": "4.23.0",
"caniuse-lite": "1.0.30001596",
"css-has-pseudo": "6.0.2",
"caniuse-lite": "1.0.30001607",
"css-has-pseudo": "6.0.3",
"csstype": "3.1.3",
"cypress": "13.6.6",
"esbuild": "0.20.1",
"cypress": "13.7.2",
"esbuild": "0.20.2",
"eslint": "8.57.0",
"eslint-plugin-vue": "9.22.0",
"happy-dom": "13.7.1",
"histoire": "0.17.9",
"postcss": "8.4.35",
"eslint-plugin-vue": "9.24.0",
"happy-dom": "14.7.1",
"histoire": "0.17.15",
"postcss": "8.4.38",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.1",
"postcss-preset-env": "9.5.0",
"rollup": "4.12.1",
"postcss-preset-env": "9.5.4",
"rollup": "4.14.1",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.71.1",
"sass": "1.74.1",
"start-server-and-test": "2.0.3",
"typescript": "5.4.2",
"vite": "5.1.5",
"typescript": "5.4.4",
"vite": "5.2.8",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.19.2",
"vite-plugin-pwa": "0.19.8",
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.3.1",
"vue-tsc": "2.0.6",
"vitest": "1.4.0",
"vue-tsc": "2.0.11",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
},
"pnpm": {
"patchedDependencies": {
"flexsearch@0.7.31": "patches/flexsearch@0.7.31.patch"
"flexsearch@0.7.31": "patches/flexsearch@0.7.31.patch",
"@github/hotkey@3.1.0": "patches/@github__hotkey@3.1.0.patch"
}
}
}

View File

@ -0,0 +1,28 @@
diff --git a/dist/index.js b/dist/index.js
index b6e6e0a6864cb00bc085b8d4503a705cb3bc8404..0466ef46406b0df41c8d0bb9a5bac9eabf4a50de 100644
--- a/dist/index.js
+++ b/dist/index.js
@@ -368,10 +368,12 @@ const sequenceTracker = new SequenceTracker({
function keyDownHandler(event) {
if (event.defaultPrevented)
return;
- if (!(event.target instanceof Node))
+ const target = event.explicitOriginalTarget || event.target;
+ if (target.shadowRoot)
return;
- if (isFormField(event.target)) {
- const target = event.target;
+ if (!(target instanceof Node))
+ return;
+ if (isFormField(target)) {
if (!target.id)
return;
if (!target.ownerDocument.querySelector(`[data-hotkey-scope="${target.id}"]`))
@@ -385,7 +387,6 @@ function keyDownHandler(event) {
sequenceTracker.registerKeypress(event);
currentTriePosition = newTriePosition;
if (newTriePosition instanceof Leaf) {
- const target = event.target;
let shouldFire = false;
let elementToFire;
const formField = isFormField(target);

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -47,7 +47,7 @@ import AddToHomeScreen from '@/components/home/AddToHomeScreen.vue'
import DemoMode from '@/components/home/DemoMode.vue'
const importAccountDeleteService = () => import('@/services/accountDelete')
const importMessage = () => import('@/message')
import {success} from '@/message'
const baseStore = useBaseStore()
const authStore = useAuthStore()
@ -69,11 +69,9 @@ watch(accountDeletionConfirm, async (accountDeletionConfirm) => {
return
}
const messageP = importMessage()
const AccountDeleteService = (await importAccountDeleteService()).default
const accountDeletionService = new AccountDeleteService()
await accountDeletionService.confirm(accountDeletionConfirm)
const {success} = await messageP
success({message: t('user.deletion.confirmSuccess')})
authStore.refreshUserInfo()
}, { immediate: true })

View File

@ -26,7 +26,6 @@
:project="project"
:is-loading="projectUpdating[project.id]"
:can-collapse="canCollapse"
:level="level"
:data-project-id="project.id"
/>
</template>
@ -49,7 +48,6 @@ const props = defineProps<{
modelValue?: IProject[],
canEditOrder: boolean,
canCollapse?: boolean,
level?: number,
}>()
const emit = defineEmits<{
(e: 'update:modelValue', projects: IProject[]): void

View File

@ -58,7 +58,6 @@
<ProjectSettingsDropdown
class="menu-list-dropdown"
:project="project"
:level="level"
>
<template #trigger="{toggleOpen}">
<BaseButton
@ -78,7 +77,6 @@
:model-value="childProjects"
:can-edit-order="true"
:can-collapse="canCollapse"
:level="level + 1"
/>
</li>
</template>
@ -101,12 +99,10 @@ const {
project,
isLoading,
canCollapse,
level = 0,
} = defineProps<{
project: IProject,
isLoading?: boolean,
canCollapse?: boolean,
level?: number,
}>()
const projectStore = useProjectStore()

View File

@ -37,7 +37,7 @@
v-slot="{ Component }"
:route="routeWithModal"
>
<keep-alive :include="['project.list', 'project.gantt', 'project.table', 'project.kanban']">
<keep-alive :include="['project.view']">
<component :is="Component" />
</keep-alive>
</router-view>

View File

@ -33,11 +33,15 @@ import {useBaseStore} from '@/stores/base'
import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue'
import {useProjectStore} from '@/stores/projects'
const baseStore = useBaseStore()
const currentProject = computed(() => baseStore.currentProject)
const background = computed(() => baseStore.background)
const logoVisible = computed(() => baseStore.logoVisible)
const projectStore = useProjectStore()
projectStore.loadAllProjects()
</script>
<style lang="scss" scoped>

View File

@ -0,0 +1,197 @@
<script setup lang="ts">
import type {IReactionPerEntity, ReactionKind} from '@/modelTypes/IReaction'
import {VuemojiPicker} from 'vuemoji-picker'
import ReactionService from '@/services/reactions'
import ReactionModel from '@/models/reaction'
import BaseButton from '@/components/base/BaseButton.vue'
import type {IUser} from '@/modelTypes/IUser'
import {getDisplayName} from '@/models/user'
import {useI18n} from 'vue-i18n'
import {nextTick, onBeforeUnmount, onMounted, ref} from 'vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {useAuthStore} from '@/stores/auth'
import {useColorScheme} from '@/composables/useColorScheme'
const {
entityKind,
entityId,
disabled = false,
} = defineProps<{
entityKind: ReactionKind,
entityId: number,
disabled?: boolean,
}>()
const authStore = useAuthStore()
const {t} = useI18n()
const reactionService = new ReactionService()
const {isDark} = useColorScheme()
const model = defineModel<IReactionPerEntity>()
async function addReaction(value: string) {
const reaction = new ReactionModel({
id: entityId,
kind: entityKind,
value,
})
await reactionService.create(reaction)
showEmojiPicker.value = false
if (typeof model.value === 'undefined') {
model.value = {}
}
if (typeof model.value[reaction.value] === 'undefined') {
model.value[reaction.value] = [authStore.info]
} else {
model.value[reaction.value].push(authStore.info)
}
}
async function removeReaction(value: string) {
const reaction = new ReactionModel({
id: entityId,
kind: entityKind,
value,
})
await reactionService.delete(reaction)
showEmojiPicker.value = false
const userIndex = model.value[reaction.value].findIndex(u => u.id === authStore.info?.id)
if (userIndex !== -1) {
model.value[reaction.value].splice(userIndex, 1)
}
if(model.value[reaction.value].length === 0) {
delete model.value[reaction.value]
}
}
function getReactionTooltip(users: IUser[], value: string) {
const names = users.map(u => getDisplayName(u))
if (names.length === 1) {
return t('reaction.reactedWith', {user: names[0], value})
}
if (names.length > 1 && names.length < 10) {
return t('reaction.reactedWithAnd', {
users: names.slice(0, names.length - 1).join(', '),
lastUser: names[names.length - 1],
value,
})
}
return t('reaction.reactedWithAndMany', {
users: names.slice(0, 10).join(', '),
num: names.length - 10,
value,
})
}
const showEmojiPicker = ref(false)
const emojiPickerRef = ref<HTMLElement | null>(null)
function hideEmojiPicker(e: MouseEvent) {
if (showEmojiPicker.value) {
closeWhenClickedOutside(e, emojiPickerRef.value.$el, () => showEmojiPicker.value = false)
}
}
onMounted(() => document.addEventListener('click', hideEmojiPicker))
onBeforeUnmount(() => document.removeEventListener('click', hideEmojiPicker))
const emojiPickerButtonRef = ref<HTMLElement | null>(null)
const reactionContainerRef = ref<HTMLElement | null>(null)
const emojiPickerPosition = ref()
function toggleEmojiPicker() {
if (!showEmojiPicker.value) {
const rect = emojiPickerButtonRef.value?.$el.getBoundingClientRect()
const container = reactionContainerRef.value?.getBoundingClientRect()
const left = rect.left - container.left + rect.width
emojiPickerPosition.value = {
left: left === 0 ? undefined : left,
}
}
nextTick(() => showEmojiPicker.value = !showEmojiPicker.value)
}
function hasCurrentUserReactedWithEmoji(value: string): boolean {
const user = model.value[value].find(u => u.id === authStore.info.id)
return typeof user !== 'undefined'
}
async function toggleReaction(value: string) {
if (hasCurrentUserReactedWithEmoji(value)) {
return removeReaction(value)
}
return addReaction(value)
}
</script>
<template>
<div
ref="reactionContainerRef"
class="reactions"
>
<BaseButton
v-for="(users, value) in (model as IReactionPerEntity)"
:key="'button' + value"
v-tooltip="getReactionTooltip(users, value)"
class="reaction-button"
:class="{'current-user-has-reacted': hasCurrentUserReactedWithEmoji(value)}"
:disabled
@click="toggleReaction(value)"
>
{{ value }} {{ users.length }}
</BaseButton>
<BaseButton
v-if="!disabled"
ref="emojiPickerButtonRef"
v-tooltip="$t('reaction.add')"
class="reaction-button"
@click.stop="toggleEmojiPicker"
>
<icon :icon="['far', 'face-laugh']" />
</BaseButton>
<CustomTransition name="fade">
<VuemojiPicker
v-if="showEmojiPicker"
ref="emojiPickerRef"
class="emoji-picker"
:style="{left: emojiPickerPosition?.left + 'px'}"
data-source="/emojis.json"
:is-dark="isDark"
@emojiClick="detail => addReaction(detail.unicode)"
/>
</CustomTransition>
</div>
</template>
<style scoped lang="scss">
.reaction-button {
margin-right: .25rem;
margin-bottom: .25rem;
padding: .175rem .5rem .15rem;
border: 1px solid var(--grey-400);
background: var(--grey-100);
border-radius: 100px;
font-size: .75rem;
&.current-user-has-reacted {
border-color: var(--primary);
background-color: hsla(var(--primary-h), var(--primary-s), var(--primary-light-l), 0.5);
}
}
.emoji-picker {
position: absolute;
z-index: 99;
margin-top: .5rem;
}
</style>

View File

@ -13,7 +13,7 @@
}"
>
<template v-if="icon">
<icon
<icon
v-if="showIconOnly"
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : undefined}"
@ -22,7 +22,7 @@
v-else
class="icon is-small"
>
<icon
<icon
:icon="icon"
:style="{'color': iconColor !== '' ? iconColor : undefined}"
/>
@ -34,20 +34,20 @@
<script lang="ts">
const BUTTON_TYPES_MAP = {
primary: 'is-primary',
secondary: 'is-outlined',
tertiary: 'is-text is-inverted underline-none',
primary: 'is-primary',
secondary: 'is-outlined',
tertiary: 'is-text is-inverted underline-none',
} as const
export type ButtonTypes = keyof typeof BUTTON_TYPES_MAP
export default { name: 'XButton' }
export default {name: 'XButton'}
</script>
<script setup lang="ts">
import {computed, useSlots} from 'vue'
import BaseButton, {type BaseButtonProps} from '@/components/base/BaseButton.vue'
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
// extending the props of the BaseButton
export interface ButtonProps extends /* @vue-ignore */ BaseButtonProps {
@ -76,37 +76,38 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
<style lang="scss" scoped>
.button {
transition: all $transition;
border: 0;
text-transform: uppercase;
font-size: 0.85rem;
font-weight: bold;
height: auto;
min-height: $button-height;
box-shadow: var(--shadow-sm);
display: inline-flex;
white-space: var(--button-white-space);
transition: all $transition;
border: 0;
text-transform: uppercase;
font-size: 0.85rem;
font-weight: bold;
height: auto;
min-height: $button-height;
box-shadow: var(--shadow-sm);
display: inline-flex;
white-space: var(--button-white-space);
line-height: 1;
&:hover {
box-shadow: var(--shadow-md);
}
&:hover {
box-shadow: var(--shadow-md);
}
&.fullheight {
padding-right: 7px;
height: 100%;
}
&.fullheight {
padding-right: 7px;
height: 100%;
}
&.is-active,
&.is-focused,
&:active,
&:focus,
&:focus:not(:active) {
box-shadow: var(--shadow-xs) !important;
}
&.is-active,
&.is-focused,
&:active,
&:focus,
&:focus:not(:active) {
box-shadow: var(--shadow-xs) !important;
}
&.is-primary.is-outlined:hover {
color: var(--white);
}
&.is-primary.is-outlined:hover {
color: var(--white);
}
}
.is-small {
@ -114,6 +115,6 @@ const showIconOnly = computed(() => icon !== '' && typeof slots.default === 'und
}
.underline-none {
text-decoration: none !important;
text-decoration: none !important;
}
</style>

View File

@ -63,6 +63,7 @@
<div class="flatpickr-container">
<flat-pickr
ref="flatPickrRef"
v-model="flatPickrDate"
:config="flatPickerConfig"
/>
@ -70,7 +71,7 @@
</template>
<script lang="ts" setup>
import {ref, toRef, watch, computed, type PropType} from 'vue'
import {computed, onBeforeUnmount, onMounted, type PropType, ref, toRef, watch} from 'vue'
import flatPickr from 'vue-flatpickr-component'
import 'flatpickr/dist/flatpickr.css'
@ -81,7 +82,7 @@ import {calculateDayInterval} from '@/helpers/time/calculateDayInterval'
import {calculateNearestHours} from '@/helpers/time/calculateNearestHours'
import {createDateFromString} from '@/helpers/time/createDateFromString'
import {useI18n} from 'vue-i18n'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
const props = defineProps({
modelValue: {
@ -105,6 +106,7 @@ watch(
{immediate: true},
)
const flatPickrRef = ref<HTMLElement | null>(null)
const flatPickerConfig = computed(() => ({
altFormat: t('date.altFormatLong'),
altInput: true,
@ -142,6 +144,41 @@ const flatPickrDate = computed({
},
})
onMounted(() => {
const inputs = flatPickrRef.value?.$el.parentNode.querySelectorAll('.numInputWrapper > input.numInput')
inputs.forEach(i => {
i.addEventListener('input', handleFlatpickrInput)
})
})
onBeforeUnmount(() => {
const inputs = flatPickrRef.value?.$el.parentNode.querySelectorAll('.numInputWrapper > input.numInput')
inputs.forEach(i => {
i.removeEventListener('input', handleFlatpickrInput)
})
})
// Flatpickr only returns a change event when the value in the input it's referring to changes.
// That means it will usually only trigger when the focus is moved out of the input field.
// This is fine most of the time. However, since we're displaying flatpickr in a popup,
// the whole html dom instance might get destroyed, before the change event had a
// chance to fire. In that case, it would not update the date value. To fix
// this, we're now listening on every change and bubble them up as soon
// as they happen.
function handleFlatpickrInput(e) {
const newDate = new Date(date?.value || 'now')
if (e.target.classList.contains('flatpickr-minute')) {
newDate.setMinutes(e.target.value)
}
if (e.target.classList.contains('flatpickr-hour')) {
newDate.setHours(e.target.value)
}
if (e.target.classList.contains('cur-year')) {
newDate.setFullYear(e.target.value)
}
flatPickrDate.value = newDate
}
function setDateValue(dateString: string | Date | null) {
if (dateString === null) {

View File

@ -139,7 +139,7 @@
<BaseButton
v-tooltip="$t('input.editor.image')"
class="editor-toolbar__button"
@click="openImagePicker"
@click="e => emit('imageUploadClicked', e)"
>
<span class="icon">
<icon icon="fa-image" />
@ -347,16 +347,14 @@ const {
editor: Editor,
}>()
const emit = defineEmits(['imageUploadClicked'])
const tableMode = ref(false)
function toggleTableMode() {
tableMode.value = !tableMode.value
}
function openImagePicker() {
document.getElementById('tiptap__image-upload').click()
}
function setLink(event) {
setLinkInEditor(event.target.getBoundingClientRect(), editor)
}

View File

@ -6,7 +6,7 @@
<EditorToolbar
v-if="editor && isEditing"
:editor="editor"
:upload-callback="uploadCallback"
@imageUploadClicked="triggerImageInput"
/>
<BubbleMenu
v-if="editor && isEditing"
@ -374,7 +374,7 @@ const editor = useEditor({
Typography,
Underline,
Link.configure({
openOnClick: true,
openOnClick: false,
validate: (href: string) => /^https?:\/\//.test(href),
}),
Table.configure({
@ -489,6 +489,15 @@ function uploadAndInsertFiles(files: File[] | FileList) {
})
}
function triggerImageInput(event) {
if (typeof uploadCallback !== 'undefined') {
uploadInputRef.value?.click()
return
}
addImage(event)
}
async function addImage(event) {
if (typeof uploadCallback !== 'undefined') {
@ -522,16 +531,20 @@ onMounted(async () => {
await nextTick()
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.addEventListener('paste', handleImagePaste)
if (typeof uploadCallback !== 'undefined') {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.addEventListener('paste', handleImagePaste)
}
setModeAndValue(modelValue)
})
onBeforeUnmount(() => {
nextTick(() => {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.removeEventListener('paste', handleImagePaste)
if (typeof uploadCallback !== 'undefined') {
const input = tiptapInstanceRef.value?.querySelectorAll('.tiptap__editor')[0]?.children[0]
input?.removeEventListener('paste', handleImagePaste)
}
})
if (editShortcut !== '') {
document.removeEventListener('keydown', setFocusToEditor)
@ -558,6 +571,10 @@ function handleImagePaste(event) {
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
function setFocusToEditor(event) {
if (event.target.shadowRoot) {
return
}
const hotkeyString = eventToHotkeyString(event)
if (!hotkeyString) return
if (hotkeyString !== editShortcut ||
@ -596,7 +613,7 @@ watch(
() => isEditing.value,
async editing => {
await nextTick()
let checkboxes = tiptapInstanceRef.value?.querySelectorAll('[data-checked]')
if (typeof checkboxes === 'undefined' || checkboxes.length === 0) {
// For some reason, this works when we check a second time.

View File

@ -87,7 +87,7 @@ import {
faStar,
faSun,
faTimesCircle,
faCircleQuestion,
faCircleQuestion, faFaceLaugh,
} from '@fortawesome/free-regular-svg-icons'
import {FontAwesomeIcon} from '@fortawesome/vue-fontawesome'
@ -186,6 +186,7 @@ library.add(faXmarksLines)
library.add(faFont)
library.add(faRulerHorizontal)
library.add(faUnderline)
library.add(faFaceLaugh)
// overwriting the wrong types
export default FontAwesomeIcon as unknown as FontAwesomeIconFixedTypes

View File

@ -62,7 +62,7 @@ export const KEYBOARD_SHORTCUTS : ShortcutGroup[] = [
},
{
title: 'project.kanban.title',
available: (route) => route.name === 'project.kanban',
available: (route) => route.name === 'project.view',
shortcuts: [
{
title: 'keyboardShortcuts.task.done',

View File

@ -188,12 +188,6 @@ $modal-width: 1024px;
.info {
font-style: italic;
}
p {
display: flex;
justify-content: space-between;
align-items: center;
}
}
}

View File

@ -6,44 +6,23 @@
<h1 class="project-title-print">
{{ getProjectTitle(currentProject) }}
</h1>
<div class="switch-view-container d-print-none">
<div class="switch-view">
<div
class="switch-view-container d-print-none"
:class="{'is-justify-content-flex-end': views.length === 1}"
>
<div
v-if="views.length > 1"
class="switch-view"
>
<BaseButton
v-shortcut="'g l'"
:title="$t('keyboardShortcuts.project.switchToListView')"
v-for="v in views"
:key="v.id"
class="switch-view-button"
:class="{'is-active': viewName === 'project'}"
:to="{ name: 'project.list', params: { projectId } }"
:class="{'is-active': v.id === viewId}"
:to="{ name: 'project.view', params: { projectId, viewId: v.id } }"
>
{{ $t('project.list.title') }}
</BaseButton>
<BaseButton
v-shortcut="'g g'"
:title="$t('keyboardShortcuts.project.switchToGanttView')"
class="switch-view-button"
:class="{'is-active': viewName === 'gantt'}"
:to="{ name: 'project.gantt', params: { projectId } }"
>
{{ $t('project.gantt.title') }}
</BaseButton>
<BaseButton
v-shortcut="'g t'"
:title="$t('keyboardShortcuts.project.switchToTableView')"
class="switch-view-button"
:class="{'is-active': viewName === 'table'}"
:to="{ name: 'project.table', params: { projectId } }"
>
{{ $t('project.table.title') }}
</BaseButton>
<BaseButton
v-shortcut="'g k'"
:title="$t('keyboardShortcuts.project.switchToKanbanView')"
class="switch-view-button"
:class="{'is-active': viewName === 'kanban'}"
:to="{ name: 'project.kanban', params: { projectId } }"
>
{{ $t('project.kanban.title') }}
{{ getViewTitle(v) }}
</BaseButton>
</div>
<slot name="header" />
@ -63,7 +42,7 @@
</template>
<script setup lang="ts">
import {ref, computed, watch} from 'vue'
import {computed, ref, watch} from 'vue'
import {useRoute} from 'vue-router'
import BaseButton from '@/components/base/BaseButton.vue'
@ -79,26 +58,27 @@ import {useTitle} from '@/composables/useTitle'
import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects'
import type {IProject} from '@/modelTypes/IProject'
import type {IProjectView} from '@/modelTypes/IProjectView'
import {useI18n} from 'vue-i18n'
const props = defineProps({
projectId: {
type: Number,
required: true,
},
viewName: {
type: String,
required: true,
},
})
const {
projectId,
viewId,
} = defineProps<{
projectId: IProject['id'],
viewId: IProjectView['id'],
}>()
const route = useRoute()
const {t} = useI18n()
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const projectService = ref(new ProjectService())
const loadedProjectId = ref(0)
const currentProject = computed(() => {
const currentProject = computed<IProject>(() => {
return typeof baseStore.currentProject === 'undefined' ? {
id: 0,
title: '',
@ -108,13 +88,15 @@ const currentProject = computed(() => {
})
useTitle(() => currentProject.value?.id ? getProjectTitle(currentProject.value) : '')
const views = computed(() => currentProject.value?.views)
// 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 caused wired bugs where the project background would be set on the home page but only right after setting a new
// project background and then navigating to home. It also highlighted the project in the menu and didn't allow changing any
// of it, most likely due to the rights not being properly populated.
watch(
() => props.projectId,
() => projectId,
// loadProject
async (projectIdToLoad: number) => {
const projectData = {id: projectIdToLoad}
@ -130,11 +112,11 @@ watch(
)
&& typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null
) {
loadedProjectId.value = props.projectId
loadedProjectId.value = projectId
return
}
console.debug(`Loading project, props.viewName = ${props.viewName}, $route.params =`, route.params, `, loadedProjectId = ${loadedProjectId.value}, currentProject = `, currentProject.value)
console.debug('Loading project, $route.params =', route.params, `, loadedProjectId = ${loadedProjectId.value}, currentProject = `, currentProject.value)
// 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
@ -149,31 +131,50 @@ watch(
const loadedProject = await projectService.value.get(project)
baseStore.handleSetCurrentProject({project: loadedProject})
} finally {
loadedProjectId.value = props.projectId
loadedProjectId.value = projectId
}
},
{immediate: true},
)
function getViewTitle(view: IProjectView) {
switch (view.title) {
case 'List':
return t('project.list.title')
case 'Gantt':
return t('project.gantt.title')
case 'Table':
return t('project.table.title')
case 'Kanban':
return t('project.kanban.title')
}
return view.title
}
</script>
<style lang="scss" scoped>
.switch-view-container {
@media screen and (max-width: $tablet) {
display: flex;
justify-content: center;
flex-direction: column;
}
min-height: $switch-view-height;
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
@media screen and (max-width: $tablet) {
justify-content: center;
flex-direction: column;
}
}
.switch-view {
background: var(--white);
display: inline-flex;
border-radius: $radius;
font-size: .75rem;
box-shadow: var(--shadow-sm);
height: $switch-view-height;
margin: 0 auto 1rem;
padding: .5rem;
background: var(--white);
display: inline-flex;
border-radius: $radius;
font-size: .75rem;
box-shadow: var(--shadow-sm);
padding: .5rem;
}
.switch-view-button {
@ -201,7 +202,7 @@ watch(
// FIXME: this should be in notification and set via a prop
.is-archived .notification.is-warning {
margin-bottom: 1rem;
margin-bottom: 1rem;
}
.project-title-print {
@ -209,7 +210,7 @@ watch(
font-size: 1.75rem;
text-align: center;
margin-bottom: .5rem;
@media print {
display: block;
}

View File

@ -10,21 +10,27 @@ import User from '@/components/misc/user.vue'
import ProjectUserService from '@/services/projectUsers'
import {useProjectStore} from '@/stores/projects'
import {
DATE_FIELDS,
ASSIGNEE_FIELDS,
AUTOCOMPLETE_FIELDS,
AVAILABLE_FILTER_FIELDS,
DATE_FIELDS,
FILTER_JOIN_OPERATOR,
FILTER_OPERATORS,
FILTER_OPERATORS_REGEX, LABEL_FIELDS,
FILTER_OPERATORS_REGEX,
getFilterFieldRegexPattern,
LABEL_FIELDS,
} from '@/helpers/filters'
import {useDebounceFn} from '@vueuse/core'
import {createRandomID} from '@/helpers/randomId'
const {
modelValue,
projectId,
inputLabel = undefined,
} = defineProps<{
modelValue: string,
projectId?: number,
inputLabel?: string,
}>()
const emit = defineEmits(['update:modelValue', 'blur'])
@ -35,6 +41,8 @@ const {
height,
} = useAutoHeightTextarea(filterQuery)
const id = ref(createRandomID())
watch(
() => modelValue,
() => {
@ -83,7 +91,15 @@ const highlightedFilterQuery = computed(() => {
value = ''
}
return `${o}${spacesBefore}${token}${spacesAfter}<button class="is-primary filter-query__date_value" data-position="${position}">${value}</button><span class="filter-query__date_value_placeholder">${value}</span>`
let endPadding = ''
if(value.endsWith(' ')) {
const fullLength = value.length
value = value.trimEnd()
const numberOfRemovedSpaces = fullLength - value.length
endPadding = endPadding.padEnd(numberOfRemovedSpaces, ' ')
}
return `${o}${spacesBefore}${token}${spacesAfter}<button class="is-primary filter-query__date_value" data-position="${position}">${value}</button><span class="filter-query__date_value_placeholder">${value}</span>${endPadding}`
})
})
ASSIGNEE_FIELDS
@ -104,15 +120,26 @@ const highlightedFilterQuery = computed(() => {
})
LABEL_FIELDS
.forEach(f => {
const pattern = new RegExp(f + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*([\'"]?)([^\'"\\s]+\\1?)?', 'ig')
highlighted = highlighted.replaceAll(pattern, (match, token, start, value) => {
const pattern = getFilterFieldRegexPattern(f)
highlighted = highlighted.replaceAll(pattern, (match, prefix, operator, space, value) => {
if (typeof value === 'undefined') {
value = ''
}
const label = labelStore.getLabelsByExactTitles([value])[0] || undefined
let labelTitles = [value.trim()]
if (operator === 'in' || operator === '?=') {
labelTitles = value.split(',').map(v => v.trim())
}
return `${f} ${token} <span class="filter-query__label_value" style="background-color: ${label?.hexColor}; color: ${label?.textColor}">${label?.title ?? value}<span>`
const labelsHtml: string[] = []
labelTitles.forEach(t => {
const label = labelStore.getLabelByExactTitle(t) || undefined
labelsHtml.push(`<span class="filter-query__label_value" style="background-color: ${label?.hexColor}; color: ${label?.textColor}">${label?.title ?? t}</span>`)
})
const endSpace = value.endsWith(' ') ? ' ' : ''
return `${f} ${operator} ${labelsHtml.join(', ')}${endSpace}`
})
})
FILTER_OPERATORS
@ -175,6 +202,7 @@ const projectStore = useProjectStore()
function handleFieldInput() {
const cursorPosition = filterInput.value.selectionStart
const textUpToCursor = filterQuery.value.substring(0, cursorPosition)
autocompleteResults.value = []
AUTOCOMPLETE_FIELDS.forEach(field => {
const pattern = new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?$', 'ig')
@ -184,26 +212,31 @@ function handleFieldInput() {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match
if (keyword) {
let search = keyword
if (operator === 'in' || operator === '?=') {
const keywords = keyword.split(',')
search = keywords[keywords.length - 1].trim()
}
if (matched.startsWith('label')) {
autocompleteResultType.value = 'labels'
autocompleteResults.value = labelStore.filterLabelsByQuery([], keyword)
autocompleteResults.value = labelStore.filterLabelsByQuery([], search)
}
if (matched.startsWith('assignee')) {
autocompleteResultType.value = 'assignees'
if (projectId) {
projectUserService.getAll({projectId}, {s: keyword})
projectUserService.getAll({projectId}, {s: search})
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
} else {
userService.getAll({}, {s: keyword})
userService.getAll({}, {s: search})
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
}
}
if (!projectId && matched.startsWith('project')) {
autocompleteResultType.value = 'projects'
autocompleteResults.value = projectStore.searchProject(keyword)
autocompleteResults.value = projectStore.searchProject(search)
}
autocompleteMatchText.value = keyword
autocompleteMatchPosition.value = prefix.length - 1
autocompleteMatchPosition.value = match.index + prefix.length - 1 + keyword.replace(search, '').length
}
}
})
@ -211,18 +244,27 @@ function handleFieldInput() {
function autocompleteSelect(value) {
filterQuery.value = filterQuery.value.substring(0, autocompleteMatchPosition.value + 1) +
(autocompleteResultType.value === 'labels'
? value.title
: value.username) +
(autocompleteResultType.value === 'assignees'
? value.username
: value.title) +
filterQuery.value.substring(autocompleteMatchPosition.value + autocompleteMatchText.value.length + 1)
autocompleteResults.value = []
}
// The blur from the textarea might happen before the replacement after autocomplete select was done.
// That caused listeners to try and replace values earlier, resulting in broken queries.
const blurDebounced = useDebounceFn(() => emit('blur'), 500)
</script>
<template>
<div class="field">
<label class="label">{{ $t('filters.query.title') }}</label>
<label
class="label"
:for="id"
>
{{ inputLabel ?? $t('filters.query.title') }}
</label>
<AutocompleteDropdown
:options="autocompleteResults"
@blur="filterInput?.blur()"
@ -233,10 +275,10 @@ function autocompleteSelect(value) {
>
<div class="control filter-input">
<textarea
:id
ref="filterInput"
v-model="filterQuery"
autocomplete="off"
autocorrect="off"
autocapitalize="off"
spellcheck="false"
@ -246,7 +288,7 @@ function autocompleteSelect(value) {
@input="handleFieldInput"
@focus="onFocusField"
@keydown="onKeydown"
@blur="e => emit('blur', e)"
@blur="blurDebounced"
/>
<div
class="filter-input-highlight"
@ -283,6 +325,11 @@ function autocompleteSelect(value) {
<style lang="scss">
.filter-input-highlight {
&, button.filter-query__date_value {
color: var(--card-color);
}
span {
&.filter-query__field {
color: var(--code-literal);
@ -333,7 +380,7 @@ function autocompleteSelect(value) {
resize: none;
text-fill-color: transparent;
-webkit-text-fill-color: transparent;
&::placeholder {
text-fill-color: var(--input-placeholder-color);
-webkit-text-fill-color: var(--input-placeholder-color);
@ -345,6 +392,7 @@ function autocompleteSelect(value) {
}
.filter-input-highlight {
background: var(--white);
height: 2.5em;
line-height: 1.5;
padding: .5em .75em;

View File

@ -63,6 +63,10 @@ const filteredProjects = computed(() => {
@media screen and (min-width: $widescreen) {
--project-grid-columns: 5;
.project-grid-item:nth-child(6) {
display: none;
}
}
}

View File

@ -1,14 +1,8 @@
<template>
<x-button
v-if="hasFilters"
variant="secondary"
@click="clearFilters"
>
{{ $t('filters.clear') }}
</x-button>
<x-button
variant="secondary"
icon="filter"
:class="{'has-filters': hasFilters}"
@click="() => modalOpen = true"
>
{{ $t('filters.title') }}
@ -37,20 +31,38 @@ import {computed, ref, watch} from 'vue'
import Filters from '@/components/project/partials/filters.vue'
import {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection'
import {useRouteQuery} from '@vueuse/router'
const modelValue = defineModel<TaskFilterParams>({})
const value = ref<TaskFilterParams>({})
const filter = useRouteQuery('filter')
watch(
() => modelValue.value,
(modelValue: TaskFilterParams) => {
value.value = modelValue
if (value.value.filter !== '' && value.value.filter !== getDefaultTaskFilterParams().filter) {
filter.value = value.value.filter
}
},
{immediate: true},
)
watch(
() => filter.value,
val => {
if (modelValue.value?.filter === val || typeof val === 'undefined') {
return
}
modelValue.value.filter = val
},
{immediate: true},
)
function emitChanges(newValue: TaskFilterParams) {
filter.value = newValue.filter
if (modelValue.value?.filter === newValue.filter && modelValue.value?.s === newValue.s) {
return
}
@ -60,25 +72,11 @@ function emitChanges(newValue: TaskFilterParams) {
}
const hasFilters = computed(() => {
// this.value also contains the page parameter which we don't want to include in filters
// eslint-disable-next-line no-unused-vars
const {filter, s} = value.value
const def = {...getDefaultTaskFilterParams()}
const params = {filter, s}
const defaultParams = {
filter: def.filter,
s: s ? def.s : undefined,
}
return JSON.stringify(params) !== JSON.stringify(defaultParams)
return value.value.filter !== '' ||
value.value.s !== ''
})
const modalOpen = ref(false)
function clearFilters() {
value.value = {...getDefaultTaskFilterParams()}
}
</script>
<style scoped lang="scss">
@ -89,4 +87,21 @@ function clearFilters() {
margin: 2rem 0 1rem;
}
}
$filter-bubble-size: .75rem;
.has-filters {
position: relative;
&::after {
content: '';
position: absolute;
top: math.div($filter-bubble-size, -2);
right: math.div($filter-bubble-size, -2);
width: $filter-bubble-size;
height: $filter-bubble-size;
border-radius: 100%;
background: var(--primary);
}
}
</style>

View File

@ -25,6 +25,14 @@
v-if="hasFooter"
#footer
>
<x-button
variant="secondary"
class="mr-2"
:disabled="params.filter === ''"
@click.prevent.stop="clearFiltersAndEmit"
>
{{ $t('filters.clear') }}
</x-button>
<x-button
variant="primary"
@click.prevent.stop="changeAndEmitButton"
@ -40,8 +48,7 @@ export const ALPHABETICAL_SORT = 'title'
</script>
<script setup lang="ts">
import {computed, ref} from 'vue'
import {watchDebounced} from '@vueuse/core'
import {computed, ref, watch} from 'vue'
import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import FilterInput from '@/components/project/partials/FilterInput.vue'
import {useRoute} from 'vue-router'
@ -51,8 +58,8 @@ import {useProjectStore} from '@/stores/projects'
import {FILTER_OPERATORS, transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
const {
hasTitle= false,
const {
hasTitle = false,
hasFooter = true,
modelValue,
} = defineProps<{
@ -81,18 +88,18 @@ const params = ref<TaskFilterParams>({
})
// Using watchDebounced to prevent the filter re-triggering itself.
watchDebounced(
watch(
() => modelValue,
(value: TaskFilterParams) => {
const val = {...value}
val.filter = transformFilterStringFromApi(
val?.filter || '',
labelId => labelStore.getLabelById(labelId)?.title,
projectId => projectStore.projects.value[projectId]?.title || null,
projectId => projectStore.projects[projectId]?.title || null,
)
params.value = val
},
{immediate: true, debounce: 500, maxWait: 1000},
{immediate: true},
)
const labelStore = useLabelStore()
@ -102,7 +109,10 @@ function change() {
const filter = transformFilterStringForApi(
params.value.filter,
labelTitle => labelStore.filterLabelsByQuery([], labelTitle)[0]?.id || null,
projectTitle => projectStore.searchProject(projectTitle)[0]?.id || null,
projectTitle => {
const found = projectStore.findProjectByExactname(projectTitle)
return found?.id || null
},
)
let s = ''
@ -130,4 +140,9 @@ function changeAndEmitButton() {
change()
emit('showResultsButtonClicked')
}
function clearFiltersAndEmit() {
params.value.filter = ''
changeAndEmitButton()
}
</script>

View File

@ -47,6 +47,12 @@
>
{{ $t('menu.edit') }}
</DropdownItem>
<DropdownItem
:to="{ name: 'project.settings.views', params: { projectId: project.id } }"
icon="eye"
>
{{ $t('menu.views') }}
</DropdownItem>
<DropdownItem
v-if="backgroundsEnabled"
:to="{ name: 'project.settings.background', params: { projectId: project.id } }"
@ -90,7 +96,6 @@
{{ $t('project.webhooks.title') }}
</DropdownItem>
<DropdownItem
v-if="level < 2"
:to="{ name: 'project.createFromParent', params: { parentProjectId: project.id } }"
icon="layer-group"
>
@ -129,9 +134,6 @@ const props = defineProps({
type: Object as PropType<IProject>,
required: true,
},
level: {
type: Number,
},
})
const projectStore = useProjectStore()

View File

@ -2,9 +2,9 @@
<ProjectWrapper
class="project-gantt"
:project-id="filters.projectId"
view-name="gantt"
:view-id
>
<template #header>
<template #default>
<card :has-content="false">
<div class="gantt-options">
<div class="field">
@ -45,9 +45,7 @@
</Fancycheckbox>
</div>
</card>
</template>
<template #default>
<div class="gantt-chart-container">
<card
:has-content="false"
@ -79,7 +77,7 @@ import {useI18n} from 'vue-i18n'
import type {RouteLocationNormalized} from 'vue-router'
import {useBaseStore} from '@/stores/base'
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
import Foo from '@/components/misc/flatpickr/Flatpickr.vue'
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
@ -87,15 +85,19 @@ import Fancycheckbox from '@/components/input/fancycheckbox.vue'
import TaskForm from '@/components/tasks/TaskForm.vue'
import {createAsyncComponent} from '@/helpers/createAsyncComponent'
import {useGanttFilters} from './helpers/useGanttFilters'
import {useGanttFilters} from '../../../views/project/helpers/useGanttFilters'
import {RIGHTS} from '@/constants/rights'
import type {DateISO} from '@/types/DateISO'
import type {ITask} from '@/modelTypes/ITask'
import type {IProjectView} from '@/modelTypes/IProjectView'
type Options = Flatpickr.Options.Options
const props = defineProps<{route: RouteLocationNormalized}>()
const props = defineProps<{
route: RouteLocationNormalized
viewId: IProjectView['id']
}>()
const GanttChart = createAsyncComponent(() => import('@/components/tasks/GanttChart.vue'))
@ -111,7 +113,7 @@ const {
isLoading,
addTask,
updateTask,
} = useGanttFilters(route)
} = useGanttFilters(route, props.viewId)
const DEFAULT_DATE_RANGE_DAYS = 7

View File

@ -2,16 +2,14 @@
<ProjectWrapper
class="project-kanban"
:project-id="projectId"
view-name="kanban"
:view-id
>
<template #header>
<div
v-if="!isSavedFilter(project)"
class="filter-container"
>
<div class="items">
<FilterPopup v-model="params" />
</div>
<div class="filter-container">
<FilterPopup
v-if="!isSavedFilter(project)"
v-model="params"
/>
</div>
</template>
@ -43,7 +41,7 @@
@click="() => unCollapseBucket(bucket)"
>
<span
v-if="project?.doneBucketId === bucket.id"
v-if="view?.doneBucketId === bucket.id"
v-tooltip="$t('project.kanban.doneBucketHint')"
class="icon is-small has-text-success mr-2"
>
@ -111,7 +109,7 @@
</DropdownItem>
<DropdownItem
v-tooltip="$t('project.kanban.doneBucketHintExtended')"
:icon-class="{'has-text-success': bucket.id === project?.doneBucketId}"
:icon-class="{'has-text-success': bucket.id === view?.doneBucketId}"
icon="check-double"
@click.stop="toggleDoneBucket(bucket)"
>
@ -119,7 +117,7 @@
</DropdownItem>
<DropdownItem
v-tooltip="$t('project.kanban.defaultBucketHint')"
:icon-class="{'has-text-primary': bucket.id === project.defaultBucketId}"
:icon-class="{'has-text-primary': bucket.id === view?.defaultBucketId}"
icon="th"
@click.stop="toggleDefaultBucket(bucket)"
>
@ -197,7 +195,9 @@
variant="secondary"
@click="toggleShowNewTaskInput(bucket.id)"
>
{{ bucket.tasks.length === 0 ? $t('project.kanban.addTask') : $t('project.kanban.addAnotherTask') }}
{{
bucket.tasks.length === 0 ? $t('project.kanban.addTask') : $t('project.kanban.addAnotherTask')
}}
</x-button>
</div>
</template>
@ -277,7 +277,6 @@ import {RIGHTS as Rights} from '@/constants/rights'
import BucketModel from '@/models/bucket'
import type {IBucket} from '@/modelTypes/IBucket'
import type {IProject} from '@/modelTypes/IProject'
import type {ITask} from '@/modelTypes/ITask'
import {useBaseStore} from '@/stores/base'
@ -290,18 +289,30 @@ import KanbanCard from '@/components/tasks/partials/kanban-card.vue'
import Dropdown from '@/components/misc/dropdown.vue'
import DropdownItem from '@/components/misc/dropdown-item.vue'
import {getCollapsedBucketState, saveCollapsedBucketState, type CollapsedBuckets} from '@/helpers/saveCollapsedBucketState'
import {
type CollapsedBuckets,
getCollapsedBucketState,
saveCollapsedBucketState,
} from '@/helpers/saveCollapsedBucketState'
import {calculateItemPosition} from '@/helpers/calculateItemPosition'
import {isSavedFilter} from '@/services/savedFilter'
import {success} from '@/message'
import {useProjectStore} from '@/stores/projects'
import type {TaskFilterParams} from '@/services/taskCollection'
import type {IProjectView} from '@/modelTypes/IProjectView'
import TaskPositionService from '@/services/taskPosition'
import TaskPositionModel from '@/models/taskPosition'
import {i18n} from '@/i18n'
import ProjectViewService from '@/services/projectViews'
import ProjectViewModel from '@/models/projectView'
const {
projectId = undefined,
projectId,
viewId,
} = defineProps<{
projectId: number,
viewId: IProjectView['id'],
}>()
const DRAG_OPTIONS = {
@ -321,8 +332,9 @@ const baseStore = useBaseStore()
const kanbanStore = useKanbanStore()
const taskStore = useTaskStore()
const projectStore = useProjectStore()
const taskPositionService = ref(new TaskPositionService())
const taskContainerRefs = ref<{[id: IBucket['id']]: HTMLElement}>({})
const taskContainerRefs = ref<{ [id: IBucket['id']]: HTMLElement }>({})
const bucketLimitInputRef = ref<HTMLInputElement | null>(null)
const drag = ref(false)
@ -334,18 +346,18 @@ const bucketToDelete = ref(0)
const bucketTitleEditable = ref(false)
const newTaskText = ref('')
const showNewTaskInput = ref<{[id: IBucket['id']]: boolean}>({})
const showNewTaskInput = ref<{ [id: IBucket['id']]: boolean }>({})
const newBucketTitle = ref('')
const showNewBucketInput = ref(false)
const newTaskError = ref<{[id: IBucket['id']]: boolean}>({})
const newTaskError = ref<{ [id: IBucket['id']]: boolean }>({})
const newTaskInputFocused = ref(false)
const showSetLimitInput = ref(false)
const collapsedBuckets = ref<CollapsedBuckets>({})
// We're using this to show the loading animation only at the task when updating it
const taskUpdating = ref<{[id: ITask['id']]: boolean}>({})
const taskUpdating = ref<{ [id: ITask['id']]: boolean }>({})
const oneTaskUpdating = ref(false)
const params = ref<TaskFilterParams>({
@ -359,7 +371,7 @@ const params = ref<TaskFilterParams>({
const getTaskDraggableTaskComponentData = computed(() => (bucket: IBucket) => {
return {
ref: (el: HTMLElement) => setTaskContainerRef(bucket.id, el),
onScroll: (event: Event) => handleTaskContainerScroll(bucket.id, bucket.projectId, event.target as HTMLElement),
onScroll: (event: Event) => handleTaskContainerScroll(bucket.id, event.target as HTMLElement),
type: 'transition-group',
name: !drag.value ? 'move-card' : null,
class: [
@ -378,24 +390,27 @@ const bucketDraggableComponentData = computed(() => ({
],
}))
const canWrite = computed(() => baseStore.currentProject?.maxRight > Rights.READ)
const project = computed(() => projectId ? projectStore.projects[projectId]: null)
const project = computed(() => projectId ? projectStore.projects[projectId] : null)
const buckets = computed(() => kanbanStore.buckets)
const loading = computed(() => kanbanStore.isLoading)
const taskLoading = computed(() => taskStore.isLoading)
const view = computed<IProjectView | null>(() => project.value?.views.find(v => v.id === viewId) || null)
const taskLoading = computed(() => taskStore.isLoading || taskPositionService.value.loading)
watch(
() => ({
params: params.value,
projectId,
viewId,
}),
({params}) => {
if (projectId === undefined || Number(projectId) === 0) {
return
}
collapsedBuckets.value = getCollapsedBucketState(projectId)
kanbanStore.loadBucketsForProject({projectId, params})
kanbanStore.loadBucketsForProject(projectId, viewId, params)
},
{
immediate: true,
@ -408,7 +423,7 @@ function setTaskContainerRef(id: IBucket['id'], el: HTMLElement) {
taskContainerRefs.value[id] = el
}
function handleTaskContainerScroll(id: IBucket['id'], projectId: IProject['id'], el: HTMLElement) {
function handleTaskContainerScroll(id: IBucket['id'], el: HTMLElement) {
if (!el) {
return
}
@ -420,6 +435,7 @@ function handleTaskContainerScroll(id: IBucket['id'], projectId: IProject['id'],
kanbanStore.loadNextTasksForBucket(
projectId,
viewId,
params.value,
id,
)
@ -469,7 +485,7 @@ async function updateTaskPosition(e) {
const newTask = klona(task) // cloning the task to avoid pinia store manipulation
newTask.bucketId = newBucket.id
newTask.kanbanPosition = calculateItemPosition(
const position = calculateItemPosition(
taskBefore !== null ? taskBefore.kanbanPosition : null,
taskAfter !== null ? taskAfter.kanbanPosition : null,
)
@ -479,6 +495,8 @@ async function updateTaskPosition(e) {
) {
newTask.done = project.value?.doneBucketId === newBucket.id
}
let bucketHasChanged = false
if (
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
newBucket.id !== oldBucket.id
@ -491,13 +509,23 @@ async function updateTaskPosition(e) {
...newBucket,
count: newBucket.count + 1,
})
bucketHasChanged = true
}
try {
await taskStore.update(newTask)
const newPosition = new TaskPositionModel({
position,
projectViewId: viewId,
taskId: newTask.id,
})
await taskPositionService.value.update(newPosition)
if(bucketHasChanged) {
await taskStore.update(newTask)
}
// Make sure the first and second task don't both get position 0 assigned
if(newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
if (newTaskIndex === 0 && taskAfter !== null && taskAfter.kanbanPosition === 0) {
const taskAfterAfter = newBucket.tasks[newTaskIndex + 2] ?? null
const newTaskAfter = klona(taskAfter) // cloning the task to avoid pinia store manipulation
newTaskAfter.bucketId = newBucket.id
@ -552,9 +580,9 @@ async function createNewBucket() {
await kanbanStore.createBucket(new BucketModel({
title: newBucketTitle.value,
projectId: project.value.id,
projectViewId: viewId,
}))
newBucketTitle.value = ''
showNewBucketInput.value = false
}
function deleteBucketModal(bucketId: IBucket['id']) {
@ -572,6 +600,7 @@ async function deleteBucket() {
bucket: new BucketModel({
id: bucketToDelete.value,
projectId: project.value.id,
projectViewId: viewId,
}),
params: params.value,
})
@ -590,10 +619,19 @@ async function focusBucketTitle(e: Event) {
}
async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
await kanbanStore.updateBucketTitle({
const bucket = kanbanStore.getBucketById(bucketId)
if (bucket?.title === bucketTitle) {
bucketTitleEditable.value = false
return
}
await kanbanStore.updateBucket({
id: bucketId,
title: bucketTitle,
projectId,
})
success({message: i18n.global.t('project.kanban.bucketTitleSavedSuccess')})
bucketTitleEditable.value = false
}
@ -603,7 +641,7 @@ function updateBuckets(value: IBucket[]) {
}
// TODO: fix type
function updateBucketPosition(e: {newIndex: number}) {
function updateBucketPosition(e: { newIndex: number }) {
// (2) bucket positon is changed
dragBucket.value = false
@ -613,6 +651,7 @@ function updateBucketPosition(e: {newIndex: number}) {
kanbanStore.updateBucket({
id: bucket.id,
projectId,
position: calculateItemPosition(
bucketBefore !== null ? bucketBefore.position : null,
bucketAfter !== null ? bucketAfter.position : null,
@ -627,24 +666,25 @@ async function saveBucketLimit(bucketId: IBucket['id'], limit: number) {
await kanbanStore.updateBucket({
...kanbanStore.getBucketById(bucketId),
projectId,
limit,
})
success({message: t('project.kanban.bucketLimitSavedSuccess')})
}
const setBucketLimitCancel = ref<number|null>(null)
const setBucketLimitCancel = ref<number | null>(null)
async function setBucketLimit(bucketId: IBucket['id'], now: boolean = false) {
const limit = parseInt(bucketLimitInputRef.value?.value || '')
if (setBucketLimitCancel.value !== null) {
clearTimeout(setBucketLimitCancel.value)
}
if (now) {
return saveBucketLimit(bucketId, limit)
}
setBucketLimitCancel.value = setTimeout(saveBucketLimit, 2500, bucketId, limit)
}
@ -665,26 +705,46 @@ function dragstart(bucket: IBucket) {
}
async function toggleDefaultBucket(bucket: IBucket) {
const defaultBucketId = project.value.defaultBucketId === bucket.id
const defaultBucketId = view.value?.defaultBucketId === bucket.id
? 0
: bucket.id
await projectStore.updateProject({
...project.value,
const projectViewService = new ProjectViewService()
const updatedView = await projectViewService.update(new ProjectViewModel({
...view.value,
defaultBucketId,
})
}))
const views = project.value.views.map(v => v.id === view.value?.id ? updatedView : v)
const updatedProject = {
...project.value,
views,
}
projectStore.setProject(updatedProject)
success({message: t('project.kanban.defaultBucketSavedSuccess')})
}
async function toggleDoneBucket(bucket: IBucket) {
const doneBucketId = project.value?.doneBucketId === bucket.id
const doneBucketId = view.value?.doneBucketId === bucket.id
? 0
: bucket.id
await projectStore.updateProject({
...project.value,
const projectViewService = new ProjectViewService()
const updatedView = await projectViewService.update(new ProjectViewModel({
...view.value,
doneBucketId,
})
}))
const views = project.value.views.map(v => v.id === view.value?.id ? updatedView : v)
const updatedProject = {
...project.value,
views,
}
projectStore.setProject(updatedProject)
success({message: t('project.kanban.doneBucketSavedSuccess')})
}
@ -740,6 +800,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
* {
opacity: 0;
}
&::after {
content: '';
position: absolute;
@ -781,6 +842,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
&:first-of-type {
padding-top: .5rem;
}
&:last-of-type {
padding-bottom: .5rem;
}

View File

@ -2,55 +2,15 @@
<ProjectWrapper
class="project-list"
:project-id="projectId"
view-name="project"
:view-id
>
<template #header>
<div
v-if="!isSavedFilter(project)"
class="filter-container"
>
<div class="items">
<div class="search">
<div
:class="{ hidden: !showTaskSearch }"
class="field has-addons"
>
<div class="control has-icons-left has-icons-right">
<input
v-model="searchTerm"
v-focus
class="input"
:placeholder="$t('misc.search')"
type="text"
@blur="hideSearchBar()"
@keyup.enter="searchTasks"
>
<span class="icon is-left">
<icon icon="search" />
</span>
</div>
<div class="control">
<x-button
:loading="loading"
:shadow="false"
@click="searchTasks"
>
{{ $t('misc.search') }}
</x-button>
</div>
</div>
<x-button
v-if="!showTaskSearch"
icon="search"
variant="secondary"
@click="showTaskSearch = !showTaskSearch"
/>
</div>
<FilterPopup
v-model="params"
@update:modelValue="prepareFiltersAndLoadTasks()"
/>
</div>
<div class="filter-container">
<FilterPopup
v-if="!isSavedFilter(project)"
v-model="params"
@update:modelValue="prepareFiltersAndLoadTasks()"
/>
</div>
</template>
@ -120,7 +80,7 @@
</template>
</draggable>
<Pagination
<Pagination
:total-pages="totalPages"
:current-page="currentPage"
/>
@ -131,13 +91,12 @@
</template>
<script lang="ts">
export default { name: 'List' }
export default {name: 'List'}
</script>
<script setup lang="ts">
import {ref, computed, nextTick, onMounted, watch} from 'vue'
import draggable from 'zhyswan-vuedraggable'
import {useRoute, useRouter} from 'vue-router'
import ProjectWrapper from '@/components/project/ProjectWrapper.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
@ -155,18 +114,21 @@ import type {ITask} from '@/modelTypes/ITask'
import {isSavedFilter} from '@/services/savedFilter'
import {useBaseStore} from '@/stores/base'
import {useTaskStore} from '@/stores/tasks'
import type {IProject} from '@/modelTypes/IProject'
import type {IProjectView} from '@/modelTypes/IProjectView'
import TaskPositionService from '@/services/taskPosition'
import TaskPositionModel from '@/models/taskPosition'
const {
projectId,
viewId,
} = defineProps<{
projectId: IProject['id'],
viewId: IProjectView['id'],
}>()
const ctaVisible = ref(false)
const showTaskSearch = ref(false)
const drag = ref(false)
const DRAG_OPTIONS = {
@ -180,10 +142,11 @@ const {
totalPages,
currentPage,
loadTasks,
searchTerm,
params,
sortByParam,
} = useTaskList(() => projectId, {position: 'asc' })
} = useTaskList(() => projectId, () => viewId, {position: 'asc'})
const taskPositionService = ref(new TaskPositionService())
const tasks = ref<ITask[]>([])
watch(
@ -203,7 +166,7 @@ watch(
// If the task is a subtask, make sure the parent task is available in the current view as well
for (const pt of t.relatedTasks.parenttask) {
if(typeof tasksById[pt.id] === 'undefined') {
if (typeof tasksById[pt.id] === 'undefined') {
return true
}
}
@ -225,7 +188,6 @@ const firstNewPosition = computed(() => {
return calculateItemPosition(null, tasks.value[0].position)
})
const taskStore = useTaskStore()
const baseStore = useBaseStore()
const project = computed(() => baseStore.currentProject)
@ -238,43 +200,17 @@ onMounted(async () => {
ctaVisible.value = true
})
const route = useRoute()
const router = useRouter()
function searchTasks() {
// Only search if the search term changed
if (route.query as unknown as string === searchTerm.value) {
return
}
router.push({
name: 'project.list',
query: {search: searchTerm.value},
})
}
function hideSearchBar() {
// This is a workaround.
// When clicking on the search button, @blur from the input is fired. If we
// would then directly hide the whole search bar directly, no click event
// from the button gets fired. To prevent this, we wait 200ms until we hide
// everything so the button has a chance of firing the search event.
setTimeout(() => {
showTaskSearch.value = false
}, 200)
}
const addTaskRef = ref<typeof AddTask | null>(null)
function focusNewTaskInput() {
addTaskRef.value?.focusTaskInput()
}
function updateTaskList(task: ITask) {
if (isAlphabeticalSorting.value ) {
if (isAlphabeticalSorting.value) {
// reload tasks with current filter and sorting
loadTasks()
}
else {
} else {
allTasks.value = [
task,
...allTasks.value,
@ -300,18 +236,22 @@ async function saveTaskPosition(e) {
const taskBefore = tasks.value[e.newIndex - 1] ?? null
const taskAfter = tasks.value[e.newIndex + 1] ?? null
const newTask = {
...task,
position: calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null),
}
const position = calculateItemPosition(taskBefore !== null ? taskBefore.position : null, taskAfter !== null ? taskAfter.position : null)
const updatedTask = await taskStore.update(newTask)
tasks.value[e.newIndex] = updatedTask
await taskPositionService.value.update(new TaskPositionModel({
position,
projectViewId: viewId,
taskId: task.id,
}))
tasks.value[e.newIndex] = {
...task,
position,
}
}
function prepareFiltersAndLoadTasks() {
if(isAlphabeticalSorting.value) {
sortByParam.value = {}
if (isAlphabeticalSorting.value) {
sortByParam.value = {}
sortByParam.value[ALPHABETICAL_SORT] = 'asc'
}
@ -328,7 +268,7 @@ function prepareFiltersAndLoadTasks() {
border-radius: $radius;
background: var(--grey-100);
border: 2px dashed var(--grey-300);
* {
opacity: 0;
}
@ -339,8 +279,8 @@ function prepareFiltersAndLoadTasks() {
}
.link-share-view .card {
border: none;
box-shadow: none;
border: none;
box-shadow: none;
}
.control.has-icons-left .icon,

View File

@ -2,73 +2,72 @@
<ProjectWrapper
class="project-table"
:project-id="projectId"
view-name="table"
:view-id
>
<template #header>
<div class="filter-container">
<div class="items">
<Popup>
<template #trigger="{toggle}">
<x-button
icon="th"
variant="secondary"
@click.prevent.stop="toggle()"
>
{{ $t('project.table.columns') }}
</x-button>
</template>
<template #content="{isOpen}">
<card
class="columns-filter"
:class="{'is-open': isOpen}"
>
<Fancycheckbox v-model="activeColumns.index">
#
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.done">
{{ $t('task.attributes.done') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.title">
{{ $t('task.attributes.title') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.priority">
{{ $t('task.attributes.priority') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.labels">
{{ $t('task.attributes.labels') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.doneAt">
{{ $t('task.attributes.doneAt') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.created">
{{ $t('task.attributes.created') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.updated">
{{ $t('task.attributes.updated') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
</Fancycheckbox>
</card>
</template>
</Popup>
<FilterPopup v-model="params" />
</div>
<Popup>
<template #trigger="{toggle}">
<x-button
icon="th"
variant="secondary"
class="mr-2"
@click.prevent.stop="toggle()"
>
{{ $t('project.table.columns') }}
</x-button>
</template>
<template #content="{isOpen}">
<card
class="columns-filter"
:class="{'is-open': isOpen}"
>
<Fancycheckbox v-model="activeColumns.index">
#
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.done">
{{ $t('task.attributes.done') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.title">
{{ $t('task.attributes.title') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.priority">
{{ $t('task.attributes.priority') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.labels">
{{ $t('task.attributes.labels') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.assignees">
{{ $t('task.attributes.assignees') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.dueDate">
{{ $t('task.attributes.dueDate') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.startDate">
{{ $t('task.attributes.startDate') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.endDate">
{{ $t('task.attributes.endDate') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.percentDone">
{{ $t('task.attributes.percentDone') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.doneAt">
{{ $t('task.attributes.doneAt') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.created">
{{ $t('task.attributes.created') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.updated">
{{ $t('task.attributes.updated') }}
</Fancycheckbox>
<Fancycheckbox v-model="activeColumns.createdBy">
{{ $t('task.attributes.createdBy') }}
</Fancycheckbox>
</card>
</template>
</Popup>
<FilterPopup v-model="params" />
</div>
</template>
@ -284,17 +283,19 @@ import FilterPopup from '@/components/project/partials/filter-popup.vue'
import Pagination from '@/components/misc/pagination.vue'
import Popup from '@/components/misc/popup.vue'
import {useTaskList} from '@/composables/useTaskList'
import type {SortBy} from '@/composables/useTaskList'
import {useTaskList} from '@/composables/useTaskList'
import type {ITask} from '@/modelTypes/ITask'
import type {IProject} from '@/modelTypes/IProject'
import AssigneeList from '@/components/tasks/partials/assigneeList.vue'
import type {IProjectView} from '@/modelTypes/IProjectView'
const {
projectId,
viewId,
} = defineProps<{
projectId: IProject['id'],
viewId: IProjectView['id'],
}>()
const ACTIVE_COLUMNS_DEFAULT = {
@ -321,7 +322,7 @@ const SORT_BY_DEFAULT: SortBy = {
const activeColumns = useStorage('tableViewColumns', {...ACTIVE_COLUMNS_DEFAULT})
const sortBy = useStorage<SortBy>('tableViewSortBy', {...SORT_BY_DEFAULT})
const taskList = useTaskList(() => projectId, sortBy.value)
const taskList = useTaskList(() => projectId, () => viewId, sortBy.value)
const {
loading,
@ -333,9 +334,7 @@ const {
const tasks: Ref<ITask[]> = taskList.tasks
Object.assign(params.value, {
filter_by: [],
filter_value: [],
filter_comparator: [],
filter: '',
})
// FIXME: by doing this we can have multiple sort orders
@ -383,6 +382,11 @@ const taskDetailRoutes = computed(() => Object.fromEntries(
.columns-filter {
margin: 0;
:deep(.card-content .content) {
display: flex;
flex-direction: column;
}
&.is-open {
margin: 2rem 0 1rem;
}
@ -392,4 +396,8 @@ const taskDetailRoutes = computed(() => Object.fromEntries(
border: none;
box-shadow: none;
}
.filter-container :deep(.popup) {
top: 7rem;
}
</style>

View File

@ -0,0 +1,233 @@
<script setup lang="ts">
import type {IProjectView} from '@/modelTypes/IProjectView'
import XButton from '@/components/input/button.vue'
import FilterInput from '@/components/project/partials/FilterInput.vue'
import {ref, watch} from 'vue'
import {transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
import {useLabelStore} from '@/stores/labels'
import {useProjectStore} from '@/stores/projects'
const {
modelValue,
} = defineProps<{
modelValue: IProjectView,
}>()
const emit = defineEmits(['update:modelValue'])
const view = ref<IProjectView>()
const labelStore = useLabelStore()
const projectStore = useProjectStore()
watch(
() => modelValue,
newValue => {
const transformed = {
...newValue,
filter: transformFilterStringFromApi(
newValue.filter,
labelId => labelStore.getLabelById(labelId)?.title,
projectId => projectStore.projects[projectId]?.title || null,
),
}
if (JSON.stringify(view.value) !== JSON.stringify(transformed)) {
view.value = transformed
}
},
{immediate: true, deep: true},
)
watch(
() => view.value,
newView => {
emit('update:modelValue', {
...newView,
filter: transformFilterStringForApi(
newView.filter,
labelTitle => labelStore.filterLabelsByQuery([], labelTitle)[0]?.id || null,
projectTitle => {
const found = projectStore.findProjectByExactname(projectTitle)
return found?.id || null
},
),
})
},
{deep: true},
)
const titleValid = ref(true)
function validateTitle() {
titleValid.value = view.value?.title !== ''
}
</script>
<template>
<form>
<div class="field">
<label
class="label"
for="title"
>
{{ $t('project.views.title') }}
</label>
<div class="control">
<input
id="title"
v-model="view.title"
v-focus
class="input"
:placeholder="$t('project.share.links.namePlaceholder')"
@blur="validateTitle"
>
</div>
<p
v-if="!titleValid"
class="help is-danger"
>
{{ $t('project.views.titleRequired') }}
</p>
</div>
<div class="field">
<label
class="label"
for="kind"
>
{{ $t('project.views.kind') }}
</label>
<div class="control">
<div class="select">
<select
id="kind"
v-model="view.viewKind"
>
<option value="list">
{{ $t('project.list.title') }}
</option>
<option value="gantt">
{{ $t('project.gantt.title') }}
</option>
<option value="table">
{{ $t('project.table.title') }}
</option>
<option value="kanban">
{{ $t('project.kanban.title') }}
</option>
</select>
</div>
</div>
</div>
<FilterInput
v-model="view.filter"
:input-label="$t('project.views.filter')"
/>
<div
v-if="view.viewKind === 'kanban'"
class="field"
>
<label
class="label"
for="configMode"
>
{{ $t('project.views.bucketConfigMode') }}
</label>
<div class="control">
<div class="select">
<select
id="configMode"
v-model="view.bucketConfigurationMode"
>
<option value="manual">
{{ $t('project.views.bucketConfigManual') }}
</option>
<option value="filter">
{{ $t('project.views.filter') }}
</option>
</select>
</div>
</div>
</div>
<div
v-if="view.viewKind === 'kanban' && view.bucketConfigurationMode === 'filter'"
class="field"
>
<label class="label">
{{ $t('project.views.bucketConfig') }}
</label>
<div class="control">
<div
v-for="(b, index) in view.bucketConfiguration"
:key="'bucket_'+index"
class="filter-bucket"
>
<button
class="is-danger"
@click.prevent="() => view.bucketConfiguration.splice(index, 1)"
>
<icon icon="trash-alt" />
</button>
<div class="filter-bucket-form">
<div class="field">
<label
class="label"
:for="'bucket_'+index+'_title'"
>
{{ $t('project.views.title') }}
</label>
<div class="control">
<input
:id="'bucket_'+index+'_title'"
v-model="view.bucketConfiguration[index].title"
class="input"
:placeholder="$t('project.share.links.namePlaceholder')"
>
</div>
</div>
<FilterInput
v-model="view.bucketConfiguration[index].filter"
:input-label="$t('project.views.filter')"
/>
</div>
</div>
<div class="is-flex is-justify-content-end">
<XButton
variant="secondary"
icon="plus"
@click="() => view.bucketConfiguration.push({title: '', filter: ''})"
>
{{ $t('project.kanban.addBucket') }}
</XButton>
</div>
</div>
</div>
</form>
</template>
<style scoped lang="scss">
.filter-bucket {
display: flex;
button {
background: transparent;
border: none;
color: var(--danger);
padding-right: .75rem;
cursor: pointer;
}
&-form {
margin-bottom: .5rem;
padding: .5rem;
border: 1px solid var(--grey-200);
border-radius: $radius;
width: 100%;
}
}
</style>

View File

@ -85,7 +85,7 @@
</template>
<script setup lang="ts">
import {ref, computed, watchEffect, shallowReactive, type ComponentPublicInstance} from 'vue'
import {type ComponentPublicInstance, computed, ref, shallowReactive, watchEffect} from 'vue'
import {useI18n} from 'vue-i18n'
import {useRouter} from 'vue-router'
@ -107,13 +107,14 @@ import {useTaskStore} from '@/stores/tasks'
import {useAuthStore} from '@/stores/auth'
import {getHistory} from '@/modules/projectHistory'
import {parseTaskText, PrefixMode, PREFIXES} from '@/modules/parseTaskText'
import {parseTaskText, PREFIXES, PrefixMode} from '@/modules/parseTaskText'
import {success} from '@/message'
import type {ITeam} from '@/modelTypes/ITeam'
import type {ITask} from '@/modelTypes/ITask'
import type {IProject} from '@/modelTypes/IProject'
import type {IAbstract} from '@/modelTypes/IAbstract'
import {isSavedFilter} from '@/services/savedFilter'
const {t} = useI18n({useScope: 'global'})
const router = useRouter()
@ -280,10 +281,13 @@ const commands = computed<{ [key in COMMAND_TYPE]: Command }>(() => ({
const placeholder = computed(() => selectedCmd.value?.placeholder || t('quickActions.placeholder'))
const currentProject = computed(() => Object.keys(baseStore.currentProject).length === 0
? null
: baseStore.currentProject,
)
const currentProject = computed(() => {
if (Object.keys(baseStore.currentProject).length === 0 || isSavedFilter(baseStore.currentProject)) {
return null
}
return baseStore.currentProject
})
const hintText = computed(() => {
if (selectedCmd.value !== null && currentProject.value !== null) {
@ -350,26 +354,6 @@ const isNewTaskCommand = computed(() => (
const taskSearchTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
type Filter = { by: string, value: string | number, comparator: string }
function filtersToParams(filters: Filter[]) {
const filter_by: Filter['by'][] = []
const filter_value: Filter['value'][] = []
const filter_comparator: Filter['comparator'][] = []
filters.forEach(({by, value, comparator}) => {
filter_by.push(by)
filter_value.push(value)
filter_comparator.push(comparator)
})
return {
filter_by,
filter_value,
filter_comparator,
}
}
function searchTasks() {
if (
searchMode.value !== SEARCH_MODE.ALL &&
@ -391,40 +375,27 @@ function searchTasks() {
const {text, project: projectName, labels} = parsedQuery.value
const filters: Filter[] = []
// FIXME: improve types
function addFilter(
by: Filter['by'],
value: Filter['value'],
comparator: Filter['comparator'],
) {
filters.push({
by,
value,
comparator,
})
}
let filter = ''
if (projectName !== null) {
const project = projectStore.findProjectByExactname(projectName)
console.log({project})
if (project !== null) {
addFilter('project_id', project.id, 'equals')
filter += ' project = ' + project.id
}
}
if (labels.length > 0) {
const labelIds = labelStore.getLabelsByExactTitles(labels).map((l) => l.id)
if (labelIds.length > 0) {
addFilter('labels', labelIds.join(), 'in')
filter += 'labels in ' + labelIds.join(', ')
}
}
const params = {
s: text,
sort_by: 'done',
...filtersToParams(filters),
filter,
}
taskSearchTimeout.value = setTimeout(async () => {

View File

@ -173,11 +173,11 @@
<div class="select">
<select v-model="selectedView[s.id]">
<option
v-for="(title, key) in availableViews"
:key="key"
:value="key"
v-for="(view) in availableViews"
:key="view.id"
:value="view.id"
>
{{ title }}
{{ view.title }}
</option>
</select>
</div>
@ -230,9 +230,9 @@ import LinkShareService from '@/services/linkShare'
import {useCopyToClipboard} from '@/composables/useCopyToClipboard'
import {success} from '@/message'
import {getDisplayName} from '@/models/user'
import type {ProjectView} from '@/types/ProjectView'
import {PROJECT_VIEWS} from '@/types/ProjectView'
import {useConfigStore} from '@/stores/config'
import {useProjectStore} from '@/stores/projects'
import type {IProjectView} from '@/modelTypes/IProjectView'
const props = defineProps({
projectId: {
@ -252,17 +252,13 @@ const showDeleteModal = ref(false)
const linkIdToDelete = ref(0)
const showNewForm = ref(false)
type SelectedViewMapper = Record<IProject['id'], ProjectView>
type SelectedViewMapper = Record<IProject['id'], IProjectView['id']>
const selectedView = ref<SelectedViewMapper>({})
const availableViews = computed<Record<ProjectView, string>>(() => ({
list: t('project.list.title'),
gantt: t('project.gantt.title'),
table: t('project.table.title'),
kanban: t('project.kanban.title'),
}))
const projectStore = useProjectStore()
const availableViews = computed<IProjectView[]>(() => projectStore.projects[props.projectId]?.views || [])
const copy = useCopyToClipboard()
watch(
() => props.projectId,
@ -281,7 +277,7 @@ async function load(projectId: IProject['id']) {
const links = await linkShareService.getAll({projectId})
links.forEach((l: ILinkShare) => {
selectedView.value[l.id] = 'list'
selectedView.value[l.id] = availableViews.value[0].id
})
linkShares.value = links
}
@ -315,8 +311,8 @@ async function remove(projectId: IProject['id']) {
}
}
function getShareLink(hash: string, view: ProjectView = PROJECT_VIEWS.LIST) {
return frontendUrl.value + 'share/' + hash + '/auth?view=' + view
function getShareLink(hash: string, viewId: IProjectView['id']) {
return frontendUrl.value + 'share/' + hash + '/auth?view=' + viewId
}
</script>

View File

@ -16,7 +16,22 @@
:search-results="found"
:label="searchLabel"
@search="find"
/>
>
<template #searchResult="{option: result}">
<User
v-if="shareType === 'user'"
:avatar-size="24"
:show-username="true"
:user="result"
/>
<span
v-else
class="search-result"
>
{{ result.name }}
</span>
</template>
</Multiselect>
</p>
<p class="control">
<x-button @click="add()">
@ -172,6 +187,8 @@ import Multiselect from '@/components/input/multiselect.vue'
import Nothing from '@/components/misc/nothing.vue'
import {success} from '@/message'
import {useAuthStore} from '@/stores/auth'
import {useConfigStore} from '@/stores/config'
import User from '@/components/misc/user.vue'
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
@ -210,8 +227,8 @@ const selectedRight = ref({})
const sharables = ref([])
const showDeleteModal = ref(false)
const authStore = useAuthStore()
const configStore = useConfigStore()
const userInfo = computed(() => authStore.info)
function createShareTypeNameComputed(count: number) {
@ -360,7 +377,15 @@ async function find(query: string) {
found.value = []
return
}
const results = await searchService.getAll({}, {s: query})
// Include public teams here if we are sharing with teams and its enabled in the config
let results = []
if (props.shareType === 'team' && configStore.publicTeamsEnabled) {
results = await searchService.getAll({}, {s: query, includePublic: true})
} else {
results = await searchService.getAll({}, {s: query})
}
found.value = results
.filter(m => {
if(props.shareType === 'user' && m.id === currentUserId.value) {

View File

@ -21,12 +21,12 @@
@dragendBar="updateGanttTask"
@dblclickBar="openTask"
>
<template #timeunit="{value, date}">
<template #timeunit="{date}">
<div
class="timeunit-wrapper"
:class="{'today': dateIsToday(date)}"
>
<span>{{ value }}</span>
<span>{{ date.getDate() }}</span>
<span class="weekday">
{{ weekDayFromDate(date) }}
</span>

View File

@ -97,6 +97,13 @@
editComment()
}"
/>
<Reactions
v-model="c.reactions"
class="mt-2"
entity-kind="comments"
:entity-id="c.id"
:disabled="!canWrite"
/>
</div>
</div>
<div
@ -190,6 +197,7 @@ import {formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
import {getAvatarUrl, getDisplayName} from '@/models/user'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
import Reactions from '@/components/input/Reactions.vue'
const props = defineProps({
taskId: {

View File

@ -37,7 +37,7 @@
</template>
<script setup lang="ts">
import {ref, computed, watch} from 'vue'
import {ref, computed, watch, onBeforeUnmount} from 'vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Editor from '@/components/input/AsyncEditor'
@ -88,6 +88,12 @@ async function saveWithDelay() {
}, 5000)
}
onBeforeUnmount(() => {
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
})
async function save() {
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)

View File

@ -47,7 +47,6 @@
<ReminderPeriod
v-if="activeForm === 'relative'"
v-model="reminder"
@update:modelValue="updateDataAndMaybeClose(close)"
/>
<DatepickerInline
@ -60,7 +59,7 @@
v-if="showFormSwitch !== null"
class="reminder__close-button"
:shadow="false"
@click="updateDataAndMaybeClose(close)"
@click="updateDataAndMaybeCloseNow(close)"
>
{{ $t('misc.confirm') }}
</x-button>
@ -73,10 +72,10 @@
<script setup lang="ts">
import {computed, ref, watch} from 'vue'
import {SECONDS_A_DAY, SECONDS_A_HOUR} from '@/constants/date'
import {IReminderPeriodRelativeTo, REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import {type IReminderPeriodRelativeTo, REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import {useI18n} from 'vue-i18n'
import {PeriodUnit, secondsToPeriod} from '@/helpers/time/period'
import {type PeriodUnit, secondsToPeriod} from '@/helpers/time/period'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import {formatDateShort} from '@/helpers/time/formatDate'
@ -87,6 +86,7 @@ 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'
import {useDebounceFn} from '@vueuse/core'
const {
modelValue,
@ -112,7 +112,7 @@ const presets = computed<TaskReminderModel[]>(() => [
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 7, relativeTo: defaultRelativeTo},
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 30, relativeTo: defaultRelativeTo},
])
const reminderDate = ref<Date|null>(null)
const reminderDate = ref<Date | null>(null)
type availableForms = null | 'relative' | 'absolute'
@ -142,16 +142,16 @@ const reminderText = computed(() => {
watch(
() => modelValue,
(newReminder) => {
if(newReminder) {
if (newReminder) {
reminder.value = newReminder
if(newReminder.relativeTo === null) {
if (newReminder.relativeTo === null) {
reminderDate.value = new Date(newReminder.reminder)
}
return
}
reminder.value = new TaskReminderModel()
},
{immediate: true},
@ -181,7 +181,9 @@ function setReminderFromPreset(preset, close) {
close()
}
function updateDataAndMaybeClose(close) {
const updateDataAndMaybeClose = useDebounceFn(updateDataAndMaybeCloseNow, 500)
function updateDataAndMaybeCloseNow(close) {
updateData()
if (clearAfterUpdate) {
close()

View File

@ -71,8 +71,7 @@ import {periodToSeconds, PeriodUnit, secondsToPeriod} from '@/helpers/time/perio
import TaskReminderModel from '@/models/taskReminder'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES, type IReminderPeriodRelativeTo} from '@/types/IReminderPeriodRelativeTo'
import {useDebounceFn} from '@vueuse/core'
import {type IReminderPeriodRelativeTo, REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
const {
modelValue,
@ -123,7 +122,7 @@ function updateData() {
reminder.value.relativeTo = period.value.relativeTo
reminder.value.reminder = null
useDebounceFn(() => emit('update:modelValue', reminder.value), 1000)
emit('update:modelValue', reminder.value)
}
</script>

View File

@ -30,7 +30,7 @@
<router-link
v-if="showProject && typeof project !== 'undefined'"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
class="task-project mr-1"
:class="{'mr-2': task.hexColor !== ''}"
>
@ -136,7 +136,7 @@
<router-link
v-if="showProjectSeparately"
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
:to="{ name: 'project.list', params: { projectId: task.projectId } }"
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
class="task-project"
>
{{ project.title }}

View File

@ -1,12 +1,14 @@
import {computed, shallowRef, watchEffect, h, type VNode} from 'vue'
import {computed, h, shallowRef, type VNode, watchEffect} from 'vue'
import {useRoute, useRouter} from 'vue-router'
import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects'
export function useRouteWithModal() {
const router = useRouter()
const route = useRoute()
const backdropView = computed(() => route.fullPath && window.history.state.backdropView)
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const routeWithModal = computed(() => {
return backdropView.value
@ -29,7 +31,7 @@ export function useRouteWithModal() {
if (routePropsOption === true) {
routeProps = route.params
} else {
if(typeof routePropsOption === 'function') {
if (typeof routePropsOption === 'function') {
routeProps = routePropsOption(route)
} else {
routeProps = routePropsOption
@ -52,7 +54,7 @@ export function useRouteWithModal() {
}
currentModal.value = h(component, routeProps)
})
const historyState = computed(() => route.fullPath && window.history.state)
function closeModal() {
@ -60,12 +62,23 @@ export function useRouteWithModal() {
// If the current project was changed because the user moved the currently opened task while coming from kanban,
// we need to reflect that change in the route when they close the task modal.
// The last route is only available as resolved string, therefore we need to use a regex for matching here
const kanbanRouteMatch = new RegExp('\\/projects\\/\\d+\\/kanban', 'g')
const kanbanRouter = {name: 'project.kanban', params: {projectId: baseStore.currentProject?.id}}
if (kanbanRouteMatch.test(historyState.value.back)
&& baseStore.currentProject
&& historyState.value.back !== router.resolve(kanbanRouter).fullPath) {
router.push(kanbanRouter)
const routeMatch = new RegExp('\\/projects\\/\\d+\\/(\\d+)', 'g')
const match = routeMatch.exec(historyState.value.back)
if (match !== null && baseStore.currentProject) {
let viewId: string | number = match[1]
if (!viewId) {
viewId = projectStore.projects[baseStore.currentProject?.id].views[0]?.id
}
const newRoute = {
name: 'project.view',
params: {
projectId: baseStore.currentProject?.id,
viewId,
},
}
router.push(newRoute)
return
}

View File

@ -2,10 +2,12 @@ import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
import {useRoute} from 'vue-router'
import {useRouteQuery} from '@vueuse/router'
import TaskCollectionService, {getDefaultTaskFilterParams} from '@/services/taskCollection'
import TaskCollectionService, {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection'
import type {ITask} from '@/modelTypes/ITask'
import {error} from '@/message'
import type {IProject} from '@/modelTypes/IProject'
import {useAuthStore} from '@/stores/auth'
import type {IProjectView} from '@/modelTypes/IProjectView'
export type Order = 'asc' | 'desc' | 'none'
@ -53,11 +55,16 @@ const SORT_BY_DEFAULT: SortBy = {
/**
* This mixin provides a base set of methods and properties to get tasks.
*/
export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sortByDefault: SortBy = SORT_BY_DEFAULT) {
export function useTaskList(
projectIdGetter: ComputedGetter<IProject['id']>,
projectViewIdGetter: ComputedGetter<IProjectView['id']>,
sortByDefault: SortBy = SORT_BY_DEFAULT,
) {
const projectId = computed(() => projectIdGetter())
const projectViewId = computed(() => projectViewIdGetter())
const params = ref({...getDefaultTaskFilterParams()})
const params = ref<TaskFilterParams>({...getDefaultTaskFilterParams()})
const search = ref('')
const page = useRouteQuery('page', '1', { transform: Number })
@ -81,11 +88,19 @@ export function useTaskList(projectIdGetter: ComputedGetter<IProject['id']>, sor
page.value = 1
},
)
const authStore = useAuthStore()
const getAllTasksParams = computed(() => {
return [
{projectId: projectId.value},
allParams.value,
{
projectId: projectId.value,
viewId: projectViewId.value,
},
{
...allParams.value,
filter_timezone: authStore.settings.timezone,
},
page.value,
]
})

View File

@ -17,8 +17,8 @@ describe('Filter Transformation', () => {
'assignees': 'assignees',
'labels': 'labels',
}
describe('For api', () => {
describe('For API', () => {
for (const c in fieldCases) {
it('should transform all filter params for ' + c + ' to snake_case', () => {
const transformed = transformFilterStringForApi(c + ' = ipsum', nullTitleToIdResolver, nullTitleToIdResolver)
@ -37,23 +37,35 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('labels = 1')
})
const multipleDummyResolver = (title: string) => {
switch (title) {
case 'lorem':
return 1
case 'ipsum':
return 2
default:
return null
}
}
it('should correctly resolve multiple labels', () => {
const transformed = transformFilterStringForApi(
'labels = lorem && dueDate = now && labels = ipsum',
(title: string) => {
switch (title) {
case 'lorem':
return 1
case 'ipsum':
return 2
default:
return null
}
},
multipleDummyResolver,
nullTitleToIdResolver,
)
expect(transformed).toBe('labels = 1&& due_date = now && labels = 2')
expect(transformed).toBe('labels = 1 && due_date = now && labels = 2')
})
it('should correctly resolve multiple labels with an in clause', () => {
const transformed = transformFilterStringForApi(
'labels in lorem, ipsum && dueDate = now',
multipleDummyResolver,
nullTitleToIdResolver,
)
expect(transformed).toBe('labels in 1, 2 && due_date = now')
})
it('should correctly resolve projects', () => {
@ -70,19 +82,50 @@ describe('Filter Transformation', () => {
const transformed = transformFilterStringForApi(
'project = lorem && dueDate = now || project = ipsum',
nullTitleToIdResolver,
(title: string) => {
switch (title) {
case 'lorem':
return 1
case 'ipsum':
return 2
default:
return null
}
},
multipleDummyResolver,
)
expect(transformed).toBe('project = 1&& due_date = now || project = 2')
expect(transformed).toBe('project = 1 && due_date = now || project = 2')
})
it('should correctly resolve multiple projects with in', () => {
const transformed = transformFilterStringForApi(
'project in lorem, ipsum',
nullTitleToIdResolver,
multipleDummyResolver,
)
expect(transformed).toBe('project in 1, 2')
})
it('should resolve projects at the correct position', () => {
const transformed = transformFilterStringForApi(
'project = pr',
nullTitleToIdResolver,
(title: string) => 1,
)
expect(transformed).toBe('project = 1')
})
it('should resolve project and labels independently', () => {
const transformed = transformFilterStringForApi(
'project = lorem && labels = ipsum',
multipleDummyResolver,
multipleDummyResolver,
)
expect(transformed).toBe('project = 1 && labels = 2')
})
it('should transform the same attribute multiple times', () => {
const transformed = transformFilterStringForApi(
'dueDate = now/d || dueDate > now/w+1w',
nullTitleToIdResolver,
nullTitleToIdResolver,
)
expect(transformed).toBe('due_date = now/d || due_date > now/w+1w')
})
})
@ -104,24 +147,36 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('labels = lorem')
})
const multipleIdToTitleResolver = (id: number) => {
switch (id) {
case 1:
return 'lorem'
case 2:
return 'ipsum'
default:
return null
}
}
it('should correctly resolve multiple labels', () => {
const transformed = transformFilterStringFromApi(
'labels = 1 && due_date = now && labels = 2',
(id: number) => {
switch (id) {
case 1:
return 'lorem'
case 2:
return 'ipsum'
default:
return null
}
},
multipleIdToTitleResolver,
nullIdToTitleResolver,
)
expect(transformed).toBe('labels = lorem&& dueDate = now && labels = ipsum')
expect(transformed).toBe('labels = lorem && dueDate = now && labels = ipsum')
})
it('should correctly resolve multiple labels in', () => {
const transformed = transformFilterStringFromApi(
'labels in 1, 2',
multipleIdToTitleResolver,
nullIdToTitleResolver,
)
expect(transformed).toBe('labels in lorem, ipsum')
})
it('should correctly resolve projects', () => {
@ -136,21 +191,32 @@ describe('Filter Transformation', () => {
it('should correctly resolve multiple projects', () => {
const transformed = transformFilterStringFromApi(
'project = lorem && due_date = now || project = ipsum',
'project = 1 && due_date = now || project = 2',
nullIdToTitleResolver,
(id: number) => {
switch (id) {
case 1:
return 'lorem'
case 2:
return 'ipsum'
default:
return null
}
},
multipleIdToTitleResolver,
)
expect(transformed).toBe('project = lorem && dueDate = now || project = ipsum')
})
it('should correctly resolve multiple projects in', () => {
const transformed = transformFilterStringFromApi(
'project in 1, 2',
nullIdToTitleResolver,
multipleIdToTitleResolver,
)
expect(transformed).toBe('project in lorem, ipsum')
})
it('should transform the same attribute multiple times', () => {
const transformed = transformFilterStringFromApi(
'due_date = now/d || due_date > now/w+1w',
nullIdToTitleResolver,
nullIdToTitleResolver,
)
expect(transformed).toBe('dueDate = now/d || dueDate > now/w+1w')
})
})
})

View File

@ -27,12 +27,13 @@ export const AUTOCOMPLETE_FIELDS = [
]
export const AVAILABLE_FILTER_FIELDS = [
'done',
'priority',
'percentDone',
...DATE_FIELDS,
...ASSIGNEE_FIELDS,
...LABEL_FIELDS,
...PROJECT_FIELDS,
'done',
'priority',
'percentDone',
]
export const FILTER_OPERATORS = [
@ -54,10 +55,10 @@ export const FILTER_JOIN_OPERATOR = [
')',
]
export const FILTER_OPERATORS_REGEX = '(&lt;|&gt;|&lt;=|&gt;=|=|!=)'
export const FILTER_OPERATORS_REGEX = '(&lt;|&gt;|&lt;=|&gt;=|=|!=|in)'
function getFieldPattern(field: string): RegExp {
return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()]+\\1?)?', 'ig')
export function getFilterFieldRegexPattern(field: string): RegExp {
return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()<]+\\1?)?', 'ig')
}
export function transformFilterStringForApi(
@ -65,47 +66,68 @@ export function transformFilterStringForApi(
labelResolver: (title: string) => number | null,
projectResolver: (title: string) => number | null,
): string {
if (filter.trim() === '') {
return ''
}
// Transform labels to ids
LABEL_FIELDS.forEach(field => {
const pattern = getFieldPattern(field)
const pattern = getFilterFieldRegexPattern(field)
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match
if (keyword) {
const labelId = labelResolver(keyword.trim())
if (labelId !== null) {
filter = filter.replace(keyword, String(labelId))
let keywords = [keyword.trim()]
if (operator === 'in' || operator === '?=') {
keywords = keyword.trim().split(',').map(k => k.trim())
}
keywords.forEach(k => {
const labelId = labelResolver(k)
if (labelId !== null) {
filter = filter.replace(k, String(labelId))
}
})
}
}
})
// Transform projects to ids
PROJECT_FIELDS.forEach(field => {
const pattern = getFieldPattern(field)
const pattern = getFilterFieldRegexPattern(field)
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match
if (keyword) {
const projectId = projectResolver(keyword.trim())
if (projectId !== null) {
filter = filter.replace(keyword, String(projectId))
let keywords = [keyword.trim()]
if (operator === 'in' || operator === '?=') {
keywords = keyword.trim().split(',').map(k => k.trim())
}
let replaced = keyword
keywords.forEach(k => {
const projectId = projectResolver(k)
if (projectId !== null) {
replaced = replaced.replace(k, String(projectId))
}
})
const actualKeywordStart = (match?.index || 0) + prefix.length
filter = filter.substring(0, actualKeywordStart) +
replaced +
filter.substring(actualKeywordStart + keyword.length)
}
}
})
// Transform all attributes to snake case
AVAILABLE_FILTER_FIELDS.forEach(f => {
filter = filter.replace(f, snakeCase(f))
filter = filter.replaceAll(f, snakeCase(f))
})
return filter
@ -116,46 +138,60 @@ export function transformFilterStringFromApi(
labelResolver: (id: number) => string | null,
projectResolver: (id: number) => string | null,
): string {
if (filter.trim() === '') {
return ''
}
// Transform all attributes from snake case
AVAILABLE_FILTER_FIELDS.forEach(f => {
filter = filter.replace(snakeCase(f), f)
filter = filter.replaceAll(snakeCase(f), f)
})
// Transform labels to their titles
LABEL_FIELDS.forEach(field => {
const pattern = getFieldPattern(field)
const pattern = getFilterFieldRegexPattern(field)
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match
if (keyword) {
const labelTitle = labelResolver(Number(keyword.trim()))
if (labelTitle !== null) {
filter = filter.replace(keyword, labelTitle)
let keywords = [keyword.trim()]
if (operator === 'in' || operator === '?=') {
keywords = keyword.trim().split(',').map(k => k.trim())
}
keywords.forEach(k => {
const labelTitle = labelResolver(parseInt(k))
if (labelTitle !== null) {
filter = filter.replace(k, labelTitle)
}
})
}
}
})
// Transform projects to ids
PROJECT_FIELDS.forEach(field => {
const pattern = getFieldPattern(field)
const pattern = getFilterFieldRegexPattern(field)
let match: RegExpExecArray | null
while ((match = pattern.exec(filter)) !== null) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const [matched, prefix, operator, space, keyword] = match
if (keyword) {
const project = projectResolver(Number(keyword.trim()))
if (project !== null) {
filter = filter.replace(keyword, project)
let keywords = [keyword.trim()]
if (operator === 'in' || operator === '?=') {
keywords = keyword.trim().split(',').map(k => k.trim())
}
keywords.forEach(k => {
const project = projectResolver(parseInt(k))
if (project !== null) {
filter = filter.replace(k, project)
}
})
}
}
})

View File

@ -1,64 +1,17 @@
import type { RouteRecordName } from 'vue-router'
import router from '@/router'
import type {IProject} from '@/modelTypes/IProject'
export type ProjectRouteName = Extract<RouteRecordName, string>
export type ProjectViewSettings = Record<
IProject['id'],
Extract<RouteRecordName, ProjectRouteName>
>
export type ProjectViewSettings = Record<IProject['id'], number>
const SETTINGS_KEY_PROJECT_VIEW = 'projectView'
// TODO: remove migration when releasing 1.0
type ListViewSettings = ProjectViewSettings
const SETTINGS_KEY_DEPRECATED_LIST_VIEW = 'listView'
function migrateStoredProjectRouteSettings() {
try {
const listViewSettingsString = localStorage.getItem(SETTINGS_KEY_DEPRECATED_LIST_VIEW)
if (listViewSettingsString === null) {
return
}
// A) the first version stored one setting for all lists in a string
if (listViewSettingsString.startsWith('list.')) {
const projectView = listViewSettingsString.replace('list.', 'project.')
if (!router.hasRoute(projectView)) {
return
}
return projectView as RouteRecordName
}
// B) the last version used a 'list.' prefix
const listViewSettings: ListViewSettings = JSON.parse(listViewSettingsString)
const projectViewSettingEntries = Object.entries(listViewSettings).map(([id, value]) => {
return [id, value.replace('list.', 'project.')]
})
const projectViewSettings = Object.fromEntries(projectViewSettingEntries)
localStorage.setItem(SETTINGS_KEY_PROJECT_VIEW, JSON.stringify(projectViewSettings))
} catch(e) {
//
} finally {
localStorage.removeItem(SETTINGS_KEY_DEPRECATED_LIST_VIEW)
}
}
/**
* Save the current project view to local storage
*/
export function saveProjectView(projectId: IProject['id'], routeName: string) {
if (routeName.includes('settings.')) {
export function saveProjectView(projectId: IProject['id'], viewId: number) {
if (!projectId || !viewId) {
return
}
if (!projectId) {
return
}
// We use local storage and not the store here to make it persistent across reloads.
const savedProjectView = localStorage.getItem(SETTINGS_KEY_PROJECT_VIEW)
let savedProjectViewSettings: ProjectViewSettings | false = false
@ -71,30 +24,19 @@ export function saveProjectView(projectId: IProject['id'], routeName: string) {
projectViewSettings = savedProjectViewSettings
}
projectViewSettings[projectId] = routeName
projectViewSettings[projectId] = viewId
localStorage.setItem(SETTINGS_KEY_PROJECT_VIEW, JSON.stringify(projectViewSettings))
}
export const getProjectView = (projectId: IProject['id']) => {
// TODO: remove migration when releasing 1.0
const migratedProjectView = migrateStoredProjectRouteSettings()
if (migratedProjectView !== undefined && router.hasRoute(migratedProjectView)) {
return migratedProjectView
export function getProjectViewId(projectId: IProject['id']): number {
const projectViewSettingsString = localStorage.getItem(SETTINGS_KEY_PROJECT_VIEW)
if (!projectViewSettingsString) {
return 0
}
try {
const projectViewSettingsString = localStorage.getItem(SETTINGS_KEY_PROJECT_VIEW)
if (!projectViewSettingsString) {
throw new Error()
}
const projectViewSettings = JSON.parse(projectViewSettingsString) as ProjectViewSettings
if (!router.hasRoute(projectViewSettings[projectId])) {
throw new Error()
}
return projectViewSettings[projectId]
} catch (e) {
return
}
const projectViewSettings = JSON.parse(projectViewSettingsString) as ProjectViewSettings
if (isNaN(projectViewSettings[projectId])) {
return 0
}
return projectViewSettings[projectId]
}

View File

@ -248,6 +248,7 @@
"text2": "هذا يشمل جميع المهام ولا يمكن التراجع عن هذا الإجراء!",
"success": "تم حذف المشروع بنجاح.",
"tasksToDelete": "سيؤدي هذا إلى حذف ما يقارب {count} من المهام.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "هذا المشروع لا يحتوي على أي مهام، يمكن حذفه بشكل آمن."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "الفلاتر",
"clear": "مسح الفلاتر",
"showResults": "Show results",
"attributes": {
"title": "العنوان",
"titlePlaceholder": "عنوان الفلتر المحفوظ هنا…",
@ -415,6 +433,52 @@
"edit": {
"title": "تعديل هذا الفلتر المحفوظ",
"success": "تم حفظ الفلتر بنجاح."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "إلى",
"from": "من",
"fromto": "{from} إلى {to}",
"date": "Date",
"ranges": {
"today": "اليوم",
"thisWeek": "هذا الأسبوع",
@ -598,6 +663,27 @@
"lastMonth": "الشهر الماضي",
"thisYear": "هذه السنة",
"restOfThisYear": "المتبقي من هذه السنة"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "الوصف",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "مشرف",
"member": "عضو"
"member": "عضو",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "مشروع جديد",
"createProject": "إنشاء مشروع",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "رابط Vikunja",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "خطأ",
"success": "تم بنجاح",

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +663,27 @@
"lastMonth": "Last Month",
"thisYear": "This Year",
"restOfThisYear": "The Rest of This Year"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Description",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
"member": "Member",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

View File

@ -57,11 +57,11 @@
"logout": "Odhlásit se",
"emailInvalid": "Prosím zadejte platnou emailovou adresu.",
"usernameRequired": "Zadejte prosím uživatelské jméno.",
"usernameMustNotContainSpace": "The username must not contain spaces.",
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"usernameMustNotContainSpace": "Uživatelské jméno nesmí obsahovat mezery.",
"usernameMustNotLookLikeUrl": "Uživatelské jméno nesmí vypadat jako adresa URL.",
"passwordRequired": "Zadejte prosím heslo.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMin": "Heslo musí mít nejméně 8 znaků.",
"passwordNotMax": "Heslo může mít maximálně 250 znaků.",
"showPassword": "Ukázat heslo",
"hidePassword": "Skrýt heslo",
"noAccountYet": "Ještě nemáte účet?",
@ -248,6 +248,7 @@
"text2": "To zahrnuje všechny úkoly a JE TO NEVRATNÉ!",
"success": "Projekt byl úspěšně smazán.",
"tasksToDelete": "Neodvolatelně tím odstraníme asi {count} úloh.",
"tasksAndChildProjectsToDelete": "Chystáte se neodvolatelně odstranit nějaké úkoly (cca {tasks}) a projekty (cca {projects}).",
"noTasksToDelete": "Tento projekt neobsahuje žádné úkoly, mělo by být bezpečné ho smazat."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Tajný klíč",
"secretHint": "Pokud je zadáno, všechny požadavky na cílovou adresu URL webhooku budou podepsány pomocí HMAC.",
"secretDocs": "Další podrobnosti o používání tajných klíčů naleznete v dokumentaci."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filtry",
"clear": "Vymazat filtry",
"showResults": "Zobrazit výsledky",
"attributes": {
"title": "Název",
"titlePlaceholder": "Název uloženého filtru přijde sem…",
@ -415,6 +433,52 @@
"edit": {
"title": "Upravit tento uložený filtr",
"success": "Filtr byl úspěšně uložen."
},
"query": {
"title": "Dotaz",
"placeholder": "Zadejte vyhledávací nebo filtrační dotaz…",
"help": {
"intro": "K filtrování úkolů můžete použít syntaxi dotazů podobnou SQL. Dostupná pole pro filtrování zahrnují:",
"link": "Jak to funguje?",
"canUseDatemath": "Můžete použít matematiku a nastavit relativní data. Pro více informací klikněte na datum v dotazu.",
"fields": {
"done": "Zda je úkol dokončen nebo ne",
"priority": "Úroveň priority úkolu (1-5)",
"percentDone": "Procento dokončení úkolu (0-100)",
"dueDate": "Datum dokončení úkolu",
"startDate": "Datum zahájení úkolu",
"endDate": "Datum dokončení úkolu",
"doneAt": "Datum a čas, kdy byl úkol dokončen",
"assignees": "Přiřazení uživatelé",
"labels": "Štítky přiřazené úkolu",
"project": "Projekt, do kterého úkol patří (k dispozici pouze pro uložené filtry, ne na úrovni projektu)"
},
"operators": {
"intro": "Dostupné operátory pro filtrování zahrnují:",
"notEqual": "Nerovná se",
"equal": "Rovná se",
"greaterThan": "Větší než",
"greaterThanOrEqual": "Větší nebo rovno než",
"lessThan": "Menší než",
"lessThanOrEqual": "Menší nebo rovno než",
"like": "Odpovídá vzoru (s použitím zástupného znaku %)",
"in": "Odpovídá libovolné hodnotě v seznamu hodnot oddělených čárkou"
},
"logicalOperators": {
"intro": "Pro kombinování více podmínek můžete použít tyto logické operátory:",
"and": "AND - shoduje se, pokud jsou splněny všechny podmínky",
"or": "OR - shoduje se, pokud je splněna alespoň jedna podmínka",
"parentheses": "Závorky seskupují podmínky"
},
"examples": {
"intro": "Zde jsou některé příklady filtrovacích dotazů:",
"priorityEqual": "Odpovídá úkolům s úrovní priority 4",
"dueDatePast": "Odpovídá úkolům s termínem dokončení v minulosti",
"undoneHighPriority": "Odpovídá nedokončeným úkolům s úrovní priority 3 nebo vyšší",
"assigneesIn": "Odpovídá úkolům přiřazeným buď \"uživatel1\", nebo \"uživatel2\"",
"priorityOneOrTwoPastDue": "Odpovídá úkolům s prioritou 1 nebo 2 a termínem dokončení v minulosti"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Do",
"from": "Od",
"fromto": "{from} {to}",
"date": "Datum",
"ranges": {
"today": "Dnes",
"thisWeek": "Tento týden",
@ -598,6 +663,27 @@
"lastMonth": "Minulý měsíc",
"thisYear": "Tento rok",
"restOfThisYear": "Zbytek tohoto roku"
},
"values": {
"now": "Teď",
"startOfToday": "Začátek dnes",
"endOfToday": "Konec dnes",
"beginningOflastWeek": "Začátek minulého týdne",
"endOfLastWeek": "Konec posledního týdne",
"beginningOfThisWeek": "Začátek tohoto týdne",
"endOfThisWeek": "Konec tohoto týdne",
"startOfNextWeek": "Začátek příštího týdne",
"endOfNextWeek": "Konec tohoto týdne",
"in7Days": "Za 7 dní",
"beginningOfLastMonth": "Začátek minulého měsíce",
"endOfLastMonth": "Konec minulého měsíce",
"startOfThisMonth": "Začátek tohoto měsíce",
"endOfThisMonth": "Konec tohoto měsíce",
"startOfNextMonth": "Začátek příštího měsíce",
"endOfNextMonth": "Konec příštího měsíce",
"in30Days": "Za 30 dní",
"startOfThisYear": "Začátek tohoto roku",
"endOfThisYear": "Konec tohoto roku"
}
},
"datemathHelp": {
@ -713,7 +799,7 @@
"startDate": "Počáteční datum",
"title": "Název",
"updated": "Aktualizováno",
"doneAt": "Done At"
"doneAt": "Dokončeno"
},
"subscription": {
"subscribedTaskThroughParentProject": "Zde se nemůžete odhlásit, protože jste přihlášeni k odběru tohoto úkolu prostřednictvím jeho projektu.",
@ -916,7 +1002,9 @@
"description": "Popis",
"descriptionPlaceholder": "Popište tým, stiskněte '/' pro více možností…",
"admin": "Administrátor",
"member": "Člen"
"member": "Člen",
"isPublic": "Veřejný tým",
"isPublicDescription": "Učinit tým veřejně dostupným. Pokud je povoleno, každý může s tímto týmem sdílet projekty, i když není přímým členem."
}
},
"keyboardShortcuts": {
@ -975,8 +1063,9 @@
"share": "Sdílet",
"newProject": "Nový projekt",
"createProject": "Vytvořit projekt",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantArchiveIsDefault": "Nemůžete archivovat svůj výchozí projekt.",
"cantDeleteIsDefault": "Nemůžete smazat svůj výchozí projekt.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Chyba",
"success": "Úspěch",
@ -1096,7 +1191,7 @@
},
"about": {
"title": "O aplikaci",
"version": "Version: {version}"
"version": "Verze: {version}"
},
"time": {
"units": {

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filtre",
"clear": "Ryd Filtre",
"showResults": "Show results",
"attributes": {
"title": "Titel",
"titlePlaceholder": "Det gemte filters titel skrives her…",
@ -415,6 +433,52 @@
"edit": {
"title": "Rediger Dette Gemte Filter",
"success": "Filteret blev slettet."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Til",
"from": "Fra",
"fromto": "{from} til {to}",
"date": "Date",
"ranges": {
"today": "I dag",
"thisWeek": "Denne uge",
@ -598,6 +663,27 @@
"lastMonth": "Sidste måned",
"thisYear": "Dette år",
"restOfThisYear": "Resten af dette år"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Beskrivelse",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Administrator",
"member": "Medlem"
"member": "Medlem",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "d m Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Fejl",
"success": "Succes",

View File

@ -248,6 +248,7 @@
"text2": "Dies umfasst alle Aufgaben und kann NICHT rückgängig gemacht werden!",
"success": "Das Projekt wurde erfolgreich gelöscht.",
"tasksToDelete": "Dies löscht unwiderruflich ca. {count} Aufgaben.",
"tasksAndChildProjectsToDelete": "Dies löscht unwiderruflich ca. {tasks} Aufgaben und {projects} Projekte.",
"noTasksToDelete": "Dieses Projekt enthält keine Aufgaben, es kann sicher gelöscht werden."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Schlüssel",
"secretHint": "Wenn angegeben, werden alle Anfragen an die Webhook Ziel-URL mit HMAC signiert.",
"secretDocs": "In der Dokumentation findest du weitere Informationen zum Umgang mit Schlüsseln."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filter",
"clear": "Filter zurücksetzen",
"showResults": "Ergebnisse anzeigen",
"attributes": {
"title": "Titel",
"titlePlaceholder": "Einen gespeicherten Filternamen eingeben …",
@ -415,6 +433,52 @@
"edit": {
"title": "Diesen gespeicherten Filter bearbeiten",
"success": "Filter gespeichert."
},
"query": {
"title": "Abfrage",
"placeholder": "Gib eine Suche oder Filterabfrage ein…",
"help": {
"intro": "Um Aufgaben zu filtern, kannst du eine SQL-ähnliche Abfragesyntax verwenden. Du kannst die folgenden Felder in deinem Filter verwenden:",
"link": "Wie funktioniert das?",
"canUseDatemath": "Du kannst Date Math verwenden, um relative Daten festzulegen. Klicke auf den Datumswert in einer Abfrage, um mehr zu erfahren.",
"fields": {
"done": "Ob die Aufgabe erledigt ist oder nicht",
"priority": "Die Priorität der Aufgabe (1-5)",
"percentDone": "Prozentsatz der Fertigstellung der Aufgabe (0-100)",
"dueDate": "Das Fälligkeitsdatum der Aufgabe",
"startDate": "Das Startdatum der Aufgabe",
"endDate": "Das Enddatum der Aufgabe",
"doneAt": "Datum und Uhrzeit, an dem die Aufgabe als erledigt markiert wurde",
"assignees": "Die der Aufgabe Zugewiesenen",
"labels": "Die der Aufgabe zugeordneten Labels",
"project": "Das Projekt, zu dem die Aufgabe gehört (nur verfügbar für gespeicherte Filter, nicht auf Projektebene)"
},
"operators": {
"intro": "Die verfügbaren Operatoren für die Filterung sind:",
"notEqual": "Ungleich",
"equal": "Gleich",
"greaterThan": "Größer als",
"greaterThanOrEqual": "Größer oder gleich",
"lessThan": "Kleiner als",
"lessThanOrEqual": "Kleiner als oder gleich",
"like": "Vergleicht zu einem Muster (mit Platzhalter %)",
"in": "Filtert einen beliebigen Wert in einer kommaseparierten Liste"
},
"logicalOperators": {
"intro": "Um mehrere Bedingungen zu kombinieren, kannst du folgende logische Operatoren verwenden:",
"and": "UND Operator, stimmt überein, wenn alle Bedingungen wahr sind",
"or": "ODER Operator, stimmt überein, wenn eine der Bedingungen wahr ist",
"parentheses": "Klammern zum Gruppieren von Bedingungen"
},
"examples": {
"intro": "Hier sind einige Beispiele für Filterabfragen:",
"priorityEqual": "Findet Aufgaben mit Priorität Level 4",
"dueDatePast": "Findet Aufgaben mit einem Fälligkeitsdatum in der Vergangenheit",
"undoneHighPriority": "Findet Aufgaben, die nicht erledigt sind und Priorität Level mindestens 3 haben",
"assigneesIn": "Findet Aufgaben, die entweder \"user1\" oder \"user2\" zugewiesen sind",
"priorityOneOrTwoPastDue": "Findet Aufgaben mit Priorität Level 1 oder 2 und einem Fälligkeitsdatum in der Vergangenheit"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Bis",
"from": "Von",
"fromto": "{from} bis {to}",
"date": "Datum",
"ranges": {
"today": "Heute",
"thisWeek": "Diese Woche",
@ -598,6 +663,27 @@
"lastMonth": "Letzter Monat",
"thisYear": "Dieses Jahr",
"restOfThisYear": "Der Rest des Jahres"
},
"values": {
"now": "Jetzt",
"startOfToday": "Beginn des heutigen Tages",
"endOfToday": "Ende des heutigen Tages",
"beginningOflastWeek": "Beginn der letzten Woche",
"endOfLastWeek": "Ende der letzten Woche",
"beginningOfThisWeek": "Beginn dieser Woche",
"endOfThisWeek": "Ende dieser Woche",
"startOfNextWeek": "Beginn der nächsten Woche",
"endOfNextWeek": "Ende der nächsten Woche",
"in7Days": "In 7 Tagen",
"beginningOfLastMonth": "Beginn des letzten Monats",
"endOfLastMonth": "Ende des letzten Monats",
"startOfThisMonth": "Beginn diesen Monats",
"endOfThisMonth": "Ende diesen Monats",
"startOfNextMonth": "Beginn des nächsten Monats",
"endOfNextMonth": "Ende des nächsten Monats",
"in30Days": "In 30 Tagen",
"startOfThisYear": "Beginn dieses Jahres",
"endOfThisYear": "Ende dieses Jahres"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Beschreibung",
"descriptionPlaceholder": "Gib eine Beschreibung für dieses Team ein, drücke '/' für mehr Optionen…",
"admin": "Admin",
"member": "Mitglied"
"member": "Mitglied",
"isPublic": "Öffentliches Team",
"isPublicDescription": "Machs das Team öffentlich sichtbar. Wenn aktiviert, kann jede:r mit diesem Team Projekte teilen, auch wenn er oder sie kein direktes Mitglied des Teams ist."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Neues Projekt",
"createProject": "Projekt erstellen",
"cantArchiveIsDefault": "Du kannst dieses Projekt nicht archivieren, da es dein Standardprojekt ist.",
"cantDeleteIsDefault": "Du kannst dieses Projekt nicht löschen, da es dein Standardprojekt ist."
"cantDeleteIsDefault": "Du kannst dieses Projekt nicht löschen, da es dein Standardprojekt ist.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja-URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} hat mit {value} reagiert",
"reactedWithAnd": "{users} und {lastUser} haben mit {value} reagiert",
"reactedWithAndMany": "{users} und {num} weitere haben mit {value} reagiert",
"add": "Reaktion hinzufügen"
},
"error": {
"error": "Fehler",
"success": "Erfolgreich",

View File

@ -248,6 +248,7 @@
"text2": "Dies umfasst alle Aufgaben und kann NICHT rückgängig gemacht werden!",
"success": "Das Projekt wurde erfolgreich gelöscht.",
"tasksToDelete": "Dies löscht unwiderruflich ca. {count} Aufgaben.",
"tasksAndChildProjectsToDelete": "Dies löscht unwiderruflich ca. {tasks} Aufgaben und {projects} Projekte.",
"noTasksToDelete": "Dieses Projekt enthält keine Aufgaben, es kann sicher gelöscht werden."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Schlüssel",
"secretHint": "Wenn angegeben, werden alle Anfragen an die Webhook Ziel-URL mit HMAC signiert.",
"secretDocs": "In der Dokumentation findest du weitere Informationen zum Umgang mit Schlüsseln."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filter",
"clear": "Filter zurücksetzen",
"showResults": "Ergebnisse anzeigen",
"attributes": {
"title": "Titl",
"titlePlaceholder": "De Name für de g'speicheret Filter chunt da ahne…",
@ -415,6 +433,52 @@
"edit": {
"title": "De g'speicheret Filter bearbeite",
"success": "De filter isch erfolgriich g'speichered wore."
},
"query": {
"title": "Abfrage",
"placeholder": "Gib eine Suche oder Filterabfrage ein…",
"help": {
"intro": "Um Aufgaben zu filtern, kannst du eine SQL-ähnliche Abfragesyntax verwenden. Du kannst die folgenden Felder in deinem Filter verwenden:",
"link": "Wie funktioniert das?",
"canUseDatemath": "Du kannst Date Math verwenden, um relative Daten festzulegen. Klicke auf den Datumswert in einer Abfrage, um mehr zu erfahren.",
"fields": {
"done": "Ob die Aufgabe erledigt ist oder nicht",
"priority": "Die Priorität der Aufgabe (1-5)",
"percentDone": "Prozentsatz der Fertigstellung der Aufgabe (0-100)",
"dueDate": "Das Fälligkeitsdatum der Aufgabe",
"startDate": "Das Startdatum der Aufgabe",
"endDate": "Das Enddatum der Aufgabe",
"doneAt": "Datum und Uhrzeit, an dem die Aufgabe als erledigt markiert wurde",
"assignees": "Die der Aufgabe Zugewiesenen",
"labels": "Die der Aufgabe zugeordneten Labels",
"project": "Das Projekt, zu dem die Aufgabe gehört (nur verfügbar für gespeicherte Filter, nicht auf Projektebene)"
},
"operators": {
"intro": "Die verfügbaren Operatoren für die Filterung sind:",
"notEqual": "Ungleich",
"equal": "Gleich",
"greaterThan": "Größer als",
"greaterThanOrEqual": "Größer oder gleich",
"lessThan": "Kleiner als",
"lessThanOrEqual": "Kleiner als oder gleich",
"like": "Vergleicht zu einem Muster (mit Platzhalter %)",
"in": "Filtert einen beliebigen Wert in einer kommaseparierten Liste"
},
"logicalOperators": {
"intro": "Um mehrere Bedingungen zu kombinieren, kannst du folgende logische Operatoren verwenden:",
"and": "UND Operator, stimmt überein, wenn alle Bedingungen wahr sind",
"or": "ODER Operator, stimmt überein, wenn eine der Bedingungen wahr ist",
"parentheses": "Klammern zum Gruppieren von Bedingungen"
},
"examples": {
"intro": "Hier sind einige Beispiele für Filterabfragen:",
"priorityEqual": "Findet Aufgaben mit Priorität Level 4",
"dueDatePast": "Findet Aufgaben mit einem Fälligkeitsdatum in der Vergangenheit",
"undoneHighPriority": "Findet Aufgaben, die nicht erledigt sind und Priorität Level mindestens 3 haben",
"assigneesIn": "Findet Aufgaben, die entweder \"user1\" oder \"user2\" zugewiesen sind",
"priorityOneOrTwoPastDue": "Findet Aufgaben mit Priorität Level 1 oder 2 und einem Fälligkeitsdatum in der Vergangenheit"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Bis",
"from": "Von",
"fromto": "{from} bis {to}",
"date": "Datum",
"ranges": {
"today": "Heute",
"thisWeek": "Diese Woche",
@ -598,6 +663,27 @@
"lastMonth": "Letzter Monat",
"thisYear": "Dieses Jahr",
"restOfThisYear": "Der Rest des Jahres"
},
"values": {
"now": "Jetzt",
"startOfToday": "Beginn des heutigen Tages",
"endOfToday": "Ende des heutigen Tages",
"beginningOflastWeek": "Beginn der letzten Woche",
"endOfLastWeek": "Ende der letzten Woche",
"beginningOfThisWeek": "Beginn dieser Woche",
"endOfThisWeek": "Ende dieser Woche",
"startOfNextWeek": "Beginn der nächsten Woche",
"endOfNextWeek": "Ende der nächsten Woche",
"in7Days": "In 7 Tagen",
"beginningOfLastMonth": "Beginn des letzten Monats",
"endOfLastMonth": "Ende des letzten Monats",
"startOfThisMonth": "Beginn diesen Monats",
"endOfThisMonth": "Ende diesen Monats",
"startOfNextMonth": "Beginn des nächsten Monats",
"endOfNextMonth": "Ende des nächsten Monats",
"in30Days": "In 30 Tagen",
"startOfThisYear": "Beginn dieses Jahres",
"endOfThisYear": "Ende dieses Jahres"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Beschriibig",
"descriptionPlaceholder": "Gib eine Beschreibung für dieses Team ein, drücke '/' für mehr Optionen…",
"admin": "Chef",
"member": "Mitglied"
"member": "Mitglied",
"isPublic": "Öffentliches Team",
"isPublicDescription": "Machs das Team öffentlich sichtbar. Wenn aktiviert, kann jede:r mit diesem Team Projekte teilen, auch wenn er oder sie kein direktes Mitglied des Teams ist."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Neues Projekt",
"createProject": "Projekt erstellen",
"cantArchiveIsDefault": "Du kannst dieses Projekt nicht archivieren, da es dein Standardprojekt ist.",
"cantDeleteIsDefault": "Du kannst dieses Projekt nicht löschen, da es dein Standardprojekt ist."
"cantDeleteIsDefault": "Du kannst dieses Projekt nicht löschen, da es dein Standardprojekt ist.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} hat mit {value} reagiert",
"reactedWithAnd": "{users} und {lastUser} haben mit {value} reagiert",
"reactedWithAndMany": "{users} und {num} weitere haben mit {value} reagiert",
"add": "Reaktion hinzufügen"
},
"error": {
"error": "Fähler",
"success": "Erfolg",

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,6 +381,22 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
@ -445,7 +462,7 @@
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a list"
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
@ -986,7 +1003,9 @@
"description": "Description",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
"member": "Member",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -1046,7 +1065,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1093,6 +1113,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +663,27 @@
"lastMonth": "Last Month",
"thisYear": "This Year",
"restOfThisYear": "The Rest of This Year"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Description",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
"member": "Member",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

View File

@ -248,6 +248,7 @@
"text2": "¡Esto incluye todas las tareas y NO SE PUEDE DESHACER!",
"success": "El proyecto se eliminó con éxito.",
"tasksToDelete": "Esto eliminará de forma definitiva aprox. {count} tareas.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "Este proyecto no contiene tareas. Debería ser seguro eliminarlo."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filtros",
"clear": "Limpiar Filtros",
"showResults": "Show results",
"attributes": {
"title": "Título",
"titlePlaceholder": "El título del filtro guardado va aquí…",
@ -415,6 +433,52 @@
"edit": {
"title": "Editar este Filtro Guardado",
"success": "El filtro se guardó con éxito."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Para",
"from": "De",
"fromto": "De {from} para {to}",
"date": "Date",
"ranges": {
"today": "Hoy",
"thisWeek": "Esta Semana",
@ -598,6 +663,27 @@
"lastMonth": "El Mes Pasado",
"thisYear": "Este Año",
"restOfThisYear": "El Resto del Año"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Descripción",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Miembro"
"member": "Miembro",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Nuevo proyecto",
"createProject": "Crear proyecto",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "URL de Vikunja",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Éxito",

View File

@ -248,6 +248,7 @@
"text2": "Ceci inclut toutes les tâches et NE PEUT PAS ÊTRE ANNULÉ !",
"success": "Le projet a bien été supprimé.",
"tasksToDelete": "Cela supprimera définitivement environ {count} tâches.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "Ce projet ne contient aucune tâche, vous pouvez le supprimer sans problème."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filtres",
"clear": "Effacer les filtres",
"showResults": "Show results",
"attributes": {
"title": "Nom",
"titlePlaceholder": "Entre un nom de filtre enregistré…",
@ -415,6 +433,52 @@
"edit": {
"title": "Modifier ce filtre enregistré",
"success": "Filtre enregistré."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "À",
"from": "De",
"fromto": "Du {from} au {to}",
"date": "Date",
"ranges": {
"today": "Aujourdhui",
"thisWeek": "Cette semaine",
@ -598,6 +663,27 @@
"lastMonth": "Le mois dernier",
"thisYear": "Cette année",
"restOfThisYear": "Le reste de cette année"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Description",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Membre"
"member": "Membre",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Nouveau projet",
"createProject": "Créer un projet",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "URL Vikunja",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Erreur",
"success": "Succès",

View File

@ -6,7 +6,7 @@
"welcomeEvening": "Jó estét {username}!",
"lastViewed": "Utoljára megtekintve",
"addToHomeScreen": "Adja hozzá ezt az alkalmazást a kezdőképernyőhöz a gyorsabb hozzáférés és a jobb élmény érdekében.",
"goToOverview": "Go to overview",
"goToOverview": "Tovább az áttekintéshez",
"project": {
"importText": "Importálja projektjeit és feladatait más szolgáltatásokból a Vikunjába:",
"import": "Importálja adatait a Vikunjába"
@ -57,11 +57,11 @@
"logout": "Kijelentkezés",
"emailInvalid": "Kérjük, adjon meg egy valós email címet!",
"usernameRequired": "Kérjük adjon meg egy felhasználónevet.",
"usernameMustNotContainSpace": "The username must not contain spaces.",
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"usernameMustNotContainSpace": "A felhasználónév nem tartalmazhat szóközt.",
"usernameMustNotLookLikeUrl": "A felhasználónév nem nézhet ki URL-nek.",
"passwordRequired": "Kérjük, adjon meg új jelszót.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMin": "A jelszónak legalább 8 karakterből kell állnia.",
"passwordNotMax": "A jelszó legfeljebb 250 karakterből állhat.",
"showPassword": "Jelszó megjelenítése",
"hidePassword": "A jelszó elrejtése",
"noAccountYet": "Még nincs fiókja?",
@ -164,7 +164,7 @@
"expired": "Ez a token lejárt {ago}.",
"tokenCreatedSuccess": "Íme az új API tokenje: {token}",
"tokenCreatedNotSeeAgain": "Tárolja el biztonságos helyen, többé nem fogja látni!",
"selectAll": "Select all",
"selectAll": "Összes kijelölése",
"delete": {
"header": "Token törlése",
"text1": "Biztos benne, hogy törölni akarja ezt a tokent \"{token}\"?",
@ -248,6 +248,7 @@
"text2": "Ez magában foglalja az összes feladatot és NEM VISSZAVONHATÓ!",
"success": "A projekt sikeresen törölve.",
"tasksToDelete": "Ezzel visszavonhatatlanul eltávolítjuk kb. {count} feladatát.",
"tasksAndChildProjectsToDelete": "Ezzel visszavonhatatlanul eltávolítunk kb. {tasks} feladatot és {projects} projektet.",
"noTasksToDelete": "Ez a projekt nem tartalmaz feladatokat, biztonságosan törölhető."
},
"duplicate": {
@ -264,7 +265,7 @@
"identifier": "Projektazonosító",
"identifierPlaceholder": "Írja be a projekt projektazonosítót...",
"description": "Leírás",
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
"descriptionPlaceholder": "Adja meg a projekt leírását, a további lehetőségekért nyomja meg a „/” gombot…",
"color": "Szín",
"success": "A projekt sikeresen frissítve."
},
@ -367,29 +368,46 @@
}
},
"webhooks": {
"title": "Webhooks",
"targetUrl": "Target URL",
"targetUrlInvalid": "Please provide a valid URL.",
"events": "Events",
"eventsHint": "Select all events this webhook should recieve updates for (within the current project).",
"mustSelectEvents": "You must select at least one event.",
"delete": "Delete this webhook",
"deleteText": "Are you sure you want to delete this webhook? External targets will not be notified of its events anymore.",
"deleteSuccess": "The webhook was successfully deleted.",
"create": "Create webhook",
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
"title": "Webhookok",
"targetUrl": "Cél URL",
"targetUrlInvalid": "Kérjük adjon meg egy érvényes URL-t.",
"events": "Események",
"eventsHint": "Válassza ki az összes olyan eseményt, amelyhez ez a webhook frissítéseket kap (az aktuális projekten belül).",
"mustSelectEvents": "Legalább egy eseményt ki kell választania.",
"delete": "Webhook törlése",
"deleteText": "Biztosan törli ezt a webhook-ot? A külső célpontok többé nem kapnak értesítést az eseményeiről.",
"deleteSuccess": "A webhook törlése sikeresen megtörtént.",
"create": "Webhook létrehozása",
"secret": "Kulcs",
"secretHint": "Amennyiben meg van adva, a webhook cél URL-címére irányuló összes kérés HMAC használatával lesz aláírva.",
"secretDocs": "Tekintse meg a dokumentációt a titkok használatának további részleteiért."
},
"views": {
"header": "Nézetek szerkesztése",
"title": "Cím",
"actions": "Műveletek",
"kind": "Fajta",
"bucketConfigMode": "Vödör konfigurációs mód",
"bucketConfig": "Vödör konfiguráció",
"bucketConfigManual": "Manuális",
"filter": "Szűrő",
"create": "Nézet létrehozása",
"createSuccess": "A nézet létrehozása sikeres volt.",
"titleRequired": "Kérjük, adjon meg egy címet.",
"delete": "Törölje ezt a nézetet",
"deleteText": "Biztosan eltávolítja ezt a nézetet? A továbbiakban nem lesz használható a projektben szereplő feladatok megtekintésére. Ez a művelet nem töröl semmilyen feladatot. Ezt nem lehet visszacsinálni!",
"deleteSuccess": "A nézet sikeresen törölve"
}
},
"filters": {
"title": "Szűrők",
"clear": "Szűrők törlése",
"showResults": "Eredmények megjelenítése",
"attributes": {
"title": "Cím",
"titlePlaceholder": "A mentett szűrő címe ide kerül…",
"description": "Leírás",
"descriptionPlaceholder": "Add a description for this filter here, hit '/' for more options…",
"descriptionPlaceholder": "Adjon hozzá leírást ehhez a szűrőhöz, nyomja meg a „/” gombot a további lehetőségekért…",
"includeNulls": "Tartalmazza az olyan feladatokat, amelyeknek nincs beállított értéke",
"requireAll": "A feladat megjelenítéséhez minden szűrőnek igaznak kell lennie",
"showDoneTasks": "Elkészült feladatok megjelenítése",
@ -415,6 +433,52 @@
"edit": {
"title": "Mentett szűrő szerkesztése",
"success": "A szűrőt sikeresen mentette."
},
"query": {
"title": "Lekérdezés",
"placeholder": "Írjon be egy keresési, vagy szűrési lekérdezést…",
"help": {
"intro": "A feladatok szűréséhez az SQL-hez hasonló lekérdezési szintaxist használhat. A szűréshez rendelkezésre álló mezők a következők:",
"link": "Hogyan működik?",
"canUseDatemath": "A relatív dátumok beállításához a matematikai dátumokat is használhat. További információért kattintson a dátum értékére a lekérdezésben.",
"fields": {
"done": "Akár befejeződött a feladat, akár nem",
"priority": "A feladat prioritási szintje (1-5)",
"percentDone": "A feladat teljesítésének százalékos aránya (0-100)",
"dueDate": "A feladat teljesítésének határideje",
"startDate": "A feladat kezdési dátuma",
"endDate": "A feladat befejezési dátuma",
"doneAt": "A feladat befejezésének dátuma és időpontja",
"assignees": "A feladattal megbízottak",
"labels": "A feladathoz társított címkék",
"project": "A projekt, amelyhez a feladat tartozik (csak a mentett szűrőkhöz érhető el, projekt szinten nem)"
},
"operators": {
"intro": "A szűréshez elérhető operátorok a következők:",
"notEqual": "Nem egyenlő",
"equal": "Egyenlő",
"greaterThan": "Nagyobb, mint",
"greaterThanOrEqual": "Nagyobb, vagy egyenlő",
"lessThan": "Kevesebb, mint",
"lessThanOrEqual": "Kevesebb, vagy egyenlő mint",
"like": "Megfelel egy mintának (helyettesítő karakterrel %)",
"in": "A vesszővel elválasztott értéklistában szereplő bármely értéknek megfelel"
},
"logicalOperators": {
"intro": "Több feltétel kombinálásához a következő logikai operátorokat használhatja:",
"and": "ÉS operátor, akkor egyezik, ha minden feltétel igaz",
"or": "VAGY operátor, akkor felel meg, ha valamelyik feltétel igaz",
"parentheses": "Zárójelek a csoportosítási feltételekhez"
},
"examples": {
"intro": "Íme néhány példa a szűrőlekérdezésekre:",
"priorityEqual": "4. prioritási szintű feladatokat egyezteti",
"dueDatePast": "A múltbeli esedékességgel rendelkező feladatokat egyezteti",
"undoneHighPriority": "Megfelel a 3-as, vagy magasabb prioritási szintű még nem elvégzett feladatoknak",
"assigneesIn": "Megfelel a \"felhasználó1\", vagy a \"felhasználó2\"-nek rendelt feladatoknak",
"priorityOneOrTwoPastDue": "Megfelel az 1-es, vagy 2-es prioritási szinttel és a múltbeli esedékességgel rendelkező feladatoknak"
}
}
}
},
"migrate": {
@ -431,8 +495,8 @@
"confirm": "Biztos vagyok benne, kezdje el a migrációt most!",
"importUpload": "Ha adatokat szeretne importálni a(z) {name} webhelyről a Vikunjába, kattintson az alábbi gombra a fájl kiválasztásához.",
"upload": "Fájl feltöltése",
"migrationStartedWillReciveEmail": "Vikunja will now import your lists/projects, tasks, notes, reminders and files from {service}. As this will take a while, we will send you an email once done. You can close this window now.",
"migrationInProgress": "A migration is currently in progress. Please wait until it is done."
"migrationStartedWillReciveEmail": "A Vikunja mostantól importálja listáit/projektjeit, feladatait, jegyzeteit, emlékeztetőit és fájljait a {service} szolgáltatásból. Mivel ez eltart egy ideig, amint elkészült, küldünk Önnek egy e-mailt. Most bezárhatja ezt az ablakot.",
"migrationInProgress": "A migráció folyamatban van. Kérjük várjon, amíg elkészül."
},
"label": {
"title": "Címkék",
@ -503,7 +567,7 @@
"custom": "Egyéni",
"id": "Azonosító",
"created": "Létrehozva ekkor:",
"createdBy": "Created by {0}",
"createdBy": "Létrehozta: {0}",
"actions": "Műveletek",
"cannotBeUndone": "Ezt nem lehet visszavonni!"
},
@ -522,59 +586,59 @@
"edit": "Szerkesztés",
"done": "Befejezve",
"heading1": "Címsor 1",
"heading1Tooltip": "Big section heading.",
"heading1Tooltip": "Nagy szakasz címsora.",
"heading2": "Címsor 2",
"heading2Tooltip": "Medium section heading.",
"heading2Tooltip": "Közepes szakasz címsora.",
"heading3": "Címsor 3",
"heading3Tooltip": "Smaller section header.",
"heading3Tooltip": "Kisebb szakasz címsora.",
"headingSmaller": "Kisebb címsor",
"headingBigger": "Nagyobb címsor",
"bold": "Félkövér",
"italic": "Dőlt",
"strikethrough": "Áthúzott",
"underline": "Underline",
"underline": "Aláhúzott",
"code": "Kód",
"codeTooltip": "Capture a code snippet.",
"codeTooltip": "Rögzítsen egy kódrészletet.",
"quote": "Idézet",
"quoteTooltip": "Capture a quote.",
"bulletList": "Bullet list",
"bulletListTooltip": "Create a simple bullet list.",
"unorderedList": "Unordered list",
"orderedList": "Ordered list",
"orderedListTooltip": "Create a list with numbering.",
"quoteTooltip": "Rögzítsen egy idézetet.",
"bulletList": "Felsorolásos lista",
"bulletListTooltip": "Hozzon létre egy egyszerű felsoroláslistát.",
"unorderedList": "Rendezetlen lista",
"orderedList": "Rendezett lista",
"orderedListTooltip": "Készítsen listát számozással.",
"cleanBlock": "Blokk kitisztítása",
"link": "Hivatkozás",
"image": "Kép",
"imageTooltip": "Upload an image from your computer.",
"imageTooltip": "Fájl feltöltése a számítógépről.",
"table": {
"title": "Table",
"insert": "Insert table",
"addColumnBefore": "Add column before",
"addColumnAfter": "Add column after",
"deleteColumn": "Delete column",
"addRowBefore": "Add row before",
"addRowAfter": "Add row after",
"deleteRow": "Delete row",
"deleteTable": "Delete table",
"mergeCells": "Merge cells",
"splitCell": "Split cell",
"toggleHeaderColumn": "Toggle header column",
"toggleHeaderRow": "Toggle header row",
"toggleHeaderCell": "Toggle header cell",
"mergeOrSplit": "Merge or split",
"fixTables": "Fix tables"
"title": "Táblázat",
"insert": "Táblázat beszúrása",
"addColumnBefore": "Oszlop hozzáadása előtte",
"addColumnAfter": "Oszlop hozzáadása utána",
"deleteColumn": "Oszlop törlése",
"addRowBefore": "Sor hozzáadása előtte",
"addRowAfter": "Sor hozzáadása utána",
"deleteRow": "Sor törlése",
"deleteTable": "Táblázat törlése",
"mergeCells": "Cellák egyesítése",
"splitCell": "Cellák szétválasztása",
"toggleHeaderColumn": "A fejléc oszlopának váltása",
"toggleHeaderRow": "Váltás a fejlécsorra",
"toggleHeaderCell": "Kapcsolja be a fejléccellát",
"mergeOrSplit": "Egyesítés vagy felosztás",
"fixTables": "Táblázatok javítása"
},
"horizontalRule": "Vízszintes vonal",
"horizontalRuleTooltip": "Divide a section.",
"horizontalRuleTooltip": "Egy szakasz felosztása.",
"sideBySide": "Egymás mellett",
"guide": "Útmutató",
"text": "Text",
"textTooltip": "Just start typing with plain text.",
"taskList": "Task list",
"taskListTooltip": "Track tasks with a to-do list.",
"undo": "Undo",
"redo": "Redo",
"placeholder": "Type some text or hit '/' to see more options…"
"text": "Szöveg",
"textTooltip": "Csak kezdje el a gépelést egyszerű szöveggel.",
"taskList": "Feladatlista",
"taskListTooltip": "Kövesse nyomon a feladatokat egy teendőlistával.",
"undo": "Visszavonás",
"redo": "Újra",
"placeholder": "Írjon be egy szöveget, vagy nyomja meg a „/” gombot a további lehetőségek megtekintéséhez…"
},
"multiselect": {
"createPlaceholder": "Új létrehozása",
@ -584,6 +648,7 @@
"to": "Eddig",
"from": "Ettől",
"fromto": "{from} - tól {to} - ig",
"date": "Dátum",
"ranges": {
"today": "Ma",
"thisWeek": "Ezen a héten",
@ -598,6 +663,27 @@
"lastMonth": "Előző hónap",
"thisYear": "Aktuális év",
"restOfThisYear": "Az év hátralévő része"
},
"values": {
"now": "Most",
"startOfToday": "A mai nap kezdete",
"endOfToday": "A mai nap vége",
"beginningOflastWeek": "Múlt hét eleje",
"endOfLastWeek": "Múlt hét vége",
"beginningOfThisWeek": "E hét eleje",
"endOfThisWeek": "E hét vége",
"startOfNextWeek": "Jövő hét eleje",
"endOfNextWeek": "Jövő hét vége",
"in7Days": "7 napon belül",
"beginningOfLastMonth": "Múlt hónap eleje",
"endOfLastMonth": "Múlt hónap vége",
"startOfThisMonth": "E hónap eleje",
"endOfThisMonth": "E hónap vége",
"startOfNextMonth": "Jövő hónap eleje",
"endOfNextMonth": "Jövő hónap vége",
"in30Days": "30 napon belül",
"startOfThisYear": "Ez év eleje",
"endOfThisYear": "Ez év vége"
}
},
"datemathHelp": {
@ -713,7 +799,7 @@
"startDate": "Kezdő dátum",
"title": "Cím",
"updated": "Frissítve",
"doneAt": "Done At"
"doneAt": "Befejezve ekkor"
},
"subscription": {
"subscribedTaskThroughParentProject": "Itt nem iratkozhat le, mert a projektjén keresztül feliratkozott erre a feladatra.",
@ -749,7 +835,7 @@
"loading": "Hozzászólások betöltése…",
"edited": "Szerkesztve: {date}",
"creating": "Hozzászólás létrehozása…",
"placeholder": "Add your comment, hit '/' for more options…",
"placeholder": "Adja hozzá megjegyzését, nyomja meg a „/” gombot a további lehetőségekért…",
"comment": "Hozzászólás",
"delete": "Hozzászólás törlése",
"deleteText1": "Biztos benne, hogy törölni akarja ezt a hozzászólást?",
@ -763,7 +849,7 @@
"1week": "1 hét"
},
"description": {
"placeholder": "Enter a description, hit '/' for more options…",
"placeholder": "Írja be a leírást, nyomja meg a '/' gombot a további lehetőségekért…",
"empty": "Nem érhető el leírás."
},
"assignee": {
@ -914,9 +1000,11 @@
"namePlaceholder": "A csapat nevét ide írja…",
"nameRequired": "Kérjük, adjon meg egy nevet.",
"description": "Leírás",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"descriptionPlaceholder": "Jellemezze a csapatot itt, kattintson a '/' gombra a további lehetőségekért…",
"admin": "Adminisztrátor",
"member": "Tag"
"member": "Tag",
"isPublic": "Nyilvános csapat",
"isPublicDescription": "Tedd nyilvánosan felfedezhetővé a csapatot. Ha engedélyezve van, bárki megoszthat projekteket ezzel a csapattal, még akkor is, ha nem közvetlen tagja."
}
},
"keyboardShortcuts": {
@ -975,8 +1063,9 @@
"share": "Megosztás",
"newProject": "Új projekt",
"createProject": "Projekt létrehozása",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantArchiveIsDefault": "Ezt nem archiválhatja, mert ez az alapértelmezett projektje.",
"cantDeleteIsDefault": "Ezt nem törölheti, mert ez az alapértelmezett projektje.",
"views": "Nézetek"
},
"apiConfig": {
"url": "Vikunja URL",
@ -996,8 +1085,8 @@
"title": "Értesítések",
"none": "Nincsenek értesítései. Legyen szép napja!",
"explainer": "Az értesítések itt jelennek meg, amikor olyan projektek vagy feladatok történnek, amelyekre feliratkozott.",
"markAllRead": "Mark all notifications as read",
"markAllReadSuccess": "Successfully marked all notifications as read."
"markAllRead": "Minden értesítés megjelölése olvasottként",
"markAllReadSuccess": "Az összes értesítést olvasottként jelölte meg."
},
"quickActions": {
"commands": "Parancsok",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} a következővel reagált: {value}",
"reactedWithAnd": "{users} és {lastUser} a következővel reagált: {value}",
"reactedWithAndMany": "{users} és további {num} felhasználó reagált a következővel: {value}",
"add": "Adja hozzá reakcióját"
},
"error": {
"error": "Hiba",
"success": "Siker",
@ -1096,7 +1191,7 @@
},
"about": {
"title": "Névjegy",
"version": "Version: {version}"
"version": "Verzió: {version}"
},
"time": {
"units": {

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filtri",
"clear": "Pulisci Filtri",
"showResults": "Show results",
"attributes": {
"title": "Titolo",
"titlePlaceholder": "Il titolo del filtro salvato va qui…",
@ -415,6 +433,52 @@
"edit": {
"title": "Modifica Questo Filtro Salvato",
"success": "Filtro salvato."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "A",
"from": "Da",
"fromto": "da {from} a {to}",
"date": "Date",
"ranges": {
"today": "Oggi",
"thisWeek": "Questa Settimana",
@ -598,6 +663,27 @@
"lastMonth": "Mese scorso",
"thisYear": "Quest'anno",
"restOfThisYear": "Il resto di quest'anno"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Descrizione",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Amministratore",
"member": "Membro"
"member": "Membro",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "URL Vikunja",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Errore",
"success": "Fatto",

View File

@ -248,6 +248,7 @@
"text2": "このプロジェクトに含まれるタスクはすべて削除されます。この操作は元に戻せません。",
"success": "プロジェクトは正常に削除されました。",
"tasksToDelete": "約{count}件のタスクが抹消されます。",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "このプロジェクトにはタスクが含まれていないので問題なく削除できます。"
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "絞り込み",
"clear": "絞り込みの解除",
"showResults": "Show results",
"attributes": {
"title": "絞り込み条件名",
"titlePlaceholder": "絞り込み条件名を入力…",
@ -415,6 +433,52 @@
"edit": {
"title": "絞り込み条件の編集",
"success": "絞り込み条件は正常に保存されました。"
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "タスクの終了日",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "終了",
"from": "開始",
"fromto": "{from} 〜 {to} まで",
"date": "Date",
"ranges": {
"today": "今日",
"thisWeek": "今週",
@ -598,6 +663,27 @@
"lastMonth": "先月",
"thisYear": "今年",
"restOfThisYear": "今から年末まで"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "今日の終わり",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "先週の終わり",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "今週の終わり",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "来週の終わり",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "先月の終わり",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "今月の終わり",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "来月の終わり",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "今年の終わり"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "説明",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "管理者",
"member": "メンバー"
"member": "メンバー",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "新しいプロジェクトの作成",
"createProject": "プロジェクトの作成",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "Y/n/j H:i",
"altFormatShort": "Y/n/j"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "프로젝트가 성공적으로 삭제되었습니다.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "필터",
"clear": "필터 초기화",
"showResults": "Show results",
"attributes": {
"title": "제목",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "To",
"from": "From",
"fromto": "{from} 에서 {to}",
"date": "Date",
"ranges": {
"today": "오늘",
"thisWeek": "이번 주",
@ -598,6 +663,27 @@
"lastMonth": "지난달",
"thisYear": "올해",
"restOfThisYear": "The Rest of This Year"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Description",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
"member": "Member",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Titel",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edit This Saved Filter",
"success": "Het filter is succesvol opgeslagen."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +663,27 @@
"lastMonth": "Last Month",
"thisYear": "This Year",
"restOfThisYear": "The Rest of This Year"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Beschrijving",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Beheerder",
"member": "Lid"
"member": "Lid",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Fout",
"success": "Succes",

View File

@ -248,6 +248,7 @@
"text2": "Dette inkluderer alle oppgaver og KAN IKKE ANGRES!",
"success": "Prosjektet ble slettet.",
"tasksToDelete": "Dette vil ugjenkallelig fjerne ca. {count} oppgaver.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "Dette prosjektet inneholder ingen oppgaver, det bør være trygt å slette."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filtre",
"clear": "Fjern filtre",
"showResults": "Show results",
"attributes": {
"title": "Tittel",
"titlePlaceholder": "Den lagrede filtertittelen kommer hit…",
@ -415,6 +433,52 @@
"edit": {
"title": "Rediger dette lagrede filteret",
"success": "Filteret ble lagret."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Til",
"from": "Fra",
"fromto": "{from} til {to}",
"date": "Date",
"ranges": {
"today": "Idag",
"thisWeek": "Denne uken",
@ -598,6 +663,27 @@
"lastMonth": "Forrige måned",
"thisYear": "Dette året",
"restOfThisYear": "Resten av denne uken"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Beskrivelse",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Administrator",
"member": "Medlem"
"member": "Medlem",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Nytt prosjekt",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "d.m.Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Feil",
"success": "Suksess",

View File

@ -57,11 +57,11 @@
"logout": "Wyloguj",
"emailInvalid": "Proszę podać poprawny adres e-mail.",
"usernameRequired": "Proszę podać nazwę użytkownika.",
"usernameMustNotContainSpace": "The username must not contain spaces.",
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"usernameMustNotContainSpace": "Nazwa użytkownika nie może zawierać spacji.",
"usernameMustNotLookLikeUrl": "Nazwa użytkownika nie może wyglądać jak adres URL.",
"passwordRequired": "Proszę podać hasło.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMin": "Hasło musi zawierać co najmniej 8 znaków.",
"passwordNotMax": "Hasło musi zawierać co najwyżej 250 znaków.",
"showPassword": "Pokaż hasło",
"hidePassword": "Ukryj hasło",
"noAccountYet": "Nie masz jeszcze konta?",
@ -248,6 +248,7 @@
"text2": "Dotyczy to wszystkich zadań i tego NIE DA SIĘ COFNĄĆ!",
"success": "Projekt został pomyślnie usunięty.",
"tasksToDelete": "To nieodwracalnie usunie około {count} zadań.",
"tasksAndChildProjectsToDelete": "To nieodwracalnie usunie ok. {tasks} zadań i {projects} projektów.",
"noTasksToDelete": "Ten projekt nie zawiera żadnych zadań, więc można go bezpiecznie usunąć."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Sekret",
"secretHint": "Jeśli podane, wszystkie żądania do adresu docelowego webhooka zostaną podpisane przy użyciu HMAC.",
"secretDocs": "Sprawdź dokumentację, aby uzyskać więcej informacji na temat korzystania z sekretów."
},
"views": {
"header": "Edytuj widoki",
"title": "Tytuł",
"actions": "Działania",
"kind": "Rodzaj",
"bucketConfigMode": "Tryb konfiguracji kolumny",
"bucketConfig": "Konfiguracja kolumny",
"bucketConfigManual": "Instrukcja",
"filter": "Filtr",
"create": "Utwórz widok",
"createSuccess": "Widok utworzony pomyślnie.",
"titleRequired": "Proszę podać tytuł.",
"delete": "Usuń ten widok",
"deleteText": "Czy na pewno chcesz usunąć ten widok? Nie będzie już możliwe wyświetlanie zadań w tym projekcie. Ta akcja nie usunie żadnych zadań. Tej operacji nie można cofnąć!",
"deleteSuccess": "Widok został pomyślnie usunięty"
}
},
"filters": {
"title": "Filtry",
"clear": "Wyczyść filtry",
"showResults": "Wyświetl wyniki",
"attributes": {
"title": "Tytuł",
"titlePlaceholder": "Tu wpisz tytuł filtra stałego…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edytuj ten filtr stały",
"success": "Filtr został pomyślnie zapisany."
},
"query": {
"title": "Zapytanie",
"placeholder": "Wpisz zapytanie wyszukiwania lub filtruj…",
"help": {
"intro": "Aby filtrować zadania, możesz użyć składni zapytań podobnej do SQL. Pola dostępne do filtrowania to:",
"link": "Jak to działa?",
"canUseDatemath": "Możesz użyć matematyki dat, aby ustawić daty względne. Kliknij wartość daty w zapytaniu, aby dowiedzieć się więcej.",
"fields": {
"done": "Czy zadanie zostało zakończone, czy nie",
"priority": "Priorytet zadania (1-5)",
"percentDone": "Procent ukończenia zadania (0-100)",
"dueDate": "Termin wykonania zadania",
"startDate": "Data rozpoczęcia zadania",
"endDate": "Data zakończenia zadania",
"doneAt": "Data i czas ukończenia zadania",
"assignees": "Osoby przypisane do zadania",
"labels": "Etykiety przypisane do zadania",
"project": "Projekt, do którego należy zadanie (dostępne tylko dla zapisanych filtrów, nie na poziomie projektu)"
},
"operators": {
"intro": "Dostępne operatory do filtrowania to:",
"notEqual": "Nie równa się",
"equal": "Równa się",
"greaterThan": "Większe niż",
"greaterThanOrEqual": "Większe niż lub równe",
"lessThan": "Mniejsze niż",
"lessThanOrEqual": "Mniejsze niż lub równe",
"like": "Pasuje do wzorca (używając symbolu %)",
"in": "Dopasuje dowolną wartość z listy wartości oddzielonych przecinkami"
},
"logicalOperators": {
"intro": "Aby połączyć wiele warunków, możesz użyć następujących operatorów logicznych:",
"and": "Operator AND, dopasowuje, jeśli wszystkie warunki są prawdziwe",
"or": "Operator OR, dopasowuje, jeśli którykolwiek z warunków jest spełniony",
"parentheses": "Nawiasy do grupowania warunków"
},
"examples": {
"intro": "Oto kilka przykładów zapytań filtrujących:",
"priorityEqual": "Dopasowuje zadania z priorytetem 4",
"dueDatePast": "Dopasowuje zadania z terminem wykonania w przeszłości",
"undoneHighPriority": "Dopasowuje niewykonane zadania z poziomem 3 lub wyższym",
"assigneesIn": "Dopasowuje zadania przypisane do \"user1\" lub \"user2\"",
"priorityOneOrTwoPastDue": "Dopasowuje zadania z poziomem 1 lub 2 i terminem wykonania w przeszłości"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Do",
"from": "Od",
"fromto": "{from} do {to}",
"date": "Data",
"ranges": {
"today": "Dziś",
"thisWeek": "W tym tygodniu",
@ -598,6 +663,27 @@
"lastMonth": "Zeszły miesiąc",
"thisYear": "Ten rok",
"restOfThisYear": "Reszta tego roku"
},
"values": {
"now": "Teraz",
"startOfToday": "Początek dzisiejszego dnia",
"endOfToday": "Koniec dzisiejszego dnia",
"beginningOflastWeek": "Początek zeszłego tygodnia",
"endOfLastWeek": "Koniec zeszłego tygodnia",
"beginningOfThisWeek": "Początek tego tygodnia",
"endOfThisWeek": "Koniec tego tygodnia",
"startOfNextWeek": "Początek następnego tygodnia",
"endOfNextWeek": "Koniec następnego tygodnia",
"in7Days": "Za 7 dni",
"beginningOfLastMonth": "Początek zeszłego miesiąca",
"endOfLastMonth": "Koniec zeszłego miesiąca",
"startOfThisMonth": "Początek tego miesiąca",
"endOfThisMonth": "Koniec tego miesiąca",
"startOfNextMonth": "Początek następnego miesiąca",
"endOfNextMonth": "Koniec następnego miesiąca",
"in30Days": "Za 30 dni",
"startOfThisYear": "Początek tego roku",
"endOfThisYear": "Koniec tego roku"
}
},
"datemathHelp": {
@ -713,7 +799,7 @@
"startDate": "Data rozpoczęcia",
"title": "Tytuł",
"updated": "Zaktualizowano",
"doneAt": "Done At"
"doneAt": "Wykonano"
},
"subscription": {
"subscribedTaskThroughParentProject": "Nie możesz zrezygnować z subskrypcji, ponieważ subskrybujesz to zadanie poprzez jego projekt.",
@ -916,7 +1002,9 @@
"description": "Opis",
"descriptionPlaceholder": "Opisz tutaj zespół, naciśnij '/' aby uzyskać więcej opcji…",
"admin": "Administrator",
"member": "Członek"
"member": "Członek",
"isPublic": "Publiczny zespół",
"isPublicDescription": "Udostępnij zespół publicznie. Gdy ta opcja jest włączona, każdy może udostępniać projekty temu zespołowi, nawet nie będąc jego bezpośrednim członkiem."
}
},
"keyboardShortcuts": {
@ -975,8 +1063,9 @@
"share": "Udostępnij",
"newProject": "Nowy projekt",
"createProject": "Utwórz projekt",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantArchiveIsDefault": "Nie możesz tego zarchiwizować, ponieważ jest to twój domyślny projekt.",
"cantDeleteIsDefault": "Nie możesz tego usunąć, ponieważ jest to twój domyślny projekt.",
"views": "Widoki"
},
"apiConfig": {
"url": "URL Vikunji",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} zareagował z {value}",
"reactedWithAnd": "{users} i {lastUser} zareagowali z {value}",
"reactedWithAndMany": "{users} i {num} innych osób zareagowali z {value}",
"add": "Dodaj swoją reakcję"
},
"error": {
"error": "Błąd",
"success": "Sukces",
@ -1096,7 +1191,7 @@
},
"about": {
"title": "O aplikacji",
"version": "Version: {version}"
"version": "Wersja: {version}"
},
"time": {
"units": {

View File

@ -248,6 +248,7 @@
"text2": "Isso inclui todas as tarefas e NÃO PODE SER DESFEITO!",
"success": "Seu projeto foi excluído com sucesso.",
"tasksToDelete": "Isto irá remover permanentemente approx. {count} tarefas.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "Este projeto não contém tarefas, é seguro excluir."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Segredo",
"secretHint": "Se fornecido, todas as requisições para a URL de destino do webhook serão assinadas usando HMAC.",
"secretDocs": "Confira a documentação para obter mais detalhes sobre como usar segredos."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filtros",
"clear": "Limpar Filtros",
"showResults": "Show results",
"attributes": {
"title": "Título",
"titlePlaceholder": "O título do filtro salvo fica aqui…",
@ -415,6 +433,52 @@
"edit": {
"title": "Editar este filtro salvo",
"success": "O filtro foi salvo com sucesso."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Para",
"from": "De",
"fromto": "{from} até {to}",
"date": "Date",
"ranges": {
"today": "Hoje",
"thisWeek": "Esta semana",
@ -598,6 +663,27 @@
"lastMonth": "Último mês",
"thisYear": "Este ano",
"restOfThisYear": "O resto deste ano"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Descrição",
"descriptionPlaceholder": "Descreva a equipe aqui, aperte '/' para mais opções…",
"admin": "Administrador",
"member": "Membro"
"member": "Membro",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Novo projeto",
"createProject": "Criar projeto",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Erro",
"success": "Sucesso",

View File

@ -248,6 +248,7 @@
"text2": "Isto inclui todas as tarefas e NÃO PODE SER REVERTIDO!",
"success": "O projeto foi eliminado com sucesso.",
"tasksToDelete": "Isto irá remover irrevogavelmente aprox. {count} tarefas.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "Este projeto não contém tarefas, deve ser seguro eliminá-lo."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Segredo",
"secretHint": "Se fornecido, todos os pedidos para o URL de destino do webhook serão assinados utilizando HMAC.",
"secretDocs": "Verifica a documentação para mais detalhes sobre como utilizar segredos."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filtros",
"clear": "Limpar Filtros",
"showResults": "Show results",
"attributes": {
"title": "Título",
"titlePlaceholder": "O título do filtro memorizado será aqui…",
@ -415,6 +433,52 @@
"edit": {
"title": "Editar Este Filtro Memorizado",
"success": "O filtro foi memorizado com sucesso."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Até",
"from": "De",
"fromto": "{from} até {to}",
"date": "Date",
"ranges": {
"today": "Hoje",
"thisWeek": "Esta semana",
@ -598,6 +663,27 @@
"lastMonth": "Mês Passado",
"thisYear": "Este Ano",
"restOfThisYear": "O Resto Deste Ano"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Descrição",
"descriptionPlaceholder": "Descreve aqui a equipa, pressiona '/' para mais opções…",
"admin": "Administrador",
"member": "Membro"
"member": "Membro",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Novo projeto",
"createProject": "Criar projeto",
"cantArchiveIsDefault": "Não podes arquivar isto porque é o teu projeto padrão.",
"cantDeleteIsDefault": "Não podes eliminar isto porque é o teu projeto padrão."
"cantDeleteIsDefault": "Não podes eliminar isto porque é o teu projeto padrão.",
"views": "Views"
},
"apiConfig": {
"url": "URL do Vikunja",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Erro",
"success": "Sucesso",

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +663,27 @@
"lastMonth": "Last Month",
"thisYear": "This Year",
"restOfThisYear": "The Rest of This Year"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Description",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
"member": "Member",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

View File

@ -248,6 +248,7 @@
"text2": "Это включает в себя все задачи, и отменить это будет нельзя!",
"success": "Проект успешно удалён.",
"tasksToDelete": "Это безвозвратно удалит примерно {count} задач.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "В этом проекте нет никаких задач, можно спокойно удалять."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Секрет",
"secretHint": "Если указан, все запросы к URL обработчика будут подписаны с помощью HMAC.",
"secretDocs": "Подробнее об использовании секретов в документации."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Фильтры",
"clear": "Сбросить фильтры",
"showResults": "Show results",
"attributes": {
"title": "Название",
"titlePlaceholder": "Введите название сохранённого фильтра…",
@ -415,6 +433,52 @@
"edit": {
"title": "Изменить этот сохранённый фильтр",
"success": "Фильтр сохранён."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "По",
"from": "С",
"fromto": "С {from} по {to}",
"date": "Date",
"ranges": {
"today": "Сегодня",
"thisWeek": "Эта неделя",
@ -598,6 +663,27 @@
"lastMonth": "Прошлый месяц",
"thisYear": "Этот год",
"restOfThisYear": "Остаток этого года"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Описание",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Администратор",
"member": "Участник"
"member": "Участник",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Создать проект",
"createProject": "Создать проект",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Ошибка",
"success": "Успех",

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +663,27 @@
"lastMonth": "Last Month",
"thisYear": "This Year",
"restOfThisYear": "The Rest of This Year"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Description",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
"member": "Member",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

View File

@ -57,11 +57,11 @@
"logout": "Odjava",
"emailInvalid": "Prosim vnesite veljaven e-poštni naslov.",
"usernameRequired": "Prosim vnesite uporabniško ime.",
"usernameMustNotContainSpace": "The username must not contain spaces.",
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"usernameMustNotContainSpace": "Uporabniško ime ne sme vsebovati presledkov.",
"usernameMustNotLookLikeUrl": "Uporabniško ime ne sme izgledati kot URL.",
"passwordRequired": "Prosim vnesite geslo.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMin": "Geslo mora imeti vsaj 8 znakov.",
"passwordNotMax": "Geslo mora imeti največ 250 znakov.",
"showPassword": "Prikažite geslo",
"hidePassword": "Skrijte geslo",
"noAccountYet": "Še nimate računa?",
@ -248,6 +248,7 @@
"text2": "To vključuje vse naloge in GA NI MOGOČE RAZVELJAVITI!",
"success": "Projekt je bil uspešno izbrisan.",
"tasksToDelete": "S tem bo nepreklicno odstranjeno približno {count} nalog.",
"tasksAndChildProjectsToDelete": "S tem bo nepreklicno odstranjeno cca. {tasks} nalog in {projects} projektov.",
"noTasksToDelete": "Ta projekt ne vsebuje nobenih nalog, zato ga lahko varno izbrišete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Skrita koda",
"secretHint": "Če je podana, bodo vse zahteve do ciljnega webhook URL-ja podpisane s HMAC.",
"secretDocs": "Poglejte si dokumentacijo za več podrobnosti o uporabi skrite kode."
},
"views": {
"header": "Uredi pogled",
"title": "Naslov",
"actions": "Dejanja",
"kind": "Vrsta",
"bucketConfigMode": "Način nastavitve vedra",
"bucketConfig": "Nastavitev vedra",
"bucketConfigManual": "Ročno",
"filter": "Filter",
"create": "Ustvari pogled",
"createSuccess": "Pogled je bil uspešno ustvarjen.",
"titleRequired": "Prosim navedite naslov.",
"delete": "Izbriši pogled",
"deleteText": "Ali ste prepričani, da želite odstraniti ta pogled? Ne bo ga več mogoče uporabljati za ogled nalog v tem projektu. To dejanje ne bo izbrisalo nobenih opravil. Tega ni mogoče razveljaviti!",
"deleteSuccess": "Pogled je bil uspešno izbrisan"
}
},
"filters": {
"title": "Filtri",
"clear": "Počisti filtre",
"showResults": "Prikaži rezultate",
"attributes": {
"title": "Naslov",
"titlePlaceholder": "Tu je naslov shranjenega filtra…",
@ -415,6 +433,52 @@
"edit": {
"title": "Uredi shranjeni filter",
"success": "Filter je bil uspešno shranjen."
},
"query": {
"title": "Poizvedba",
"placeholder": "Vnesite poizvedbo za iskanje ali filtriranje…",
"help": {
"intro": "Za filtriranje nalog lahko uporabite sintakso poizvedbe, podobno SQL. Razpoložljiva polja za filtriranje vključujejo:",
"link": "Kako to deluje?",
"canUseDatemath": "Za nastavitev relativnih datumov lahko določite datume. Če želite izvedeti več, kliknite vrednost datuma v poizvedbi.",
"fields": {
"done": "Ali je naloga opravljena ali ne",
"priority": "Stopnja prioritete naloge (1-5)",
"percentDone": "Odstotek dokončanja naloge (0100)",
"dueDate": "Datum zapadlosti naloge",
"startDate": "Začetni datum opravila",
"endDate": "Končni datum opravila",
"doneAt": "Datum in čas, ko je bila naloga opravljena",
"assignees": "Dodeljeni k nalogi",
"labels": "Oznake, povezane z opravilom",
"project": "Projekt, ki mu naloga pripada (na voljo samo za shranjene filtre, ne na ravni projekta)"
},
"operators": {
"intro": "Razpoložljivi operaterji za filtriranje vključujejo:",
"notEqual": "Ni enako",
"equal": "Je enako",
"greaterThan": "Večje kot",
"greaterThanOrEqual": "Večje ali enako kot",
"lessThan": "Manj kot",
"lessThanOrEqual": "Manjše ali enako kot",
"like": "Ujema se z vzorcem (z nadomestnimi znaki %)",
"in": "Ujema se s katero koli vrednostjo na seznamu vrednosti, ločenih z vejico"
},
"logicalOperators": {
"intro": "Če želite združiti več pogojev, lahko uporabite naslednje logične operatorje:",
"and": "IN operator, se ujema, če so izpolnjeni vsi pogoji",
"or": "ALI operator, se ujema, če je kateri od pogojev resničen",
"parentheses": "Oklepaji za združevanje pogojev"
},
"examples": {
"intro": "Tukaj je nekaj primerov filtrirnih poizvedb:",
"priorityEqual": "Ujema se z nalogami s prioritetno stopnjo 4",
"dueDatePast": "Ujema se z opravili z rokom v preteklosti",
"undoneHighPriority": "Ujema se z neopravljenimi opravili s prioritetno stopnjo 3 ali višjo",
"assigneesIn": "Ujema se z nalogami, dodeljenimi k \"user1\" ali \"user2\"",
"priorityOneOrTwoPastDue": "Ujema se z opravili s prioritetno stopnjo 1 ali 2 in rokom v preteklosti"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Za",
"from": "Od",
"fromto": "{from} do {to}",
"date": "Datum",
"ranges": {
"today": "Danes",
"thisWeek": "Ta teden",
@ -598,6 +663,27 @@
"lastMonth": "Prejšnji mesec",
"thisYear": "Letos",
"restOfThisYear": "Preostanek tega leta"
},
"values": {
"now": "Zdaj",
"startOfToday": "Začetek današnjega dne",
"endOfToday": "Konec današnjega dne",
"beginningOflastWeek": "Začetek prejšnjega tedna",
"endOfLastWeek": "Konec prejšnjega tedna",
"beginningOfThisWeek": "Začetek tega tedna",
"endOfThisWeek": "Konec tega tedna",
"startOfNextWeek": "Začetek naslednjega tedna",
"endOfNextWeek": "Konec naslednjega tedna",
"in7Days": "V 7 dneh",
"beginningOfLastMonth": "Začetek prejšnjega meseca",
"endOfLastMonth": "Konec prejšnjega meseca",
"startOfThisMonth": "Začetek tega meseca",
"endOfThisMonth": "Konec tega meseca",
"startOfNextMonth": "Začetek naslednjega meseca",
"endOfNextMonth": "Konec naslednjega meseca",
"in30Days": "V 30 dneh",
"startOfThisYear": "Začetek tega leta",
"endOfThisYear": "Konec tega leta"
}
},
"datemathHelp": {
@ -713,7 +799,7 @@
"startDate": "Začetni datum",
"title": "Naslov",
"updated": "Posodobljeno",
"doneAt": "Done At"
"doneAt": "Končano ob"
},
"subscription": {
"subscribedTaskThroughParentProject": "Ker ste na to nalogo naročeni prek njenega projekta, se tu ne morete odjaviti.",
@ -916,7 +1002,9 @@
"description": "Opis",
"descriptionPlaceholder": "Tukaj opiši ekipo, pritisni '/' za več možnosti…",
"admin": "Administrator",
"member": "Član"
"member": "Član",
"isPublic": "Javna ekipa",
"isPublicDescription": "Naj bo ekipa javno vidna. Ko je omogočeno, lahko vsakdo deli projekte s to ekipo, tudi če ni neposredni član."
}
},
"keyboardShortcuts": {
@ -975,8 +1063,9 @@
"share": "Skupna raba",
"newProject": "Nov projekt",
"createProject": "Ustvari projekt",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantArchiveIsDefault": "Tega ne morete arhivirati, ker je to vaš privzeti projekt.",
"cantDeleteIsDefault": "Tega ne morete izbrisati, ker je to vaš privzeti projekt.",
"views": "Pogledi"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} se je odzval z {value}",
"reactedWithAnd": "{users} in {lastUser} sta reagirala z {value}",
"reactedWithAndMany": "{users} in {num} drugih se je odzvalo z {value}",
"add": "Dodaj svoj odziv"
},
"error": {
"error": "Napaka",
"success": "Uspeh",
@ -1096,7 +1191,7 @@
},
"about": {
"title": "O programu",
"version": "Version: {version}"
"version": "Verzija: {version}"
},
"time": {
"units": {

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Equal to",
"greaterThan": "Greater than",
"greaterThanOrEqual": "Greater than or equal to",
"lessThan": "Less than",
"lessThanOrEqual": "Less than or equal to",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +663,27 @@
"lastMonth": "Last Month",
"thisYear": "This Year",
"restOfThisYear": "The Rest of This Year"
},
"values": {
"now": "Now",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Description",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Member"
"member": "Member",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "New project",
"createProject": "Create project",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

View File

@ -248,6 +248,7 @@
"text2": "This includes all tasks and CANNOT BE UNDONE!",
"success": "The project was successfully deleted.",
"tasksToDelete": "This will irrevocably remove approx. {count} tasks.",
"tasksAndChildProjectsToDelete": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "This project does not contain any tasks, it should be safe to delete."
},
"duplicate": {
@ -380,11 +381,28 @@
"secret": "Secret",
"secretHint": "If provided, all requests to the webhook target URL will be signed using HMAC.",
"secretDocs": "Check out the docs for more details about how to use secrets."
},
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
}
},
"filters": {
"title": "Filter",
"clear": "Rensa filter",
"showResults": "Visa resultat",
"attributes": {
"title": "Titel",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +433,52 @@
"edit": {
"title": "Edit This Saved Filter",
"success": "The filter was saved successfully."
},
"query": {
"title": "Query",
"placeholder": "Type a search or filter query…",
"help": {
"intro": "To filter tasks, you can use a query syntax similar to SQL. The available fields for filtering include:",
"link": "How does this work?",
"canUseDatemath": "You can date math to set relative dates. Click on the date value in a query to find out more.",
"fields": {
"done": "Whether the task is completed or not",
"priority": "The priority level of the task (1-5)",
"percentDone": "The percentage of completion for the task (0-100)",
"dueDate": "The due date of the task",
"startDate": "The start date of the task",
"endDate": "The end date of the task",
"doneAt": "The date and time when the task was completed",
"assignees": "The assignees of the task",
"labels": "The labels associated with the task",
"project": "The project the task belongs to (only available for saved filters, not on a project level)"
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"equal": "Lika med",
"greaterThan": "Större än",
"greaterThanOrEqual": "Större än eller lika med",
"lessThan": "Mindre än",
"lessThanOrEqual": "Mindre än eller lika med",
"like": "Matches a pattern (using wildcard %)",
"in": "Matches any value in a comma-seperated list of values"
},
"logicalOperators": {
"intro": "To combine multiple conditions, you can use the following logical operators:",
"and": "AND operator, matches if all conditions are true",
"or": "OR operator, matches if any of the conditions are true",
"parentheses": "Parentheses for grouping conditions"
},
"examples": {
"intro": "Here are some examples of filter queries:",
"priorityEqual": "Matches tasks with priority level 4",
"dueDatePast": "Matches tasks with a due date in the past",
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
}
}
}
},
"migrate": {
@ -584,6 +648,7 @@
"to": "Till",
"from": "Från",
"fromto": "{from} till {to}",
"date": "Datum",
"ranges": {
"today": "I dag",
"thisWeek": "Denna vecka",
@ -598,6 +663,27 @@
"lastMonth": "Förra månaden",
"thisYear": "This Year",
"restOfThisYear": "The Rest of This Year"
},
"values": {
"now": "Nu",
"startOfToday": "Start of today",
"endOfToday": "End of today",
"beginningOflastWeek": "Beginning of last week",
"endOfLastWeek": "End of last week",
"beginningOfThisWeek": "Beginning of this week",
"endOfThisWeek": "End of this week",
"startOfNextWeek": "Start of next week",
"endOfNextWeek": "End of next week",
"in7Days": "In 7 days",
"beginningOfLastMonth": "Beginning of last month",
"endOfLastMonth": "End of last month",
"startOfThisMonth": "Start of this month",
"endOfThisMonth": "End of this month",
"startOfNextMonth": "Start of next month",
"endOfNextMonth": "End of next month",
"in30Days": "In 30 days",
"startOfThisYear": "Beginning of this year",
"endOfThisYear": "End of this year"
}
},
"datemathHelp": {
@ -916,7 +1002,9 @@
"description": "Beskrivning",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Admin",
"member": "Medlem"
"member": "Medlem",
"isPublic": "Public Team",
"isPublicDescription": "Make the team publicly discoverable. When enabled, anyone can share projects with this team even when not being a direct member."
}
},
"keyboardShortcuts": {
@ -976,7 +1064,8 @@
"newProject": "Nytt projekt",
"createProject": "Skapa projekt",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantDeleteIsDefault": "You cannot delete this because it is your default project.",
"views": "Views"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1112,12 @@
"altFormatLong": "j M Y H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
},
"error": {
"error": "Error",
"success": "Success",

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