Compare commits

..

179 Commits

Author SHA1 Message Date
renovate acaf0e660b chore(deps): update golangci/golangci-lint docker tag to v1.57.0
continuous-integration/drone/pr Build is pending Details
2024-03-20 00:07:51 +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
199 changed files with 13216 additions and 3596 deletions

View File

@ -139,7 +139,7 @@ steps:
event: [ push, tag, pull_request ]
- name: api-lint
image: golangci/golangci-lint:v1.56.2
image: golangci/golangci-lint:v1.57.0
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'

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,7 +51,7 @@
}
},
"devDependencies": {
"electron": "29.1.1",
"electron": "29.1.4",
"electron-builder": "24.13.3"
},
"dependencies": {

View File

@ -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.1.4:
version "29.1.4"
resolved "https://registry.yarnpkg.com/electron/-/electron-29.1.4.tgz#6c47467ba50be5dd60b99b8737f69cd12fc0733f"
integrity sha512-IWXys0SqgmIfrqXusUGQC0gGG7CCqA5vfmNsUMj8dFkAnK3lisKyjSESStWlrsste/OX/AAC5wsVlf23reUNnw==
dependencies:
"@electron/get" "^2.0.0"
"@types/node" "^20.9.0"

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

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

@ -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()
@ -12,23 +13,28 @@ describe('Project History', () => {
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
const projects = ProjectFactory.create(6)
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('/')

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,65 @@ 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 +76,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 +95,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 +109,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 +130,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 +141,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 +164,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 +179,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 +188,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 +226,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 +260,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 +279,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 +292,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,7 +19,7 @@ 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')
.contains('Columns')
@ -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,
@ -469,7 +472,7 @@ describe('Task', () => {
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)
@ -836,7 +839,7 @@ describe('Task', () => {
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

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.15.4",
"packageManager": "pnpm@8.15.5",
"keywords": [
"todo",
"productivity",
@ -57,9 +57,9 @@
"@github/hotkey": "3.1.0",
"@infectoone/vue-ganttastic": "2.2.0",
"@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.107.0",
"@sentry/vue": "7.107.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.10",
"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.2",
"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,8 +132,8 @@
"@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",
"@histoire/plugin-screenshot": "0.17.14",
"@histoire/plugin-vue": "0.17.14",
"@rushstack/eslint-patch": "1.7.2",
"@tsconfig/node18": "18.2.2",
"@types/codemirror": "5.60.15",
@ -141,50 +142,51 @@
"@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.11.29",
"@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.3.1",
"@typescript-eslint/parser": "7.3.1",
"@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",
"browserslist": "4.23.0",
"caniuse-lite": "1.0.30001596",
"caniuse-lite": "1.0.30001599",
"css-has-pseudo": "6.0.2",
"csstype": "3.1.3",
"cypress": "13.6.6",
"esbuild": "0.20.1",
"cypress": "13.7.0",
"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.23.0",
"happy-dom": "14.0.0",
"histoire": "0.17.14",
"postcss": "8.4.36",
"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.2",
"rollup": "4.13.0",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.71.1",
"sass": "1.72.0",
"start-server-and-test": "2.0.3",
"typescript": "5.4.2",
"vite": "5.1.5",
"vite": "5.1.6",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.19.2",
"vite-plugin-pwa": "0.19.4",
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.3.1",
"vitest": "1.4.0",
"vue-tsc": "2.0.6",
"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

@ -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,193 @@
<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,
} = defineProps<{
entityKind: ReactionKind,
entityId: number,
}>()
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)}"
@click="toggleReaction(value)"
>
{{ value }} {{ users.length }}
</BaseButton>
<BaseButton
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

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

@ -6,44 +6,17 @@
<h1 class="project-title-print">
{{ getProjectTitle(currentProject) }}
</h1>
<div class="switch-view-container d-print-none">
<div 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 +36,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 +52,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 +82,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 +106,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 +125,46 @@ 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;
}
@media screen and (max-width: $tablet) {
display: flex;
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);
height: $switch-view-height;
margin: 0 auto 1rem;
padding: .5rem;
}
.switch-view-button {
@ -201,7 +192,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 +200,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,
() => {
@ -104,15 +112,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 +194,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 +204,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 +236,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 +267,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 +280,7 @@ function autocompleteSelect(value) {
@input="handleFieldInput"
@focus="onFocusField"
@keydown="onKeydown"
@blur="e => emit('blur', e)"
@blur="blurDebounced"
/>
<div
class="filter-input-highlight"
@ -333,7 +367,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);

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 } }"

View File

@ -1,8 +1,24 @@
<!-- Vikunja is a to-do list application to facilitate your life. -->
<!-- Copyright 2018-present Vikunja and contributors. All rights reserved. -->
<!-- -->
<!-- This program is free software: you can redistribute it and/or modify -->
<!-- it under the terms of the GNU Affero General Public Licensee as published by -->
<!-- the Free Software Foundation, either version 3 of the License, or -->
<!-- (at your option) any later version. -->
<!-- -->
<!-- This program is distributed in the hope that it will be useful, -->
<!-- but WITHOUT ANY WARRANTY; without even the implied warranty of -->
<!-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -->
<!-- GNU Affero General Public Licensee for more details. -->
<!-- -->
<!-- You should have received a copy of the GNU Affero General Public Licensee -->
<!-- along with this program. If not, see <https://www.gnu.org/licenses/>. -->
<template>
<ProjectWrapper
class="project-gantt"
:project-id="filters.projectId"
view-name="gantt"
:view
>
<template #header>
<card :has-content="false">
@ -87,15 +103,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 +131,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>
@ -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,28 @@ 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'
const {
projectId = undefined,
projectId,
viewId,
} = defineProps<{
projectId: number,
viewId: IProjectView['id'],
}>()
const DRAG_OPTIONS = {
@ -321,8 +330,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 +344,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 +369,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 +388,25 @@ 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 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 +419,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 +431,7 @@ function handleTaskContainerScroll(id: IBucket['id'], projectId: IProject['id'],
kanbanStore.loadNextTasksForBucket(
projectId,
viewId,
params.value,
id,
)
@ -469,7 +481,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 +491,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 +505,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 +576,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 +596,7 @@ async function deleteBucket() {
bucket: new BucketModel({
id: bucketToDelete.value,
projectId: project.value.id,
projectViewId: viewId,
}),
params: params.value,
})
@ -590,10 +615,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 +637,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 +647,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 +662,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)
}
@ -740,6 +776,7 @@ $filter-container-height: '1rem - #{$switch-view-height}';
* {
opacity: 0;
}
&::after {
content: '';
position: absolute;
@ -781,6 +818,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,7 +2,7 @@
<ProjectWrapper
class="project-table"
:project-id="projectId"
view-name="table"
:view-id
>
<template #header>
<div class="filter-container">
@ -284,17 +284,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 +323,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 +335,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 +383,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;
}

View File

@ -0,0 +1,180 @@
<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} from 'vue'
const model = defineModel<IProjectView>()
const titleValid = ref(true)
function validateTitle() {
titleValid.value = model.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="model.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="model.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="model.filter"
:input-label="$t('project.views.filter')"
/>
<div
v-if="model.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="model.bucketConfigurationMode"
>
<option value="manual">
{{ $t('project.views.bucketConfigManual') }}
</option>
<option value="filter">
{{ $t('project.views.filter') }}
</option>
</select>
</div>
</div>
</div>
<div
v-if="model.viewKind === 'kanban' && model.bucketConfigurationMode === 'filter'"
class="field"
>
<label class="label">
{{ $t('project.views.bucketConfig') }}
</label>
<div class="control">
<div
v-for="(b, index) in model.bucketConfiguration"
:key="'bucket_'+index"
class="filter-bucket"
>
<button
class="is-danger"
@click.prevent="() => model.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="model.bucketConfiguration[index].title"
class="input"
:placeholder="$t('project.share.links.namePlaceholder')"
>
</div>
</div>
<FilterInput
v-model="model.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="() => model.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

@ -172,6 +172,7 @@ 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'
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
@ -210,8 +211,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 +361,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

@ -97,6 +97,12 @@
editComment()
}"
/>
<Reactions
v-model="c.reactions"
class="mt-2"
entity-kind="comments"
:entity-id="c.id"
/>
</div>
</div>
<div
@ -190,6 +196,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

