Compare commits

...

264 Commits

Author SHA1 Message Date
kolaente c934124b88
fix: release bucket name
(cherry picked from commit af11a6527f)
2024-12-22 21:24:18 +01:00
kolaente b7bba814e3
chore: release preperations 2024-12-22 20:02:08 +01:00
kolaente 8c285968af
chore: do not generate config yaml 2024-12-22 20:00:44 +01:00
kolaente b91a5d9adf
chore: sign drone config 2024-12-22 19:54:02 +01:00
kolaente 17b281072f
chore: release preparation 2024-12-22 19:52:12 +01:00
kolaente d47555e3c2
fix(export): update only current user export file id 2024-12-22 19:49:39 +01:00
kolaente 3f98f47256
feat: use hetzner object storage for releases
(cherry picked from commit 0472acac98)
2024-11-21 16:58:44 +01:00
kolaente 00a4d63344
chore: 0.24.5 release preperation 2024-11-21 16:20:40 +01:00
kolaente 82f37df16d
fix(ci): do not build linux 368 docker images
Related to 6fe22aba39

(cherry picked from commit da0e110ee4)
2024-11-21 16:20:18 +01:00
kolaente dfd3e276ab
chore: go mod tidy 2024-11-21 16:05:36 +01:00
kolaente 3a0a3b49af
chore: sign drone config 2024-11-21 16:05:36 +01:00
kolaente 6d7f81fa7e
fix!: disable 368 releases
We can either support arm64 or 368 release binaries - because the arm64 binaries are probably wider used today, we'll prioritize that. It is still possible to compile Vikunja from source on a 368 system, but would take quite a bit of effort in providing pre-built binaries.
Please reach out if you need assistance for 368 binaries.

See https://github.com/techknowlogick/xgo/issues/256#issuecomment-2462135108

(cherry picked from commit 6fe22aba39)
2024-11-21 16:05:36 +01:00
kolaente 154e3e4587
fix: upgrade xgo docker image everywhere
(cherry picked from commit d6194b8f10)
2024-11-21 16:05:36 +01:00
kolaente 372363d254
fix: upgrade xgo
(cherry picked from commit f826fb9a91)
2024-11-21 16:05:36 +01:00
kolaente 61e6990ab3
fix: pin xgo to 1.22.x
Hopefully resolves build issues like https://drone.kolaente.de/vikunja/vikunja/8996/3/10

(cherry picked from commit de1eac5d36)
2024-11-21 16:05:36 +01:00
kolaente 714298a94e
fix(attachments): check permissions when accessing all attachments
(cherry picked from commit 3659b7b58d)
2024-11-21 15:44:30 +01:00
kolaente a0d05211ff
fix(saved filters): check permissions when accessing tasks of a filter
(cherry picked from commit bbbd936868)
2024-11-21 15:44:30 +01:00
kolaente 4d454de228
chore: 0.24.4 release preperation 2024-09-29 15:45:46 +02:00
kolaente fcb70b884c
fix(test): use correct selector for modal header
(cherry picked from commit 3ce477e1d1)
2024-09-29 15:38:45 +02:00
kolaente 33c9ea802a
fix(task): improve task delete modal on mobile
(cherry picked from commit 9f9b00144b)
2024-09-29 15:38:45 +02:00
kolaente ab29ac7a5c
fix(task): do not show close button when the task was not opened via modal
(cherry picked from commit 308e634c71)
2024-09-29 15:38:44 +02:00
kolaente 7a1519da43
fix(task): paginate task comments
Resolves https://github.com/go-vikunja/vikunja/issues/329

(cherry picked from commit 54994a1671)
2024-09-29 15:38:43 +02:00
kolaente 7a839925ea
fix(filters): explicitly search in json when using postgres
Resolves https://github.com/go-vikunja/vikunja/issues/330
Resolves https://community.vikunja.io/t/add-task-to-filter-view-cron-error-fetching-filters-pq-operator-does-not-exist-json-unknown/2831