@ -60,7 +60,7 @@
v-if="showFormSwitch !== null"
class="reminder__close-button"
:shadow="false"
@click="updateDataAndMaybeClose(close)"
@click="updateDataAndMaybeCloseNow(close)"
>
{{ $t('misc.confirm') }}
</x-button>
@ -73,10 +73,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 +87,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 +113,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 +143,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 +182,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,40 @@ 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')
})
})
@ -104,24 +137,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 +181,22 @@ 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')
})
})
})

View File

@ -33,6 +33,7 @@ export const AVAILABLE_FILTER_FIELDS = [
...DATE_FIELDS,
...ASSIGNEE_FIELDS,
...LABEL_FIELDS,
...PROJECT_FIELDS,
]
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,40 +66,61 @@ 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)
}
}
})
@ -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)
})
// 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": {
@ -385,6 +386,7 @@
"filters": {
"title": "الفلاتر",
"clear": "مسح الفلاتر",
"showResults": "Show results",
"attributes": {
"title": "العنوان",
"titlePlaceholder": "عنوان الفلتر المحفوظ هنا…",
@ -415,6 +417,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 +632,7 @@
"to": "إلى",
"from": "من",
"fromto": "{from} إلى {to}",
"date": "Date",
"ranges": {
"today": "اليوم",
"thisWeek": "هذا الأسبوع",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"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 +417,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 +632,7 @@
"to": "Do",
"from": "Od",
"fromto": "{from} {to}",
"date": "Datum",
"ranges": {
"today": "Dnes",
"thisWeek": "Tento týden",
@ -598,6 +647,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 +783,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 +986,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 +1047,8 @@
"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."
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1095,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 +1174,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtre",
"clear": "Ryd Filtre",
"showResults": "Show results",
"attributes": {
"title": "Titel",
"titlePlaceholder": "Det gemte filters titel skrives her…",
@ -415,6 +417,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 +632,7 @@
"to": "Til",
"from": "Fra",
"fromto": "{from} til {to}",
"date": "Date",
"ranges": {
"today": "I dag",
"thisWeek": "Denne uge",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filter",
"clear": "Filter zurücksetzen",
"showResults": "Ergebnisse anzeigen",
"attributes": {
"title": "Titel",
"titlePlaceholder": "Einen gespeicherten Filternamen eingeben …",
@ -415,6 +417,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 +632,7 @@
"to": "Bis",
"from": "Von",
"fromto": "{from} bis {to}",
"date": "Datum",
"ranges": {
"today": "Heute",
"thisWeek": "Diese Woche",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"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 +417,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 +632,7 @@
"to": "Bis",
"from": "Von",
"fromto": "{from} bis {to}",
"date": "Datum",
"ranges": {
"today": "Heute",
"thisWeek": "Diese Woche",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtros",
"clear": "Limpiar Filtros",
"showResults": "Show results",
"attributes": {
"title": "Título",
"titlePlaceholder": "El título del filtro guardado va aquí…",
@ -415,6 +417,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 +632,7 @@
"to": "Para",
"from": "De",
"fromto": "De {from} para {to}",
"date": "Date",
"ranges": {
"today": "Hoy",
"thisWeek": "Esta Semana",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtres",
"clear": "Effacer les filtres",
"showResults": "Show results",
"attributes": {
"title": "Nom",
"titlePlaceholder": "Entre un nom de filtre enregistré…",
@ -415,6 +417,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 +632,7 @@
"to": "À",
"from": "De",
"fromto": "Du {from} au {to}",
"date": "Date",
"ranges": {
"today": "Aujourdhui",
"thisWeek": "Cette semaine",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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

@ -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": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "Ez a projekt nem tartalmaz feladatokat, biztonságosan törölhető."
},
"duplicate": {
@ -385,6 +386,7 @@
"filters": {
"title": "Szűrők",
"clear": "Szűrők törlése",
"showResults": "Show results",
"attributes": {
"title": "Cím",
"titlePlaceholder": "A mentett szűrő címe ide kerül…",
@ -415,6 +417,52 @@
"edit": {
"title": "Mentett szűrő szerkesztése",
"success": "A szűrőt sikeresen mentette."
},
"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 +632,7 @@
"to": "Eddig",
"from": "Ettől",
"fromto": "{from} - tól {to} - ig",
"date": "Date",
"ranges": {
"today": "Ma",
"thisWeek": "Ezen a héten",
@ -598,6 +647,27 @@
"lastMonth": "Előző hónap",
"thisYear": "Aktuális év",
"restOfThisYear": "Az év hátralévő része"
},
"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 +986,9 @@
"description": "Leírás",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Adminisztrátor",
"member": "Tag"
"member": "Tag",
"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": {
@ -1023,6 +1095,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": "Hiba",
"success": "Siker",

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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtri",
"clear": "Pulisci Filtri",
"showResults": "Show results",
"attributes": {
"title": "Titolo",
"titlePlaceholder": "Il titolo del filtro salvato va qui…",
@ -415,6 +417,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 +632,7 @@
"to": "A",
"from": "Da",
"fromto": "da {from} a {to}",
"date": "Date",
"ranges": {
"today": "Oggi",
"thisWeek": "Questa Settimana",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "絞り込み",
"clear": "絞り込みの解除",
"showResults": "Show results",
"attributes": {
"title": "絞り込み条件名",
"titlePlaceholder": "絞り込み条件名を入力…",
@ -415,6 +417,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 +632,7 @@
"to": "終了",
"from": "開始",
"fromto": "{from} 〜 {to} まで",
"date": "Date",
"ranges": {
"today": "今日",
"thisWeek": "今週",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "필터",
"clear": "필터 초기화",
"showResults": "Show results",
"attributes": {
"title": "제목",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} 에서 {to}",
"date": "Date",
"ranges": {
"today": "오늘",
"thisWeek": "이번 주",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Titel",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtre",
"clear": "Fjern filtre",
"showResults": "Show results",
"attributes": {
"title": "Tittel",
"titlePlaceholder": "Den lagrede filtertittelen kommer hit…",
@ -415,6 +417,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 +632,7 @@
"to": "Til",
"from": "Fra",
"fromto": "{from} til {to}",
"date": "Date",
"ranges": {
"today": "Idag",
"thisWeek": "Denne uken",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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

@ -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": "This will irrevocably remove approx. {tasks} tasks and {projects} projects.",
"noTasksToDelete": "Ten projekt nie zawiera żadnych zadań, więc można go bezpiecznie usunąć."
},
"duplicate": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtry",
"clear": "Wyczyść filtry",
"showResults": "Show results",
"attributes": {
"title": "Tytuł",
"titlePlaceholder": "Tu wpisz tytuł filtra stałego…",
@ -415,6 +417,52 @@
"edit": {
"title": "Edytuj ten filtr stały",
"success": "Filtr został pomyślnie zapisany."
},
"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 +632,7 @@
"to": "Do",
"from": "Od",
"fromto": "{from} do {to}",
"date": "Date",
"ranges": {
"today": "Dziś",
"thisWeek": "W tym tygodniu",
@ -598,6 +647,27 @@
"lastMonth": "Zeszły miesiąc",
"thisYear": "Ten rok",
"restOfThisYear": "Reszta tego roku"
},
"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 +986,9 @@
"description": "Opis",
"descriptionPlaceholder": "Opisz tutaj zespół, naciśnij '/' aby uzyskać więcej opcji…",
"admin": "Administrator",
"member": "Członek"
"member": "Członek",
"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": {
@ -1023,6 +1095,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": "Błąd",
"success": "Sukces",

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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtros",
"clear": "Limpar Filtros",
"showResults": "Show results",
"attributes": {
"title": "Título",
"titlePlaceholder": "O título do filtro salvo fica aqui…",
@ -415,6 +417,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 +632,7 @@
"to": "Para",
"from": "De",
"fromto": "{from} até {to}",
"date": "Date",
"ranges": {
"today": "Hoje",
"thisWeek": "Esta semana",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtros",
"clear": "Limpar Filtros",
"showResults": "Show results",
"attributes": {
"title": "Título",
"titlePlaceholder": "O título do filtro memorizado será aqui…",
@ -415,6 +417,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 +632,7 @@
"to": "Até",
"from": "De",
"fromto": "{from} até {to}",
"date": "Date",
"ranges": {
"today": "Hoje",
"thisWeek": "Esta semana",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Фильтры",
"clear": "Сбросить фильтры",
"showResults": "Show results",
"attributes": {
"title": "Название",
"titlePlaceholder": "Введите название сохранённого фильтра…",
@ -415,6 +417,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 +632,7 @@
"to": "По",
"from": "С",
"fromto": "С {from} по {to}",
"date": "Date",
"ranges": {
"today": "Сегодня",
"thisWeek": "Эта неделя",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filtri",
"clear": "Počisti filtre",
"showResults": "Prikaži rezultate",
"attributes": {
"title": "Naslov",
"titlePlaceholder": "Tu je naslov shranjenega filtra…",
@ -415,6 +417,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 +632,7 @@
"to": "Za",
"from": "Od",
"fromto": "{from} do {to}",
"date": "Datum",
"ranges": {
"today": "Danes",
"thisWeek": "Ta teden",
@ -598,6 +647,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 +783,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 +986,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 +1047,8 @@
"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."
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1095,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 +1174,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filter",
"clear": "Rensa filter",
"showResults": "Visa resultat",
"attributes": {
"title": "Titel",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "Till",
"from": "Från",
"fromto": "{from} till {to}",
"date": "Datum",
"ranges": {
"today": "I dag",
"thisWeek": "Denna vecka",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Bộ lọc",
"clear": "Xoá các bộ lọc",
"showResults": "Show results",
"attributes": {
"title": "Tiêu đề",
"titlePlaceholder": "Tiêu đề bộ lọc đã lưu ở đây…",
@ -415,6 +417,52 @@
"edit": {
"title": "Sửa bộ lọc sẵn này",
"success": "Bộ lọc đã được lưu thành công."
},
"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 +632,7 @@
"to": "Đến",
"from": "Từ",
"fromto": "{from} đến {to}",
"date": "Date",
"ranges": {
"today": "Hôm nay",
"thisWeek": "Tuần này",
@ -598,6 +647,27 @@
"lastMonth": "Tháng trước",
"thisYear": "Năm nay",
"restOfThisYear": "Toàn bộ ngày còn lại trong năm"
},
"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 +986,9 @@
"description": "Mô tả",
"descriptionPlaceholder": "Describe the team here, hit '/' for more options…",
"admin": "Quản trị viên",
"member": "Thành viên"
"member": "Thành viên",
"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": {
@ -1023,6 +1095,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": "Lỗi",
"success": "Thành công",

View File

@ -57,11 +57,11 @@
"logout": "注销",
"emailInvalid": "请输入有效的电子邮件地址。",
"usernameRequired": "请输入用户名",
"usernameMustNotContainSpace": "The username must not contain spaces.",
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"usernameMustNotContainSpace": "用户名不能包含空格。",
"usernameMustNotLookLikeUrl": "用户名不能像一个 URL。",
"passwordRequired": "请提供密码",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMin": "密码至少有8个字符",
"passwordNotMax": "密码不能超过250个字符",
"showPassword": "显示密码",
"hidePassword": "隐藏密码",
"noAccountYet": "还没有账号?",
@ -248,6 +248,7 @@
"text2": "这包括所有的任务,并且无法撤销!",
"success": "项目已成功删除。",
"tasksToDelete": "此操作无法撤消!将移除大约 {count} 个任务。",
"tasksAndChildProjectsToDelete": "删除将不可挽回。 {tasks} 任务和 {projects} 项目。",
"noTasksToDelete": "此列表不包含任何任务,可以安全删除。"
},
"duplicate": {
@ -385,6 +386,7 @@
"filters": {
"title": "筛选器",
"clear": "清除筛选条件",
"showResults": "显示结果",
"attributes": {
"title": "标题",
"titlePlaceholder": "填写筛选器标题",
@ -415,6 +417,52 @@
"edit": {
"title": "编辑此保存的过滤器",
"success": "过滤器保存成功。"
},
"query": {
"title": "查询",
"placeholder": "输入搜索或过滤查询…",
"help": {
"intro": "要过滤任务您可以使用类似于SQL的查询语法。可用的过滤字段包括",
"link": "这是如何运作的?",
"canUseDatemath": "您可以将数学日期设定为相对日期。点击查询中的日期值来查找更多信息。",
"fields": {
"done": "任务是否完成",
"priority": "任务的优先级(1-5)",
"percentDone": "任务完成百分比 (0-100)",
"dueDate": "任务的截止日期",
"startDate": "任务的开始日期",
"endDate": "任务结束日期",
"doneAt": "任务完成的日期和时间",
"assignees": "任务的指派人",
"labels": "与任务相关的标签",
"project": "任务属于的项目 (仅适用于保存的过滤器,不适用于项目级别)"
},
"operators": {
"intro": "可用的过滤操作员包括:",
"notEqual": "不等于",
"equal": "等于",
"greaterThan": "大于",
"greaterThanOrEqual": "大于或等于",
"lessThan": "小于",
"lessThanOrEqual": "小于或等于",
"like": "匹配模式 (使用通配符%)",
"in": "匹配任意一个逗号分隔的值列表"
},
"logicalOperators": {
"intro": "若要合并多个条件,您可以使用以下逻辑运算符:",
"and": "和操作者,如果所有条件都是真实的,匹配的",
"or": "或操作员,匹配任何条件都是正确的",
"parentheses": "分组条件的括号:"
},
"examples": {
"intro": "以下是一些过滤查询示例:",
"priorityEqual": "匹配任务优先级 4",
"dueDatePast": "匹配任务与过去到期日期",
"undoneHighPriority": "匹配取消优先级别3或更高的任务",
"assigneesIn": "匹配分配给\"user1\" 或 \"user2\" 的任务",
"priorityOneOrTwoPastDue": "匹配优先级别1或2以及过去到期的任务"
}
}
}
},
"migrate": {
@ -584,6 +632,7 @@
"to": "到",
"from": "开始",
"fromto": "{from} 到 {to}",
"date": "日期",
"ranges": {
"today": "今天",
"thisWeek": "本周",
@ -598,6 +647,27 @@
"lastMonth": "上个月",
"thisYear": "今年",
"restOfThisYear": "本年度剩余时间"
},
"values": {
"now": "现在",
"startOfToday": "今天开始",
"endOfToday": "今天结束",
"beginningOflastWeek": "上周开始",
"endOfLastWeek": "上周结束",
"beginningOfThisWeek": "本周开始",
"endOfThisWeek": "本周结束",
"startOfNextWeek": "下周开始",
"endOfNextWeek": "下周结束",
"in7Days": "7 天内",
"beginningOfLastMonth": "上月开始",
"endOfLastMonth": "上个月底",
"startOfThisMonth": "本月开始",
"endOfThisMonth": "这个月底",
"startOfNextMonth": "下月开始",
"endOfNextMonth": "下个月底",
"in30Days": "30 天内",
"startOfThisYear": "今年开始",
"endOfThisYear": "今年年底"
}
},
"datemathHelp": {
@ -713,7 +783,7 @@
"startDate": "开始日期",
"title": "标题",
"updated": "已更新",
"doneAt": "Done At"
"doneAt": "完成于"
},
"subscription": {
"subscribedTaskThroughParentProject": "你无法在此处取消订阅,因为你已通过其项目订阅了此任务。",
@ -916,7 +986,9 @@
"description": "描述信息",
"descriptionPlaceholder": "在此描述团队,点击'/'获取更多选项…",
"admin": "管理员",
"member": "成员"
"member": "成员",
"isPublic": "公开团队",
"isPublicDescription": "让团队公开发现。如果启用,任何人都可以与这个团队分享项目,即使不是直接成员。"
}
},
"keyboardShortcuts": {
@ -975,8 +1047,8 @@
"share": "共享",
"newProject": "新项目",
"createProject": "创建项目",
"cantArchiveIsDefault": "You cannot archive this because it is your default project.",
"cantDeleteIsDefault": "You cannot delete this because it is your default project."
"cantArchiveIsDefault": "您不能归档,因为这是您的默认项目。",
"cantDeleteIsDefault": "您不能删除这个项目,因为这是您的默认项目。"
},
"apiConfig": {
"url": "Vikunja URL",
@ -1023,6 +1095,12 @@
"altFormatLong": "Y M d H:i",
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} {value} 反应",
"reactedWithAnd": "{users} 和{lastUser} 的{value} 反应",
"reactedWithAndMany": "{users} 和{num} 的{value} 反应",
"add": "添加您的反应"
},
"error": {
"error": "错误",
"success": "成功",
@ -1096,7 +1174,7 @@
},
"about": {
"title": "关于",
"version": "Version: {version}"
"version": "版本:{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": {
@ -385,6 +386,7 @@
"filters": {
"title": "Filters",
"clear": "Clear Filters",
"showResults": "Show results",
"attributes": {
"title": "Title",
"titlePlaceholder": "The saved filter title goes here…",
@ -415,6 +417,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 +632,7 @@
"to": "To",
"from": "From",
"fromto": "{from} to {to}",
"date": "Date",
"ranges": {
"today": "Today",
"thisWeek": "This Week",
@ -598,6 +647,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 +986,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": {
@ -1023,6 +1095,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

@ -1,6 +1,7 @@
import type {IAbstract} from './IAbstract'
import type {IUser} from './IUser'
import type {ITask} from './ITask'
import type {IProjectView} from '@/modelTypes/IProjectView'
export interface IBucket extends IAbstract {
id: number
@ -10,6 +11,7 @@ export interface IBucket extends IAbstract {
tasks: ITask[]
position: number
count: number
projectViewId: IProjectView['id']
createdBy: IUser
created: Date

View File

@ -2,6 +2,7 @@ import type {IAbstract} from './IAbstract'
import type {ITask} from './ITask'
import type {IUser} from './IUser'
import type {ISubscription} from './ISubscription'
import type {IProjectView} from '@/modelTypes/IProjectView'
export interface IProject extends IAbstract {
@ -21,6 +22,7 @@ export interface IProject extends IAbstract {
parentProjectId: number
doneBucketId: number
defaultBucketId: number
views: IProjectView[]
created: Date
updated: Date

View File

@ -0,0 +1,31 @@
import type {IAbstract} from './IAbstract'
import type {IProject} from '@/modelTypes/IProject'
export const PROJECT_VIEW_KINDS = ['list', 'gantt', 'table', 'kanban']
export type ProjectViewKind = typeof PROJECT_VIEW_KINDS[number]
export const PROJECT_VIEW_BUCKET_CONFIGURATION_MODES = ['none', 'manual', 'filter']
export type ProjectViewBucketConfigurationMode = typeof PROJECT_VIEW_BUCKET_CONFIGURATION_MODES[number]
export interface IProjectViewBucketConfiguration {
title: string
filter: string
}
export interface IProjectView extends IAbstract {
id: number
title: string
projectId: IProject['id']
viewKind: ProjectViewKind
filter: string
position: number
bucketConfigurationMode: ProjectViewBucketConfigurationMode
bucketConfiguration: IProjectViewBucketConfiguration[]
defaultBucketId: number
doneBucketId: number
created: Date
updated: Date
}

View File

@ -0,0 +1,14 @@
import type {IAbstract} from '@/modelTypes/IAbstract'
import type {IUser} from '@/modelTypes/IUser'
export type ReactionKind = 'tasks' | 'comments'
export interface IReaction extends IAbstract {
id: number
kind: ReactionKind
value: string
}
export interface IReactionPerEntity {
[reaction: string]: IUser[]
}

View File

@ -14,6 +14,7 @@ import type {IRepeatMode} from '@/types/IRepeatMode'
import type {PartialWithId} from '@/types/PartialWithId'
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
import type {IReactionPerEntity} from '@/modelTypes/IReaction'
export interface ITask extends IAbstract {
id: number
@ -45,6 +46,8 @@ export interface ITask extends IAbstract {
position: number
kanbanPosition: number
reactions: IReactionPerEntity
createdBy: IUser
created: Date

View File

@ -1,12 +1,15 @@
import type {IAbstract} from './IAbstract'
import type {IUser} from './IUser'
import type {ITask} from './ITask'
import type {IReactionPerEntity} from '@/modelTypes/IReaction'
export interface ITaskComment extends IAbstract {
id: number
taskId: ITask['id']
comment: string
author: IUser
reactions: IReactionPerEntity
created: Date
updated: Date

View File

@ -0,0 +1,8 @@
import type {IProjectView} from '@/modelTypes/IProjectView'
import type {IAbstract} from '@/modelTypes/IAbstract'
export interface ITaskPosition extends IAbstract {
position: number
projectViewId: IProjectView['id']
taskId: number
}

View File

@ -10,6 +10,7 @@ export interface ITeam extends IAbstract {
members: ITeamMember[]
right: Right
oidcId: string
isPublic: boolean
createdBy: IUser
created: Date

View File

@ -7,6 +7,7 @@ import type {IProject} from '@/modelTypes/IProject'
import type {IUser} from '@/modelTypes/IUser'
import type {ITask} from '@/modelTypes/ITask'
import type {ISubscription} from '@/modelTypes/ISubscription'
import ProjectViewModel from '@/models/projectView'
export default class ProjectModel extends AbstractModel<IProject> implements IProject {
id = 0
@ -25,6 +26,7 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
parentProjectId = 0
doneBucketId = 0
defaultBucketId = 0
views = []
created: Date = null
updated: Date = null
@ -48,6 +50,8 @@ export default class ProjectModel extends AbstractModel<IProject> implements IPr
this.subscription = new SubscriptionModel(this.subscription)
}
this.views = this.views.map(v => new ProjectViewModel(v))
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}

View File

@ -0,0 +1,29 @@
import type {IProjectView, ProjectViewBucketConfigurationMode, ProjectViewKind} from '@/modelTypes/IProjectView'
import AbstractModel from '@/models/abstractModel'
export default class ProjectViewModel extends AbstractModel<IProjectView> implements IProjectView {
id = 0
title = ''
projectId = 0
viewKind: ProjectViewKind = 'list'
filter = ''
position = 0
bucketConfiguration = []
bucketConfigurationMode: ProjectViewBucketConfigurationMode = 'manual'
defaultBucketId = 0
doneBucketId = 0
created: Date = new Date()
updated: Date = new Date()
constructor(data: Partial<IProjectView>) {
super()
this.assignData(data)
if (!this.bucketConfiguration) {
this.bucketConfiguration = []
}
}
}

View File

@ -0,0 +1,14 @@
import type {IReaction} from '@/modelTypes/IReaction'
import AbstractModel from '@/models/abstractModel'
export default class ReactionModel extends AbstractModel<IReaction> implements IReaction {
id: number = 0
kind: 'tasks' | 'comments' = 'tasks'
value: string = ''
constructor(data: Partial<IReaction>) {
super()
this.assignData(data)
}
}

View File

@ -86,6 +86,8 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
position = 0
kanbanPosition = 0
reactions = {}
createdBy: IUser = UserModel
created: Date = null
@ -148,6 +150,12 @@ export default class TaskModel extends AbstractModel<ITask> implements ITask {
this.updated = new Date(this.updated)
this.projectId = Number(this.projectId)
// We can't convert emojis to camel case, hence we do this manually
this.reactions = {}
Object.keys(data.reactions || {}).forEach(reaction => {
this.reactions[reaction] = data.reactions[reaction].map(u => new UserModel(u))
})
}
getTextIdentifier() {

View File

@ -10,6 +10,8 @@ export default class TaskCommentModel extends AbstractModel<ITaskComment> implem
taskId: ITask['id'] = 0
comment = ''
author: IUser = UserModel
reactions = {}
created: Date = null
updated: Date = null
@ -21,5 +23,11 @@ export default class TaskCommentModel extends AbstractModel<ITaskComment> implem
this.author = new UserModel(this.author)
this.created = new Date(this.created)
this.updated = new Date(this.updated)
// We can't convert emojis to camel case, hence we do this manually
this.reactions = {}
Object.keys(data.reactions || {}).forEach(reaction => {
this.reactions[reaction] = data.reactions[reaction].map(u => new UserModel(u))
})
}
}

View File

@ -0,0 +1,13 @@
import AbstractModel from '@/models/abstractModel'
import type {ITaskPosition} from '@/modelTypes/ITaskPosition'
export default class TaskPositionModel extends AbstractModel<ITaskPosition> implements ITaskPosition {
position = 0
projectViewId = 0
taskId = 0
constructor(data: Partial<ITaskPosition>) {
super()
this.assignData(data)
}
}

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