(cherry picked from commit ca967782e8)
2024-09-29 15:38:10 +02:00
jd 60fa211334
fix: partial fix to allow list tasks in ios reminders app (#2717)
This PR introduces a partial fix for the CalDAV task listing bug (#753) when handling PROPFIND requests with `Depth: 1`, improving task visibility in the iOS Reminders app.

Notes:
* This might make Thunderbird somewhat usable when interacting with tasks using the `/dav/projects/{id} url`.
* This does not fully resolve the issue where the Reminders app will only display the last project after some time when adding the URL.

This is my first time working with Golang and CalDAV, so I’d really appreciate any feedback or suggestions on the code structure, style, or any improvements I could make.

Co-authored-by: JD <43763092+jdw1023@users.noreply.github.com>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2717
Reviewed-by: konrad <k@knt.li>
Co-authored-by: jd <jd@noreply.kolaente.dev>
Co-committed-by: jd <jd@noreply.kolaente.dev>
(cherry picked from commit 84dbc5fd84)
2024-09-29 15:38:09 +02:00
renovate e318e604b5
chore(deps): update dev-dependencies
(cherry picked from commit 4d8c957f75)
2024-09-29 15:38:07 +02:00
renovate 6ea482a10b
fix(deps): update dependency vue to v3.5.10
(cherry picked from commit 654f375d39)
2024-09-29 15:38:05 +02:00
renovate a9b37db504
fix(deps): update tiptap to v2.7.4
(cherry picked from commit bdb3df8395)
2024-09-29 15:38:03 +02:00
renovate 7f3c074958
fix(deps): update dependency dompurify to v3.1.7
(cherry picked from commit df41a96148)
2024-09-29 15:38:02 +02:00
renovate 2e4de2919f
fix(deps): update dependency vue to v3.5.9
(cherry picked from commit 892b8ad89a)
2024-09-29 15:38:00 +02:00
renovate addcb9c1f6
chore(deps): update dev-dependencies
(cherry picked from commit b4ac1adacb)
2024-09-29 15:37:59 +02:00
renovate 6b3e7f0d25
fix(deps): update tiptap to v2.7.3
(cherry picked from commit 0706ca60b5)
2024-09-29 15:37:57 +02:00
kolaente 6dbfbc43da
fix(checkbox): use sibling css selector instead of has
This makes the checkbox work as intended on older browsers which do not support the :has selector.

Resolves https://kolaente.dev/vikunja/vikunja/issues/2713

(cherry picked from commit 15d95f16da)
2024-09-29 15:37:56 +02:00
renovate 3e3efa85ea
fix(deps): update dependency @sentry/vue to v8.32.0
(cherry picked from commit f7f1b1638b)
2024-09-29 15:37:55 +02:00
renovate 5dc94bfa64
chore(deps): update dev-dependencies
(cherry picked from commit 2ece97fc2b)
2024-09-29 15:37:53 +02:00
renovate 04c6b6ba36
chore(deps): update dev-dependencies
(cherry picked from commit 71901a62f2)
2024-09-29 15:37:52 +02:00
renovate 85d0c8b1b9
fix(deps): update dependency tailwindcss to v3.4.13
(cherry picked from commit cda0848b2a)
2024-09-29 15:37:51 +02:00
kolaente f32472eb24
fix(files): only use service rootpath for files when the files path is not absolute
Resolves https://community.vikunja.io/t/images-failing-to-load-500-internal-server-error/2835

(cherry picked from commit 261c6e6c9e)
2024-09-29 15:37:49 +02:00
renovate 99bed5c616
fix(deps): update dependency @sentry/vue to v8.31.0
(cherry picked from commit c85cfcce6a)
2024-09-29 15:37:48 +02:00
renovate 166d9cf860
fix(deps): update dependency @intlify/unplugin-vue-i18n to v5.2.0
(cherry picked from commit 51bdbba286)
2024-09-29 15:37:47 +02:00
renovate 16994c5d36
fix(deps): update dependency vue-i18n to v10.0.3
(cherry picked from commit 2d5e2f70b9)
2024-09-29 15:37:45 +02:00
renovate 8d849b3d30
fix(deps): update dependency @intlify/unplugin-vue-i18n to v5.1.0
(cherry picked from commit 2c8ca629de)
2024-09-29 15:37:44 +02:00
renovate 884eef5407
fix(deps): update dependency vue to v3.5.8
(cherry picked from commit a61ac5ac32)
2024-09-29 15:37:43 +02:00
renovate 96d8de06da
chore(deps): update dev-dependencies
(cherry picked from commit 8c9d75e9a4)
2024-09-29 15:37:41 +02:00
renovate d6bab95376
fix(deps): update dependency vue-i18n to v10.0.2
(cherry picked from commit 28f208542f)
2024-09-29 15:37:40 +02:00
renovate a983142ad6
chore(deps): update dev-dependencies
(cherry picked from commit 6a2a78d014)
2024-09-29 15:37:39 +02:00
renovate 98c878b201
fix(deps): update dependency vue to v3.5.7
(cherry picked from commit 2c791e6dd2)
2024-09-29 15:37:39 +02:00
kolaente b2e983fde6
chore(deps): update desktop lockfile
(cherry picked from commit 4d2021997a)
2024-09-29 15:37:30 +02:00
kolaente 724ec5751f
fix(attachment): do not use image previews
Image previews were partially backported from main which caused error messages because some parts are missing.

Resolved https://community.vikunja.io/t/preview-size-is-not-defined/2843
2024-09-29 11:59:18 +02:00
kolaente 17618301bc
Revert "chore(attachments): refactor building image preview"
This reverts commit e70f5bcce3.
2024-09-29 11:54:20 +02:00
kolaente 83b27d813a
chore: 0.24.3 release preperation 2024-09-20 14:46:33 +02:00
kolaente a0ad6e7d28
fix(typesense): use typesense bulk insert, log all errors
(cherry picked from commit 207b88a286)
2024-09-20 14:28:02 +02:00
kolaente 940d149e2a
fix(typesense): force position to always be float instead of auto-inferring
This fixes an issue where it would be impossible to update a task in Typesense when the position for a view of it was previously saved as int64. This happened because the field is created per view on demand and its type is automatically inferred from the data saved. Now, when the first value for a particular position field is a float which could as well be an int (for example, 42.0), that field gets created as an int64 instead of float. Subsequent tries to save a float into that field will then fail.

Additionally, errors about this are silently discarded when using bulk insert. That's why the problem was not really debuggable at first.

(cherry picked from commit 50a0674835)
2024-09-20 14:28:00 +02:00
kolaente 802f661f0d
fix(typesense): index tasks one by one
(cherry picked from commit 8181829c9e)
2024-09-20 14:27:59 +02:00
kolaente f9829a6338
chore(logging): simplify log template string
(cherry picked from commit 83aeb90376)
2024-09-20 14:27:58 +02:00
kolaente a87419fa07
chore(typesense): add more debug logging
(cherry picked from commit e4584109df)
2024-09-20 14:27:55 +02:00
renovate b321ad5fc0
fix(deps): update dependency vuemoji-picker to v0.3.1
(cherry picked from commit 6d7748988a)
2024-09-20 14:27:54 +02:00
renovate edfe41ef87
chore(deps): update dev-dependencies
(cherry picked from commit 363da6c80f)
2024-09-20 14:27:52 +02:00
kolaente 4cbed7c2ed
fix(typesense): use emplace instead of upsert to update documents
(cherry picked from commit 3f1d0f390b)
2024-09-20 14:27:50 +02:00
kolaente 25c8476883
fix(typesense): make sure task positions are recreated properly when updating them
Related https://community.vikunja.io/t/version-0-24-0-internal-server-error-breaking-change/2558
Related https://github.com/go-vikunja/vikunja/issues/317

(cherry picked from commit edf206aba6)
2024-09-20 14:27:49 +02:00
kolaente 15cde08db6
fix(typesense): add new tasks to typesense properly
Maybe related to https://github.com/go-vikunja/vikunja/issues/317

(cherry picked from commit 9e39ef9397)
2024-09-20 14:27:46 +02:00
kolaente 96835f6a28
fix(deps): update module github.com/typesense/typesense-go to v2
(cherry picked from commit c54181eeda)
2024-09-20 14:27:44 +02:00
kolaente b1c4f0c216
fix(typesense): only fail silently when a project was not found during indexing
(cherry picked from commit b3bf92b7ce)
2024-09-20 14:27:42 +02:00
renovate d2fbd468e6
fix(deps): update tiptap to v2.7.2
(cherry picked from commit 05dd05216d)
2024-09-20 14:27:40 +02:00
kolaente a751178c8b
fix(views): add migration for filtered kanban buckets
(cherry picked from commit cd0ab54d57)
2024-09-20 14:27:38 +02:00
kolaente 2b9b9216dc
feat(task): use focus-visible for task focus styles
(cherry picked from commit a7bd9dad24)
2024-09-20 14:27:37 +02:00
kolaente f732082708
fix(a11y): hide unfocusable buttons
(cherry picked from commit 10e245f6d0)
2024-09-20 14:27:35 +02:00
kolaente f56df8aa34
fix(task): open focused task when pressing enter
(cherry picked from commit 010dd1ea22)
2024-09-20 14:27:34 +02:00
kolaente e290d79c15
feat(navigation): use focus-visible for nav items
(cherry picked from commit 65a2814b2a)
2024-09-20 14:27:32 +02:00
kolaente 51f26d14b9
fix(filters): correctly transform and populate saved filter when creating and editing
This fixes a bug where the checkbox "include nulls" during creation was not checked but would be saved as such.

(cherry picked from commit 4dd9d5de67)
2024-09-20 14:27:30 +02:00
kolaente 320fc75c27
fix(task): mark related task as done from the task detail view
Resolves https://github.com/go-vikunja/vikunja/issues/326

(cherry picked from commit ec902b051b)
2024-09-20 14:27:28 +02:00
kolaente 4857bfbbdb
chore: rearrange cron registers
(cherry picked from commit 4b2b8e3b83)
2024-09-20 14:27:28 +02:00
kolaente 62c238e4bc
fix(filter): make sure tasks are in a correct bucket and position when they are part of a date filter
Whenever a task is part of a date filter, it might fall in or out of a filter bucket without anything changing, other than the current time. For example, a filter condition like due_date > now may include different tasks depending on the current time.
For these kinds of tasks to properly show up in the kanban view of a filter, there has to be an entry in the task_buckets table. These entries only got updated when either a task was updated or the filter itself was updated. To account for th changing of time, we also need to check periodically if tasks are now part or not anymore part of that filter.
This change adds a cron task to do precisely that.
We'll have to see if this works resource-wise, but the cron is not the only one doing a bunch of sql queries so it might be fine after all.

Resolves https://community.vikunja.io/t/tasks-in-saved-filter-appear-in-list-view-but-are-not-visible-in-kanban-view/2800

(cherry picked from commit bc52da4029)
2024-09-20 14:27:26 +02:00
kolaente d27b62db6e
fix(task): cleanup old task positions and task buckets when adding an updated or created task to filter
(cherry picked from commit 2123da49a3)
2024-09-20 14:27:23 +02:00
kolaente a21036340e
fix(task): add task to filter view after it was updated
Maybe resolves https://community.vikunja.io/t/tasks-in-saved-filter-appear-in-list-view-but-are-not-visible-in-kanban-view/2800

(cherry picked from commit c53a761396)
2024-09-20 14:27:21 +02:00
kolaente 3398dee481
fix(labels): trigger task updated for bulk label task update
(cherry picked from commit c84b50b3ee)
2024-09-20 14:27:19 +02:00
kolaente 063aa7afec
fix: test selector
(cherry picked from commit c5b82fc591)
2024-09-20 14:27:17 +02:00
renovate df3f5e73ed
fix(deps): update dependency vue to v3.5.6
(cherry picked from commit 1f00180af6)
2024-09-20 14:27:16 +02:00
renovate c57636182a
fix(deps): update dependency tailwindcss to v3.4.12
(cherry picked from commit 33e851f543)
2024-09-20 14:27:14 +02:00
kolaente 4d2a567c46
fix(modal): make scrolling on iOS Safari work
This fixes a bug where the full-screen modal would not work on iOS Safari (and only there). It's unclear why this is happening due to a single overflow: visible statement though.

Resolves https://github.com/go-vikunja/vikunja/issues/325

(cherry picked from commit 16d7386975)
2024-09-20 14:27:12 +02:00
kolaente 21cc3722a1
fix(projects): description not visible on mobile
(cherry picked from commit 6d9bda8cc1)
2024-09-20 14:27:11 +02:00
kolaente 6a68736ca5
fix(task): dragging and dropping on mobile
This change fixes a regression introduced in 1cbb93ea9b.
In that change, the whole task area was made clickable using mouse events directly. Unfortunately, this also prevented the parent component of the task component to recieve them, essentially never getting notified about the mouse movement and thus never dragging the task. I don't know why this is only a problem on Safari, but it might be related to https://github.com/SortableJS/Sortable/issues/1571#issuecomment-535684451

Resolves https://community.vikunja.io/t/task-re-ordering-is-not-working-in-safari/1916
Resolves https://kolaente.dev/vikunja/vikunja/issues/2092
Resolves https://github.com/go-vikunja/vikunja/issues/304

(cherry picked from commit abf92e29fa)
2024-09-20 14:27:09 +02:00
kolaente 9e5d6bad8d
chore(tasks): move drag options to direct attributes instead of v-bind
(cherry picked from commit f4d628550f)
2024-09-20 14:27:08 +02:00
kolaente 2b0fbe2575
chore(devenv): do not install cypress on darwin
(cherry picked from commit 1d352fcbf3)
2024-09-20 14:27:06 +02:00
kolaente 1e7d9c982d
chore: remove console.log
(cherry picked from commit a1105434bf)
2024-09-20 14:27:03 +02:00
kolaente 20c52a8325
fix(editor): restore the current value, not the one from a previous task
Resolves https://community.vikunja.io/t/task-description-is-overwritten-when-pressing-esc-key/2813

(cherry picked from commit acc7c9f8f5)
2024-09-20 14:27:02 +02:00
kolaente a46009dfc9
fix(task): align task title on mobile popup
(cherry picked from commit cc64ca6406)
2024-09-20 14:27:00 +02:00
kolaente 6ab12b9dd1
chore: add go and direnv to recommended vscode extensions
(cherry picked from commit 89f78cd369)
2024-09-20 14:26:59 +02:00
renovate aeb404f313
fix(deps): update tiptap to v2.7.1
(cherry picked from commit 14c3b13823)
2024-09-20 14:26:57 +02:00
renovate 9c5df7d389
chore(deps): update dev-dependencies
(cherry picked from commit c2a7438814)
2024-09-20 14:26:56 +02:00
renovate ffd36261cb
fix(deps): update tiptap to v2.7.0
(cherry picked from commit 8fe37fd900)
2024-09-20 14:26:54 +02:00
renovate 8812af2d77
fix(deps): update module github.com/prometheus/client_golang to v1.20.4
(cherry picked from commit 88b4ab1768)
2024-09-20 14:26:52 +02:00
kolaente 93f7dd611a
fix: reset id before creating
(cherry picked from commit c252c8f0cd)
2024-09-20 14:26:51 +02:00
renovate 0e683ae7bf
fix(deps): update vueuse to v11.1.0
(cherry picked from commit 410ff28b71)
2024-09-20 14:26:49 +02:00
renovate deef8da370
chore(deps): update dev-dependencies
(cherry picked from commit d98dd7ecbb)
2024-09-20 14:26:47 +02:00
renovate a0988daeaf
fix(deps): update dependency date-fns to v4
(cherry picked from commit fc73085876)
2024-09-20 14:26:46 +02:00
renovate 6225b24da8
chore(deps): update dev-dependencies
(cherry picked from commit c71f7334b4)
2024-09-20 14:26:44 +02:00
kolaente 983c02964c
fix(modal): do not prevent scrolling on mobile
Maybe related to https://github.com/go-vikunja/vikunja/issues/325

(cherry picked from commit 6a5342bd49)
2024-09-20 14:26:44 +02:00
renovate 7bc93757c7
fix(deps): update dependency vue-router to v4.4.5
(cherry picked from commit b4ea1bb86a)
2024-09-20 14:26:42 +02:00
renovate b4c5df1ef8
fix(deps): update dependency vue to v3.5.5
(cherry picked from commit 378d0ae9bb)
2024-09-20 14:26:41 +02:00
kolaente fd7d83bdaf
fix(user): do not create user with existing id
Resolves https://vikunja.sentry.io/share/issue/6f1e37d4b8b248188e20650234a45cde/

(cherry picked from commit 6f27e1401a)
2024-09-20 14:26:40 +02:00
kolaente 53d62d35f4
fix: lint
(cherry picked from commit bf5cafc03f)
2024-09-20 14:26:40 +02:00
kolaente a266fbf2b9
fix(filter): do not replace labels keyword when the value is 'label'
Resolves https://community.vikunja.io/t/filtering-by-label-ux-issues/2393/16

(cherry picked from commit f4a7326b68)
2024-09-20 14:26:39 +02:00
kolaente 971f328256
fix(kanban): correctly paginate filtered kanban buckets
Resolves https://github.com/go-vikunja/vikunja/issues/314

(cherry picked from commit 1451f6e46f)
2024-09-20 14:26:39 +02:00
Frederick [Bot] 89d643b9cf
chore(i18n): update translations via Crowdin
(cherry picked from commit 427eb2a618)
2024-09-20 14:26:38 +02:00
kolaente a1c4fbf936
fix(view): correctly get paginated task results
(cherry picked from commit e4b541e653)
2024-09-20 14:26:36 +02:00
kolaente 1049b27d37
fix(view): correctly resolve bucket filter when paginating
(cherry picked from commit 45ff5907e6)
2024-09-20 14:26:34 +02:00
kolaente 6b850c56f7
fix(view): correctly resolve label for filtered views or buckets
(cherry picked from commit 5a7c3927f3)
2024-09-20 14:26:33 +02:00
kolaente 614f70db36
fix(filters): do not replace filter or project values when the id value resolves to undefined
This change fixes a bug where the label title in the query string would be replaced to undefined, resulting in an invalid filter. The underlying problem was the resolved filter query string got re-parsed and the id value of the labels were resolved to undefined (and rendered as that string) in the process.

Resolves https://community.vikunja.io/t/filtering-by-label-ux-issues/2393/14

(cherry picked from commit f425d98b4d)
2024-09-20 14:26:32 +02:00
kolaente b297cb5398
fix(task): multiple overlapping defer due date popups
Resolves https://github.com/go-vikunja/vikunja/issues/131

(cherry picked from commit 4c55016c1a)
2024-09-20 14:26:31 +02:00
kolaente 8e32d099c4
fix(kanban): do not mark first bucked as done bucket in filter bucket mode
Resolves https://github.com/go-vikunja/vikunja/issues/313

(cherry picked from commit 1a8f12ac13)
2024-09-20 14:26:31 +02:00
kolaente 19a1dc9daf
fix(table): make sorting for two-word properties work
Resolves https://community.vikunja.io/t/various-sorting-filtering-issues/2781/8

(cherry picked from commit 7b873ec31c)
2024-09-20 14:26:29 +02:00
kolaente 7f28a514ea
fix(test): cypress test selector
(cherry picked from commit 8b77832af2)
2024-09-20 14:26:28 +02:00
Frederick [Bot] 44f5e42c0b
chore(i18n): update translations via Crowdin
(cherry picked from commit 14710d0bba)
2024-09-20 14:26:27 +02:00
kolaente 2fc9504285
fix(kanban): make kanban full width on mobile
Related to https://github.com/go-vikunja/vikunja/issues/309

(cherry picked from commit 945f25b818)
2024-09-20 14:26:26 +02:00
kolaente 2b15bb5154
fix(modal): make sure modal and its content scrolls properly on mobile
Related to https://github.com/go-vikunja/vikunja/issues/309

(cherry picked from commit f53b93d98a)
2024-09-20 14:26:26 +02:00
kolaente f1451abebe
fix(home): explicitly use filter for tasks on home page when one is set
Resolves https://github.com/go-vikunja/vikunja/issues/289
Resolves https://community.vikunja.io/t/various-sorting-filtering-issues/2781/5

(cherry picked from commit 97e030a1fc)
2024-09-20 14:26:24 +02:00
renovate c9f47797cf
chore(deps): update golangci/golangci-lint docker tag to v1.61.0 (#2678)
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>

(cherry picked from commit b708db7025)
2024-09-20 14:26:23 +02:00
kolaente 42b33d6553
fix(filters): immediately propagate changes
Resolves https://community.vikunja.io/t/filtering-startdate-now-is-invalid/2613/6

(cherry picked from commit 6bfe71b30e)
2024-09-20 14:26:21 +02:00
renovate b4b2136869
fix(deps): update dependency @intlify/unplugin-vue-i18n to v5
(cherry picked from commit db196bdffe)
2024-09-20 14:26:19 +02:00
renovate 3d9a719424
fix(deps): update dependency express to v4.21.0
(cherry picked from commit 9428acd5e1)
2024-09-20 14:26:18 +02:00
renovate 5c5f42d4ca
fix(deps): update dependency vue-router to v4.4.4
(cherry picked from commit 58a60fb4ae)
2024-09-20 14:26:17 +02:00
renovate fc1208029e
fix(deps): update dependency flexsearch to v0.7.43 (#2095)
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>

(cherry picked from commit f4ca22a6ad)
2024-09-20 14:26:14 +02:00
kolaente 4a40bdfe89
fix(kanban): make task creation loading spinner actually visible
(cherry picked from commit 7e38a9f442)
2024-09-20 14:26:13 +02:00
renovate b01e531fb7
fix(deps): update dependency vue-i18n to v10.0.1
(cherry picked from commit a0a77818b1)
2024-09-20 14:26:11 +02:00
kolaente 02ab944020
fix(labels): remove input interactivity when label edit is disabled
(cherry picked from commit bdccd633fe)
2024-09-20 14:26:10 +02:00
kolaente 1df4a4ea2e
chore: fix comment
(cherry picked from commit 1f3eb8f2a3)
2024-09-20 14:26:08 +02:00
renovate 61cb072ba3
fix(deps): update dependency tailwindcss to v3.4.11
(cherry picked from commit 8891ef347e)
2024-09-20 14:26:07 +02:00
kolaente 02b8ea40af
fix(multiselect): make selectPlaceholder optional
(cherry picked from commit 3b8ccbad4f)
2024-09-20 14:26:05 +02:00
kolaente b5eaa51560
fix(task): make print styles work when printing task detail view from kanban
Resolves https://community.vikunja.io/t/feature-request-export-a-task-as-pdf/2735/6

(cherry picked from commit cc1b4bbd1b)
2024-09-20 14:26:03 +02:00
Frederick [Bot] eb6663e1f5
[skip ci] Updated swagger docs
(cherry picked from commit 9dd95101f9)
2024-09-20 14:26:01 +02:00
kolaente b94802169c
fix(task): cyclomatic complexity
(cherry picked from commit 20724f6fb5)
2024-09-20 14:26:00 +02:00
Frederick [Bot] dfda2e5500
chore(i18n): update translations via Crowdin
(cherry picked from commit e1ea84fcd4)
2024-09-20 14:25:58 +02:00
kolaente d23484c8f3
fix(project): reset id before creating
Resolves https://vikunja.sentry.io/share/issue/6f1e37d4b8b248188e20650234a45cde/

(cherry picked from commit 8d730543c3)
2024-09-20 14:25:57 +02:00
renovate 78b8ea6211
fix(deps): update dependency vue-i18n to v10
(cherry picked from commit a3d133bfee)
2024-09-20 14:25:55 +02:00
renovate 93cc2e1cb0
fix(deps): update dependency @sentry/vue to v8.30.0
(cherry picked from commit 7b37ad0594)
2024-09-20 14:25:54 +02:00
kolaente 52fb43a6f7
fix(project): show description in title attribute without html
Resolves https://community.vikunja.io/t/feedback-to-vikunja-0-24-0/2557/6

(cherry picked from commit 4aeb228c3a)
2024-09-20 14:25:53 +02:00
kolaente 8f5273600a
fix(projects): do not hide 6th project on project overview
(cherry picked from commit b52d6dbf59)
2024-09-20 14:25:51 +02:00
kolaente 40105ee4ce
chore: remove console.log
(cherry picked from commit 76f7797e56)
2024-09-20 14:25:50 +02:00
kolaente 5bfd99dd77
fix(task): specify task index when creating multiple tasks at once
This change allows to specify the task index when creating a task, which will then be checked to avoid duplicates and used. This allows us to calculate the indexes for all tasks beforehand when creating them at once using quick add magic.
The method is not bulletproof, but already fixes a problem where multiple tasks would have the same index when created that way.

Resolves https://community.vikunja.io/t/add-multiple-tasks-at-once/333/16

(cherry picked from commit 55dd7d2981)
2024-09-20 14:25:48 +02:00
kolaente ac87035742
fix(auth): restrict max password length to 72 bytes
Bcrypt allows a maximum of 72 bytes. This is part of the algorithm and not something we could change in Vikunja. The solution here was to restrict the password during registration to a max length of 72 bytes. In the future, this should be changed to hash passwords with sha512 or similar before hashing them with bcrypt. Because they should also be salted in that case and the added complexity during the migration phase, this was not implemented yet.
The change in this commit only improves the error handling to return an input error instead of a server error when the user enters a password > 72 bytes.

Resolves https://vikunja.sentry.io/share/issue/e8e0b64612d84504942feee002ac498a/

(cherry picked from commit 44a43b9f86)
2024-09-20 14:25:46 +02:00
kolaente 10edeafa46
fix(password): validate password before sending request to api
(cherry picked from commit eb95caf757)
2024-09-20 14:25:42 +02:00
kolaente 01a7a62541
fix(caldav): reject invalid project id with error 400
Resolves https://vikunja.sentry.io/share/issue/6fc18edefa0e4db3b2e10efe36deeaa4/

(cherry picked from commit 1085a6583b)
2024-09-20 14:25:41 +02:00
kolaente c5f043c346
fix(label): ignore existing ID during creation
(cherry picked from commit e698ac5a34)
2024-09-20 14:25:39 +02:00
renovate 1b55be6a15
fix(deps): update module github.com/getsentry/sentry-go to v0.29.0
(cherry picked from commit 020fea686d)
2024-09-20 14:25:37 +02:00
renovate 99e401cc67
fix(deps): update dependency express to v4.20.0
(cherry picked from commit addf8523f4)
2024-09-20 14:25:36 +02:00
renovate 9deda69baf
fix(deps): update dependency vue to v3.5.4
(cherry picked from commit 044aa1c742)
2024-09-20 14:25:35 +02:00
renovate 34fe37bfb7
chore(deps): update goreleaser/nfpm docker tag to v2.40.0 (#2647)
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>

(cherry picked from commit 552e1ce796)
2024-09-20 14:25:33 +02:00
renovate a88939ff9a
fix(deps): update dependency @sentry/vue to v8.29.0
(cherry picked from commit c1246d73d4)
2024-09-20 14:25:31 +02:00
renovate 89e4c3722a
chore(deps): update pnpm to v9.10.0
(cherry picked from commit 85fa397a46)
2024-09-20 14:25:30 +02:00
renovate 4e27d7da70
chore(deps): update dev-dependencies
(cherry picked from commit af47e43c34)
2024-09-20 14:25:28 +02:00
Frederick [Bot] be5822712e
[skip ci] Updated swagger docs
(cherry picked from commit 2539f15dfb)
2024-09-20 14:25:27 +02:00
kolaente a5a54a40f6
docs(api): use correct return type for the /user endpoint
(cherry picked from commit 5c1b2846a1)
2024-09-20 14:25:25 +02:00
renovate ac977f02df
fix(deps): update dependency vue-router to v4.4.3
(cherry picked from commit af8100d262)
2024-09-20 14:25:23 +02:00
renovate 3d43603b23
chore(deps): update dependency go to v1.23.1
(cherry picked from commit 63f85c97ae)
2024-09-20 14:25:22 +02:00
renovate 9d3f8635d6
chore(deps): update dev-dependencies
(cherry picked from commit 2b1e3ffd2a)
2024-09-20 14:25:20 +02:00
renovate dab0c39b70
fix(deps): update module golang.org/x/image to v0.20.0
(cherry picked from commit 5f470afd7c)
2024-09-20 14:25:19 +02:00
renovate f71cc9e77b
fix(deps): update module github.com/prometheus/client_golang to v1.20.3
(cherry picked from commit 37c6f94ba2)
2024-09-20 14:25:17 +02:00
renovate 5aa6b230cf
fix(deps): update dependency vue to v3.5.3
(cherry picked from commit b42cd5786e)
2024-09-20 14:25:15 +02:00
renovate 785ee36921
fix(deps): update module dario.cat/mergo to v1.0.1
(cherry picked from commit 5072ab3d72)
2024-09-20 14:25:14 +02:00
renovate 476cadbbed
fix(deps): update module github.com/redis/go-redis/v9 to v9.6.1
(cherry picked from commit 4b5ec018f5)
2024-09-20 14:25:12 +02:00
renovate 69c609478f
fix(deps): update module golang.org/x/crypto to v0.27.0
(cherry picked from commit efe5c7b3cb)
2024-09-20 14:25:10 +02:00
renovate 77ac70c47a
fix(deps): update module golang.org/x/oauth2 to v0.23.0
(cherry picked from commit ba0c32aacf)
2024-09-20 14:25:08 +02:00
renovate 1371692bb6
fix(deps): update vueuse to v11
(cherry picked from commit b6e142e8d0)
2024-09-20 14:25:07 +02:00
renovate bd1b1ca23c
fix(deps): update dependency tailwindcss to v3.4.10
(cherry picked from commit 750d565477)
2024-09-20 14:25:04 +02:00
Frederick [Bot] 85445ea032
[skip ci] Updated swagger docs
(cherry picked from commit 2b8515b91d)
2024-09-20 14:25:03 +02:00
kolaente 0b9f3070fd
fix(files): use absolute path everywhere
(cherry picked from commit 68636f27da)
2024-09-20 14:25:02 +02:00
kolaente cca02a3f2e
chore(docs): clarify usage of related model creation
Resolves https://github.com/go-vikunja/vikunja/issues/323

(cherry picked from commit cf94cc8cab)
2024-09-20 14:25:00 +02:00
renovate 5c82333977
fix(deps): update dependency axios to v1.7.7
(cherry picked from commit 669b7452cd)
2024-09-20 14:24:58 +02:00
renovate 239cabd34a
fix(deps): update dependency sortablejs to v1.15.3
(cherry picked from commit 1601c9098d)
2024-09-20 14:24:57 +02:00
renovate 4701c91c1a
fix(deps): update module github.com/gabriel-vasile/mimetype to v1.4.5
(cherry picked from commit f132b0e718)
2024-09-20 14:24:56 +02:00
renovate 5775c51be5
fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.23
(cherry picked from commit 5d12986e3f)
2024-09-20 14:24:56 +02:00
renovate 1cdf8f4271
fix(deps): update module github.com/threedotslabs/watermill to v1.3.7
(cherry picked from commit 74e0197dc6)
2024-09-20 14:24:55 +02:00
renovate 442009a47b
fix(deps): update dependency @kyvg/vue3-notification to v3.3.0
(cherry picked from commit 2dd133fb4c)
2024-09-20 14:24:54 +02:00
kolaente e70f5bcce3
chore(attachments): refactor building image preview
(cherry picked from commit 02c1de55c4)
2024-09-20 14:24:53 +02:00
renovate a355d9798e
fix(deps): update dependency @sentry/vue to v8.28.0
(cherry picked from commit cbc63c853d)
2024-09-20 14:23:51 +02:00
renovate 7fa171c3d3
fix(deps): update dependency pinia to v2.2.2
(cherry picked from commit b5c4a3a80c)
2024-09-20 14:23:50 +02:00
renovate 672fc5d9c7
fix(deps): update dependency vue-i18n to v9.14.0
(cherry picked from commit a8ea5e11ab)
2024-09-20 14:23:50 +02:00
renovate 586132ce8c
fix(deps): update module golang.org/x/text to v0.18.0
(cherry picked from commit 812430b9cf)
2024-09-20 14:23:49 +02:00
kolaente bf08dc2585
chore(files): use absolute file path to retrieve and save files
(cherry picked from commit c2b116de70)
2024-09-20 14:23:48 +02:00
kolaente 761d278b9a
fix(kanban): save updated position to store
This fixes a bug where the position of a task would not be calculated correctly when the task was moved next to another recently moved task. The problem was caused by the calculation of the new position referring to the old value of the position attribute, because it was not updated in the local store.

Resolves https://community.vikunja.io/t/kanban-cards-in-wrong-order/2731/6

(cherry picked from commit 22e594e253)
2024-09-20 14:22:54 +02:00
kolaente 6da9bc964e
chore(magefile): use tx.Sync instead of Sync2
(cherry picked from commit 0d9c03e0f2)
2024-09-20 14:22:52 +02:00
renovate d508fe3fb8
fix(deps): update module golang.org/x/term to v0.24.0
(cherry picked from commit 72bb6609a1)
2024-09-20 14:22:50 +02:00
renovate e7075762ab
fix(deps): update module github.com/typesense/typesense-go to v2
(cherry picked from commit b259072e6a)
2024-09-20 14:22:49 +02:00
kolaente eb89f68f73
fix(caldav): make sure colors are correctly saved and returned
Resolves https://community.vikunja.io/t/caldav-sync-tasks-org-strips-colour-and-end-date-values/2753/2

(cherry picked from commit ffcc48ec87)
2024-09-20 14:22:47 +02:00
Frederick [Bot] 08b4bcaff9
[skip ci] Updated swagger docs
(cherry picked from commit 5aa0b6a0cf)
2024-09-20 14:22:45 +02:00
kolaente 0a29a88a26
chore(subscription): return subscription entity type using json Marshaler
(cherry picked from commit b60efbd259)
2024-09-20 14:22:43 +02:00
kolaente 313b99e296
fix(reminders): notify subscribed users as well
Resolves https://community.vikunja.io/t/no-reminder-notification-by-e-mail-from-my-colleague/2779

(cherry picked from commit 34ac29fcce)
2024-09-20 14:22:41 +02:00
kolaente 95ef4e1045
fix(subscriptions): do not panic when a task does not have a subscription
(cherry picked from commit 75f3e930cd)
2024-09-20 14:22:39 +02:00
kolaente 8b8ec19bb3
fix(subscriptions): cleanup and simplify fetching subscribers for tasks and projects logic
Vikunja now uses one recursive CTE and a few optimizations to fetch all subscribers for a task or project. This makes the relevant code easier to maintain and more performant.

(cherry picked from commit 4ff8815fe1)
2024-09-20 14:22:37 +02:00
renovate 7646c7f0c9
fix(deps): update dependency dayjs to v1.11.13
(cherry picked from commit 850ec7efb0)
2024-09-20 14:22:36 +02:00
kolaente c8f7a57566
fix(subscription): always return task subscription when subscribed to task and project
(cherry picked from commit 115d1c3618)
2024-09-20 14:22:34 +02:00
kolaente 1c64b75f86
chore(deps): update github.com/wneessen/go-mail to v0.4.4
(cherry picked from commit 6d26497bbb)
2024-09-20 14:22:33 +02:00
kolaente fc8252e751
fix(subscriptions): correctly inherit subscriptions
Resolves https://community.vikunja.io/t/e-mail-notification-twice/2740/20

(cherry picked from commit 06305eb6b3)
2024-09-20 14:22:33 +02:00
Frederick [Bot] 1c9590075a
chore(i18n): update translations via Crowdin
(cherry picked from commit 7bd84a845c)
2024-09-20 14:22:31 +02:00
kolaente 800f4545c1
fix(service worker): use correct workbox version
Resolves https://github.com/go-vikunja/vikunja/issues/322

(cherry picked from commit 165ee9e7f3)
2024-09-20 14:22:29 +02:00
kolaente a462697b30
fix(notifications): only add project subscription as task subscription when the user is not already subscribed to the task
Before this fix, a project subscription object was added twice to the list of subscriptions for a task when the user did not subscribe to the task directly. This caused the user to receive a comment notification twice for a given task.
This was probably a regression from efde364224.

Resolves https://community.vikunja.io/t/e-mail-notification-twice/2740/18

(cherry picked from commit 2c9becec10)
2024-09-20 14:22:29 +02:00
kolaente 5049cbf236
chore(web): always set internal error
(cherry picked from commit 5c56d07215)
2024-09-20 14:22:28 +02:00
kolaente a2ef74cade
chore(web): use logger directly
(cherry picked from commit 6fb314b326)
2024-09-20 14:22:25 +02:00
kolaente fe44b7d473
chore(web): use web auth factory directly
(cherry picked from commit 9b01666ec6)
2024-09-20 14:22:23 +02:00
Frederick [Bot] 01c4f1fc0e
chore(i18n): update translations via Crowdin
(cherry picked from commit 14c862d284)
2024-09-20 14:22:21 +02:00
kolaente 5fba4ed6ef
chore(web): use config directly
(cherry picked from commit bcfd72c645)
2024-09-20 14:22:19 +02:00
kolaente d885b43328
chore(web): directly use new db session
(cherry picked from commit 499f66b7ae)
2024-09-20 14:22:16 +02:00
kolaente 459c8daed6
chore(web): remove redundant use of fmt.Sprintf
(cherry picked from commit 8e37d5cb76)
2024-09-20 14:22:16 +02:00
kolaente 5768648760
chore(web): use errors.As instead of type assertion
(cherry picked from commit 57ba073874)
2024-09-20 14:22:11 +02:00
kolaente 198b2e3b70
chore(web): remove unused echo context
(cherry picked from commit 329de3aab3)
2024-09-20 14:22:11 +02:00
kolaente be8ecb6d36
fix(labels): test error assertion
(cherry picked from commit 74a74b7ec7)
2024-09-20 14:22:09 +02:00
kolaente 4c73c74587
chore(web): move web handler package to Vikunja
(cherry picked from commit 2063da9eec)
2024-09-20 14:22:08 +02:00
kolaente cfa58ae599
chore(errors): always add internal error to echo error
(cherry picked from commit 7a7e97770c)
2024-09-20 14:21:18 +02:00
kolaente 39d0409f57
fix(labels): trigger task.updated event when removing a label from a task
(cherry picked from commit bea131cfd9)
2024-09-20 14:21:16 +02:00
kolaente 7b804efbe2
feat(event): simplify dispatching task updated event from only a task id
(cherry picked from commit 5a0d1f1dc6)
2024-09-20 14:21:14 +02:00
kolaente f812a67269
chore(docker): use new env format
(cherry picked from commit b5035f9d2b)
2024-09-20 14:21:13 +02:00
kolaente 1a131d79f9
fix(subscriptions): ignore task subscription when the user is subscribed to the project
Resolves https://community.vikunja.io/t/e-mail-notification-twice/2740/12
Resolves https://github.com/go-vikunja/vikunja/issues/316

(cherry picked from commit efde364224)
2024-09-20 14:21:11 +02:00
kolaente a253f76060
fix(api): return 404 response when using a token and the route does not exist
(cherry picked from commit fde1763eef)
2024-09-20 14:21:09 +02:00
kolaente c85da01294
fix(task): set done at date when moving a task to the done bucket
Resolves https://github.com/go-vikunja/vikunja/issues/320

(cherry picked from commit bf56311faa)
2024-09-20 14:21:06 +02:00
Frederick [Bot] 499f3f000c
chore(i18n): update translations via Crowdin
(cherry picked from commit c38aaeb224)
2024-09-20 14:21:02 +02:00
Frederick [Bot] 1059b00298
chore(i18n): update translations via Crowdin
(cherry picked from commit 1b9139b9b0)
2024-09-20 14:21:00 +02:00
kolaente 37324aeaf2
chore(deps): update tiptap to 2.6.6
(cherry picked from commit 13763e3540)
2024-09-20 14:20:58 +02:00
kolaente 7b275794dc
fix(deps): update golangci
(cherry picked from commit 8bfd0493b2)
2024-09-20 14:20:56 +02:00
kolaente 72db97203a
fix(view): do not crash when saving a view
Resolves https://github.com/go-vikunja/vikunja/issues/312

(cherry picked from commit 435cb2e7f7)
2024-09-20 14:20:54 +02:00
Frederick [Bot] 27c14b6903
chore(i18n): update translations via Crowdin
(cherry picked from commit 9ed33f5c08)
2024-09-20 14:20:53 +02:00
Frederick [Bot] ec94cf6813
chore(i18n): update translations via Crowdin
(cherry picked from commit 83669fd9e0)
2024-09-20 14:20:52 +02:00
Frederick [Bot] 6a9b5cb7d0
chore(i18n): update translations via Crowdin
(cherry picked from commit 35578e0021)
2024-09-20 14:20:51 +02:00
Frederick [Bot] 8248049530
chore(i18n): update translations via Crowdin
(cherry picked from commit e281de30b5)
2024-09-20 14:20:51 +02:00
Frederick [Bot] d1678fe420
chore(i18n): update translations via Crowdin
(cherry picked from commit ec2c2e74f0)
2024-09-20 14:20:46 +02:00
kolaente 08821ea8a8
chore: 0.24.2 release preperations 2024-08-12 20:22:07 +02:00
kolaente e9a466fa31
fix(kanban): always make cover image full width
(cherry picked from commit f22420d502)
2024-08-12 20:17:10 +02:00
kolaente de9f686480
fix(migration): ensure project background gets exported and imported
(cherry picked from commit ebfd5f54d2)
2024-08-12 20:17:10 +02:00
kolaente d08e9650ba
fix(migration): make sure tasks are associated to the correct view and bucket for data imported from Vikunja dump
This change fixes a bug where imported projects would contain the default views additionally to the ones included in the export. This also caused the tasks to not show up in the views and buckets where they should show up, the newly imported ones.

Resolves https://community.vikunja.io/t/migration-from-vikunja-export-duplicated-boards-local-to-oidc/2690

(cherry picked from commit 28b4eaee31)
2024-08-12 20:17:09 +02:00
kolaente 0434a96c3a
fix(views): do not create task bucket and task position entries when duplicating a project
Resolves https://github.com/go-vikunja/vikunja/issues/297
Resolves https://community.vikunja.io/t/duplicating-a-board-puts-the-buckets-in-the-original-board/2579

(cherry picked from commit b83448b7a6)
2024-08-12 20:17:07 +02:00
kolaente 2b9b77bef2
fix(task): move task into new kanban bucket when moving between projects
Resolves https://github.com/go-vikunja/vikunja/issues/290
Resolves https://community.vikunja.io/t/tasks-moved-from-one-project-to-another-do-not-show-up-in-kanban/2689

(cherry picked from commit eed783e42f)
2024-08-12 20:17:05 +02:00
kolaente f5040ad2f4
chore: use nixpkgs unstable for more recent packages
(cherry picked from commit 1d624612ee)
2024-08-12 20:17:04 +02:00
kolaente 7bc77ae8c0
fix(task): set current project after moving a task
(cherry picked from commit 9576f72d7c)
2024-08-12 20:17:02 +02:00
kolaente 715269a5d0
fix(task): do not allow moving a task to the project the task already belongs to
(cherry picked from commit 7efc4d1bc8)
2024-08-12 20:17:01 +02:00
kolaente 53605c24f0
chore(deps): update dependency node to v20.16.0
(cherry picked from commit 670e605275)
2024-08-12 20:17:00 +02:00
kolaente da0b741b69
chore(deps): update go toolchain to 1.22.5
(cherry picked from commit 871b3ce071)
2024-08-12 20:16:59 +02:00
kolaente 73f923bc47
feat: switch from nix flakes to devenv
(cherry picked from commit ad3c5fcee5)
2024-08-12 20:16:56 +02:00
kolaente 25a8c7ea80
feat(editor): support custom protocol for links
Resolves https://github.com/go-vikunja/vikunja/issues/306

(cherry picked from commit e2a87036e0)
2024-08-12 20:16:55 +02:00
kolaente 950de7c954
fix(mail): do not fail testmail command when the connection could not be closed.
Resolves https://github.com/go-vikunja/vikunja/issues/300

(cherry picked from commit 40bb86bee5)
2024-08-12 20:16:54 +02:00
Frederick [Bot] 42ba9240b0
chore(i18n): update translations via Crowdin
(cherry picked from commit 4a4ed2cf89)
2024-08-12 20:16:52 +02:00
kolaente 3da6299021
chore(deps): update flake
(cherry picked from commit 7751e124a7)
2024-08-12 20:16:50 +02:00
Frederick [Bot] 1ac0f412b9
chore(i18n): update translations via Crowdin
(cherry picked from commit 6137d07138)
2024-08-12 20:16:48 +02:00
kolaente e7fbdc2727
fix(i18n): change casing of Ukrainian language in selector
(cherry picked from commit 66d316f0b5)
2024-08-12 20:16:46 +02:00
kolaente 7117303d57
docs: clarify Todoist redirect url
Resolves https://github.com/go-vikunja/vikunja/issues/302

(cherry picked from commit bf2d56c9d4)
2024-08-12 20:16:45 +02:00
Dominik Pschenitschni 0ca43dc147
fix: remove console log
(cherry picked from commit 413798b321)
2024-08-12 20:16:43 +02:00
Frederick [Bot] c47ea2716c
chore(i18n): update translations via Crowdin
(cherry picked from commit bf1c8c6119)
2024-08-12 20:16:42 +02:00
Frederick [Bot] cbdcc61414
chore(i18n): update translations via Crowdin
(cherry picked from commit 49fa0a8468)
2024-08-12 20:16:41 +02:00
Frederick [Bot] 194a7d4cf8
chore(i18n): update translations via Crowdin
(cherry picked from commit 784cd42a7a)
2024-08-12 20:16:37 +02:00
Frederick [Bot] 9695785a0c
chore(i18n): update translations via Crowdin
(cherry picked from commit 4f74be0247)
2024-08-12 20:16:35 +02:00
Dominik Pschenitschni d9f555554e
fix: textarea autosize for LanguageTool
(cherry picked from commit 7ef1e0a3e5)
2024-08-12 20:16:32 +02:00
Dominik Pschenitschni 6e38bcf349
chore: improve error message
(cherry picked from commit bc5fd380e5)
2024-08-12 20:16:30 +02:00
Dominik Pschenitschni fc780a90ae
chore: remove lodash.debounce
(cherry picked from commit bcd306b84d)
2024-08-12 20:16:28 +02:00
Dominik Pschenitschni d586f691b7
feat: add missing peer dependency
(cherry picked from commit ad2870335d)
2024-08-12 20:16:26 +02:00
Frederick [Bot] 9e8e43bd12
chore(i18n): update translations via Crowdin
(cherry picked from commit 917748777f)
2024-08-12 20:16:24 +02:00
renovate c554a96b4b
fix(deps): update module github.com/coreos/go-oidc/v3 to v3.11.0
(cherry picked from commit 324c966dda)
2024-08-12 20:16:23 +02:00
Frederick [Bot] 3ecc81094f
[skip ci] Updated swagger docs
(cherry picked from commit 0984821b41)
2024-08-12 20:16:21 +02:00
Dominik Pschenitschni 811ccc1baa
feat: improve ProjectSettingsViews
(cherry picked from commit e8be657d97)
2024-08-12 20:16:17 +02:00
Dominik Pschenitschni d7c5451729
feat: add tailwind with prefix (#2513)
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2513
Co-authored-by: Dominik Pschenitschni <mail@celement.de>
Co-committed-by: Dominik Pschenitschni <mail@celement.de>
(cherry picked from commit bbfd5270db)
2024-08-12 20:16:16 +02:00
Dominik Pschenitschni 96884372b4
fix(projects): trigger only single mutation
Previously these store methods created multiple edits on deep objects in the store or edited a deep nested object directly.

This is bad because:
- multiple edits lead to multiple triggers of all the watchers on the project.
- in theory we should listen deep to a project if we use some deep reactive value of a project, if we want deep updates. Because this is easy to forget, it's better to update the project directly. For this the method `setProject` already existed in the store. This is no real overhead because Vue is smart enough to only trigger listeners that use data of the modified state.

By modifying only a copy of the view and submitting the modified result __once__ we can save us a lot of headache.

PS: I'm not sure if there were any visible problems, because Vue is really fast and the reactivity system works quite well. Regardless of this we should try not to modify state unnecessarily.
(cherry picked from commit 1e632397d2)
2024-08-12 20:16:15 +02:00
Dominik Pschenitschni dddba4d64a
feat: improve priority visibility
(cherry picked from commit d35454c099)
2024-08-12 20:16:13 +02:00
Dominik Pschenitschni a0e3efe2d1
feat: improve label store
(cherry picked from commit 2a6ba7e7f0)
2024-08-12 20:16:12 +02:00
Dominik Pschenitschni d707e1576a
feat: improve projects store
(cherry picked from commit 1e523a1a39)
2024-08-12 20:16:11 +02:00
Dominik Pschenitschni 3aaf363413
fix: emit for DatepickerWithValues
(cherry picked from commit fb91e73a3c)
2024-08-12 20:16:08 +02:00
Frederick [Bot] 28fff10fec
chore(i18n): update translations via Crowdin
(cherry picked from commit ebd6139c1f)
2024-08-12 20:16:07 +02:00
Dominik Pschenitschni 8729c24e1d
feat: use withDefaults for Reminders
(cherry picked from commit 6990be705c)
2024-08-12 20:16:01 +02:00
258 changed files with 10473 additions and 5712 deletions

View File

@ -139,7 +139,7 @@ steps:
event: [ push, tag, pull_request ]
- name: api-lint
image: golangci/golangci-lint:v1.59.1
image: golangci/golangci-lint:v1.61.0
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
@ -364,7 +364,7 @@ steps:
- api-build
- name: frontend-dependencies
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -378,7 +378,7 @@ steps:
# - restore-cache
- name: frontend-lint
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -390,7 +390,7 @@ steps:
- frontend-dependencies
- name: frontend-build-prod
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -402,7 +402,7 @@ steps:
- frontend-dependencies
- name: frontend-test-unit
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
commands:
- cd frontend
@ -413,7 +413,7 @@ steps:
- name: frontend-typecheck
failure: ignore
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -544,7 +544,7 @@ steps:
- git fetch --tags
- name: frontend-dependencies
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -556,7 +556,7 @@ steps:
- pnpm install --fetch-timeout 100000
- name: frontend-build
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -580,7 +580,7 @@ steps:
event: [ push, tag, pull_request ]
- name: before-static-build
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
commands:
- export PATH=$PATH:$GOPATH/bin
@ -589,7 +589,7 @@ steps:
depends_on: [ fetch-tags, mage ]
- name: static-build-windows
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
@ -605,7 +605,7 @@ steps:
- frontend-build
- name: static-build-linux
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
@ -621,7 +621,7 @@ steps:
- frontend-build
- name: static-build-darwin
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
@ -647,7 +647,7 @@ steps:
- ./mage-static release:compress
- name: after-build-static
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
depends_on:
- after-build-compress
@ -676,13 +676,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: dist/zip/
source: dist/zip/*
@ -698,13 +698,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: dist/zip/
source: dist/zip/*
@ -716,7 +716,7 @@ steps:
# Build os packages and push it to our bucket
- name: build-os-packages-unstable
image: goreleaser/nfpm:v2.38.0
image: goreleaser/nfpm:v2.40.0
pull: always
commands:
- apk add git go
@ -732,7 +732,7 @@ steps:
depends_on: [ after-build-compress ]
- name: build-os-packages-version
image: goreleaser/nfpm:v2.38.0
image: goreleaser/nfpm:v2.40.0
pull: always
commands:
- apk add git go
@ -750,13 +750,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: dist/os-packages/
source: dist/os-packages/*
@ -772,13 +772,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: dist/os-packages/
source: dist/os-packages/*
@ -837,7 +837,6 @@ steps:
repo: vikunja/vikunja
tags: unstable
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
@ -869,7 +868,6 @@ steps:
from_secret: docker_password
repo: vikunja/vikunja
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
@ -901,7 +899,7 @@ steps:
- git fetch --tags
- name: build
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -931,13 +929,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
source: frontend/vikunja-frontend-unstable.zip
target: /
@ -962,7 +960,7 @@ steps:
- git fetch --tags
- name: build
image: node:20.14.0-alpine
image: node:20.16.0-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -990,13 +988,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
source: frontend/vikunja-frontend-${DRONE_TAG##v}.zip
target: /
@ -1095,8 +1093,8 @@ steps:
# settings:
# restore: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# endpoint: https://fsn1.your-objectstorage.com
# region: fsn1
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
@ -1133,8 +1131,8 @@ steps:
# settings:
# rebuild: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# endpoint: https://fsn1.your-objectstorage.com
# region: fsn1
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
@ -1176,8 +1174,8 @@ steps:
# settings:
# restore: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# endpoint: https://fsn1.your-objectstorage.com
# region: fsn1
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
@ -1200,30 +1198,30 @@ steps:
- unzip vikunja-frontend-$$VERSION.zip -d frontend
- sed -i 's/\\/api\\/v1//g' frontend/index.html
- ./bumpp.sh
- yarn install
- cat package.json
- yarn dist --linux --windows
# - name: rebuild-cache
# image: meltwater/drone-cache:dev
# pull: true
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
# AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key
# settings:
# rebuild: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
# - '.cache'
# depends_on:
# - build
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000
- pnpm dist --linux --windows
# - name: rebuild-cache
# image: meltwater/drone-cache:dev
# pull: true
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
# AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key
# settings:
# rebuild: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://fsn1.your-objectstorage.com
# region: fsn1
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
# - '.cache'
# depends_on:
# - build
- name: rename-unstable
image: bash
pull: true
@ -1241,13 +1239,13 @@ steps:
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: desktop/dist/
source: desktop/dist/Vikunja-Desktop*
@ -1263,13 +1261,13 @@ steps:
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: desktop/dist/
source: desktop/dist/*
@ -1298,9 +1296,9 @@ steps:
# - name: build
# environment:
# ACCESS_KEY:
# from_secret: aws_access_key_id
# from_secret: hetzner_access_key_id
# SECRET_KEY:
# from_secret: aws_secret_access_key
# from_secret: hetzner_secret_access_key
# commands:
# - git fetch --tags
# - export VERSION=${DRONE_TAG##v}
@ -1313,9 +1311,9 @@ steps:
# - sed -i '' "s/\$${version}/$$VERSION/g" package.json
# - yarn install
# - yarn dist --mac
# - mc config host add scw-fr-par https://s3.fr-par.scw.cloud $ACCESS_KEY $SECRET_KEY --api S3v4
# - mc cp ./dist/*.dmg scw-fr-par/vikunja-releases/desktop/$VERSION/
# - mc cp ./dist/*.dmg.blockmap scw-fr-par/vikunja-releases/desktop/$VERSION/
# - mc config host add scw-fsn1 https://fsn1.your-objectstorage.com $ACCESS_KEY $SECRET_KEY --api S3v4
# - mc cp ./dist/*.dmg scw-fsn1/vikunja/desktop/$VERSION/
# - mc cp ./dist/*.dmg.blockmap scw-fsn1/vikunja/desktop/$VERSION/
---
kind: pipeline
@ -1352,6 +1350,6 @@ steps:
- failure
---
kind: signature
hmac: 196e215a44cc1b6b5cb89e6ef09196166af17969f69e3e7e8c571a0e44285714
hmac: 02fb3097f3e50facb2579de5c3b94e513b97e535a049a28ddb67f85e494d65b1
...

4
.envrc
View File

@ -1 +1,3 @@
use flake
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
use devenv

12
.gitignore vendored
View File

@ -28,4 +28,14 @@ vendor/
os-packages/
mage_output_file.go
mage-static
.direnv/
.DS_Store
# Devenv
.devenv*
devenv.local.nix
# direnv
.direnv
# pre-commit
.pre-commit-config.yaml

View File

@ -109,3 +109,13 @@ issues:
text: 'structtag: struct field Position repeats json tag "position" also at'
linters:
- govet
- path: pkg/cmd/user.go
text: 'G115: integer overflow conversion uintptr -> int'
linters:
- gosec
- text: 'G115: integer overflow conversion int64 -> uint64'
linters:
- gosec
- text: 'G115: integer overflow conversion int -> uint64'
linters:
- gosec

View File

@ -1,13 +1,15 @@
{
"recommendations": [
"codezombiech.gitignore",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"vue.volar",
"lokalise.i18n-ally",
"mgmcdermott.vscode-language-babel",
"mikestead.dotenv",
"Syler.sass-indented",
"vitest.explorer"
]
"recommendations": [
"codezombiech.gitignore",
"dbaeumer.vscode-eslint",
"editorconfig.editorconfig",
"vue.volar",
"lokalise.i18n-ally",
"mgmcdermott.vscode-language-babel",
"mikestead.dotenv",
"Syler.sass-indented",
"vitest.explorer",
"mkhl.direnv",
"golang.Go"
]
}

View File

@ -5,7 +5,319 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All releases can be found on https://code.vikunja.io/api/releases.
All releases can be found on https://code.vikunja.io/vikunja/releases.
## [0.24.6] - 2024-12-22
### Bug Fixes
* *(export)* Update only current user export file id
### Features
* Use hetzner object storage for releases ([3f98f47](3f98f47256628ac559a947f6c9541c0bf03001b6))
### Miscellaneous Tasks
* Release preparation ([17b2810](17b281072f095730a0fbb28458d713fa883f43ec))
* Sign drone config ([b91a5d9](b91a5d9adfb7144126f5824224970ad4015dcd6a))
* Do not generate config yaml ([8c28596](8c285968afff7ebd087ce824b52b0a9f28a8ec20))
## [0.24.5] - 2024-11-21
### Bug Fixes
* *(attachments)* Check permissions when accessing all attachments
* *(saved filters)* Check permissions when accessing tasks of a filter
* Pin xgo to 1.22.x ([87b2aac](87b2aaccb8cdcbe1ecb6092951a0bfe224ad7006))
* Upgrade xgo ([19b63c8](19b63c86c51f67614b867c75a58cda1774685edd))
* Upgrade xgo docker image everywhere ([04b40f8](04b40f8a7dcd01a86ddb8b27596073d1e50f9e97))
* *(ci)* Do not build linux 368 docker images
* Disable 368 releases ([73db10f](73db10fb02268e07d29842493df55f4d645ac503))
- **BREAKING**: disable 368 releases
### Miscellaneous Tasks
* Sign drone config ([17c4878](17c487875b5771c0971ee8bf030807171de2dddc))
* Go mod tidy ([9639025](96390257e0911089ae33a9565e8be7fa954c772c))
## [0.24.4] - 2024-09-29
### Bug Fixes
* *(attachment)* Do not use image previews
* *(checkbox)* Use sibling css selector instead of has
* *(files)* Only use service rootpath for files when the files path is not absolute
* *(filters)* Explicitly search in json when using postgres
* *(task)* Paginate task comments
* *(task)* Do not show close button when the task was not opened via modal
* *(task)* Improve task delete modal on mobile
* *(test)* Use correct selector for modal header
* Partial fix to allow list tasks in ios reminders app (#2717)
* *(attachments)* Revert "chore(attachments): refactor building image preview"
### Dependencies
* *(deps)* Update desktop lockfile
* *(deps)* Update dependency vue to v3.5.7
* *(deps)* Update dev-dependencies
* *(deps)* Update dependency vue-i18n to v10.0.2
* *(deps)* Update dev-dependencies
* *(deps)* Update dependency vue to v3.5.8
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v5.1.0
* *(deps)* Update dependency vue-i18n to v10.0.3
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v5.2.0
* *(deps)* Update dependency @sentry/vue to v8.31.0
* *(deps)* Update dependency tailwindcss to v3.4.13
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dependency @sentry/vue to v8.32.0
* *(deps)* Update tiptap to v2.7.3
* *(deps)* Update dev-dependencies
* *(deps)* Update dependency vue to v3.5.9
* *(deps)* Update dependency dompurify to v3.1.7
* *(deps)* Update tiptap to v2.7.4
* *(deps)* Update dependency vue to v3.5.10
* *(deps)* Update dev-dependencies
## [0.24.3] - 2024-09-20
### Bug Fixes
* *(a11y)* Hide unfocusable buttons
* *(api)* Return 404 response when using a token and the route does not exist
* *(auth)* Restrict max password length to 72 bytes
* *(caldav)* Make sure colors are correctly saved and returned
* *(caldav)* Reject invalid project id with error 400
* *(editor)* Restore the current value, not the one from a previous task
* *(files)* Use absolute path everywhere
* *(filter)* Do not replace labels keyword when the value is 'label'
* *(filter)* Make sure tasks are in a correct bucket and position when they are part of a date filter
* *(filters)* Immediately propagate changes
* *(filters)* Do not replace filter or project values when the id value resolves to undefined
* *(filters)* Correctly transform and populate saved filter when creating and editing
* *(home)* Explicitly use filter for tasks on home page when one is set
* *(kanban)* Save updated position to store
* *(kanban)* Make task creation loading spinner actually visible
* *(kanban)* Make kanban full width on mobile
* *(kanban)* Do not mark first bucked as done bucket in filter bucket mode
* *(kanban)* Correctly paginate filtered kanban buckets
* *(label)* Ignore existing ID during creation
* *(labels)* Trigger task.updated event when removing a label from a task
* *(labels)* Test error assertion
* *(labels)* Remove input interactivity when label edit is disabled
* *(labels)* Trigger task updated for bulk label task update
* *(modal)* Make sure modal and its content scrolls properly on mobile
* *(modal)* Do not prevent scrolling on mobile
* *(modal)* Make scrolling on iOS Safari work
* *(multiselect)* Make selectPlaceholder optional
* *(notifications)* Only add project subscription as task subscription when the user is not already subscribed to the task
* *(password)* Validate password before sending request to api
* *(project)* Show description in title attribute without html
* *(project)* Reset id before creating
* *(projects)* Do not hide 6th project on project overview
* *(projects)* Description not visible on mobile
* *(reminders)* Notify subscribed users as well
* *(service worker)* Use correct workbox version
* *(subscription)* Always return task subscription when subscribed to task and project
* *(subscriptions)* Ignore task subscription when the user is subscribed to the project
* *(subscriptions)* Correctly inherit subscriptions
* *(subscriptions)* Cleanup and simplify fetching subscribers for tasks and projects logic
* *(subscriptions)* Do not panic when a task does not have a subscription
* *(table)* Make sorting for two-word properties work
* *(task)* Set done at date when moving a task to the done bucket
* *(task)* Specify task index when creating multiple tasks at once
* *(task)* Cyclomatic complexity
* *(task)* Make print styles work when printing task detail view from kanban
* *(task)* Multiple overlapping defer due date popups
* *(task)* Align task title on mobile popup
* *(task)* Dragging and dropping on mobile
* *(task)* Add task to filter view after it was updated
* *(task)* Cleanup old task positions and task buckets when adding an updated or created task to filter
* *(task)* Mark related task as done from the task detail view
* *(task)* Open focused task when pressing enter
* *(test)* Cypress test selector
* *(typesense)* Only fail silently when a project was not found during indexing
* *(typesense)* Add new tasks to typesense properly
* *(typesense)* Make sure task positions are recreated properly when updating them
* *(typesense)* Use emplace instead of upsert to update documents
* *(typesense)* Index tasks one by one
* *(typesense)* Force position to always be float instead of auto-inferring
* *(typesense)* Use typesense bulk insert, log all errors
* *(user)* Do not create user with existing id
* *(view)* Do not crash when saving a view
* *(view)* Correctly resolve label for filtered views or buckets
* *(view)* Correctly resolve bucket filter when paginating
* *(view)* Correctly get paginated task results
* *(views)* Add migration for filtered kanban buckets* Lint ([53d62d3](53d62d35f4488940a96d755de93ded64b8ac34a3))
* Reset id before creating ([93f7dd6](93f7dd611ad288a149f5da5463867d224334815f))
* Test selector ([063aa7a](063aa7afec717c3ed05be9d2ca73bde3d0bd8d35))
### Dependencies
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v5
* *(deps)* Update dependency @kyvg/vue3-notification to v3.3.0
* *(deps)* Update dependency @sentry/vue to v8.28.0
* *(deps)* Update dependency @sentry/vue to v8.29.0
* *(deps)* Update dependency @sentry/vue to v8.30.0
* *(deps)* Update dependency axios to v1.7.7
* *(deps)* Update dependency date-fns to v4
* *(deps)* Update dependency dayjs to v1.11.13
* *(deps)* Update dependency express to v4.20.0
* *(deps)* Update dependency express to v4.21.0
* *(deps)* Update dependency go to v1.23.1
* *(deps)* Update dependency pinia to v2.2.2
* *(deps)* Update dependency sortablejs to v1.15.3
* *(deps)* Update dependency tailwindcss to v3.4.10
* *(deps)* Update dependency tailwindcss to v3.4.11
* *(deps)* Update dependency tailwindcss to v3.4.12
* *(deps)* Update dependency vue to v3.5.3
* *(deps)* Update dependency vue to v3.5.4
* *(deps)* Update dependency vue to v3.5.5
* *(deps)* Update dependency vue to v3.5.6
* *(deps)* Update dependency vue-i18n to v10
* *(deps)* Update dependency vue-i18n to v10.0.1
* *(deps)* Update dependency vue-i18n to v9.14.0
* *(deps)* Update dependency vue-router to v4.4.3
* *(deps)* Update dependency vue-router to v4.4.4
* *(deps)* Update dependency vue-router to v4.4.5
* *(deps)* Update dependency vuemoji-picker to v0.3.1* Chore(deps): update goreleaser/nfpm docker tag to v2.40.0 (#2647)
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update github.com/wneessen/go-mail to v0.4.4
* *(deps)* Update golangci
* *(deps)* Update module dario.cat/mergo to v1.0.1
* *(deps)* Update module github.com/gabriel-vasile/mimetype to v1.4.5
* *(deps)* Update module github.com/getsentry/sentry-go to v0.29.0
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.23
* *(deps)* Update module github.com/prometheus/client_golang to v1.20.3
* *(deps)* Update module github.com/prometheus/client_golang to v1.20.4
* *(deps)* Update module github.com/redis/go-redis/v9 to v9.6.1
* *(deps)* Update module github.com/threedotslabs/watermill to v1.3.7
* *(deps)* Update module github.com/typesense/typesense-go to v2
* *(deps)* Update module github.com/typesense/typesense-go to v2
* *(deps)* Update module golang.org/x/crypto to v0.27.0
* *(deps)* Update module golang.org/x/image to v0.20.0
* *(deps)* Update module golang.org/x/oauth2 to v0.23.0
* *(deps)* Update module golang.org/x/term to v0.24.0
* *(deps)* Update module golang.org/x/text to v0.18.0
* *(deps)* Update pnpm to v9.10.0
* *(deps)* Update tiptap to 2.6.6
* *(deps)* Update tiptap to v2.7.0
* *(deps)* Update tiptap to v2.7.1
* *(deps)* Update tiptap to v2.7.2
* *(deps)* Update vueuse to v11
* *(deps)* Update vueuse to v11.1.0
* *(deps)*: update dependency flexsearch to v0.7.43 (#2095)
* *(deps)*: update golangci/golangci-lint docker tag to v1.61.0 (#2678)
### Documentation
* *(api)* Use correct return type for the /user endpoint
### Features
* *(event)* Simplify dispatching task updated event from only a task id
* *(navigation)* Use focus-visible for nav items
* *(task)* Use focus-visible for task focus styles
### Miscellaneous Tasks
* *(attachments)* Refactor building image preview
* *(devenv)* Do not install cypress on darwin
* *(docker)* Use new env format
* *(docs)* Clarify usage of related model creation
* *(errors)* Always add internal error to echo error
* *(files)* Use absolute file path to retrieve and save files
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(i18n)* Update translations via Crowdin
* *(logging)* Simplify log template string
* *(magefile)* Use tx.Sync instead of Sync2
* *(subscription)* Return subscription entity type using json Marshaler
* *(tasks)* Move drag options to direct attributes instead of v-bind
* *(typesense)* Add more debug logging
* *(web)* Move web handler package to Vikunja
* *(web)* Remove unused echo context
* *(web)* Use errors.As instead of type assertion
* *(web)* Remove redundant use of fmt.Sprintf
* *(web)* Directly use new db session
* *(web)* Use config directly
* *(web)* Use web auth factory directly
* *(web)* Use logger directly
* *(web)* Always set internal error* Remove console.log ([40105ee](40105ee4ced980f52565baec4c3219b0ddd4f6ec))
* Fix comment ([1df4a4e](1df4a4ea2e2ca4332347468e8973a2dcbab06ed7))
* Add go and direnv to recommended vscode extensions ([6ab12b9](6ab12b9dd133b52ed7267b6e9334081c2f9719ca))
* Remove console.log ([1e7d9c9](1e7d9c982d3d472e9b4082991b41e6567556f2b2))
* Rearrange cron registers ([4857bfb](4857bfbbdb8401b6ef02b1dc8de93f2a09e8bc3a))
### Other
* *(other)* [skip ci] Updated swagger docs
## [0.24.2] - 2024-08-12
### Bug Fixes
* *(i18n)* Change casing of Ukrainian language in selector
* *(kanban)* Always make cover image full width
* *(mail)* Do not fail testmail command when the connection could not be closed.
* *(migration)* Make sure tasks are associated to the correct view and bucket for data imported from Vikunja dump
* *(migration)* Ensure project background gets exported and imported
* *(projects)* Trigger only single mutation
* *(task)* Do not allow moving a task to the project the task already belongs to
* *(task)* Set current project after moving a task
* *(task)* Move task into new kanban bucket when moving between projects
* *(views)* Do not create task bucket and task position entries when duplicating a project* Emit for DatepickerWithValues ([3aaf363](3aaf3634134a6989337bad02ac99a9329d33b17f))
* Textarea autosize for LanguageTool ([d9f5555](d9f555554e5ecfa9d1243c565e2f42c77f7a7597))
* Remove console log ([0ca43dc](0ca43dc147acd04d9f9b566325ccde0a5782680f))
### Dependencies
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.11.0
* *(deps)* Update flake
* *(deps)* Update go toolchain to 1.22.5
* *(deps)* Update dependency node to v20.16.0
### Documentation
* Clarify Todoist redirect url ([7117303](7117303d5705199bb39fb6661b20deebea96959f))
### Features
* *(editor)* Support custom protocol for links* Use withDefaults for Reminders ([8729c24](8729c24e1d2bdc783e750dc1166a8ab31a716107))
* Improve projects store ([d707e15](d707e1576a006baa73c529b432e694dee237db76))
* Improve label store ([a0e3efe](a0e3efe2d12521e773334a19ac985a2011575c3c))
* Improve priority visibility ([dddba4d](dddba4d64a9ddddd6f442c8180661d2d498de994))
* Add tailwind with prefix (#2513) ([d7c5451](d7c54517297e750d92618376f52045b11da6e82c))
* Improve ProjectSettingsViews ([811ccc1](811ccc1baa2fe1fb9fcc8b5515c8c3f3b591e96a))
* Add missing peer dependency ([d586f69](d586f691b7a245bbffd5df4e7a4937f0527047e0))
* Switch from nix flakes to devenv ([73f923b](73f923bc47d32a6a9a689e461901295b07646ebf))
### Miscellaneous Tasks
* *(i18n)* Update translations via Crowdin
* Remove lodash.debounce ([fc780a9](fc780a90ae856038db73d3ad63fcdd5627211c36))
* Improve error message ([6e38bcf](6e38bcf3498a15a1dc1f6fbef254c08513b408b3))
* Use nixpkgs unstable for more recent packages ([f5040ad](f5040ad2f4996fe1ccf03172b1a1ee6007068ee7))
### Other
* *(other)* [skip ci] Updated swagger docs
## [0.24.1] - 2024-07-18

View File

@ -1,11 +1,11 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM node:20.14.0-alpine AS frontendbuilder
FROM --platform=$BUILDPLATFORM node:20.16.0-alpine AS frontendbuilder
WORKDIR /build
ENV PNPM_CACHE_FOLDER .cache/pnpm/
ENV PUPPETEER_SKIP_DOWNLOAD true
ENV CYPRESS_INSTALL_BINARY 0
ENV PNPM_CACHE_FOLDER=.cache/pnpm/
ENV PUPPETEER_SKIP_DOWNLOAD=true
ENV CYPRESS_INSTALL_BINARY=0
COPY frontend/ ./
@ -13,7 +13,7 @@ RUN corepack enable && \
pnpm install && \
pnpm run build
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.21.x AS apibuilder
FROM --platform=$BUILDPLATFORM ghcr.io/techknowlogick/xgo:go-1.23.x AS apibuilder
RUN go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin
@ -24,7 +24,7 @@ COPY --from=frontendbuilder /build/dist ./frontend/dist
ARG TARGETOS TARGETARCH TARGETVARIANT
ENV GOPROXY https://goproxy.kolaente.de
ENV GOPROXY=https://goproxy.kolaente.de
RUN export PATH=$PATH:$GOPATH/bin && \
mage build:clean && \
mage release:xgo "${TARGETOS}/${TARGETARCH}/${TARGETVARIANT}"

View File

@ -2,7 +2,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/vikunjaa/status.svg)](https://drone.kolaente.de/vikunja/vikunja)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Install](https://img.shields.io/badge/download-v0.24.1-brightgreen.svg)](https://vikunja.io/docs/installing)
[![Install](https://img.shields.io/badge/download-v0.24.6,brightgreen.svg)](https://vikunja.io/docs/installing)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/vikunja.svg)](https://hub.docker.com/r/vikunja/vikunja/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
[![Go Report Card](https://goreportcard.com/badge/kolaente.dev/vikunja/vikunja)](https://goreportcard.com/report/kolaente.dev/vikunja/vikunja)

View File

@ -227,12 +227,13 @@ migration:
clientid:
# The client secret, also required for making requests to the todoist api
clientsecret:
# The url where clients are redirected after they authorized Vikunja to access their todoist items.
# This needs to match the url you entered when registering your Vikunja instance at todoist.
# This is usually the frontend url where the frontend then makes a request to /migration/todoist/migrate
# The url where clients are redirected after they authorized Vikunja to access their Todoist items.
# In Todoist, this is called `OAuth redirect URL` and it needs to match the url you entered when registering
# your Vikunja instance at the Todoist developer console.
# When using the official Vikunja frontend, set this to <service.publicurl>/migrate/todoist.
# Otherwise, set this to an url which then makes a request to /api/v1/migration/todoist/migrate
# with the code obtained from the todoist api.
# Note that the Vikunja frontend expects this to be /migrate/todoist
redirecturl: <frontend url>/migrate/todoist
redirecturl: <service.publicurl>/migrate/todoist
trello:
# Whether to enable the trello migrator or not
enable: false

View File

@ -51,11 +51,11 @@
}
},
"devDependencies": {
"electron": "29.4.5",
"electron": "29.4.6",
"electron-builder": "24.13.3"
},
"dependencies": {
"connect-history-api-fallback": "2.0.0",
"express": "4.19.2"
"express": "4.21.0"
}
}

File diff suppressed because it is too large Load Diff

139
devenv.lock Normal file
View File

@ -0,0 +1,139 @@
{
"nodes": {
"devenv": {
"locked": {
"dir": "src/modules",
"lastModified": 1726063457,
"owner": "cachix",
"repo": "devenv",
"rev": "39bf6ce569103c9390d37322daa59468c31b3ce7",
"treeHash": "839747a1cb35ba6d5b36cce9a739ab2ba5e4a5d4",
"type": "github"
},
"original": {
"dir": "src/modules",
"owner": "cachix",
"repo": "devenv",
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1696426674,
"owner": "edolstra",
"repo": "flake-compat",
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
"treeHash": "2addb7b71a20a25ea74feeaf5c2f6a6b30898ecb",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"gitignore": {
"inputs": {
"nixpkgs": [
"pre-commit-hooks",
"nixpkgs"
]
},
"locked": {
"lastModified": 1709087332,
"owner": "hercules-ci",
"repo": "gitignore.nix",
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
"treeHash": "ca14199cabdfe1a06a7b1654c76ed49100a689f9",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "gitignore.nix",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1716977621,
"owner": "cachix",
"repo": "devenv-nixpkgs",
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
"treeHash": "6d9f1f7ca0faf1bc2eeb397c78a49623260d3412",
"type": "github"
},
"original": {
"owner": "cachix",
"ref": "rolling",
"repo": "devenv-nixpkgs",
"type": "github"
}
},
"nixpkgs-stable": {
"locked": {
"lastModified": 1725930920,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "44a71ff39c182edaf25a7ace5c9454e7cba2c658",
"treeHash": "56e93544112b7bb7aa0c3093d537295683ef9148",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.05",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs-unstable": {
"locked": {
"lastModified": 1725983898,
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "1355a0cbfeac61d785b7183c0caaec1f97361b43",
"treeHash": "d8ce0c1af7690da6161f0991484661331f5bc999",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"pre-commit-hooks": {
"inputs": {
"flake-compat": "flake-compat",
"gitignore": "gitignore",
"nixpkgs": [
"nixpkgs"
],
"nixpkgs-stable": "nixpkgs-stable"
},
"locked": {
"lastModified": 1725513492,
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"rev": "7570de7b9b504cfe92025dd1be797bf546f66528",
"treeHash": "4b46d77870afecd8f642541cb4f4927326343b59",
"type": "github"
},
"original": {
"owner": "cachix",
"repo": "pre-commit-hooks.nix",
"type": "github"
}
},
"root": {
"inputs": {
"devenv": "devenv",
"nixpkgs": "nixpkgs",
"nixpkgs-unstable": "nixpkgs-unstable",
"pre-commit-hooks": "pre-commit-hooks"
}
}
},
"root": "root",
"version": 7
}

32
devenv.nix Normal file
View File

@ -0,0 +1,32 @@
{ pkgs, lib, config, inputs, ... }:
let
pkgs-unstable = import inputs.nixpkgs-unstable { system = pkgs.stdenv.system; };
in {
packages = with pkgs-unstable; [
# General tools
git-cliff
# API tools
golangci-lint mage
# Desktop
electron
] ++ lib.optionals (!pkgs.stdenv.isDarwin) [
# Frontend tools (exclude on Darwin)
cypress
];
languages = {
javascript = {
enable = true;
package = pkgs-unstable.nodejs-slim;
pnpm = {
enable = true;
package = pkgs-unstable.pnpm;
};
};
go = {
enable = true;
};
};
}

17
devenv.yaml Normal file
View File

@ -0,0 +1,17 @@
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
inputs:
nixpkgs:
url: github:cachix/devenv-nixpkgs/rolling
nixpkgs-unstable:
url: github:NixOS/nixpkgs/nixos-unstable
# If you're using non-OSS software, you can set allowUnfree to true.
allowUnfree: true
# If you're willing to use a package that's vulnerable
# permittedInsecurePackages:
# - "openssl-1.1.1w"
# If you have more than one devenv you can merge them
#imports:
# - ./backend

View File

@ -1,25 +0,0 @@
{
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1721116560,
"narHash": "sha256-++TYlGMAJM1Q+0nMVaWBSEvEUjRs7ZGiNQOpqbQApCU=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "9355fa86e6f27422963132c2c9aeedb0fb963d93",
"type": "github"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"root": {
"inputs": {
"nixpkgs": "nixpkgs"
}
}
},
"root": "root",
"version": 7
}

View File

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

1
frontend/.gitignore vendored
View File

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

View File

@ -1 +1 @@
20.15.0
20.16.0

View File

@ -148,7 +148,7 @@ describe('Project View Kanban', () => {
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
.contains('Delete')
.click()
cy.get('.modal-mask .modal-container .modal-content .header')
cy.get('.modal-mask .modal-container .modal-content .modal-header')
.should('contain', 'Delete the bucket')
cy.get('.modal-mask .modal-container .modal-content .actions .button')
.contains('Do it!')
@ -251,7 +251,7 @@ describe('Project View Kanban', () => {
.should('be.visible')
.contains('Delete')
.click()
cy.get('.modal-mask .modal-container .modal-content .header')
cy.get('.modal-mask .modal-container .modal-content .modal-header')
.should('contain', 'Delete this task')
cy.get('.modal-mask .modal-container .modal-content .actions .button')
.contains('Do it!')

View File

@ -356,7 +356,7 @@ describe('Task', () => {
.should('be.visible')
.contains('Delete')
.click()
cy.get('.modal-mask .modal-container .modal-content .header')
cy.get('.modal-mask .modal-container .modal-content .modal-header')
.should('contain', 'Delete this task')
cy.get('.modal-mask .modal-container .modal-content .actions .button')
.contains('Do it!')
@ -484,7 +484,7 @@ describe('Task', () => {
addLabelToTaskAndVerify(labels[0].title)
cy.get('.modal-content .close')
cy.get('.modal-container > .close')
.click()
cy.get('.bucket .task')
@ -850,7 +850,7 @@ describe('Task', () => {
uploadAttachmentAndVerify(tasks[0].id)
cy.get('.modal-content .close')
cy.get('.modal-container > .close')
.click()
cy.get('.bucket .task .footer .icon svg.fa-paperclip')

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@9.5.0",
"packageManager": "pnpm@9.10.0",
"keywords": [
"todo",
"productivity",
@ -56,131 +56,131 @@
"@fortawesome/vue-fontawesome": "3.0.8",
"@github/hotkey": "3.1.1",
"@infectoone/vue-ganttastic": "2.3.2",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@kyvg/vue3-notification": "3.2.1",
"@intlify/unplugin-vue-i18n": "5.2.0",
"@kyvg/vue3-notification": "3.3.0",
"@sentry/tracing": "7.114.0",
"@sentry/vue": "8.18.0",
"@tiptap/core": "2.5.4",
"@tiptap/extension-blockquote": "2.5.4",
"@tiptap/extension-bold": "2.5.4",
"@tiptap/extension-bullet-list": "2.5.4",
"@tiptap/extension-code": "2.5.4",
"@tiptap/extension-code-block-lowlight": "2.5.4",
"@tiptap/extension-document": "2.5.4",
"@tiptap/extension-dropcursor": "2.5.4",
"@tiptap/extension-gapcursor": "2.5.4",
"@tiptap/extension-hard-break": "2.5.4",
"@tiptap/extension-heading": "2.5.4",
"@tiptap/extension-history": "2.5.4",
"@tiptap/extension-horizontal-rule": "2.5.4",
"@tiptap/extension-image": "2.5.4",
"@tiptap/extension-italic": "2.5.4",
"@tiptap/extension-link": "2.5.4",
"@tiptap/extension-list-item": "2.5.4",
"@tiptap/extension-ordered-list": "2.5.4",
"@tiptap/extension-paragraph": "2.5.4",
"@tiptap/extension-placeholder": "2.5.4",
"@tiptap/extension-strike": "2.5.4",
"@tiptap/extension-table": "2.5.4",
"@tiptap/extension-table-cell": "2.5.4",
"@tiptap/extension-table-header": "2.5.4",
"@tiptap/extension-table-row": "2.5.4",
"@tiptap/extension-task-item": "2.5.4",
"@tiptap/extension-task-list": "2.5.4",
"@tiptap/extension-text": "2.5.4",
"@tiptap/extension-typography": "2.5.4",
"@tiptap/extension-underline": "2.5.4",
"@tiptap/pm": "2.5.4",
"@tiptap/suggestion": "2.5.4",
"@tiptap/vue-3": "2.5.4",
"@vueuse/core": "10.11.0",
"@vueuse/router": "10.11.0",
"axios": "1.7.2",
"@sentry/vue": "8.32.0",
"@tiptap/core": "2.7.4",
"@tiptap/extension-blockquote": "2.7.4",
"@tiptap/extension-bold": "2.7.4",
"@tiptap/extension-bullet-list": "2.7.4",
"@tiptap/extension-code": "2.7.4",
"@tiptap/extension-code-block": "2.7.4",
"@tiptap/extension-code-block-lowlight": "2.7.4",
"@tiptap/extension-document": "2.7.4",
"@tiptap/extension-dropcursor": "2.7.4",
"@tiptap/extension-gapcursor": "2.7.4",
"@tiptap/extension-hard-break": "2.7.4",
"@tiptap/extension-heading": "2.7.4",
"@tiptap/extension-history": "2.7.4",
"@tiptap/extension-horizontal-rule": "2.7.4",
"@tiptap/extension-image": "2.7.4",
"@tiptap/extension-italic": "2.7.4",
"@tiptap/extension-link": "2.7.4",
"@tiptap/extension-list-item": "2.7.4",
"@tiptap/extension-ordered-list": "2.7.4",
"@tiptap/extension-paragraph": "2.7.4",
"@tiptap/extension-placeholder": "2.7.4",
"@tiptap/extension-strike": "2.7.4",
"@tiptap/extension-table": "2.7.4",
"@tiptap/extension-table-cell": "2.7.4",
"@tiptap/extension-table-header": "2.7.4",
"@tiptap/extension-table-row": "2.7.4",
"@tiptap/extension-task-item": "2.7.4",
"@tiptap/extension-task-list": "2.7.4",
"@tiptap/extension-text": "2.7.4",
"@tiptap/extension-typography": "2.7.4",
"@tiptap/extension-underline": "2.7.4",
"@tiptap/pm": "2.7.4",
"@tiptap/suggestion": "2.7.4",
"@tiptap/vue-3": "2.7.4",
"@vueuse/core": "11.1.0",
"@vueuse/router": "11.1.0",
"axios": "1.7.7",
"blurhash": "2.0.5",
"bulma-css-variables": "0.9.33",
"change-case": "5.4.4",
"date-fns": "3.6.0",
"dayjs": "1.11.12",
"dompurify": "3.1.6",
"date-fns": "4.1.0",
"dayjs": "1.11.13",
"dompurify": "3.1.7",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
"flexsearch": "0.7.43",
"floating-vue": "5.2.2",
"is-touch-device": "1.0.1",
"klona": "2.0.6",
"lodash.debounce": "4.0.8",
"lowlight": "2.9.0",
"pinia": "2.1.7",
"pinia": "2.2.2",
"register-service-worker": "1.7.2",
"sortablejs": "1.15.2",
"sortablejs": "1.15.3",
"tailwindcss": "3.4.13",
"tippy.js": "6.3.7",
"ufo": "1.5.4",
"vue": "3.4.32",
"vue": "3.5.10",
"vue-advanced-cropper": "2.8.9",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "9.13.1",
"vue-router": "4.4.0",
"vuemoji-picker": "0.2.1",
"vue-i18n": "10.0.3",
"vue-router": "4.4.5",
"vuemoji-picker": "0.3.1",
"workbox-precaching": "7.1.0",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
"@4tw/cypress-drag-drop": "2.2.5",
"@cypress/vite-dev-server": "5.1.1",
"@cypress/vite-dev-server": "5.2.0",
"@cypress/vue": "6.0.1",
"@faker-js/faker": "8.4.1",
"@histoire/plugin-screenshot": "0.17.17",
"@histoire/plugin-vue": "0.17.17",
"@rushstack/eslint-patch": "1.10.3",
"@rushstack/eslint-patch": "1.10.4",
"@tsconfig/node20": "20.1.4",
"@types/codemirror": "5.60.15",
"@types/dompurify": "3.0.5",
"@types/flexsearch": "0.7.6",
"@types/is-touch-device": "1.0.2",
"@types/is-touch-device": "1.0.3",
"@types/lodash.clonedeep": "4.5.9",
"@types/lodash.debounce": "4.0.9",
"@types/node": "20.14.11",
"@types/node": "20.16.10",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.16.1",
"@typescript-eslint/parser": "7.16.1",
"@vitejs/plugin-legacy": "5.4.1",
"@vitejs/plugin-vue": "5.0.5",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
"@vitejs/plugin-legacy": "5.4.2",
"@vitejs/plugin-vue": "5.1.4",
"@vue/eslint-config-typescript": "13.0.0",
"@vue/test-utils": "2.4.6",
"@vue/tsconfig": "0.5.1",
"autoprefixer": "10.4.19",
"browserslist": "4.23.2",
"caniuse-lite": "1.0.30001642",
"autoprefixer": "10.4.20",
"browserslist": "4.24.0",
"caniuse-lite": "1.0.30001664",
"csstype": "3.1.3",
"cypress": "13.13.1",
"esbuild": "0.23.0",
"eslint": "8.57.0",
"eslint-plugin-vue": "9.27.0",
"cypress": "13.15.0",
"esbuild": "0.24.0",
"eslint": "8.57.1",
"eslint-plugin-vue": "9.28.0",
"happy-dom": "14.12.3",
"histoire": "0.17.17",
"postcss": "8.4.39",
"postcss": "8.4.47",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-preset-env": "9.6.0",
"rollup": "4.18.1",
"rollup": "4.22.5",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.77.8",
"start-server-and-test": "2.0.4",
"typescript": "5.5.3",
"unplugin-inject-preload": "2.0.4",
"vite": "5.3.4",
"vite-plugin-pwa": "0.20.0",
"sass": "1.79.4",
"start-server-and-test": "2.0.8",
"typescript": "5.6.2",
"unplugin-inject-preload": "2.0.5",
"vite": "5.4.8",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.6.0",
"vue-tsc": "2.0.26",
"vue-tsc": "2.1.6",
"wait-on": "7.2.0",
"workbox-cli": "7.1.0"
},
"pnpm": {
"patchedDependencies": {
"flexsearch@0.7.31": "patches/flexsearch@0.7.31.patch",
"@github/hotkey@3.1.1": "patches/@github__hotkey@3.1.1.patch"
"@github/hotkey@3.1.1": "patches/@github__hotkey@3.1.1.patch",
"flexsearch@0.7.43": "patches/flexsearch@0.7.43.patch"
}
}
}

View File

@ -1,16 +0,0 @@
diff --git a/index.d.ts b/index.d.ts
deleted file mode 100644
index 9f39f41073864b83968bdaa242ac4e3c3149685a..0000000000000000000000000000000000000000
diff --git a/package.json b/package.json
index 8968f5bf8010ff194240591c8b83299f7328e79d..6d84b6f590a841b129ed8b3860cb786df5a185c0 100644
--- a/package.json
+++ b/package.json
@@ -22,8 +22,6 @@
},
"main": "dist/flexsearch.bundle.js",
"browser": "dist/flexsearch.bundle.js",
- "module": "dist/module/index.js",
- "types": "./index.d.ts",
"preferGlobal": false,
"repository": {
"type": "git",

View File

@ -0,0 +1,18 @@
diff --git a/package.json b/package.json
index c154e54029c94be444916fb2249941e7182d80ed..54a65c42a42c4627506e016132becc43b47a517c 100644
--- a/package.json
+++ b/package.json
@@ -28,13 +28,11 @@
"email": "info@nextapps.de"
},
"main": "dist/flexsearch.bundle.min.js",
- "module": "dist/flexsearch.bundle.module.min.js",
"browser": {
"dist/flexsearch.bundle.min.js": "./dist/flexsearch.bundle.min.js",
"dist/flexsearch.bundle.module.min.js": "./dist/flexsearch.bundle.module.min.js",
"worker_threads": false
},
- "types": "./index.d.ts",
"scripts": {
"build": "npm run copy && npm run build:bundle",
"build:bundle": "node task/build RELEASE=bundle DEBUG=false SUPPORT_WORKER=true SUPPORT_ENCODER=true SUPPORT_CACHE=true SUPPORT_ASYNC=true SUPPORT_STORE=true SUPPORT_TAGS=true SUPPORT_SUGGESTION=true SUPPORT_SERIALIZE=true SUPPORT_DOCUMENT=true POLYFILL=false",

File diff suppressed because it is too large Load Diff

View File

@ -3,33 +3,22 @@
v-cy="'checkbox'"
class="base-checkbox"
>
<input
:id="checkboxId"
type="checkbox"
class="is-sr-only"
:checked="modelValue"
:disabled="disabled || undefined"
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
<label
class="base-checkbox__label"
>
<slot
name="label"
:checkbox-id="checkboxId"
>
<label
:for="checkboxId"
class="base-checkbox__label"
<input
type="checkbox"
class="is-sr-only"
:checked="modelValue"
:disabled="disabled || undefined"
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
>
<slot />
</label>
</slot>
<slot />
</label>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import {createRandomID} from '@/helpers/randomId'
withDefaults(defineProps<{
modelValue?: boolean,
disabled: boolean,
@ -38,10 +27,8 @@ withDefaults(defineProps<{
})
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void
(event: 'update:modelValue', value: boolean): void
}>()
const checkboxId = ref(`checkbox_${createRandomID()}`)
</script>
<style lang="scss" scoped>
@ -53,7 +40,7 @@ const checkboxId = ref(`checkbox_${createRandomID()}`)
}
.base-checkbox:has(input:disabled) .base-checkbox__label {
cursor:not-allowed;
cursor: not-allowed;
pointer-events: none;
}
</style>

View File

@ -0,0 +1,97 @@
<template>
<nav
v-if="totalPages > 1"
aria-label="pagination"
class="pagination is-centered p-4"
role="navigation"
>
<slot
name="previous"
:disabled="currentPage === 1"
>
{{ $t('misc.previous') }}
</slot>
<slot
name="next"
:disabled="currentPage === totalPages"
>
{{ $t('misc.next') }}
</slot>
<ul class="pagination-list">
<li
v-for="(p, i) in pages"
:key="`page-${i}`"
>
<span
v-if="p.isEllipsis"
class="pagination-ellipsis"
>&hellip;</span>
<slot
v-else
name="page-link"
:page="p"
:is-current="p.number === currentPage"
>
{{ p.number }}
</slot>
</li>
</ul>
</nav>
</template>
<script lang="ts" setup>
import {computed} from 'vue'
const props = defineProps<{
totalPages: number,
currentPage: number
}>()
function createPagination(totalPages: number, currentPage: number) {
const pages = []
for (let i = 0; i < totalPages; i++) {
if (
i > 0 &&
(i + 1) < totalPages &&
((i + 1) > currentPage + 1 || (i + 1) < currentPage - 1)
) {
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
pages.push({
number: 0,
isEllipsis: true,
})
}
continue
}
pages.push({
number: i + 1,
isEllipsis: false,
})
}
return pages
}
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
</script>
<style lang="scss" scoped>
.pagination {
padding-bottom: 1rem;
}
.pagination-previous,
.pagination-next {
&:not(:disabled):hover {
background: $scheme-main;
cursor: pointer;
}
}
.pagination-list {
&, & li {
margin-top: 0;
}
}
</style>

View File

@ -2,7 +2,7 @@
<div class="datepicker-with-range-container">
<Popup
:open="open"
@update:open="(open) => !open && emit('close')"
@update:open="(open) => !open && $emit('update:open', false)"
>
<template #content="{isOpen}">
<div

View File

@ -69,6 +69,10 @@ const shouldShowMessage = computed(() => {
@media screen and (min-width: $tablet) {
display: none;
}
@media print {
display: none;
}
&.has-update-available {
bottom: 5rem;

View File

@ -48,7 +48,10 @@
class="task-detail-view-modal"
@close="closeModal()"
>
<component :is="currentModal" />
<component
:is="currentModal"
@close="closeModal()"
/>
</Modal>
<BaseButton

View File

@ -3,7 +3,7 @@
class="list-menu loader-container is-loading-small"
:class="{'is-loading': isLoading}"
>
<div>
<div class="navigation-item">
<BaseButton
v-if="canCollapse && childProjects?.length > 0"
class="collapse-project-button"
@ -193,4 +193,18 @@ const childProjects = computed(() => {
.is-touch .handle.has-color-bubble {
display: none !important;
}
.navigation-item:has(*:focus-visible) {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
background-color: var(--white);
.favorite, .menu-list-dropdown {
opacity: 1;
}
}
.navigation-item a:focus-visible {
// The focus ring is already added to the navigation-item, so we don't need to add it again.
box-shadow: none;
}
</style>

View File

@ -38,7 +38,6 @@ const emit = defineEmits<{
}>()
</script>
<style lang="scss" scoped>
.fancy-checkbox {
display: inline-block;
@ -70,8 +69,8 @@ const emit = defineEmits<{
}
}
.fancy-checkbox:not(:has(input:disabled)):hover .fancy-checkbox__icon,
.fancy-checkbox:has(input:checked) .fancy-checkbox__icon {
.fancy-checkbox:hover input:not(:disabled) + .fancy-checkbox__icon,
.fancy-checkbox input:checked + .fancy-checkbox__icon {
--stroke-color: var(--primary);
}
</style>
@ -80,13 +79,13 @@ const emit = defineEmits<{
// Since css-has-pseudo doesn't work with deep classes,
// the following rules can't be scoped
.fancy-checkbox:has(:not(input:checked)) .fancy-checkbox__icon {
.fancy-checkbox :not(input:checked) + .fancy-checkbox__icon {
path {
transition-delay: 0.05s;
}
}
.fancy-checkbox:has(input:checked) .fancy-checkbox__icon {
.fancy-checkbox input:checked + .fancy-checkbox__icon {
path {
stroke-dashoffset: 60;
}

View File

@ -2,8 +2,9 @@
<div
ref="multiselectRoot"
class="multiselect"
:class="{'has-search-results': searchResultsVisible}"
:class="{'has-search-results': searchResultsVisible, 'is-disabled': disabled}"
tabindex="-1"
:aria-disabled="disabled"
@focus="focus"
>
<div
@ -12,7 +13,7 @@
>
<div
class="input-wrapper input"
:class="{'has-multiple': hasMultiple, 'has-removal-button': removalAvailable}"
:class="{'has-multiple': hasMultiple, 'has-removal-button': removalAvailable && !disabled}"
>
<slot
v-if="Array.isArray(internalValue)"
@ -31,6 +32,7 @@
>
{{ label !== '' ? item[label] : item }}
<BaseButton
v-if="!disabled"
class="delete is-small"
@click="() => remove(item)"
/>
@ -40,6 +42,7 @@
</slot>
<input
v-if="!disabled"
:id="id"
ref="searchInput"
v-model="query"
@ -55,7 +58,7 @@
@focus="handleFocus"
>
<BaseButton
v-if="removalAvailable"
v-if="removalAvailable && !disabled"
class="removal-button"
@click="resetSelectedValue"
>
@ -151,7 +154,7 @@ const props = withDefaults(defineProps<{
/** The text shown next to the new value option. */
createPlaceholder?: string
/** The text shown next to an option. */
selectPlaceholder: string
selectPlaceholder?: string
/** If true, allows for selecting multiple items. v-model will be an array with all selected values in that case. */
multiple?: boolean
/** If true, displays the search results inline instead of using a dropdown. */
@ -164,6 +167,8 @@ const props = withDefaults(defineProps<{
closeAfterSelect?: boolean
/** If false, the search input will get the autocomplete="off" attributes attached to it. */
autocompleteEnabled?: boolean
/** If true, disables the multiselect input */
disabled?: boolean
}>(), {
modelValue: null,
loading: false,
@ -179,6 +184,7 @@ const props = withDefaults(defineProps<{
searchDelay: 200,
closeAfterSelect: true,
autocompleteEnabled: true,
disabled: false,
})
const emit = defineEmits<{
@ -448,6 +454,18 @@ function focus() {
.control.is-loading::after {
top: .75rem;
}
&.is-disabled {
.input-wrapper, .input, & {
cursor: default !important;
&:focus-within,&:focus-visible, &:hover, &:focus {
border-color: transparent !important;
background: transparent !important;
box-shadow: none;
}
}
}
}
.input-wrapper {

View File

@ -36,6 +36,7 @@ import {ref, watchEffect} from 'vue'
import {useDebounceFn} from '@vueuse/core'
import {useI18n} from 'vue-i18n'
import BaseButton from '@/components/base/BaseButton.vue'
import {validatePassword} from '@/helpers/validatePasswort'
const props = withDefaults(defineProps<{
modelValue: string,
@ -60,22 +61,8 @@ const validateAfterFirst = ref(false)
watchEffect(() => props.validateInitially && validate())
const validate = useDebounceFn(() => {
if (password.value === '') {
isValid.value = t('user.auth.passwordRequired')
return
}
if (props.validateMinLength && password.value.length < 8) {
isValid.value = t('user.auth.passwordNotMin')
return
}
if (props.validateMinLength && password.value.length > 250) {
isValid.value = t('user.auth.passwordNotMax')
return
}
isValid.value = true
const valid = validatePassword(password.value, props.validateMinLength)
isValid.value = valid === true ? true : t(valid)
}, 100)
function togglePasswordFieldType() {

View File

@ -314,7 +314,17 @@ const internalMode = ref<Mode>('preview')
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
const contentHasChanged = ref<boolean>(false)
let lastSavedState = modelValue
let lastSavedState = ''
watch(
() => modelValue,
(newValue) => {
if (!contentHasChanged.value) {
lastSavedState = newValue
}
},
{ immediate: true },
)
watch(
() => internalMode.value,
@ -325,6 +335,13 @@ watch(
},
)
const additionalLinkProtocols = [
'ftp',
'git',
'obsidian',
'notion',
]
const extensions : Extensions = [
// Starterkit:
Blockquote,
@ -379,7 +396,11 @@ const extensions : Extensions = [
Underline,
Link.configure({
openOnClick: false,
validate: (href: string) => /^https?:\/\//.test(href),
validate: (href: string) => (new RegExp(
`^(https?|${additionalLinkProtocols.join('|')}):\\/\\/`,
'i',
)).test(href),
protocols: additionalLinkProtocols,
}),
Table.configure({
resizable: true,

View File

@ -28,7 +28,7 @@ import {
faDownload,
faEllipsisH,
faEllipsisV,
faExclamation,
faExclamationCircle,
faEye,
faEyeSlash,
faFile,
@ -138,7 +138,7 @@ library.add(faCopy)
library.add(faDownload)
library.add(faEllipsisH)
library.add(faEllipsisV)
library.add(faExclamation)
library.add(faExclamationCircle)
library.add(faEye)
library.add(faEyeSlash)
library.add(faFillDrip)

View File

@ -20,6 +20,12 @@
class="modal-container"
@mousedown.self.prevent.stop="$emit('close')"
>
<BaseButton
class="close"
@click="$emit('close')"
>
<Icon icon="times" />
</BaseButton>
<div
class="modal-content"
:class="{
@ -27,15 +33,8 @@
'is-wide': wide
}"
>
<BaseButton
class="close"
@click="$emit('close')"
>
<Icon icon="times" />
</BaseButton>
<slot>
<div class="header">
<div class="modal-header">
<slot name="header" />
</div>
<div class="content">
@ -137,11 +136,11 @@ $modal-width: 1024px;
@media screen and (max-width: $tablet) {
margin: 0;
top: 25%;
transform: translate(-50%, -25%);
position: static;
transform: none;
}
.header {
.modal-header {
font-size: 2rem;
font-weight: 700;
}
@ -154,12 +153,15 @@ $modal-width: 1024px;
// scrolling-content
// used e.g. for <TaskDetailViewModal>
.scrolling .modal-content {
max-width: $modal-width;
width: 100%;
margin: $modal-margin auto;
max-height: none; // reset bulma
overflow: visible; // reset bulma
@media not print {
max-width: $modal-width;
}
@media screen and (min-width: $tablet) {
max-height: none; // reset bulma
@ -167,7 +169,7 @@ $modal-width: 1024px;
width: 100%;
}
@media screen and (max-width: $desktop) {
@media screen and (max-width: $desktop), print {
margin: 0;
}
}
@ -194,7 +196,7 @@ $modal-width: 1024px;
position: fixed;
top: .5rem;
right: $close-button-padding;
color: var(--grey-900);
color: var(--white);
font-size: 2rem;
@media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
@ -203,15 +205,50 @@ $modal-width: 1024px;
// we align the close button to the modal until there is enough space outside for it
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
}
// we can only use light color when there is enough space for the close button next to the modal
@media screen and (min-width: calc(#{$desktop } + #{$close-button-min-space})) {
color: var(--white);
}
@media screen and (min-width: $tablet) and (max-width: #{$desktop + $close-button-min-space}) {
top: .75rem;
}
}
@media print, screen and (max-width: $tablet) {
.modal-mask {
position: static;
overflow: visible !important;
}
.modal-container {
height: auto;
min-height: 100vh;
}
.modal-content {
position: static;
max-height: none;
}
.close {
display: none;
}
:deep(.card) {
border: none !important;
border-radius: 0 !important;
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: space-between;
margin-bottom: 0 !important;
}
}
.modal-content:has(.modal-header) {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 1rem;
min-height: 100vh
}
</style>
<style lang="scss">
@ -219,4 +256,10 @@ $modal-width: 1024px;
.dark .close {
color: var(--grey-900);
}
</style>
@media print, screen and (max-width: $tablet) {
body:has(.modal-mask) #app {
display: none;
}
}
</style>

View File

@ -1,89 +1,49 @@
<template>
<nav
v-if="totalPages > 1"
aria-label="pagination"
class="pagination is-centered p-4"
role="navigation"
<BasePagination
:total-pages="totalPages"
:current-page="currentPage"
>
<RouterLink
:disabled="currentPage === 1 || undefined"
:to="getRouteForPagination(currentPage - 1)"
class="pagination-previous"
>
{{ $t('misc.previous') }}
</RouterLink>
<RouterLink
:disabled="currentPage === totalPages || undefined"
:to="getRouteForPagination(currentPage + 1)"
class="pagination-next"
>
{{ $t('misc.next') }}
</RouterLink>
<ul class="pagination-list">
<li
v-for="(p, i) in pages"
:key="`page-${i}`"
<template #previous="{ disabled }">
<RouterLink
:disabled="disabled || undefined"
:to="getRouteForPagination(currentPage - 1)"
class="pagination-previous"
>
<span
v-if="p.isEllipsis"
class="pagination-ellipsis"
>&hellip;</span>
<RouterLink
v-else
class="pagination-link"
:aria-label="'Goto page ' + p.number"
:class="{ 'is-current': p.number === currentPage }"
:to="getRouteForPagination(p.number)"
>
{{ p.number }}
</RouterLink>
</li>
</ul>
</nav>
{{ $t('misc.previous') }}
</RouterLink>
</template>
<template #next="{ disabled }">
<RouterLink
:disabled="disabled || undefined"
:to="getRouteForPagination(currentPage + 1)"
class="pagination-next"
>
{{ $t('misc.next') }}
</RouterLink>
</template>
<template #page-link="{ page, isCurrent }">
<RouterLink
class="pagination-link"
:aria-label="'Goto page ' + page.number"
:class="{ 'is-current': isCurrent }"
:to="getRouteForPagination(page.number)"
>
{{ page.number }}
</RouterLink>
</template>
</BasePagination>
</template>
<script lang="ts" setup>
import {computed} from 'vue'
import BasePagination from '@/components/base/BasePagination.vue'
const props = withDefaults(defineProps<{
withDefaults(defineProps<{
totalPages: number,
currentPage?: number
}>(), {
currentPage: 0,
})
function createPagination(totalPages: number, currentPage: number) {
const pages = []
for (let i = 0; i < totalPages; i++) {
// Show ellipsis instead of all pages
if (
i > 0 && // Always at least the first page
(i + 1) < totalPages && // And the last page
(
// And the current with current + 1 and current - 1
(i + 1) > currentPage + 1 ||
(i + 1) < currentPage - 1
)
) {
// Only add an ellipsis if the last page isn't already one
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
pages.push({
number: 0,
isEllipsis: true,
})
}
continue
}
pages.push({
number: i + 1,
isEllipsis: false,
})
}
return pages
}
function getRouteForPagination(page = 1, type = null) {
return {
name: type,
@ -95,20 +55,4 @@ function getRouteForPagination(page = 1, type = null) {
},
}
}
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
</script>
<style lang="scss" scoped>
.pagination {
padding-bottom: 1rem;
}
.pagination-previous,
.pagination-next {
&:not(:disabled):hover {
background: $scheme-main;
cursor: pointer;
}
}
</style>
</script>

View File

@ -0,0 +1,55 @@
<template>
<BasePagination
:total-pages="totalPages"
:current-page="currentPage"
>
<template #previous="{ disabled }">
<BaseButton
:disabled="disabled"
class="pagination-previous"
@click="changePage(currentPage - 1)"
>
{{ $t('misc.previous') }}
</BaseButton>
</template>
<template #next="{ disabled }">
<BaseButton
:disabled="disabled"
class="pagination-next"
@click="changePage(currentPage + 1)"
>
{{ $t('misc.next') }}
</BaseButton>
</template>
<template #page-link="{ page, isCurrent }">
<BaseButton
class="pagination-link"
:aria-label="'Goto page ' + page.number"
:class="{ 'is-current': isCurrent }"
@click="changePage(page.number)"
>
{{ page.number }}
</BaseButton>
</template>
</BasePagination>
</template>
<script lang="ts" setup>
import BasePagination from '@/components/base/BasePagination.vue'
import BaseButton from '@/components/base/BaseButton.vue'
const props = withDefaults(defineProps<{
totalPages: number,
currentPage: number
}>(), {
currentPage: 1,
})
const emit = defineEmits(['pageChanged'])
function changePage(page: number) {
if (page >= 1 && page <= props.totalPages) {
emit('pageChanged', page)
}
}
</script>

View File

@ -218,33 +218,35 @@ 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([], search)
}
if (matched.startsWith('assignee')) {
autocompleteResultType.value = 'assignees'
if (props.projectId) {
projectUserService.getAll({projectId: props.projectId}, {s: search})
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
} else {
userService.getAll({}, {s: search})
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
}
}
if (!props.projectId && matched.startsWith('project')) {
autocompleteResultType.value = 'projects'
autocompleteResults.value = projectStore.searchProject(search)
}
autocompleteMatchText.value = keyword
autocompleteMatchPosition.value = match.index + prefix.length - 1 + keyword.replace(search, '').length
if(!keyword) {
return
}
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([], search)
}
if (matched.startsWith('assignee')) {
autocompleteResultType.value = 'assignees'
if (props.projectId) {
projectUserService.getAll({projectId: props.projectId}, {s: search})
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
} else {
userService.getAll({}, {s: search})
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
}
}
if (!props.projectId && matched.startsWith('project')) {
autocompleteResultType.value = 'projects'
autocompleteResults.value = projectStore.searchProject(search)
}
autocompleteMatchText.value = keyword
autocompleteMatchPosition.value = match.index + prefix.length - 1 + keyword.replace(search, '').length
})
}

View File

@ -112,7 +112,7 @@ watch(
const val = {...value}
val.filter = transformFilterStringFromApi(
val?.filter || '',
labelId => labelStore.getLabelById(labelId)?.title,
labelId => labelStore.getLabelById(labelId)?.title || null,
projectId => projectStore.projects[projectId]?.title || null,
)
params.value = val

View File

@ -35,7 +35,7 @@
<BaseButton
class="project-button"
:aria-label="project.title"
:title="project.description"
:title="textOnlyDescription"
:to="{
name: 'project.index',
params: { projectId: project.id}
@ -53,6 +53,7 @@
</template>
<script lang="ts" setup>
import {computed} from 'vue'
import type {IProject} from '@/modelTypes/IProject'
import BaseButton from '@/components/base/BaseButton.vue'
@ -68,6 +69,10 @@ const props = defineProps<{
const {background, blurHashUrl} = useProjectBackground(() => props.project)
const projectStore = useProjectStore()
const textOnlyDescription = computed(() => {
return props.project.description ? props.project.description.replace(/<[^>]*>/g, '') : ''
})
</script>
<style lang="scss" scoped>

View File

@ -1,5 +1,8 @@
<template>
<ul class="project-grid">
<ul
class="project-grid"
:class="{ 'show-even-number-of-projects': showEvenNumberOfProjects }"
>
<li
v-for="(item, index) in filteredProjects"
:key="`project_${item.id}_${index}`"
@ -19,11 +22,13 @@ import ProjectCard from './ProjectCard.vue'
const props = withDefaults(defineProps<{
projects: IProject[],
showArchived?: boolean,
itemLimit?: boolean
itemLimit?: boolean,
showEvenNumberOfProjects?: boolean,
}>(), {
projects: () => [],
showArchived: false,
itemLimit: false,
showEvenNumberOfProjects: false,
})
const filteredProjects = computed(() => {
@ -58,9 +63,13 @@ const filteredProjects = computed(() => {
@media screen and (min-width: $widescreen) {
--project-grid-columns: 5;
}
.project-grid-item:nth-child(6) {
display: none;
&.show-even-number-of-projects {
@media screen and (min-width: $widescreen) {
.project-grid-item:nth-child(5) {
display: none;
}
}
}
}

View File

@ -41,7 +41,7 @@
@click="() => unCollapseBucket(bucket)"
>
<span
v-if="view?.doneBucketId === bucket.id"
v-if="bucket.id !== 0 && view?.doneBucketId === bucket.id"
v-tooltip="$t('project.kanban.doneBucketHint')"
class="icon is-small has-text-success mr-2"
>
@ -512,6 +512,7 @@ async function updateTaskPosition(e) {
taskId: newTask.id,
})
await taskPositionService.value.update(newPosition)
newTask.position = position
if(bucketHasChanged) {
const updatedTaskBucket = await taskBucketService.value.update(new TaskBucketModel({
@ -524,8 +525,8 @@ async function updateTaskPosition(e) {
if (updatedTaskBucket.bucketId !== newTask.bucketId) {
kanbanStore.moveTaskToBucket(newTask, updatedTaskBucket.bucketId)
}
kanbanStore.setTaskInBucket(newTask)
}
kanbanStore.setTaskInBucket(newTask)
// Make sure the first and second task don't both get position 0 assigned
if (newTaskIndex === 0 && taskAfter !== null && taskAfter.position === 0) {
@ -546,6 +547,9 @@ async function updateTaskPosition(e) {
}
function toggleShowNewTaskInput(bucketId: IBucket['id']) {
if (loading.value || taskLoading.value) {
return
}
showNewTaskInput.value[bucketId] = !showNewTaskInput.value[bucketId]
newTaskInputFocused.value = false
}
@ -562,6 +566,7 @@ async function addTaskToBucket(bucketId: IBucket['id']) {
bucketId,
projectId: project.value.id,
})
toggleShowNewTaskInput(bucketId)
newTaskText.value = ''
kanbanStore.addTaskToBucket(task)
scrollTaskContainerToBottom(bucketId)
@ -766,6 +771,18 @@ function unCollapseBucket(bucket: IBucket) {
}
</script>
<style lang="scss" scoped>
.control.is-loading {
&::after {
top: 30%;
right: 50%;
transform: translate(-50%, 0);
--loader-border-color: var(--grey-500);
}
}
</style>
<style lang="scss">
$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);
$bucket-width: 300px;
@ -788,8 +805,9 @@ $filter-container-height: '1rem - #{$switch-view-height}';
}
@media screen and (max-width: $tablet) {
height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
height: calc(#{$crazy-height-calculation} - #{$filter-container-height} + 9px);
scroll-snap-type: x mandatory;
margin: 0 -0.5rem;
}
&-bucket-container {
@ -867,7 +885,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
// to hide the fact we just made the button smaller.
min-width: calc(#{$bucket-width} + 1rem);
background: transparent;
padding-right: 1rem;
.button {
background: var(--grey-100);

View File

@ -45,7 +45,6 @@
<draggable
v-if="tasks && tasks.length > 0"
v-bind="DRAG_OPTIONS"
v-model="tasks"
group="tasks"
handle=".handle"
@ -59,6 +58,8 @@
},
type: 'transition-group'
}"
:animation="100"
ghost-class="task-ghost"
@start="() => drag = true"
@end="saveTaskPosition"
>
@ -73,7 +74,7 @@
>
<template v-if="canWrite">
<span class="icon handle">
<Icon icon="grip-lines"/>
<Icon icon="grip-lines" />
</span>
</template>
</SingleTaskInProject>
@ -128,10 +129,6 @@ const props = defineProps<{
const ctaVisible = ref(false)
const drag = ref(false)
const DRAG_OPTIONS = {
animation: 100,
ghostClass: 'task-ghost',
} as const
const {
tasks: allTasks,

View File

@ -289,6 +289,7 @@ 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'
import { camelCase } from 'change-case'
const props = defineProps<{
projectId: IProject['id'],
@ -355,7 +356,7 @@ function sort(property: keyof SortBy) {
function setActiveColumnsSortParam() {
sortByParam.value = Object.keys(sortBy.value)
.filter(prop => activeColumns.value[prop])
.filter(prop => activeColumns.value[camelCase(prop)])
.reduce((obj, key) => {
obj[key] = sortBy.value[key]
return obj

View File

@ -30,7 +30,7 @@ const projectStore = useProjectStore()
onBeforeMount(() => {
const transform = (filterString: string) => transformFilterStringFromApi(
filterString,
labelId => labelStore.getLabelById(labelId)?.title,
labelId => labelStore.getLabelById(labelId)?.title || null,
projectId => projectStore.projects[projectId]?.title || null,
)
@ -51,7 +51,7 @@ onBeforeMount(() => {
function save() {
const transformFilter = (filterQuery: string) => transformFilterStringForApi(
filterQuery,
labelTitle => labelStore.filterLabelsByQuery([], labelTitle)[0]?.id || null,
labelTitle => labelStore.getLabelByExactTitle(labelTitle)?.id || null,
projectTitle => {
const found = projectStore.findProjectByExactname(projectTitle)
return found?.id || null

View File

@ -51,6 +51,7 @@
import {computed, ref} from 'vue'
import {useI18n} from 'vue-i18n'
import {useElementHover} from '@vueuse/core'
import {useRouter} from 'vue-router'
import {RELATION_KIND} from '@/types/IRelationKind'
import type {ITask} from '@/modelTypes/ITask'
@ -66,6 +67,8 @@ import {useAuthStore} from '@/stores/auth'
import {useTaskStore} from '@/stores/tasks'
import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea'
import TaskService from '@/services/task'
import TaskModel from '@/models/task'
const props = withDefaults(defineProps<{
defaultPosition?: number,
@ -81,6 +84,7 @@ const {textarea: newTaskInput} = useAutoHeightTextarea(newTaskTitle)
const {t} = useI18n({useScope: 'global'})
const authStore = useAuthStore()
const taskStore = useTaskStore()
const router = useRouter()
// enable only if we don't have a modal
// onStartTyping(() => {
@ -129,21 +133,55 @@ async function addTask() {
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title, authStore.settings.frontendSettings.quickAddMagicMode) ?? [])
await taskStore.ensureLabelsExist(allLabels.flat())
const newTasks = tasksToCreate.map(async ({title, project}) => {
const taskCollectionService = new TaskService()
const projectIndices = new Map<number, number>()
let currentProjectId = authStore.settings.defaultProjectId
if (typeof router.currentRoute.value.params.projectId !== 'undefined') {
currentProjectId = Number(router.currentRoute.value.params.projectId)
}
// Create a map of project indices before creating tasks
if (tasksToCreate.length > 1) {
for (const {project} of tasksToCreate) {
const projectId = project !== null
? await taskStore.findProjectId({project, projectId: 0})
: currentProjectId
if (!projectIndices.has(projectId)) {
const newestTask = await taskCollectionService.getAll(new TaskModel({}), {
sort_by: ['id'],
order_by: ['desc'],
per_page: 1,
filter: `project_id = ${projectId}`,
})
projectIndices.set(projectId, newestTask[0]?.index || 0)
}
}
}
const newTasks = tasksToCreate.map(async ({title, project}, index) => {
if (title === '') {
return
}
// If the task has a project specified, make sure to use it
let projectId = null
if (project !== null) {
projectId = await taskStore.findProjectId({project, projectId: 0})
const projectId = project !== null
? await taskStore.findProjectId({project, projectId: 0})
: currentProjectId
// Calculate new index for this task per project
let taskIndex: number | undefined
if (tasksToCreate.length > 1) {
const lastIndex = projectIndices.get(projectId)
taskIndex = lastIndex + index + 1
}
const task = await taskStore.createNewTask({
title,
projectId: projectId || authStore.settings.defaultProjectId,
position: props.defaultPosition,
index: taskIndex,
})
createdTasks[title] = task
return task

View File

@ -1,7 +1,8 @@
<template>
<div
v-if="enabled"
class="content details"
ref="commentsRef"
class="content details comments-container"
>
<h3
v-if="canWrite || comments.length > 0"
@ -15,7 +16,7 @@
<div class="comments">
<span
v-if="taskCommentService.loading && saving === null && !creating"
class="is-inline-flex is-align-items-center"
class="is-flex is-align-items-center my-4 ml-2"
>
<span class="loader is-inline-block mr-2" />
{{ $t('task.comment.loading') }}
@ -107,6 +108,14 @@
/>
</div>
</div>
<PaginationEmit
v-if="taskCommentService.totalPages > 1"
:total-pages="taskCommentService.totalPages"
:current-page="currentPage"
@pageChanged="changePage"
/>
<div
v-if="canWrite"
class="media comment d-print-none"
@ -160,6 +169,7 @@
</div>
</div>
<Modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@ -185,6 +195,7 @@ import {useI18n} from 'vue-i18n'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Editor from '@/components/input/AsyncEditor'
import PaginationEmit from '@/components/misc/PaginationEmit.vue'
import TaskCommentService from '@/services/taskComment'
import TaskCommentModel from '@/models/taskComment'
@ -242,6 +253,12 @@ const actions = computed(() => {
])))
})
const frontendUrl = computed(() => configStore.frontendUrl)
const currentPage = ref(1)
const commentsRef = ref<HTMLElement | null>(null)
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
const uploadPromises: Promise<string>[] = []
@ -267,12 +284,21 @@ async function loadComments(taskId: ITask['id']) {
newComment.taskId = taskId
commentEdit.taskId = taskId
commentToDelete.taskId = taskId
comments.value = await taskCommentService.getAll({taskId})
comments.value = await taskCommentService.getAll({taskId}, {}, currentPage.value)
}
async function changePage(page: number) {
commentsRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
currentPage.value = page
await loadComments(props.taskId)
}
watch(
() => props.taskId,
loadComments,
() => {
currentPage.value = 1 // Reset to first page when task changes
loadComments(props.taskId)
},
{immediate: true},
)
@ -404,4 +430,8 @@ async function deleteComment(commentToDelete: ITaskComment) {
.media-content {
width: calc(100% - 48px - 2rem);
}
.comments-container {
scroll-margin-top: 4rem;
}
</style>

View File

@ -135,18 +135,8 @@ async function updateDueDate() {
$defer-task-max-width: 350px + 100px;
.defer-task {
position: absolute;
width: 100%;
max-width: $defer-task-max-width;
border-radius: $radius;
border: 1px solid var(--grey-200);
padding: 1rem;
margin: 1rem;
background: var(--white);
color: var(--text);
cursor: default;
z-index: 10;
box-shadow: var(--shadow-lg);
@media screen and (max-width: ($defer-task-max-width)) {
left: .5rem;

View File

@ -10,6 +10,7 @@
:create-placeholder="$t('task.label.createPlaceholder')"
:search-delay="10"
:close-after-select="false"
:disabled="disabled"
@search="findLabel"
@select="addLabel"
@create="createAndAddLabel"
@ -21,6 +22,7 @@
>
<span>{{ label.title }}</span>
<BaseButton
v-if="!disabled"
v-cy="'taskDetail.removeLabel'"
class="delete is-small"
@click="removeLabel(label)"

View File

@ -1,20 +1,27 @@
<template>
<div class="heading">
<div class="flex is-align-items-center">
<BaseButton @click="copyUrl">
<h1 class="title task-id">
{{ textIdentifier }}
</h1>
</BaseButton>
<div class="tw-flex tw-items-center md:tw-items-stretch tw-flex-col tw-gap-1 task-properties">
<div class="tw-flex tw-items-center tw-gap-2">
<ColorBubble
v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)"
/>
<BaseButton @click="copyUrl">
<h1 class="title task-id">
{{ textIdentifier }}
</h1>
</BaseButton>
</div>
<Done
class="heading__done"
:is-done="task.done"
/>
<ColorBubble
v-if="task.hexColor !== ''"
:color="getHexColor(task.hexColor)"
class="ml-2"
/>
<BaseButton
v-if="hasClose"
class="close"
@click="$emit('close')"
>
<Icon icon="times" />
</BaseButton>
</div>
<h1
class="title input"
@ -26,6 +33,13 @@
>
{{ task.title.trim() }}
</h1>
<BaseButton
v-if="hasClose"
class="close"
@click="$emit('close')"
>
<Icon icon="times" />
</BaseButton>
<CustomTransition name="fade">
<span
v-if="loading && saving"
@ -66,12 +80,15 @@ import {getHexColor, getTaskIdentifier} from '@/models/task'
const props = withDefaults(defineProps<{
task: ITask,
canWrite: boolean | undefined,
hasClose: boolean | undefined,
}>(), {
canWrite: false,
hasClose: false,
})
const emit = defineEmits<{
'update:task': [task: ITask]
'update:task': [task: ITask],
'close': [],
}>()
const router = useRouter()
@ -138,14 +155,11 @@ async function save(title: string) {
.title.input {
// 1.8rem is the font-size, 1.125 is the line-height, .3rem padding everywhere, 1px border around the whole thing.
min-height: calc(1.8rem * 1.125 + .6rem + 2px);
margin-right: 0;
@media screen and (max-width: $tablet) {
margin: 0 -.3rem .5rem -.3rem; // the title has 0.3rem padding - this make the text inside of it align with the rest
}
@media screen and (min-width: $tablet) and (max-width: #{$desktop + $close-button-min-space}) {
width: calc(100% - 6.5rem);
}
}
.title.task-id {
@ -153,12 +167,39 @@ async function save(title: string) {
white-space: nowrap;
}
.heading__done {
margin-left: .5rem;
}
.color-bubble {
height: .75rem;
width: .75rem;
}
.close {
font-size: 2rem;
margin-left: 0.5rem;
line-height: 1;
@media screen and (max-width: $tablet) {
display: none;
}
@media screen and (min-width: #{$desktop + 1px}) {
display: none;
}
}
.task-properties .close {
display: none;
position: absolute;
right: 1.25rem;
top: 1.1rem;
@media screen and (max-width: $tablet) {
display: block;
}
}
.task-properties {
@media screen and (max-width: $tablet) {
flex-direction: row;
}
}
</style>

View File

@ -16,7 +16,7 @@
v-if="coverImageBlobUrl"
:src="coverImageBlobUrl"
alt=""
class="cover-image"
class="tw-w-full"
>
<div class="p-2">
<span class="task-id">

View File

@ -1,14 +1,17 @@
<template>
<span
v-if="!done && (showAll || priority >= priorities.HIGH)"
:class="{'not-so-high': priority === priorities.HIGH, 'high-priority': priority >= priorities.HIGH}"
:class="{
'not-so-high': priority === priorities.HIGH,
'high-priority': priority >= priorities.HIGH
}"
class="priority-label"
>
<span
v-if="priority >= priorities.HIGH"
class="icon"
>
<Icon icon="exclamation" />
<Icon icon="exclamation-circle" />
</span>
<span>
<template v-if="priority === priorities.UNSET">{{ $t('task.priority.unset') }}</template>
@ -18,12 +21,6 @@
<template v-if="priority === priorities.URGENT">{{ $t('task.priority.urgent') }}</template>
<template v-if="priority === priorities.DO_NOW">{{ $t('task.priority.doNow') }}</template>
</span>
<span
v-if="priority === priorities.DO_NOW"
class="icon pr-0"
>
<Icon icon="exclamation" />
</span>
</span>
</template>
@ -42,18 +39,18 @@ withDefaults(defineProps<{
</script>
<style lang="scss" scoped>
span.high-priority {
.high-priority {
color: var(--danger);
width: auto !important; // To override the width set in tasks
}
.icon {
vertical-align: top;
width: auto !important;
padding: 0 .5rem;
}
.not-so-high {
color: var(--warning);
}
&.not-so-high {
color: var(--warning);
}
.icon {
vertical-align: top;
width: auto !important;
padding-right: .5rem;
}
</style>

View File

@ -36,9 +36,11 @@ import Multiselect from '@/components/input/Multiselect.vue'
const props = withDefaults(defineProps<{
modelValue?: IProject
savedFiltersOnly?: boolean
filter?: (project: IProject) => boolean,
}>(), {
modelValue: () => new ProjectModel(),
savedFiltersOnly: false,
filter: () => true,
})
const emit = defineEmits<{
@ -65,11 +67,13 @@ function findProjects(query: string) {
}
if (props.savedFiltersOnly) {
foundProjects.value = projectStore.searchSavedFilter(query)
const found = projectStore.searchSavedFilter(query)
foundProjects.value = found.filter(props.filter)
return
}
foundProjects.value = projectStore.searchProject(query)
const found = projectStore.searchProject(query)
foundProjects.value = found.filter(props.filter)
}
function select(p: IProject | null) {

View File

@ -123,7 +123,7 @@
<FancyCheckbox
v-model="task.done"
class="task-done-checkbox"
@update:modelValue="toggleTaskDone(t)"
@update:modelValue="toggleTaskDone(task)"
/>
<RouterLink
:to="{ name: route.name as string, params: { id: task.id } }"

View File

@ -3,7 +3,7 @@
<div
v-for="(r, index) in reminders"
:key="index"
:class="{ 'overdue': r.reminder < new Date() }"
:class="{ 'overdue': r.reminder < now }"
class="reminder-input"
>
<ReminderDetail
@ -40,21 +40,25 @@ import BaseButton from '@/components/base/BaseButton.vue'
import ReminderDetail from '@/components/tasks/partials/ReminderDetail.vue'
import type {ITask} from '@/modelTypes/ITask'
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
import { useNow } from '@vueuse/core'
const {
modelValue,
disabled = false,
} = defineProps<{
const props = withDefaults(defineProps<{
modelValue: ITask,
disabled?: boolean,
}>()
}>(), {
disabled: false,
})
const emit = defineEmits(['update:modelValue'])
const emit = defineEmits<{
'update:modelValue': [ITask]
}>()
const reminders = ref<ITaskReminder[]>([])
const now = useNow({interval: 1000})
watch(
() => modelValue.reminders,
() => props.modelValue.reminders,
(newVal) => {
reminders.value = newVal
},
@ -62,19 +66,19 @@ watch(
)
const defaultRelativeTo = computed(() => {
if (typeof modelValue === 'undefined') {
if (typeof props.modelValue === 'undefined') {
return null
}
if (modelValue?.dueDate) {
if (props.modelValue?.dueDate) {
return REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE
}
if (modelValue.dueDate === null && modelValue.startDate !== null) {
if (props.modelValue.dueDate === null && props.modelValue.startDate !== null) {
return REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE
}
if (modelValue.dueDate === null && modelValue.startDate === null && modelValue.endDate !== null) {
if (props.modelValue.dueDate === null && props.modelValue.startDate === null && props.modelValue.endDate !== null) {
return REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE
}
@ -83,7 +87,7 @@ const defaultRelativeTo = computed(() => {
function updateData() {
emit('update:modelValue', {
...modelValue,
...props.modelValue,
reminders: reminders.value,
})
}

View File

@ -1,17 +1,17 @@
<template>
<div>
<div
ref="taskContainerRef"
:class="{'is-loading': taskService.loading}"
class="task loader-container single-task"
tabindex="-1"
@mouseup.stop.self="openTaskDetail"
@mousedown.stop.self="focusTaskLink"
@click="openTaskDetail"
@keyup.enter="openTaskDetail"
>
<FancyCheckbox
v-model="task.done"
:disabled="(isArchived || disabled) && !canMarkAsDone"
@update:modelValue="markAsDone"
@click.stop
/>
<ColorBubble
@ -23,8 +23,6 @@
<div
:class="{ 'done': task.done, 'show-project': showProject && project}"
class="tasktext"
@mouseup.stop.self="openTaskDetail"
@mousedown.stop.self="focusTaskLink"
>
<span>
<RouterLink
@ -33,6 +31,7 @@
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
class="task-project mr-1"
:class="{'mr-2': task.hexColor !== ''}"
@click.stop
>
{{ project.title }}
</RouterLink>
@ -42,15 +41,15 @@
:color="getHexColor(task.hexColor)"
class="mr-1"
/>
<PriorityLabel
:priority="task.priority"
:done="task.done"
class="pr-2"
/>
<RouterLink
ref="taskLink"
ref="taskLinkRef"
:to="taskDetailRoute"
class="task-link"
tabindex="-1"
@ -73,29 +72,33 @@
:inline="true"
/>
<!-- FIXME: use popup -->
<BaseButton
<Popup
v-if="+new Date(task.dueDate) > 0"
v-tooltip="formatDateLong(task.dueDate)"
class="dueDate"
@click.prevent.stop="showDefer = !showDefer"
>
<time
:datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
:aria-expanded="showDefer ? 'true' : 'false'"
>
{{ $t('task.detail.due', {at: dueDateFormatted}) }}
</time>
</BaseButton>
<CustomTransition name="fade">
<DeferTask
v-if="+new Date(task.dueDate) > 0 && showDefer"
ref="deferDueDate"
v-model="task"
/>
</CustomTransition>
<template #trigger="{toggle, isOpen}">
<BaseButton
v-tooltip="formatDateLong(task.dueDate)"
class="dueDate"
@click.prevent.stop="toggle()"
>
<time
:datetime="formatISO(task.dueDate)"
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
class="is-italic"
:aria-expanded="isOpen ? 'true' : 'false'"
>
{{ $t('task.detail.due', {at: dueDateFormatted}) }}
</time>
</BaseButton>
</template>
<template #content="{isOpen}">
<DeferTask
v-if="isOpen"
v-model="task"
/>
</template>
</Popup>
<span>
<span
@ -138,6 +141,7 @@
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
class="task-project"
@click.stop
>
{{ project.title }}
</RouterLink>
@ -145,7 +149,7 @@
<BaseButton
:class="{'is-favorite': task.isFavorite}"
class="favorite"
@click="toggleFavorite"
@click.stop="toggleFavorite"
>
<Icon
v-if="task.isFavorite"
@ -176,7 +180,7 @@
</template>
<script setup lang="ts">
import {ref, watch, shallowReactive, onMounted, onBeforeUnmount, computed} from 'vue'
import {ref, watch, shallowReactive, onMounted, computed} from 'vue'
import {useI18n} from 'vue-i18n'
import TaskModel, {getHexColor} from '@/models/task'
@ -191,11 +195,10 @@ import ProgressBar from '@/components/misc/ProgressBar.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
import ColorBubble from '@/components/misc/ColorBubble.vue'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Popup from '@/components/misc/Popup.vue'
import TaskService from '@/services/task'
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
import {success} from '@/message'
@ -239,7 +242,6 @@ const {t} = useI18n({useScope: 'global'})
const taskService = shallowReactive(new TaskService())
const task = ref<ITask>(new TaskModel())
const showDefer = ref(false)
const isRepeating = computed(() => task.value.repeatAfter.amount > 0 || (task.value.repeatAfter.amount === 0 && task.value.repeatMode === TASK_REPEAT_MODES.REPEAT_MODE_MONTH))
@ -254,14 +256,6 @@ watch(
},
)
onMounted(() => {
document.addEventListener('click', hideDeferDueDatePopup)
})
onBeforeUnmount(() => {
document.removeEventListener('click', hideDeferDueDatePopup)
})
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const taskStore = useTaskStore()
@ -350,35 +344,22 @@ async function toggleFavorite() {
emit('taskUpdated', task.value)
}
const deferDueDate = ref<typeof DeferTask | null>(null)
function hideDeferDueDatePopup(e) {
if (!showDefer.value) {
return
}
closeWhenClickedOutside(e, deferDueDate.value.$el, () => {
showDefer.value = false
})
}
const taskLink = ref<HTMLElement | null>(null)
const taskContainerRef = ref<HTMLElement | null>(null)
const taskLinkRef = ref<HTMLElement | null>(null)
function hasTextSelected() {
const isTextSelected = window.getSelection().toString()
return !(typeof isTextSelected === 'undefined' || isTextSelected === '' || isTextSelected === '\n')
}
function openTaskDetail() {
if (!hasTextSelected()) {
taskLink.value.$el.click()
function openTaskDetail(event: MouseEvent | KeyboardEvent) {
if (event.target instanceof HTMLElement) {
const isInteractiveElement = event.target.closest('a, button, .favorite, [role="button"]')
if (isInteractiveElement || hasTextSelected()) {
return
}
}
}
function focusTaskLink() {
if (!hasTextSelected()) {
taskContainerRef.value.focus()
}
taskLinkRef.value?.$el.click()
}
</script>
@ -397,7 +378,7 @@ function focusTaskLink() {
background-color: var(--grey-100);
}
&:focus-within, &:focus {
&:has(*:focus-visible) {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
a.task-link {
@ -405,6 +386,16 @@ function focusTaskLink() {
}
}
@supports not selector(:focus-within) {
:focus {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
a.task-link {
box-shadow: none;
}
}
}
.tasktext,
&.tasktext {
text-overflow: ellipsis;
@ -421,6 +412,15 @@ function focusTaskLink() {
.dueDate {
display: inline-block;
margin-left: 5px;
&:focus-visible {
box-shadow: none;
time {
box-shadow: 0 0 0 1px hsla(var(--primary-hsl), 0.5);
border-radius: 3px;
}
}
}
.overdue {
@ -466,6 +466,7 @@ function focusTaskLink() {
text-align: center;
width: 27px;
transition: opacity $transition, color $transition;
border-radius: $radius;
&:hover {
color: var(--warning);
@ -542,4 +543,17 @@ function focusTaskLink() {
.subtask-nested {
margin-left: 1.75rem;
}
:deep(.popup) {
border-radius: $radius;
background-color: var(--white);
box-shadow: var(--shadow-lg);
color: var(--text);
top: unset;
&.is-open {
padding: 1rem;
border: 1px solid var(--grey-200);
}
}
</style>

View File

@ -1,4 +1,7 @@
export const calculateItemPosition = (positionBefore: number | null, positionAfter: number | null): number => {
export const calculateItemPosition = (
positionBefore: number | null = null,
positionAfter: number | null = null,
): number => {
if (positionBefore === null) {
if (positionAfter === null) {
return 0
@ -13,6 +16,6 @@ export const calculateItemPosition = (positionBefore: number | null, positionAft
return positionBefore + Math.pow(2, 16)
}
// If we have both a task before and after it, we acually calculate the position
// If we have both a task before and after it, we actually calculate the position
return positionBefore + (positionAfter - positionBefore) / 2
}

View File

@ -147,6 +147,26 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('start_date > now')
})
it('should correctly resolve label when the label is called label', () => {
const transformed = transformFilterStringForApi(
'labels = label',
(title: string) => 1,
nullTitleToIdResolver,
)
expect(transformed).toBe('labels = 1')
})
it('should correctly resolve project when the project is called project', () => {
const transformed = transformFilterStringForApi(
'project = project',
nullTitleToIdResolver,
(title: string) => 1,
)
expect(transformed).toBe('project = 1')
})
})
describe('To API', () => {
@ -199,6 +219,26 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('labels in lorem, ipsum')
})
it('should not touch the label value when it is undefined', () => {
const transformed = transformFilterStringFromApi(
'labels = one',
(id: number) => undefined,
nullIdToTitleResolver,
)
expect(transformed).toBe('labels = one')
})
it('should not touch the label value when it is null', () => {
const transformed = transformFilterStringFromApi(
'labels = one',
(id: number) => null,
nullIdToTitleResolver,
)
expect(transformed).toBe('labels = one')
})
it('should correctly resolve projects', () => {
const transformed = transformFilterStringFromApi(
'project = 1',
@ -228,6 +268,26 @@ describe('Filter Transformation', () => {
expect(transformed).toBe('project in lorem, ipsum')
})
it('should not touch the project value when it is undefined', () => {
const transformed = transformFilterStringFromApi(
'project = one',
nullIdToTitleResolver,
(id: number) => undefined,
)
expect(transformed).toBe('project = one')
})
it('should not touch the project value when it is null', () => {
const transformed = transformFilterStringFromApi(
'project = one',
nullIdToTitleResolver,
(id: number) => null,
)
expect(transformed).toBe('project = one')
})
it('should transform the same attribute multiple times', () => {
const transformed = transformFilterStringFromApi(

View File

@ -77,49 +77,34 @@ export function transformFilterStringForApi(
filter = filter.replace(new RegExp(f, 'ig'), f)
})
// Transform labels to ids
LABEL_FIELDS.forEach(field => {
const pattern = getFilterFieldRegexPattern(field)
// Transform labels and projects to ids
function transformFieldToIds(
fields: string[],
resolver: (title: string) => number | null,
filter: string,
): string {
fields.forEach(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) {
continue
}
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) {
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 = 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) {
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 id = resolver(k)
if (id !== null) {
replaced = replaced.replace(k, String(id))
}
})
@ -128,8 +113,15 @@ export function transformFilterStringForApi(
replaced +
filter.substring(actualKeywordStart + keyword.length)
}
}
})
})
return filter
}
// Transform labels to ids
filter = transformFieldToIds(LABEL_FIELDS, labelResolver, filter)
// Transform projects to ids
filter = transformFieldToIds(PROJECT_FIELDS, projectResolver, filter)
// Transform all attributes to snake case
AVAILABLE_FILTER_FIELDS.forEach(f => {
@ -141,8 +133,8 @@ export function transformFilterStringForApi(
export function transformFilterStringFromApi(
filter: string,
labelResolver: (id: number) => string | null,
projectResolver: (id: number) => string | null,
labelResolver: (id: number) => string | null | undefined,
projectResolver: (id: number) => string | null | undefined,
): string {
if (filter.trim() === '') {
@ -154,53 +146,40 @@ export function transformFilterStringFromApi(
filter = filter.replaceAll(snakeCase(f), f)
})
// Function to transform fields to their titles
function transformFieldsToTitles(
fields: string[],
resolver: (id: number) => string | null | undefined,
) {
fields.forEach(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) {
let keywords = [keyword.trim()]
if (operator === 'in' || operator === '?=') {
keywords = keyword.trim().split(',').map(k => k.trim())
}
keywords.forEach(k => {
const title = resolver(parseInt(k))
if (title) {
filter = filter.replace(k, title)
}
})
}
}
})
}
// Transform labels to their titles
LABEL_FIELDS.forEach(field => {
const pattern = getFilterFieldRegexPattern(field)
transformFieldsToTitles(LABEL_FIELDS, labelResolver)
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) {
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 = 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) {
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)
}
})
}
}
})
// Transform projects to their titles
transformFieldsToTitles(PROJECT_FIELDS, projectResolver)
return filter
}

View File

@ -0,0 +1,15 @@
export function validatePassword(password: string, validateMinLength: boolean = true): string | true {
if (password === '') {
return 'user.auth.passwordRequired'
}
if (validateMinLength && password.length < 8) {
return 'user.auth.passwordNotMin'
}
if (validateMinLength && password.length > 72) {
return 'user.auth.passwordNotMax'
}
return true
}

View File

@ -24,7 +24,7 @@ export const SUPPORTED_LOCALES = {
'sl-SI': 'Slovenščina',
'pt-BR': 'Português Brasileiro',
'hr-HR': 'Hrvatski',
'uk-UA': 'українська',
'uk-UA': 'Українська',
// IMPORTANT: Also add new languages to useDayjsLanguageSync
} as const

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "الرجاء إدخال كلمة المرور.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "إظهار كلمة المرور",
"hidePassword": "إخفاء كلمة المرور",
"noAccountYet": "ليس لديك حساب بعد؟",
@ -163,6 +163,7 @@
"90d": "90 يوماً",
"permissionExplanation": "نطاق الصلاحيات المسموح به هو ما يسمح لك القيام به رمز Api.",
"titleRequired": "العنوان إجباري",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "انتهت صلاحية هذا الرمز {ago}.",
"tokenCreatedSuccess": "إليك رمز Api الجديد: {token}",
"tokenCreatedNotSeeAgain": "قم بتخزينه في مكان آمن، لن تتمكن من عرضه مرة أخرى!",
@ -847,7 +848,8 @@
"delete": "حذف هذا التعليق",
"deleteText1": "هل أنت متأكد من رغبتك بحذف هذا التعليق؟",
"deleteSuccess": "تم حذف التعليق بنجاح.",
"addedSuccess": "تمت إضافة التعليق بنجاح."
"addedSuccess": "تمت إضافة التعليق بنجاح.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "تأجيل تاريخ الاستحقاق",
@ -1153,8 +1155,8 @@
"3006": "المشاركة في المشروع غير موجودة.",
"3007": "يوجد مشروع بهذا المعرف مسبقاً.",
"3008": "المشروع مؤرشف وبالتالي يمكن الوصول إليه للقراءة فقط، وهذا ينطبق أيضا على جميع المهام المرتبطة بهذا المشروع.",
"4001": "لا يمكن ترك نص مهمة المشروع فارغاً.",
"4002": "مهمة المشروع غير موجودة.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "يجب أن تنتمي جميع إجراءات التعديل المجمعة لنفس المشروع.",
"4004": "تحتاج إلى مهمة واحدة على الأقل عند القيام بإجراءات التعديل بالجملة.",
"4005": "ليس لديك الصلاحية لعرض المهمة.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "Uživatelské jméno nesmí vypadat jako adresa URL.",
"passwordRequired": "Zadejte prosím heslo.",
"passwordNotMin": "Heslo musí mít nejméně 8 znaků.",
"passwordNotMax": "Heslo může mít maximálně 250 znaků.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Ukázat heslo",
"hidePassword": "Skrýt heslo",
"noAccountYet": "Ještě nemáte účet?",
@ -163,6 +163,7 @@
"90d": "90 dní",
"permissionExplanation": "Oprávnění vám umožní nastavit, k čemu lze api token použít.",
"titleRequired": "Je vyžadován název",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Platnost tohoto tokenu vypršela {ago}.",
"tokenCreatedSuccess": "Zde je tvůj nový api token: {token}",
"tokenCreatedNotSeeAgain": "Ulož jej na zabezpečeném místě, už ho znovu neuvidíš!",
@ -847,7 +848,8 @@
"delete": "Smazat tento komentář",
"deleteText1": "Opravdu chcete smazat tento komentář?",
"deleteSuccess": "Komentář byl úspěšně odstraněn.",
"addedSuccess": "Komentář byl úspěšně přidán."
"addedSuccess": "Komentář byl úspěšně přidán.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Odložit datum dokončení",
@ -1153,8 +1155,8 @@
"3006": "Sdílení projektu neexistuje.",
"3007": "Projekt s tímto identifikátorem již existuje.",
"3008": "Projekt je archivován, a proto je přístupný pouze pro čtení. To platí i pro všechny úkoly spojené s tímto projektem.",
"4001": "Text úkolu projektu nemůže být prázdný.",
"4002": "Úkol projektu neexistuje.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Všechny úkoly pro hromadnou úpravu musí patřit do stejného projektu.",
"4004": "Při hromadných úpravách úkolů je potřeba alespoň jeden úkol.",
"4005": "Nemáte právo vidět tento úkol.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Angiv venligst en adgangskode.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Vis adgangskoden",
"hidePassword": "Skjul adgangskoden",
"noAccountYet": "Har du ikke en konto endnu?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Slet denne kommentar",
"deleteText1": "Er du sikker på du vil slette denne kommentar?",
"deleteSuccess": "Kommentaren blev slettet.",
"addedSuccess": "Din kommentar blev tilføjet."
"addedSuccess": "Din kommentar blev tilføjet.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Udsæt forfaldsdato",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Der skal være mindst én opgave for at masseredigere opgaver.",
"4005": "Du har ikke rettigheder til at se opgaven.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "Der Anmeldename darf nicht wie eine URL aussehen.",
"passwordRequired": "Bitte gib ein Passwort ein.",
"passwordNotMin": "Das Passwort muss aus mindestens 8 Zeichen bestehen.",
"passwordNotMax": "Das Passwort darf höchstens 250 Zeichen lang sein.",
"passwordNotMax": "Das Passwort darf höchstens 72 Zeichen lang sein.",
"showPassword": "Passwort anzeigen",
"hidePassword": "Passwort verbergen",
"noAccountYet": "Noch kein Account?",
@ -163,6 +163,7 @@
"90d": "90 Tage",
"permissionExplanation": "Mit Berechtigungen kannst du einschränken, was ein API-Token tun darf.",
"titleRequired": "Titel ist erforderlich",
"permissionRequired": "Bitte wähle mindestens eine Berechtigung aus der Liste.",
"expired": "Dieses Token ist {ago} abgelaufen.",
"tokenCreatedSuccess": "Hier ist dein neues API Token: {token}",
"tokenCreatedNotSeeAgain": "Speichere es an einem sicheren Ort, du wirst es nicht mehr sehen!",
@ -847,7 +848,8 @@
"delete": "Diesen Kommentar löschen",
"deleteText1": "Bist du sicher, dass du diesen Kommentar löschen willst?",
"deleteSuccess": "Der Kommentar wurde erfolgreich gelöscht.",
"addedSuccess": "Der Kommentar wurde erfolgreich hinzugefügt."
"addedSuccess": "Der Kommentar wurde erfolgreich hinzugefügt.",
"permalink": "Permalink zu diesem Kommentar kopieren"
},
"deferDueDate": {
"title": "Fälligkeitsdatum verschieben",
@ -1153,7 +1155,7 @@
"3006": "Diese Linkfreigabe existiert nicht.",
"3007": "Ein Projekt mit diesem Bezeichner existiert bereits.",
"3008": "Dieses Projekt ist archiviert und kann deshalb nur gelesen werden. Dies gilt auch für alle Aufgaben, die mit diesem Projekt verbunden sind.",
"4001": "Der Aufgabentitel kann nicht leer sein.",
"4001": "Der Titel darf nicht leer sein.",
"4002": "Diese Aufgabe existiert nicht.",
"4003": "Alle Massenbearbeitungen an Aufgaben müssen zum selben Projekt gehören.",
"4004": "Es benötigt mindestens einen Task, um eine Massenänderung durchzuführen.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "Der Anmeldename darf nicht wie eine URL aussehen.",
"passwordRequired": "Bitte gib ein Passwort ein.",
"passwordNotMin": "Das Passwort muss aus mindestens 8 Zeichen bestehen.",
"passwordNotMax": "Das Passwort darf höchstens 250 Zeichen lang sein.",
"passwordNotMax": "Das Passwort darf höchstens 72 Zeichen lang sein.",
"showPassword": "Passwort anzeigen",
"hidePassword": "Passwort verbergen",
"noAccountYet": "Noch kein Account?",
@ -163,6 +163,7 @@
"90d": "90 Tage",
"permissionExplanation": "Mit Berechtigungen kannst du einschränken, was ein API-Token tun darf.",
"titleRequired": "Titel ist erforderlich",
"permissionRequired": "Bitte wähle mindestens eine Berechtigung aus der Liste.",
"expired": "Dieses Token ist {ago} abgelaufen.",
"tokenCreatedSuccess": "Hier ist dein neues API Token: {token}",
"tokenCreatedNotSeeAgain": "Speichere es an einem sicheren Ort, du wirst es nicht mehr sehen!",
@ -847,7 +848,8 @@
"delete": "De Kommentar chüble",
"deleteText1": "Bisch du dir sicher, dass du de Kommentar chüble wetsch?",
"deleteSuccess": "Der Kommentar wurde erfolgreich gelöscht.",
"addedSuccess": "Din Kommentar isch erfolgriich hinzuegfüegt worde."
"addedSuccess": "Din Kommentar isch erfolgriich hinzuegfüegt worde.",
"permalink": "Permalink zu diesem Kommentar kopieren"
},
"deferDueDate": {
"title": "Fälligkeitsdatum verschiebe",
@ -1153,7 +1155,7 @@
"3006": "Diese Linkfreigabe existiert nicht.",
"3007": "Ein Projekt mit diesem Bezeichner existiert bereits.",
"3008": "Dieses Projekt ist archiviert und kann deshalb nur gelesen werden. Dies gilt auch für alle Aufgaben, die mit diesem Projekt verbunden sind.",
"4001": "Der Aufgabentitel kann nicht leer sein.",
"4001": "Der Titel darf nicht leer sein.",
"4002": "Diese Aufgabe existiert nicht.",
"4003": "Alle Massenbearbeitungen an Aufgaben müssen zum selben Projekt gehören.",
"4004": "Es bruucht mindestens ei Uufgab, um e Masseänderig durezfüehre.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -1154,8 +1154,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Por favor, proporciona una contraseña.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Mostrar la contraseña",
"hidePassword": "Ocultar la contraseña",
"noAccountYet": "¿Aún no tienes una cuenta?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Eliminar este comentario",
"deleteText1": "¿Está seguro que desea eliminar este comentario?",
"deleteSuccess": "El comentario fue eliminado con éxito.",
"addedSuccess": "El comentario fue añadido con éxito."
"addedSuccess": "El comentario fue añadido con éxito.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Aplazar fecha de vencimiento",
@ -1153,8 +1155,8 @@
"3006": "La compartición del proyecto no existe.",
"3007": "Ya existe un proyecto con este identificador.",
"3008": "El proyecto está archivado y por lo tanto sólo se puede acceder a él en modo lectura. Esto también se aplica a las tareas asociadas con este proyecto.",
"4001": "El texto de la tarea del proyecto no puede estar vacío.",
"4002": "La tarea del proyecto no existe.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Todas las tareas de edición en masa deben pertenecer al mismo proyecto.",
"4004": "Se necesita al menos una tarea cuando se editan tareas masivamente.",
"4005": "No tiene permiso para ver la tarea.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Veuillez fournir un mot de passe.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Afficher le mot de passe",
"hidePassword": "Masquer le mot de passe",
"noAccountYet": "Vous n'avez pas encore de compte?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Supprimer ce commentaire",
"deleteText1": "Supprimer ce commentaire ?",
"deleteSuccess": "Le commentaire a bien été supprimé.",
"addedSuccess": "Commentaire ajouté."
"addedSuccess": "Commentaire ajouté.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Reporter la date déchéance",
@ -1153,8 +1155,8 @@
"3006": "Le partage de ce projet nexiste pas.",
"3007": "Un projet avec cet identifiant existe déjà.",
"3008": "Le projet est archivé et ne peut donc être consulté quen lecture seule. Ceci est également vrai pour toutes les tâches associées à ce projet.",
"4001": "Le texte de la tâche du projet ne peut pas être vide.",
"4002": "La tâche de projet nexiste pas.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Toutes les tâches de modification en bloc doivent appartenir au même projet.",
"4004": "Besoin dau moins une tâche lors de la modification en bloc de tâches.",
"4005": "Vous navez pas le droit de voir la tâche.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "Korisničko ime ne smije izgledati kao URL.",
"passwordRequired": "Molimo upišite lozinku.",
"passwordNotMin": "Lozinka treba imati barem 8 znakova.",
"passwordNotMax": "Lozinka treba imati barem 250 znakova.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Prikaži lozinku",
"hidePassword": "Sakrij lozinku",
"noAccountYet": "Još nemate račun?",
@ -163,6 +163,7 @@
"90d": "90 dana",
"permissionExplanation": "Dopuštenja vam omogućuju da odredite što api token smije raditi.",
"titleRequired": "Naslov je obavezan",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Ovaj je token istekao {ago}.",
"tokenCreatedSuccess": "Evo vašeg novog api tokena: {token}",
"tokenCreatedNotSeeAgain": "Čuvajte ga na sigurnom mjestu, nećete ga više vidjeti!",
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "A felhasználónév nem nézhet ki URL-nek.",
"passwordRequired": "Kérjük, adjon meg új jelszót.",
"passwordNotMin": "A jelszónak legalább 8 karakterből kell állnia.",
"passwordNotMax": "A jelszó legfeljebb 250 karakterből állhat.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Jelszó megjelenítése",
"hidePassword": "A jelszó elrejtése",
"noAccountYet": "Még nincs fiókja?",
@ -163,6 +163,7 @@
"90d": "90 nap",
"permissionExplanation": "Az engedélyek lehetővé teszik annak hatókörét, hogy egy API Token mire jogosult.",
"titleRequired": "A cím kötelező",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Ez a token lejárt {ago}.",
"tokenCreatedSuccess": "Íme az új API tokenje: {token}",
"tokenCreatedNotSeeAgain": "Tárolja el biztonságos helyen, többé nem fogja látni!",
@ -847,7 +848,8 @@
"delete": "Hozzászólás törlése",
"deleteText1": "Biztos benne, hogy törölni akarja ezt a hozzászólást?",
"deleteSuccess": "A hozzászólás sikeresen törlődött.",
"addedSuccess": "A hozzászólás sikeresen hozzáadva."
"addedSuccess": "A hozzászólás sikeresen hozzáadva.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "A határidő elhalasztása",
@ -1153,8 +1155,8 @@
"3006": "A projektmegosztás nem létezik.",
"3007": "Ezzel az azonosítóval már létezik projekt.",
"3008": "A projekt archiválva van, ezért csak olvasható. Ez a projekthez kapcsolódó összes feladatra is igaz.",
"4001": "A projektfeladat szövege nem lehet üres.",
"4002": "A projektfeladat nem létezik.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Minden tömeges szerkesztési feladatnak ugyanahhoz a projekthez kell tartoznia.",
"4004": "A feladatok tömeges szerkesztéséhez legalább egy feladatra van szükség.",
"4005": "Nincs joga a feladat megtekintéséhez.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Inserisci una password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Mostra la password",
"hidePassword": "Nascondi la password",
"noAccountYet": "Non hai un account?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Elimina questo commento",
"deleteText1": "Sei sicuro di voler eliminare questo commento?",
"deleteSuccess": "Commento cancellato con successo.",
"addedSuccess": "Il commento è stato aggiunto correttamente."
"addedSuccess": "Il commento è stato aggiunto correttamente.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Rinvia data di scadenza",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Hai bisogno di almeno un'attività quando si modificano in blocco le attività.",
"4005": "Non hai il permesso di vedere l'attività.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "パスワードを入力してください。",
"passwordNotMin": "パスワードは8文字以上でなければなりません。",
"passwordNotMax": "パスワードは250文字以内でなければなりません。",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "パスワードの表示",
"hidePassword": "パスワードの非表示",
"noAccountYet": "まだアカウントをお持ちでないですか?",
@ -99,7 +99,7 @@
"defaultView": "デフォルトのビュー",
"timezone": "タイムゾーン",
"overdueTasksRemindersTime": "期限切れタスクのリマインダー送信時間",
"filterUsedOnOverview": "概要ページに使用される絞り込み条件"
"filterUsedOnOverview": "概要ページ絞り込み条件"
},
"totp": {
"title": "2要素認証",
@ -154,7 +154,7 @@
},
"apiTokens": {
"title": "APIトークン",
"general": "APIトークンは、認証されたVikunja APIのリクエストを行うことができます。",
"general": "APIトークンを使うとログインせずにVikunjaのAPIを利用できます。",
"apiDocs": "詳しくはAPIドキュメントをご確認ください",
"createAToken": "トークンの生成",
"createToken": "トークンの生成",
@ -163,6 +163,7 @@
"90d": "90日",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "トークン名を入力してください。",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "このトークンは二度と表示されません。安全な場所に保管してください。",
@ -847,7 +848,8 @@
"delete": "コメントの削除",
"deleteText1": "このコメントを削除して本当によろしいですか?",
"deleteSuccess": "コメントは正常に削除されました。",
"addedSuccess": "コメントは正常に追加されました。"
"addedSuccess": "コメントは正常に追加されました。",
"permalink": "コメントへのリンクをコピー"
},
"deferDueDate": {
"title": "延期",
@ -1153,8 +1155,8 @@
"3006": "その共有プロジェクトは存在しません。",
"3007": "A project with this identifier already exists.",
"3008": "このプロジェクトはアーカイブ済みのため読み取り専用です。またプロジェクトに関連するタスクも同様です。",
"4001": "The project task text cannot be empty.",
"4002": "そのプロジェクトのタスクは存在しません。",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "비밀번호를 입력하세요.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "비밀번호 표시",
"hidePassword": "비밀번호 숨김",
"noAccountYet": "아직 계정이 없으신가요?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "의견 삭제",
"deleteText1": "의견을 삭제하시겠습니까?",
"deleteSuccess": "의견이 성공적으로 삭제되었습니다!",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

File diff suppressed because it is too large Load Diff

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Verwijder deze reactie",
"deleteText1": "Weet je zeker dat je deze reactie wilt verwijderen?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "De reactie is succesvol toegevoegd."
"addedSuccess": "De reactie is succesvol toegevoegd.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Vervaldatum uitstellen",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Angi et passord.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Vis passord",
"hidePassword": "Skjul passord",
"noAccountYet": "Har du ikke konto ennå?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Slett denne kommentaren",
"deleteText1": "Er du sikker på at du vil slette denne kommentaren?",
"deleteSuccess": "Sletting av kommentaren vellykket.",
"addedSuccess": "Denne kommentaren ble lagt til."
"addedSuccess": "Denne kommentaren ble lagt til.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Utsatt forfallsdato",
@ -1153,8 +1155,8 @@
"3006": "Prosjektdeling finnes ikke.",
"3007": "Et prosjekt med denne identifikatoren eksisterer allerede.",
"3008": "Prosjektet er arkivert og kan derfor bare leses inn. Dette gjelder også for alle oppgaver som er tilknyttet dette prosjektet.",
"4001": "Prosjektets oppgavetekst kan ikke være tom.",
"4002": "Prosjektoppgaven finnes ikke.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Alle bulkredigering oppgaver må tilhøre samme prosjekt.",
"4004": "Trenger minst én oppgave når masseredigeringsoppgaver skal utføres.",
"4005": "Du har ikke rettigheter til å redigere denne siden.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "Nazwa użytkownika nie może wyglądać jak adres URL.",
"passwordRequired": "Proszę podać hasło.",
"passwordNotMin": "Hasło musi zawierać co najmniej 8 znaków.",
"passwordNotMax": "Hasło musi zawierać co najwyżej 250 znaków.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Pokaż hasło",
"hidePassword": "Ukryj hasło",
"noAccountYet": "Nie masz jeszcze konta?",
@ -163,6 +163,7 @@
"90d": "90 dni",
"permissionExplanation": "Uprawnienia pozwalają określić, co token API ma prawo robić.",
"titleRequired": "Tytuł jest wymagany",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Ten token wygasł {ago}.",
"tokenCreatedSuccess": "Oto twój nowy token: {token}",
"tokenCreatedNotSeeAgain": "Przechowuj go w bezpiecznym miejscu, nie zobaczysz go ponownie!",
@ -847,7 +848,8 @@
"delete": "Usuń ten komentarz",
"deleteText1": "Czy na pewno chcesz usunąć ten komentarz?",
"deleteSuccess": "Komentarz został pomyślnie usunięty.",
"addedSuccess": "Komentarz został pomyślnie dodany."
"addedSuccess": "Komentarz został pomyślnie dodany.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Odroczenie terminu",
@ -1153,8 +1155,8 @@
"3006": "Udostępnienie projektu nie istnieje.",
"3007": "Projekt z tym identyfikatorem już istnieje.",
"3008": "Projekt jest zarchiwizowany i dlatego można go tylko przeglądać. To samo dotyczy wszystkich zadań związanych z tym projektem.",
"4001": "Tekst zadania projektu nie może być pusty.",
"4002": "Projekt nie istnieje.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Wszystkie zadania do edycji zbiorczej muszą należeć do tego samego projektu.",
"4004": "Potrzebujesz co najmniej jednego zadania do edycji zbiorczej zadań.",
"4005": "Nie masz uprawnień, aby zobaczyć to zadanie.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Por favor, insira uma senha.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Exibir senha",
"hidePassword": "Ocultar senha",
"noAccountYet": "Não possui uma conta ainda?",
@ -163,6 +163,7 @@
"90d": "90 Dias",
"permissionExplanation": "As permissões controlam as ações que um token de API pode realizar.",
"titleRequired": "O título é obrigatório",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Este token venceu {ago}.",
"tokenCreatedSuccess": "Aqui está seu novo token de API: {token}",
"tokenCreatedNotSeeAgain": "Guarde em um local seguro, você não poderá visualizá-lo novamente!",
@ -847,7 +848,8 @@
"delete": "Apagar este comentário",
"deleteText1": "Tem certeza que deseja apagar este comentário?",
"deleteSuccess": "O comentário foi apagado com sucesso.",
"addedSuccess": "O comentário foi adicionado com sucesso."
"addedSuccess": "O comentário foi adicionado com sucesso.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Data de vencimento antecipada",
@ -1153,8 +1155,8 @@
"3006": "O compartilhamento de projeto não existe.",
"3007": "Já existe um projeto com este identificador.",
"3008": "O projeto está arquivado e, portanto, só pode ser acessado como somente leitura. Isso também se aplica a todas as tarefas associadas a este projeto.",
"4001": "O texto da tarefa do projeto não pode estar em branco.",
"4002": "A tarefa do projeto não existe.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Todas as tarefas da edição em massa devem pertencer ao mesmo projeto.",
"4004": "Precisa de pelo menos uma tarefa quando há edição em massa de tarefas.",
"4005": "Você não tem o direito de ver a tarefa.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "O nome de utilizador não se deve assemelhar a um URL.",
"passwordRequired": "Por favor, fornece uma palavra-passe.",
"passwordNotMin": "A palavra-passe deve ter no mínimo 8 caracteres.",
"passwordNotMax": "A palavra-passe deve ter no máximo 250 caracteres.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Mostrar a palavra-passe",
"hidePassword": "Esconder a palavra-passe",
"noAccountYet": "Ainda não tens uma conta?",
@ -163,6 +163,7 @@
"90d": "90 Dias",
"permissionExplanation": "As permissões permitem-te definir o âmbito para o qual o token de API pode ser utilizado.",
"titleRequired": "O título é requerido",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Este token expirou {ago}.",
"tokenCreatedSuccess": "Aqui está o teu novo token de API: {token}",
"tokenCreatedNotSeeAgain": "Guarda-o num local seguro, não o vais poder visualizar novamente!",
@ -847,7 +848,8 @@
"delete": "Eliminar este comentário",
"deleteText1": "Tens a certeza que pretendes eliminar este comentário?",
"deleteSuccess": "O comentário foi eliminado com sucesso.",
"addedSuccess": "O comentário foi adicionada com sucesso."
"addedSuccess": "O comentário foi adicionada com sucesso.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Adiar data de vencimento",
@ -1153,8 +1155,8 @@
"3006": "O projeto partiilhado não existe.",
"3007": "Já existe um projeto com este identificador.",
"3008": "O projeto está arquivado, portanto, só pode ser acedido para leitura. Isto é também verdade para todas as tarefas associadas a este projeto.",
"4001": "O texto da tarefa não pode estar vazio.",
"4002": "A tarefa não existe.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Todas as tarefas para edição em massa devem pertencer ao mesmo projeto.",
"4004": "Precisas selecionar pelo menos uma tarefa para realizar uma edição em massa.",
"4005": "Não possuis permissão para ver esta tarefa.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "Имя пользователя не должно быть похожим на URL.",
"passwordRequired": "Введите пароль.",
"passwordNotMin": "Пароль должен содержать не меньше 8 символов.",
"passwordNotMax": "Пароль должен содержать не больше 250 символов.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Показать пароль",
"hidePassword": "Скрыть пароль",
"noAccountYet": "Ещё нет аккаунта?",
@ -163,6 +163,7 @@
"90d": "90 дней",
"permissionExplanation": "Разрешения позволяют выбрать, какие действия можно выполнять с использованием этого токена.",
"titleRequired": "Название обязательно",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Срок действия этого токена истёк {ago}.",
"tokenCreatedSuccess": "Ваш новый токен: {token}",
"tokenCreatedNotSeeAgain": "Сохраните его в безопасном месте, вы не увидите его снова!",
@ -847,7 +848,8 @@
"delete": "Удалить комментарий",
"deleteText1": "Удалить этот комментарий?",
"deleteSuccess": "Комментарий удалён.",
"addedSuccess": "Комментарий добавлен."
"addedSuccess": "Комментарий добавлен.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Отложить срок",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "Проект с таким идентификатором уже существует.",
"3008": "Этот проект архивирован и поэтому доступен только для чтения. Это также касается всех задач в этом проекте.",
"4001": "Текст задачи не может быть пустым.",
"4002": "Задача не существует.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "Все задачи для массового редактирования должны принадлежать одному проекту.",
"4004": "Необходима хотя бы одна задача для массового редактирования.",
"4005": "У вас нет прав для просмотра задачи.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -43,7 +43,7 @@
"forgotPassword": "Ste pozabili svoje geslo?",
"resetPassword": "Ponastavite geslo",
"resetPasswordAction": "Pošljite mi povezavo za ponastavitev gesla",
"resetPasswordSuccess": "Check your inbox! You should have an email with instructions on how to reset your password.",
"resetPasswordSuccess": "Preverite vaš poštni predal! Prejeti bi morali e-pošto z navodili za ponastavitev gesla.",
"passwordsDontMatch": "Vneseni gesli se ne ujemata",
"confirmEmailSuccess": "Uspešno ste potrdili svoj e-poštni naslov! Sedaj se lahko prijavite.",
"totpTitle": "Koda za dvofaktorsko avtentikacijo",
@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "Uporabniško ime ne sme izgledati kot URL.",
"passwordRequired": "Prosim vnesite geslo.",
"passwordNotMin": "Geslo mora imeti vsaj 8 znakov.",
"passwordNotMax": "Geslo mora imeti največ 250 znakov.",
"passwordNotMax": "Geslo mora imeti največ 72 znakov.",
"showPassword": "Prikažite geslo",
"hidePassword": "Skrijte geslo",
"noAccountYet": "Še nimate računa?",
@ -163,6 +163,7 @@
"90d": "90 dni",
"permissionExplanation": "Dovoljenja vam omogočajo, da določite, kaj vse vam API žeton dovoli.",
"titleRequired": "Naslov je obvezen",
"permissionRequired": "Prosim izberite vsaj eno dovoljenje iz seznama.",
"expired": "Žeton je potekel pred {ago}.",
"tokenCreatedSuccess": "Tu je vaš novi API žeton: {token}",
"tokenCreatedNotSeeAgain": "Shranite ga na varno mesto, ker ga ne boste več videli!",
@ -847,7 +848,8 @@
"delete": "Izbriši ta komentar",
"deleteText1": "Ali ste prepričani, da želite izbrisati ta komentar?",
"deleteSuccess": "Komentar je bil uspešno izbrisan.",
"addedSuccess": "Komentar je bil uspešno dodan."
"addedSuccess": "Komentar je bil uspešno dodan.",
"permalink": "Kopiraj trajno povezavo do tega komentarja"
},
"deferDueDate": {
"title": "Odloži datum zapadlosti",
@ -1153,8 +1155,8 @@
"3006": "Skupna raba projekta ne obstaja.",
"3007": "Projekt s tem identifikatorjem že obstaja.",
"3008": "Projekt je arhiviran in je zato možen samo za branje. To velja tudi za vse naloge, povezane s tem projektom.",
"4001": "Besedilo projektne naloge ne sme biti prazno.",
"4002": "Projektna naloga ne obstaja.",
"4001": "Naslov naloge ne sme biti prazen.",
"4002": "Naloga ne obstaja.",
"4003": "Vse naloge množičnega urejanja morajo pripadati istemu projektu.",
"4004": "Pri množičnem urejanju nalog potrebujete vsaj eno nalogo.",
"4005": "Nimate pravic do vpogleda v nalogo.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -60,8 +60,8 @@
"usernameMustNotContainSpace": "The username must not contain spaces.",
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Vänligen ange ett lösenord.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMin": "Lösenord måste innehålla minst 8 tecken.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Har du inget konto än?",
@ -74,7 +74,7 @@
"newPasswordTitle": "Uppdatera ditt lösenord",
"newPassword": "Nytt lösenord",
"newPasswordConfirm": "New password confirmation",
"currentPassword": "Current password",
"currentPassword": "Nuvarande lösenord",
"currentPasswordPlaceholder": "Ditt nuvarande lösenord",
"passwordsDontMatch": "The new password and its confirmation don't match.",
"passwordUpdateSuccess": "The password was successfully updated.",
@ -83,7 +83,7 @@
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
"general": {
"title": "General Settings",
"name": "My Name",
"name": "Mitt namn",
"newName": "The new name",
"savedSuccess": "The settings were successfully updated.",
"emailReminders": "Send me reminders for tasks via email",
@ -163,6 +163,7 @@
"90d": "90 dagar",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Förvara den på en säker plats, du kommer aldrig att se den igen!",
@ -324,7 +325,7 @@
"add": "Lägg till",
"addPlaceholder": "Add a task…",
"empty": "This project is currently empty.",
"newTaskCta": "Create a task.",
"newTaskCta": "Skapa en uppgift.",
"editTask": "Redigera uppgift"
},
"gantt": {
@ -395,7 +396,7 @@
"bucketConfig": "Bucket configuration",
"bucketConfigManual": "Manual",
"filter": "Filter",
"create": "Create view",
"create": "Skapa vy",
"createSuccess": "The view was created successfully.",
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
@ -574,7 +575,7 @@
"id": "ID",
"created": "Created at",
"createdBy": "Created by {0}",
"actions": "Actions",
"actions": "Åtgärder",
"cannotBeUndone": "This cannot be undone!"
},
"input": {
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Är du säker på att du vill radera denna kommentar?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1109,7 +1111,7 @@
"createTask": "Create a task in the current project ({title})",
"createProject": "Skapa ett projekt",
"cmds": {
"newTask": "New task",
"newTask": "Ny uppgift",
"newProject": "Nytt projekt",
"newTeam": "Nytt team"
}
@ -1120,10 +1122,10 @@
"altFormatShort": "j M Y"
},
"reaction": {
"reactedWith": "{user} reacted with {value}",
"reactedWithAnd": "{users} and {lastUser} reacted with {value}",
"reactedWith": "{user} reagerade med {value}",
"reactedWithAnd": "{users} och {lastUser} reagerade med {value}",
"reactedWithAndMany": "{users} and {num} more reacted reacted with {value}",
"add": "Add your reaction"
"add": "Lägg till din reaktion"
},
"error": {
"error": "Error",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -56,12 +56,12 @@
"openIdGeneralError": "Сталася помилка під час автентифікації проти третьої сторони.",
"logout": "Вийти",
"emailInvalid": "Будь ласка, введіть дійсну е-скриньку.",
"usernameRequired": "Будь ласка, введіть ім'я вживача.",
"usernameRequired": "Будь ласка, введіть ім'я або е-скриньку.",
"usernameMustNotContainSpace": "Ім'я вживача не може містити пропусків.",
"usernameMustNotLookLikeUrl": "Ім'я вживача не має виглядати як посилання.",
"passwordRequired": "Будь ласка, введіть пароль.",
"passwordNotMin": "Пароль має містити принаймні 8 знаків.",
"passwordNotMax": "Пароль має містити не більше 250 знаків.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Показати пароль",
"hidePassword": "Сховати пароль",
"noAccountYet": "Досі немає обліковки?",
@ -99,7 +99,7 @@
"defaultView": "Основне подання",
"timezone": "Часовий пояс",
"overdueTasksRemindersTime": "Нагадувати про завдання на е-скриньку",
"filterUsedOnOverview": "Постійна вибірка, яка вживатиметься на сторінці огляду"
"filterUsedOnOverview": "Постійна вибірка для сторінки огляду"
},
"totp": {
"title": "Дворівнева перевірка",
@ -163,6 +163,7 @@
"90d": "90 днів",
"permissionExplanation": "Дозволи визначають, що може робити api ключ.",
"titleRequired": "Слід вказати заголовок",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Цей ключ закінчився {ago}.",
"tokenCreatedSuccess": "Ось Ваш новий api ключ: {token}",
"tokenCreatedNotSeeAgain": "Збережіть його в надійному місці, бо він зникне і не з'явиться знову!",
@ -247,11 +248,11 @@
"title": "Вилучити \"{project}\"",
"header": "Вилучається справа",
"text1": "Справді впровадити та втратити увесь вміст?",
"text2": "Це містить всі завдання і ГОДІ СПИНИТИ!",
"text2": "Це містить всі завдання і ПОДАЛЬША ДІЯ ОСТАТОЧНА!",
"success": "Справу вилучено.",
"tasksToDelete": "Близько {count} завдань пропадуть остаточно.",
"tasksAndChildProjectsToDelete": "Близько {tasks} завдань та {projects} справ пропадуть остаточно.",
"noTasksToDelete": "Справа не містить завдань та її безпечно вилучити."
"noTasksToDelete": "Справа не містить завдань та її можна безпечно вилучити."
},
"duplicate": {
"title": "Подвоїти цю справу",
@ -287,7 +288,7 @@
"passwordExplanation": "При вході, вживачеві слід ввести цей пароль.",
"noName": "Не вказано ім'я",
"remove": "Вилучається посилання для поширення",
"removeText": "Дійсно вилучити посилання для поширення? За ним надалі годі одержати доступ. Годі спинити!",
"removeText": "Дійсно вилучити посилання для поширення? За ним надалі годі одержати доступ. Подальша дія остаточна!",
"createSuccess": "Поширення посилання створено.",
"deleteSuccess": "Запрошення через посилання видалено",
"view": "Подання",
@ -399,7 +400,7 @@
"createSuccess": "Подання створено.",
"titleRequired": "Будь ласка, введіть заголовок.",
"delete": "Вилучається подання",
"deleteText": "Справді впровадити? Продовження ніяк не вплине на завдання, але унеможливить вживання у цій справі. Годі спинити!",
"deleteText": "Справді впровадити? Продовження ніяк не вплине на завдання, але унеможливить вживання у цій справі. Подальша дія остаточна!",
"deleteSuccess": "Подання вилучено.",
"onlyAdminsCanEdit": "Лише очільники справи можуть змінювати подання.",
"updateSuccess": "Подання оновлено."
@ -429,7 +430,7 @@
"title": "Постійна вибірка",
"description": "Постійна вибірка - це набір встановлених вказівок, який обчислюється щоразу як його запускають.",
"action": "Створити",
"titleRequired": "Будь ласка, введіть заголовок вибірки."
"titleRequired": "Будь ласка, введіть заголовок."
},
"delete": {
"header": "Вилучається постійна вибірка",
@ -575,7 +576,7 @@
"created": "Створено",
"createdBy": "Автор: {0}",
"actions": "Дії",
"cannotBeUndone": "Годі спинити!"
"cannotBeUndone": "Подальша дія остаточна!"
},
"input": {
"resetColor": "Скинути",
@ -847,7 +848,8 @@
"delete": "Вилучається приписка",
"deleteText1": "Справді впровадити?",
"deleteSuccess": "Приписку вилучено.",
"addedSuccess": "Приписку додано."
"addedSuccess": "Приписку додано.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Перенести строк",
@ -986,7 +988,7 @@
"delete": {
"header": "Вилучається спільнота",
"text1": "Справді впровадити та втратити всі згадки?",
"text2": "Учасники втратять доступи до спільних справ. ГОДІ СПИНИТИ!",
"text2": "Учасники втратять доступи до спільних справ. ПОДАЛЬША ДІЯ ОСТАТОЧНА!",
"success": "Спільноту вилучено."
},
"deleteUser": {
@ -1098,7 +1100,7 @@
"quickActions": {
"commands": "Вказівки",
"placeholder": "Введіть щось або вказівку щоб знайти…",
"hint": "Вжива́йте {project} щоб знайти тільки у справі. Поєднуйте {project} і {label} (позначки) з запитом знаходження, щоб знайти завдання з цими позначками або в цій справі. Вживайте {assignee}, щоб знаходити тільки вказівки.",
"hint": "Вживайте {project} щоб знайти тільки у справі. Поєднуйте {project} і {label} (позначки) з запитом знаходження, щоб знайти завдання з цими позначками або в цій справі. Вживайте {assignee}, щоб знаходити тільки вказівки.",
"tasks": "Завдання",
"projects": "Справи",
"teams": "Спільноти",
@ -1141,9 +1143,9 @@
"1012": "Е-скринька вживача не підтверджена.",
"1013": "Новий пароль не заповнений.",
"1014": "Старий пароль не заповнений.",
"1015": "TOTP is already enabled for this user.",
"1016": "TOTP is not enabled for this user.",
"1017": "The TOTP passcode is invalid.",
"1015": "Разовий пароль вже увімкнено.",
"1016": "Разовий пароль не увімкнено.",
"1017": "Разовий пароль хибний.",
"1018": "Вид облікової світлини вживача - хибний.",
"2001": "ID не може бути порожнім або 0.",
"2002": "Деякі відомості запиту були хибні.",
@ -1153,8 +1155,8 @@
"3006": "Такого поширення справи немає.",
"3007": "Справа з таким ідентифікатором вже є.",
"3008": "Справа у сховищі, тому доступна тільки для читання. Це діє для усіх завдань пов'язаних з цією справою.",
"4001": "Опис завдання не може бути порожнім.",
"4002": "Такого завдання у справі немає.",
"4001": "Назва не може бути порожньою.",
"4002": "Такого завдання немає.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Має бути хоча б одне завдання при зміні завдань.",
"4005": "У Вас немає прав дивитися завдання.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Vui lòng cung cấp một mật khẩu.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Hiển thị mật khẩu",
"hidePassword": "Ẩn mật khẩu",
"noAccountYet": "Bạn chưa có tài khoản?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Xóa bình luận này",
"deleteText1": "Bạn có chắc muốn xóa bình luận này không?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "Bình luận đã được thêm vào."
"addedSuccess": "Bình luận đã được thêm vào.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Trì hoãn ngày đến hạn",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Cần ít nhất một công việc khi chỉnh sửa hàng loạt.",
"4005": "Bạn không có quyền xem công việc.",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "用户名不能像一个 URL。",
"passwordRequired": "请提供密码",
"passwordNotMin": "密码至少有8个字符",
"passwordNotMax": "密码不能超过250个字符",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "显示密码",
"hidePassword": "隐藏密码",
"noAccountYet": "还没有账号?",
@ -163,6 +163,7 @@
"90d": "90 天",
"permissionExplanation": "权限允许您限制 api 令牌被允许做什么。",
"titleRequired": "需要指定标题",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "Token {ago} 前到期",
"tokenCreatedSuccess": "这是您的令牌: {token}",
"tokenCreatedNotSeeAgain": "将其存储在一个安全的位置,你不会再看到它了!",
@ -847,7 +848,8 @@
"delete": "删除此评论",
"deleteText1": "确实要删除此评论吗?",
"deleteSuccess": "评论已删除。",
"addedSuccess": "评论已添加。"
"addedSuccess": "评论已添加。",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "推迟截止时间",
@ -1153,8 +1155,8 @@
"3006": "项目共享不存在。",
"3007": "具有此标识符的项目已存在。",
"3008": "该项目已存档,因此只能读取。与该项目相关的所有任务也是如此。",
"4001": "项目任务文本不能为空。",
"4002": "项目任务不存在。",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "所有批量编辑任务必须属于同一项目。",
"4004": "批量编辑任务时至少需要选择一项任务。",
"4005": "你没有权限查看此任务。",

View File

@ -61,7 +61,7 @@
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
"passwordRequired": "Please provide a password.",
"passwordNotMin": "Password must have at least 8 characters.",
"passwordNotMax": "Password must have at most 250 characters.",
"passwordNotMax": "Password must have at most 72 characters.",
"showPassword": "Show the password",
"hidePassword": "Hide the password",
"noAccountYet": "Don't have an account yet?",
@ -163,6 +163,7 @@
"90d": "90 Days",
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
"titleRequired": "The title is required",
"permissionRequired": "Please select at least one permission from the list.",
"expired": "This token has expired {ago}.",
"tokenCreatedSuccess": "Here is your new api token: {token}",
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
@ -847,7 +848,8 @@
"delete": "Delete this comment",
"deleteText1": "Are you sure you want to delete this comment?",
"deleteSuccess": "The comment was deleted successfully.",
"addedSuccess": "The comment was added successfully."
"addedSuccess": "The comment was added successfully.",
"permalink": "Copy permalink to this comment"
},
"deferDueDate": {
"title": "Defer due date",
@ -1153,8 +1155,8 @@
"3006": "The project share does not exist.",
"3007": "A project with this identifier already exists.",
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
"4001": "The project task text cannot be empty.",
"4002": "The project task does not exist.",
"4001": "The task title cannot be empty.",
"4002": "The task does not exist.",
"4003": "All bulk editing tasks must belong to the same project.",
"4004": "Need at least one task when bulk editing tasks.",
"4005": "You do not have the right to see the task.",

View File

@ -1,11 +1,12 @@
import type {IAbstract} from './IAbstract'
import type {IUser} from './IUser'
// FIXME: what makes this different from TaskFilterParams?
interface Filters {
sortBy: ('start_date' | 'done' | 'id' | 'position')[],
orderBy: ('asc' | 'desc')[],
sort_by: ('start_date' | 'done' | 'id' | 'position')[],
order_by: ('asc' | 'desc')[],
filter: string,
filterIncludeNulls: boolean,
filter_include_nulls: boolean,
s: string,
}

View File

@ -1,3 +1,4 @@
import { objectToSnakeCase } from '@/helpers/case'
import AbstractModel from './abstractModel'
import UserModel from '@/models/user'
@ -9,10 +10,10 @@ export default class SavedFilterModel extends AbstractModel<ISavedFilter> implem
title = ''
description = ''
filters: ISavedFilter['filters'] = {
sortBy: ['done', 'id'],
orderBy: ['asc', 'desc'],
sort_by: ['done', 'id'],
order_by: ['asc', 'desc'],
filter: 'done = false',
filterIncludeNulls: true,
filter_include_nulls: true,
s: '',
}
@ -26,6 +27,10 @@ export default class SavedFilterModel extends AbstractModel<ISavedFilter> implem
this.owner = new UserModel(this.owner)
// Filters are in snake_case for the API - this makes it consistent with the way filter params are used with one-off filters.
// Should probably be camelCase everywhere, but that's a task for another day.
this.filters = objectToSnakeCase(this.filters)
this.created = new Date(this.created)
this.updated = new Date(this.updated)
}

View File

@ -13,7 +13,6 @@ import SavedFilterModel from '@/models/savedFilter'
import {useBaseStore} from '@/stores/base'
import {useProjectStore} from '@/stores/projects'
import {objectToSnakeCase, objectToCamelCase} from '@/helpers/case'
import {success} from '@/message'
import ProjectModel from '@/models/project'
@ -55,23 +54,6 @@ export default class SavedFilterService extends AbstractService<ISavedFilter> {
modelFactory(data) {
return new SavedFilterModel(data)
}
processModel(model) {
// Make filters from this.filters camelCase and set them to the model property:
// That's easier than making the whole filter component configurable since that still needs to provide
// the filter values in snake_sćase for url parameters.
model.filters = objectToCamelCase(model.filters)
return model
}
beforeUpdate(model) {
return this.processModel(model)
}
beforeCreate(model) {
return this.processModel(model)
}
}
export function useSavedFilter(projectId?: MaybeRefOrGetter<IProject['id']>) {
@ -98,10 +80,7 @@ export function useSavedFilter(projectId?: MaybeRefOrGetter<IProject['id']>) {
// We assume the projectId in the route is the pseudoproject
const savedFilterId = getSavedFilterIdFromProjectId(watchedProjectId)
filter.value = new SavedFilterModel({id: savedFilterId})
const response = await filterService.get(filter.value)
response.filters = objectToSnakeCase(response.filters)
filter.value = response
filter.value = await filterService.get(new SavedFilterModel({id: savedFilterId}))
await validateTitleField()
}, {immediate: true})
@ -115,7 +94,6 @@ export function useSavedFilter(projectId?: MaybeRefOrGetter<IProject['id']>) {
const response = await filterService.update(filter.value)
await projectStore.loadAllProjects()
success({message: t('filters.edit.success')})
response.filters = objectToSnakeCase(response.filters)
filter.value = response
await useBaseStore().setCurrentProject(new ProjectModel({
id: getProjectId(filter.value),

View File

@ -31,9 +31,17 @@ export default class TaskCollectionService extends AbstractService<ITask> {
constructor() {
super({
getAll: '/projects/{projectId}/views/{viewId}/tasks',
// /projects/{projectId}/tasks when viewId is not provided
})
}
getReplacedRoute(path: string, pathparams: Record<string, unknown>): string {
if (!pathparams.viewId) {
return super.getReplacedRoute('/projects/{projectId}/tasks', pathparams)
}
return super.getReplacedRoute(path, pathparams)
}
modelFactory(data) {
// FIXME: There must be a better way for this…
if (typeof data.project_view_id !== 'undefined') {

View File

@ -1,4 +1,4 @@
import {computed, ref} from 'vue'
import {computed, readonly, ref} from 'vue'
import {acceptHMRUpdate, defineStore} from 'pinia'
import LabelService from '@/services/label'
@ -21,30 +21,35 @@ async function getAllLabels(page = 1): Promise<ILabel[]> {
}
}
export interface LabelState {
[id: ILabel['id']]: ILabel
}
export const useLabelStore = defineStore('label', () => {
// The labels are stored as an object which has the label ids as keys.
const labels = ref<LabelState>({})
const isLoading = ref(false)
const labels = ref<{ [id: ILabel['id']]: ILabel }>({})
const getLabelsByIds = computed(() => {
return (ids: ILabel['id'][]) => Object.values(labels.value).filter(({id}) => ids.includes(id))
})
// Alphabetically sort the labels
const labelsArray = computed(() => Object.values(labels.value)
.sort((a, b) => a.title.localeCompare(
b.title, i18n.global.locale.value,
{ ignorePunctuation: true },
)),
)
const isLoading = ref(false)
const getLabelById = computed(() => {
return (labelId: ILabel['id']) => Object.values(labels.value).find(({id}) => id === labelId)
return (labelId: ILabel['id']) => labels.value[labelId]
})
const getLabelsByIds = computed(() => (ids: ILabel['id'][]) =>
ids.map(id => labels.value[id]).filter(Boolean),
)
// **
// * Checks if a project of labels is available in the store and filters them then query
// * Checks if a list of labels is available in the store and filters them then query
// **
const filterLabelsByQuery = computed(() => {
return (labelsToHide: ILabel[], query: string) => {
const labelIdsToHide: number[] = labelsToHide.map(({id}) => id)
return search(query)
?.filter(value => !labelIdsToHide.includes(value))
.map(id => labels.value[id])
@ -53,14 +58,12 @@ export const useLabelStore = defineStore('label', () => {
})
const getLabelsByExactTitles = computed(() => {
return (labelTitles: string[]) => Object
.values(labels.value)
return (labelTitles: string[]) => labelsArray.value
.filter(({title}) => labelTitles.some(l => l.toLowerCase() === title.toLowerCase()))
})
const getLabelByExactTitle = computed(() => {
return (labelTitle: string) => Object
.values(labels.value)
return (labelTitle: string) => labelsArray.value
.find(l => l.title.toLowerCase() === labelTitle.toLowerCase())
})
@ -144,11 +147,12 @@ export const useLabelStore = defineStore('label', () => {
}
return {
labels,
labels: readonly(labels),
labelsArray: readonly(labelsArray),
isLoading,
getLabelsByIds,
getLabelById,
getLabelsByIds,
filterLabelsByQuery,
getLabelsByExactTitles,
getLabelByExactTitle,

View File

@ -20,10 +20,6 @@ import type {IProjectView} from '@/modelTypes/IProjectView'
const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
export interface ProjectState {
[id: IProject['id']]: IProject
}
export const useProjectStore = defineStore('project', () => {
const baseStore = useBaseStore()
const router = useRouter()
@ -31,9 +27,10 @@ export const useProjectStore = defineStore('project', () => {
const isLoading = ref(false)
// The projects are stored as an object which has the project ids as keys.
const projects = ref<ProjectState>({})
const projects = ref<{ [id: IProject['id']]: IProject }>({})
const projectsArray = computed(() => Object.values(projects.value)
.sort((a, b) => a.position - b.position))
const notArchivedRootProjects = computed(() => projectsArray.value
.filter(p => p.parentProjectId === 0 && !p.isArchived && p.id > 0))
const favoriteProjects = computed(() => projectsArray.value
@ -48,7 +45,7 @@ export const useProjectStore = defineStore('project', () => {
const findProjectByExactname = computed(() => {
return (name: string) => {
const project = Object.values(projects.value).find(l => {
const project = projectsArray.value.find(l => {
return l.title.toLowerCase() === name.toLowerCase()
})
return typeof project === 'undefined' ? null : project
@ -57,7 +54,7 @@ export const useProjectStore = defineStore('project', () => {
const findProjectByIdentifier = computed(() => {
return (identifier: string) => {
const project = Object.values(projects.value).find(p => {
const project = projectsArray.value.find(p => {
return p.identifier.toLowerCase() === identifier.toLowerCase()
})
return typeof project === 'undefined' ? null : project
@ -69,7 +66,7 @@ export const useProjectStore = defineStore('project', () => {
return search(query)
?.filter(value => value > 0)
.map(id => projects.value[id])
.filter(project => project.isArchived === includeArchived)
.filter(project => project?.isArchived === includeArchived)
|| []
}
})
@ -79,7 +76,7 @@ export const useProjectStore = defineStore('project', () => {
return search(query)
?.filter(value => getSavedFilterIdFromProjectId(value) > 0)
.map(id => projects.value[id])
.filter(project => project.isArchived === includeArchived)
.filter(project => project?.isArchived === includeArchived)
|| []
}
})
@ -221,25 +218,30 @@ export const useProjectStore = defineStore('project', () => {
}
function setProjectView(view: IProjectView) {
const viewPos = projects.value[view.projectId].views.findIndex(v => v.id === view.id)
const views = [...projects.value[view.projectId].views]
const viewPos = views.findIndex(v => v.id === view.id)
if (viewPos !== -1) {
projects.value[view.projectId].views[viewPos] = view
projects.value[view.projectId].views.sort((a, b) => a.position < b.position ? -1 : 1)
setProject(projects.value[view.projectId])
return
views[viewPos] = view
} else {
views.push(view)
}
views.sort((a, b) => a.position < b.position ? -1 : 1)
projects.value[view.projectId].views.push(view)
projects.value[view.projectId].views.sort((a, b) => a.position < b.position ? -1 : 1)
setProject(projects.value[view.projectId])
setProject({
...projects.value[view.projectId],
views,
})
}
function removeProjectView(projectId: IProject['id'], viewId: IProjectView['id']) {
const viewPos = projects.value[projectId].views.findIndex(v => v.id === viewId)
if (viewPos !== -1) {
projects.value[projectId].views.splice(viewPos, 1)
}
const project = projects.value[projectId]
const updatedViews = project.views.filter(v => v.id !== viewId)
setProject({
...project,
views: updatedViews,
})
}
return {

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