Compare commits

...
Author SHA1 Message Date
kolaente 4d684992c6 feat(navigation): use focus-visible for nav items 2024-09-19 12:35:08 +02:00
kolaente 4dd9d5de67 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.
2024-09-19 12:21:19 +02:00
kolaente ec902b051b fix(task): mark related task as done from the task detail view
Resolves https://github.com/go-vikunja/vikunja/issues/326
2024-09-19 11:31:06 +02:00
kolaente 4b2b8e3b83 chore: rearrange cron registers 2024-09-19 11:24:43 +02:00
kolaente bc52da4029 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
2024-09-19 11:19:48 +02:00
kolaente 2123da49a3 fix(task): cleanup old task positions and task buckets when adding an updated or created task to filter 2024-09-19 09:30:21 +02:00
kolaente c53a761396 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
2024-09-19 09:30:20 +02:00
kolaente c84b50b3ee fix(labels): trigger task updated for bulk label task update 2024-09-19 08:59:34 +02:00
kolaente c5b82fc591 fix: test selector 2024-09-18 21:28:08 +02:00
renovateandkonrad 1f00180af6 fix(deps): update dependency vue to v3.5.6 2024-09-18 17:02:42 +00:00
renovateandkonrad 33e851f543 fix(deps): update dependency tailwindcss to v3.4.12 2024-09-18 17:02:06 +00:00
kolaente 16d7386975 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
2024-09-18 19:00:47 +02:00
kolaente 6d9bda8cc1 fix(projects): description not visible on mobile 2024-09-18 18:47:46 +02:00
kolaente abf92e29fa 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
2024-09-18 18:00:27 +02:00
kolaente f4d628550f chore(tasks): move drag options to direct attributes instead of v-bind 2024-09-18 16:34:24 +02:00
kolaente 1d352fcbf3 chore(devenv): do not install cypress on darwin 2024-09-18 14:30:38 +02:00
kolaente a1105434bf chore: remove console.log 2024-09-18 13:38:30 +02:00
kolaente acc7c9f8f5 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
2024-09-18 13:36:56 +02:00
kolaente cc64ca6406 fix(task): align task title on mobile popup 2024-09-18 11:36:06 +02:00
kolaente 89f78cd369 chore: add go and direnv to recommended vscode extensions 2024-09-18 10:43:23 +02:00
renovateandkonrad 14c3b13823 fix(deps): update tiptap to v2.7.1 2024-09-18 08:33:10 +00:00
renovate c2a7438814 chore(deps): update dev-dependencies 2024-09-18 01:18:02 +00:00
renovateandkonrad 8fe37fd900 fix(deps): update tiptap to v2.7.0 2024-09-17 10:11:13 +00:00
renovate 88b4ab1768 fix(deps): update module github.com/prometheus/client_golang to v1.20.4 2024-09-17 09:15:34 +00:00
kolaente c252c8f0cd fix: reset id before creating 2024-09-17 10:46:14 +02:00
renovateandkonrad 410ff28b71 fix(deps): update vueuse to v11.1.0 2024-09-17 08:11:36 +00:00
renovateandkonrad d98dd7ecbb chore(deps): update dev-dependencies 2024-09-17 08:10:22 +00:00
renovate fc73085876 fix(deps): update dependency date-fns to v4 2024-09-17 05:16:09 +00:00
renovate c71f7334b4 chore(deps): update dev-dependencies 2024-09-15 10:15:16 +00:00
kolaente 6a5342bd49 fix(modal): do not prevent scrolling on mobile
Maybe related to https://github.com/go-vikunja/vikunja/issues/325
2024-09-15 11:38:17 +02:00
renovate b4ea1bb86a fix(deps): update dependency vue-router to v4.4.5 2024-09-14 11:15:19 +00:00
renovateandkonrad 378d0ae9bb fix(deps): update dependency vue to v3.5.5 2024-09-14 10:17:49 +00:00
kolaente 6f27e1401a fix(user): do not create user with existing id
Resolves https://vikunja.sentry.io/share/issue/6f1e37d4b8b248188e20650234a45cde/
2024-09-14 12:16:05 +02:00
kolaente bf5cafc03f fix: lint 2024-09-14 12:12:47 +02:00
kolaente f4a7326b68 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
2024-09-14 12:11:54 +02:00
kolaente 1451f6e46f fix(kanban): correctly paginate filtered kanban buckets
Resolves https://github.com/go-vikunja/vikunja/issues/314
2024-09-14 09:37:21 +02:00
Frederick [Bot]anddrone 427eb2a618 chore(i18n): update translations via Crowdin 2024-09-14 00:25:59 +00:00
kolaente e4b541e653 fix(view): correctly get paginated task results 2024-09-13 22:56:41 +02:00
kolaente 45ff5907e6 fix(view): correctly resolve bucket filter when paginating 2024-09-13 19:45:48 +02:00
kolaente 5a7c3927f3 fix(view): correctly resolve label for filtered views or buckets 2024-09-13 19:14:54 +02:00
kolaente f425d98b4d 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
2024-09-13 18:42:42 +02:00
kolaente 4c55016c1a fix(task): multiple overlapping defer due date popups
Resolves https://github.com/go-vikunja/vikunja/issues/131
2024-09-13 17:44:26 +02:00
kolaente 1a8f12ac13 fix(kanban): do not mark first bucked as done bucket in filter bucket mode
Resolves https://github.com/go-vikunja/vikunja/issues/313
2024-09-13 11:52:44 +02:00
kolaente 7b873ec31c fix(table): make sorting for two-word properties work
Resolves https://community.vikunja.io/t/various-sorting-filtering-issues/2781/8
2024-09-13 11:03:38 +02:00
kolaente 8b77832af2 fix(test): cypress test selector 2024-09-13 10:28:48 +02:00
Frederick [Bot]anddrone 14710d0bba chore(i18n): update translations via Crowdin 2024-09-13 00:25:12 +00:00
kolaente 945f25b818 fix(kanban): make kanban full width on mobile
Related to https://github.com/go-vikunja/vikunja/issues/309
2024-09-12 16:42:12 +02:00
kolaente f53b93d98a fix(modal): make sure modal and its content scrolls properly on mobile
Related to https://github.com/go-vikunja/vikunja/issues/309
2024-09-12 16:22:18 +02:00
kolaente 97e030a1fc 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
2024-09-12 15:51:08 +02:00
renovateandkonrad b708db7025 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>
2024-09-12 13:31:26 +00:00
kolaente 6bfe71b30e fix(filters): immediately propagate changes
Resolves https://community.vikunja.io/t/filtering-startdate-now-is-invalid/2613/6
2024-09-12 15:28:39 +02:00
renovateandkonrad db196bdffe fix(deps): update dependency @intlify/unplugin-vue-i18n to v5 2024-09-12 12:42:40 +00:00
renovateandkonrad 9428acd5e1 fix(deps): update dependency express to v4.21.0 2024-09-12 12:41:45 +00:00
renovateandkonrad 58a60fb4ae fix(deps): update dependency vue-router to v4.4.4 2024-09-12 12:36:18 +00:00
renovateandkonrad f4ca22a6ad fix(deps): update dependency flexsearch to v0.7.43 (#2095)
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-09-12 12:35:35 +00:00
kolaente 7e38a9f442 fix(kanban): make task creation loading spinner actually visible 2024-09-12 14:33:44 +02:00
renovateandkonrad a0a77818b1 fix(deps): update dependency vue-i18n to v10.0.1 2024-09-12 11:39:50 +00:00
kolaente bdccd633fe fix(labels): remove input interactivity when label edit is disabled 2024-09-12 13:39:03 +02:00
kolaente 1f3eb8f2a3 chore: fix comment 2024-09-12 13:39:03 +02:00
renovate 8891ef347e fix(deps): update dependency tailwindcss to v3.4.11 2024-09-12 10:26:57 +00:00
kolaente 3b8ccbad4f fix(multiselect): make selectPlaceholder optional 2024-09-12 11:29:39 +02:00
kolaente cc1b4bbd1b 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
2024-09-12 11:02:38 +02:00
Frederick [Bot]andkolaente 9dd95101f9 [skip ci] Updated swagger docs 2024-09-12 08:42:38 +00:00
kolaente 20724f6fb5 fix(task): cyclomatic complexity 2024-09-12 10:24:16 +02:00
Frederick [Bot]anddrone e1ea84fcd4 chore(i18n): update translations via Crowdin 2024-09-12 00:26:38 +00:00
kolaente 8d730543c3 fix(project): reset id before creating
Resolves https://vikunja.sentry.io/share/issue/6f1e37d4b8b248188e20650234a45cde/
2024-09-11 23:46:13 +02:00
renovateandkonrad a3d133bfee fix(deps): update dependency vue-i18n to v10 2024-09-11 18:01:57 +00:00
renovateandkonrad 7b37ad0594 fix(deps): update dependency @sentry/vue to v8.30.0 2024-09-11 17:36:28 +00:00
kolaente 4aeb228c3a fix(project): show description in title attribute without html
Resolves https://community.vikunja.io/t/feedback-to-vikunja-0-24-0/2557/6
2024-09-11 18:23:43 +02:00
kolaente b52d6dbf59 fix(projects): do not hide 6th project on project overview 2024-09-11 18:20:57 +02:00
kolaente 76f7797e56 chore: remove console.log 2024-09-11 18:14:10 +02:00
kolaente 55dd7d2981 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
2024-09-11 17:58:42 +02:00
kolaente 44a43b9f86 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/
2024-09-10 18:23:06 +02:00
kolaente eb95caf757 fix(password): validate password before sending request to api 2024-09-10 17:44:52 +02:00
kolaente 1085a6583b fix(caldav): reject invalid project id with error 400
Resolves https://vikunja.sentry.io/share/issue/6fc18edefa0e4db3b2e10efe36deeaa4/
2024-09-10 17:32:35 +02:00
kolaente e698ac5a34 fix(label): ignore existing ID during creation 2024-09-10 16:45:40 +02:00
renovate 020fea686d fix(deps): update module github.com/getsentry/sentry-go to v0.29.0 2024-09-10 10:17:45 +00:00
renovateandkonrad addf8523f4 fix(deps): update dependency express to v4.20.0 2024-09-10 09:49:23 +00:00
renovate 044aa1c742 fix(deps): update dependency vue to v3.5.4 2024-09-10 09:14:50 +00:00
renovateandkonrad 552e1ce796 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>
2024-09-10 08:50:42 +00:00
renovateandkonrad c1246d73d4 fix(deps): update dependency @sentry/vue to v8.29.0 2024-09-10 07:13:00 +00:00
renovate 85fa397a46 chore(deps): update pnpm to v9.10.0 2024-09-09 14:35:34 +00:00
renovate af47e43c34 chore(deps): update dev-dependencies 2024-09-09 03:31:05 +00:00
Frederick [Bot]andkolaente 2539f15dfb [skip ci] Updated swagger docs 2024-09-07 11:48:49 +00:00
kolaente 5c1b2846a1 docs(api): use correct return type for the /user endpoint 2024-09-07 13:27:06 +02:00
renovate af8100d262 fix(deps): update dependency vue-router to v4.4.3 2024-09-06 18:17:15 +00:00
renovateandkonrad 63f85c97ae chore(deps): update dependency go to v1.23.1 2024-09-06 18:06:24 +00:00
renovateandkonrad 2b1e3ffd2a chore(deps): update dev-dependencies 2024-09-06 18:04:14 +00:00
renovate 5f470afd7c fix(deps): update module golang.org/x/image to v0.20.0 2024-09-06 17:17:31 +00:00
renovateandkonrad 37c6f94ba2 fix(deps): update module github.com/prometheus/client_golang to v1.20.3 2024-09-06 15:56:25 +00:00
renovate b42cd5786e fix(deps): update dependency vue to v3.5.3 2024-09-06 15:21:22 +00:00
renovateandkonrad 5072ab3d72 fix(deps): update module dario.cat/mergo to v1.0.1 2024-09-06 15:11:42 +00:00
renovateandkonrad 4b5ec018f5 fix(deps): update module github.com/redis/go-redis/v9 to v9.6.1 2024-09-06 15:11:34 +00:00
renovateandkonrad efe5c7b3cb fix(deps): update module golang.org/x/crypto to v0.27.0 2024-09-06 15:09:51 +00:00
renovateandkonrad ba0c32aacf fix(deps): update module golang.org/x/oauth2 to v0.23.0 2024-09-06 15:09:43 +00:00
renovateandkonrad b6e142e8d0 fix(deps): update vueuse to v11 2024-09-06 15:08:03 +00:00
renovate 750d565477 fix(deps): update dependency tailwindcss to v3.4.10 2024-09-06 12:15:45 +00:00
Frederick [Bot]andkolaente 2b8515b91d [skip ci] Updated swagger docs 2024-09-06 11:18:51 +00:00
kolaente 68636f27da fix(files): use absolute path everywhere 2024-09-06 12:59:48 +02:00
kolaente cf94cc8cab chore(docs): clarify usage of related model creation
Resolves https://github.com/go-vikunja/vikunja/issues/323
2024-09-06 12:06:12 +02:00
renovateandkonrad 669b7452cd fix(deps): update dependency axios to v1.7.7 2024-09-06 08:34:52 +00:00
renovateandkonrad 1601c9098d fix(deps): update dependency sortablejs to v1.15.3 2024-09-06 08:34:36 +00:00
renovateandkonrad f132b0e718 fix(deps): update module github.com/gabriel-vasile/mimetype to v1.4.5 2024-09-06 08:33:40 +00:00
renovateandkonrad 5d12986e3f fix(deps): update module github.com/mattn/go-sqlite3 to v1.14.23 2024-09-06 08:33:37 +00:00
renovateandkonrad 74e0197dc6 fix(deps): update module github.com/threedotslabs/watermill to v1.3.7 2024-09-06 08:32:06 +00:00
renovateandkonrad 2dd133fb4c fix(deps): update dependency @kyvg/vue3-notification to v3.3.0 2024-09-06 08:31:54 +00:00
kolaente 02c1de55c4 chore(attachments): refactor building image preview 2024-09-06 09:43:59 +02:00
renovateandkonrad cbc63c853d fix(deps): update dependency @sentry/vue to v8.28.0 2024-09-06 07:40:44 +00:00
renovateandkonrad b5c4a3a80c fix(deps): update dependency pinia to v2.2.2 2024-09-06 07:40:14 +00:00
renovateandkonrad a8ea5e11ab fix(deps): update dependency vue-i18n to v9.14.0 2024-09-06 07:39:02 +00:00
renovateandkonrad 812430b9cf fix(deps): update module golang.org/x/text to v0.18.0 2024-09-06 07:37:53 +00:00
kolaente 9d7b6d3d9a feat(dump): add flag to allow specifying dump path
Resolves https://community.vikunja.io/t/vikunja-0-24-docker-bin-sh/2621/3
2024-09-05 15:14:03 +02:00
kolaente c2b116de70 chore(files): use absolute file path to retrieve and save files 2024-09-05 15:03:32 +02:00
kolaente 22e594e253 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
2024-09-05 14:51:47 +02:00
kolaente 0d9c03e0f2 chore(magefile): use tx.Sync instead of Sync2 2024-09-05 14:40:22 +02:00
renovateandkonrad 72bb6609a1 fix(deps): update module golang.org/x/term to v0.24.0 2024-09-05 10:18:27 +00:00
renovate b259072e6a fix(deps): update module github.com/typesense/typesense-go to v2 2024-09-04 21:16:13 +00:00
kolaente ffcc48ec87 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
2024-09-04 23:08:29 +02:00
Frederick [Bot]andkolaente 5aa0b6a0cf [skip ci] Updated swagger docs 2024-09-04 20:43:53 +00:00
kolaente b60efbd259 chore(subscription): return subscription entity type using json Marshaler 2024-09-04 22:25:03 +02:00
kolaente 34ac29fcce fix(reminders): notify subscribed users as well
Resolves https://community.vikunja.io/t/no-reminder-notification-by-e-mail-from-my-colleague/2779
2024-09-04 22:11:07 +02:00
kolaente 75f3e930cd fix(subscriptions): do not panic when a task does not have a subscription 2024-09-04 21:55:14 +02:00
kolaente 4ff8815fe1 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.
2024-09-04 19:55:17 +02:00
renovate 850ec7efb0 fix(deps): update dependency dayjs to v1.11.13 2024-09-03 20:59:37 +00:00
kolaente 115d1c3618 fix(subscription): always return task subscription when subscribed to task and project 2024-09-03 22:37:53 +02:00
kolaente 6d26497bbb chore(deps): update github.com/wneessen/go-mail to v0.4.4 2024-09-03 22:07:11 +02:00
kolaente 06305eb6b3 fix(subscriptions): correctly inherit subscriptions
Resolves https://community.vikunja.io/t/e-mail-notification-twice/2740/20
2024-09-03 22:04:11 +02:00
Frederick [Bot]anddrone 7bd84a845c chore(i18n): update translations via Crowdin 2024-09-03 00:25:53 +00:00
kolaente 165ee9e7f3 fix(service worker): use correct workbox version
Resolves https://github.com/go-vikunja/vikunja/issues/322
2024-09-02 23:40:17 +02:00
kolaente 2c9becec10 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
2024-09-02 23:36:58 +02:00
kolaente 1463bce24c feat(dev): add mailpit to devenv services 2024-09-02 23:18:50 +02:00
kolaente 5c56d07215 chore(web): always set internal error 2024-09-01 19:37:27 +02:00
kolaente 6fb314b326 chore(web): use logger directly 2024-09-01 19:37:27 +02:00
kolaente 9b01666ec6 chore(web): use web auth factory directly 2024-09-01 19:37:27 +02:00
Frederick [Bot]anddrone 14c862d284 chore(i18n): update translations via Crowdin 2024-08-30 00:25:55 +00:00
kolaente bcfd72c645 chore(web): use config directly 2024-08-29 18:41:20 +02:00
kolaente 499f66b7ae chore(web): directly use new db session 2024-08-29 16:31:46 +02:00
kolaente 8e37d5cb76 chore(web): remove redundant use of fmt.Sprintf 2024-08-29 16:28:37 +02:00
kolaente 57ba073874 chore(web): use errors.As instead of type assertion 2024-08-29 16:28:16 +02:00
kolaente 329de3aab3 chore(web): remove unused echo context 2024-08-29 16:20:39 +02:00
kolaente 74a74b7ec7 fix(labels): test error assertion 2024-08-29 16:16:41 +02:00
kolaente 2063da9eec chore(web): move web handler package to Vikunja 2024-08-29 16:15:28 +02:00
kolaente 7a7e97770c chore(errors): always add internal error to echo error 2024-08-29 15:39:34 +02:00
kolaente bea131cfd9 fix(labels): trigger task.updated event when removing a label from a task 2024-08-29 15:19:06 +02:00
kolaente 5a0d1f1dc6 feat(event): simplify dispatching task updated event from only a task id 2024-08-29 15:18:03 +02:00
kolaente b5035f9d2b chore(docker): use new env format 2024-08-29 11:19:46 +02:00
kolaente 07bd9de0c6 feat(docker): add opencontainer labels
Resolves https://github.com/go-vikunja/vikunja/issues/308
2024-08-29 11:19:21 +02:00
kolaente efde364224 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
2024-08-29 10:35:05 +02:00
kolaente fde1763eef fix(api): return 404 response when using a token and the route does not exist 2024-08-29 10:12:20 +02:00
kolaente 429c7ca2c1 feat(task): always insert new tasks at the top
Resolves https://community.vikunja.io/t/kanban-cards-in-wrong-order/2731
2024-08-28 15:30:40 +02:00
kolaente bf56311faa fix(task): set done at date when moving a task to the done bucket
Resolves https://github.com/go-vikunja/vikunja/issues/320
2024-08-28 14:21:31 +02:00
Frederick [Bot]anddrone c38aaeb224 chore(i18n): update translations via Crowdin 2024-08-28 00:25:41 +00:00
Frederick [Bot]anddrone 1b9139b9b0 chore(i18n): update translations via Crowdin 2024-08-27 00:25:53 +00:00
kolaente 13763e3540 chore(deps): update tiptap to 2.6.6 2024-08-26 22:58:15 +02:00
kolaente 56bab7485f feat(i18n): add Lithuanian translation for selection 2024-08-26 17:05:01 +02:00
kolaente 8bfd0493b2 fix(deps): update golangci 2024-08-25 14:30:46 +00:00
kolaente 435cb2e7f7 fix(view): do not crash when saving a view
Resolves https://github.com/go-vikunja/vikunja/issues/312
2024-08-25 15:53:51 +02:00
Frederick [Bot]anddrone 9ed33f5c08 chore(i18n): update translations via Crowdin 2024-08-23 00:25:56 +00:00
Frederick [Bot]anddrone 83669fd9e0 chore(i18n): update translations via Crowdin 2024-08-21 00:25:43 +00:00
Frederick [Bot]anddrone 35578e0021 chore(i18n): update translations via Crowdin 2024-08-20 00:25:51 +00:00
Frederick [Bot]anddrone e281de30b5 chore(i18n): update translations via Crowdin 2024-08-17 00:15:36 +00:00
Frederick [Bot]anddrone ec2c2e74f0 chore(i18n): update translations via Crowdin 2024-08-15 00:03:11 +00:00
kolaente b031c9772f chore: 0.24.2 release preperations
(cherry picked from commit 08821ea8a8)
2024-08-12 20:25:31 +02:00
kolaente f22420d502 fix(kanban): always make cover image full width 2024-08-12 17:22:26 +02:00
kolaente ebfd5f54d2 fix(migration): ensure project background gets exported and imported 2024-08-12 17:18:07 +02:00
kolaente 28b4eaee31 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
2024-08-12 17:00:16 +02:00
kolaente b83448b7a6 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
2024-08-12 15:53:51 +02:00
kolaente eed783e42f 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
2024-08-12 15:35:37 +02:00
kolaente 1d624612ee chore: use nixpkgs unstable for more recent packages 2024-08-12 15:32:12 +02:00
kolaente 9576f72d7c fix(task): set current project after moving a task 2024-08-12 15:22:14 +02:00
kolaente 7efc4d1bc8 fix(task): do not allow moving a task to the project the task already belongs to 2024-08-12 15:18:45 +02:00
kolaente 670e605275 chore(deps): update dependency node to v20.16.0 2024-08-12 12:26:53 +02:00
kolaente 871b3ce071 chore(deps): update go toolchain to 1.22.5 2024-08-12 12:17:53 +02:00
kolaente ad3c5fcee5 feat: switch from nix flakes to devenv 2024-08-12 12:17:31 +02:00
kolaente e2a87036e0 feat(editor): support custom protocol for links
Resolves https://github.com/go-vikunja/vikunja/issues/306
2024-08-12 11:42:43 +02:00
kolaente 40bb86bee5 fix(mail): do not fail testmail command when the connection could not be closed.
Resolves https://github.com/go-vikunja/vikunja/issues/300
2024-08-12 11:08:16 +02:00
Frederick [Bot]anddrone 4a4ed2cf89 chore(i18n): update translations via Crowdin 2024-08-10 00:03:08 +00:00
kolaente 7751e124a7 chore(deps): update flake 2024-08-08 11:24:26 +02:00
Frederick [Bot]anddrone 6137d07138 chore(i18n): update translations via Crowdin 2024-08-08 00:03:17 +00:00
kolaente 66d316f0b5 fix(i18n): change casing of Ukrainian language in selector 2024-08-07 16:51:22 +02:00
kolaente bf2d56c9d4 docs: clarify Todoist redirect url
Resolves https://github.com/go-vikunja/vikunja/issues/302
2024-08-07 16:27:29 +02:00
Dominik Pschenitschniandkonrad 413798b321 fix: remove console log 2024-08-07 13:29:56 +00:00
Frederick [Bot]anddrone bf1c8c6119 chore(i18n): update translations via Crowdin 2024-07-31 00:02:59 +00:00
Frederick [Bot]anddrone 49fa0a8468 chore(i18n): update translations via Crowdin 2024-07-27 00:03:15 +00:00
Frederick [Bot]anddrone 784cd42a7a chore(i18n): update translations via Crowdin 2024-07-24 00:03:08 +00:00
Frederick [Bot]andDominik Pschenitschni 4f74be0247 chore(i18n): update translations via Crowdin 2024-07-23 00:02:28 +00:00
Dominik Pschenitschniandkonrad 7ef1e0a3e5 fix: textarea autosize for LanguageTool 2024-07-21 12:40:32 +00:00
Dominik Pschenitschniandkonrad bc5fd380e5 chore: improve error message 2024-07-21 11:03:20 +00:00
Dominik Pschenitschniandkonrad bcd306b84d chore: remove lodash.debounce 2024-07-21 11:02:25 +00:00
Dominik Pschenitschniandkonrad ad2870335d feat: add missing peer dependency 2024-07-21 11:01:38 +00:00
Frederick [Bot]anddrone 917748777f chore(i18n): update translations via Crowdin 2024-07-21 00:03:15 +00:00
renovateandkonrad 324c966dda fix(deps): update module github.com/coreos/go-oidc/v3 to v3.11.0 2024-07-19 10:16:39 +00:00
Frederick [Bot]andDominik Pschenitschni 0984821b41 [skip ci] Updated swagger docs 2024-07-19 10:01:46 +00:00
Dominik Pschenitschniandkonrad e8be657d97 feat: improve ProjectSettingsViews 2024-07-19 09:43:49 +00:00
75ce261f74 feat: downscaled image previews for task attachments (#2541)
As discussed in [https://community.vikunja.io/t/add-scaled-down-images-for-image-previews](https://community.vikunja.io/t/add-scaled-down-images-for-image-previews) this adds a query parameter in the task attachment request which returns a scaled down image for preview purposes to reduce network load and improve responsiveness.

Co-authored-by: Elscrux <nickposer2102@gmail.com>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2541
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Elscrux <elscrux@gmail.com>
Co-committed-by: Elscrux <elscrux@gmail.com>
2024-07-19 09:30:06 +00:00
5f9d0fe763 feat: permalinks for task comments (#2442)
Resolves #2441

Co-authored-by: Devadatta Sahoo <webmaster@nettantra.com>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2442
Reviewed-by: konrad <k@knt.li>
Co-authored-by: devadattas <devadattas@gmail.com>
Co-committed-by: devadattas <devadattas@gmail.com>
2024-07-19 08:45:50 +00:00
Dominik Pschenitschniandkonrad bbfd5270db 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>
2024-07-19 07:33:10 +00:00
Dominik Pschenitschniandkonrad 1e632397d2 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.
2024-07-19 07:26:50 +00:00
Dominik Pschenitschniandkonrad d35454c099 feat: improve priority visibility 2024-07-19 06:53:53 +00:00
Dominik Pschenitschniandkonrad 2a6ba7e7f0 feat: improve label store 2024-07-19 06:51:24 +00:00
Dominik Pschenitschniandkonrad 1e523a1a39 feat: improve projects store 2024-07-19 05:49:14 +00:00
Dominik Pschenitschniandkonrad fb91e73a3c fix: emit for DatepickerWithValues 2024-07-19 05:28:17 +00:00
Frederick [Bot]andDominik Pschenitschni ebd6139c1f chore(i18n): update translations via Crowdin 2024-07-19 00:03:11 +00:00
Dominik Pschenitschni 6990be705c feat: use withDefaults for Reminders 2024-07-18 18:31:33 +02:00
256 changed files with 8366 additions and 4147 deletions
+13 -13
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
@@ -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
@@ -901,7 +901,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
@@ -962,7 +962,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
@@ -1352,6 +1352,6 @@ steps:
- failure
---
kind: signature
hmac: 196e215a44cc1b6b5cb89e6ef09196166af17969f69e3e7e8c571a0e44285714
hmac: eff6e40dc763fa1395078daaabcbef2aaa74fe0a36818a87e89015dbc4e9dae4
...
+3 -1
View File
@@ -1 +1,3 @@
use flake
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
use devenv
+11 -1
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
+10
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
+13 -11
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"
]
}
+52 -1
View File
@@ -5,7 +5,58 @@ 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.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
+13 -6
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/ ./
@@ -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}"
@@ -35,7 +35,14 @@ RUN export PATH=$PATH:$GOPATH/bin && \
# The actual image
FROM scratch
LABEL maintainer="maintainers@vikunja.io"
LABEL org.opencontainers.image.authors='maintainers@vikunja.io'
LABEL org.opencontainers.image.url='https://vikunja.io'
LABEL org.opencontainers.image.documentation='https://vikunja.io/docs'
LABEL org.opencontainers.image.source='https://code.vikunja.io/vikunja'
LABEL org.opencontainers.image.licenses='AGPLv3'
LABEL org.opencontainers.image.title='Vikunja'
WORKDIR /app/vikunja
ENTRYPOINT [ "/app/vikunja/vikunja" ]
EXPOSE 3456
+1 -1
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.2-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)
+6 -5
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
+2 -2
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"
}
}
+39 -34
View File
@@ -9,13 +9,13 @@ dependencies:
specifier: 2.0.0
version: 2.0.0
express:
specifier: 4.19.2
version: 4.19.2
specifier: 4.21.0
version: 4.21.0
devDependencies:
electron:
specifier: 29.4.5
version: 29.4.5
specifier: 29.4.6
version: 29.4.6
electron-builder:
specifier: 24.13.3
version: 24.13.3(electron-builder-squirrel-windows@24.13.3)
@@ -445,8 +445,8 @@ packages:
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
dev: true
/body-parser@1.20.2:
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
/body-parser@1.20.3:
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
dependencies:
bytes: 3.1.2
@@ -457,7 +457,7 @@ packages:
http-errors: 2.0.0
iconv-lite: 0.4.24
on-finished: 2.4.1
qs: 6.11.0
qs: 6.13.0
raw-body: 2.5.2
type-is: 1.6.18
unpipe: 1.0.0
@@ -917,8 +917,8 @@ packages:
- supports-color
dev: true
/electron@29.4.5:
resolution: {integrity: sha512-DlEuzGbWBYl1Qr0qUYgNZdoixJg4YGHy2HC6fkRjSXSlb01UrQ5ORi8hNLzelzyYx8rNQyyE3zDUuk9EnZwYuA==}
/electron@29.4.6:
resolution: {integrity: sha512-fz8ndj8cmmf441t4Yh2FDP3Rn0JhLkVGvtUf2YVMbJ5SdJPlc0JWll9jYkhh60jDKVVCr/tBAmfxqRnXMWJpzg==}
engines: {node: '>= 12.20.55'}
hasBin: true
requiresBuild: true
@@ -943,6 +943,11 @@ packages:
engines: {node: '>= 0.8'}
dev: false
/encodeurl@2.0.0:
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
engines: {node: '>= 0.8'}
dev: false
/end-of-stream@1.4.4:
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
dependencies:
@@ -995,36 +1000,36 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/express@4.19.2:
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
/express@4.21.0:
resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==}
engines: {node: '>= 0.10.0'}
dependencies:
accepts: 1.3.8
array-flatten: 1.1.1
body-parser: 1.20.2
body-parser: 1.20.3
content-disposition: 0.5.4
content-type: 1.0.5
cookie: 0.6.0
cookie-signature: 1.0.6
debug: 2.6.9
depd: 2.0.0
encodeurl: 1.0.2
encodeurl: 2.0.0
escape-html: 1.0.3
etag: 1.8.1
finalhandler: 1.2.0
finalhandler: 1.3.1
fresh: 0.5.2
http-errors: 2.0.0
merge-descriptors: 1.0.1
merge-descriptors: 1.0.3
methods: 1.1.2
on-finished: 2.4.1
parseurl: 1.3.3
path-to-regexp: 0.1.7
path-to-regexp: 0.1.10
proxy-addr: 2.0.7
qs: 6.11.0
qs: 6.13.0
range-parser: 1.2.1
safe-buffer: 5.2.1
send: 0.18.0
serve-static: 1.15.0
send: 0.19.0
serve-static: 1.16.2
setprototypeof: 1.2.0
statuses: 2.0.1
type-is: 1.6.18
@@ -1077,12 +1082,12 @@ packages:
minimatch: 5.1.6
dev: true
/finalhandler@1.2.0:
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
/finalhandler@1.3.1:
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
engines: {node: '>= 0.8'}
dependencies:
debug: 2.6.9
encodeurl: 1.0.2
encodeurl: 2.0.0
escape-html: 1.0.3
on-finished: 2.4.1
parseurl: 1.3.3
@@ -1543,8 +1548,8 @@ packages:
engines: {node: '>= 0.6'}
dev: false
/merge-descriptors@1.0.1:
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
/merge-descriptors@1.0.3:
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
dev: false
/methods@1.1.2:
@@ -1724,8 +1729,8 @@ packages:
minipass: 7.0.4
dev: true
/path-to-regexp@0.1.7:
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
/path-to-regexp@0.1.10:
resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==}
dev: false
/pend@1.2.0:
@@ -1780,8 +1785,8 @@ packages:
requiresBuild: true
dev: true
/qs@6.11.0:
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
/qs@6.13.0:
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
engines: {node: '>=0.6'}
dependencies:
side-channel: 1.0.6
@@ -1919,8 +1924,8 @@ packages:
lru-cache: 6.0.0
dev: true
/send@0.18.0:
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
/send@0.19.0:
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
engines: {node: '>= 0.8.0'}
dependencies:
debug: 2.6.9
@@ -1949,14 +1954,14 @@ packages:
dev: true
optional: true
/serve-static@1.15.0:
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
/serve-static@1.16.2:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'}
dependencies:
encodeurl: 1.0.2
encodeurl: 2.0.0
escape-html: 1.0.3
parseurl: 1.3.3
send: 0.18.0
send: 0.19.0
transitivePeerDependencies:
- supports-color
dev: false
+139
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
}
+37
View File
@@ -0,0 +1,37 @@
{ 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;
};
};
services.mailpit = {
enable = true;
package = pkgs-unstable.mailpit;
};
}
+17
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
Generated
-25
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
}
-20
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
View File
@@ -13,6 +13,7 @@ node_modules
dist
coverage
*.zip
.vite/
# Test files
cypress/screenshots
+1 -1
View File
@@ -1 +1 @@
20.15.0
20.16.0
+2 -2
View File
@@ -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')
+75 -75
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.0.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.30.0",
"@tiptap/core": "2.7.1",
"@tiptap/extension-blockquote": "2.7.1",
"@tiptap/extension-bold": "2.7.1",
"@tiptap/extension-bullet-list": "2.7.1",
"@tiptap/extension-code": "2.7.1",
"@tiptap/extension-code-block": "2.7.1",
"@tiptap/extension-code-block-lowlight": "2.7.1",
"@tiptap/extension-document": "2.7.1",
"@tiptap/extension-dropcursor": "2.7.1",
"@tiptap/extension-gapcursor": "2.7.1",
"@tiptap/extension-hard-break": "2.7.1",
"@tiptap/extension-heading": "2.7.1",
"@tiptap/extension-history": "2.7.1",
"@tiptap/extension-horizontal-rule": "2.7.1",
"@tiptap/extension-image": "2.7.1",
"@tiptap/extension-italic": "2.7.1",
"@tiptap/extension-link": "2.7.1",
"@tiptap/extension-list-item": "2.7.1",
"@tiptap/extension-ordered-list": "2.7.1",
"@tiptap/extension-paragraph": "2.7.1",
"@tiptap/extension-placeholder": "2.7.1",
"@tiptap/extension-strike": "2.7.1",
"@tiptap/extension-table": "2.7.1",
"@tiptap/extension-table-cell": "2.7.1",
"@tiptap/extension-table-header": "2.7.1",
"@tiptap/extension-table-row": "2.7.1",
"@tiptap/extension-task-item": "2.7.1",
"@tiptap/extension-task-list": "2.7.1",
"@tiptap/extension-text": "2.7.1",
"@tiptap/extension-typography": "2.7.1",
"@tiptap/extension-underline": "2.7.1",
"@tiptap/pm": "2.7.1",
"@tiptap/suggestion": "2.7.1",
"@tiptap/vue-3": "2.7.1",
"@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",
"date-fns": "4.1.0",
"dayjs": "1.11.13",
"dompurify": "3.1.6",
"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.12",
"tippy.js": "6.3.7",
"ufo": "1.5.4",
"vue": "3.4.32",
"vue": "3.5.6",
"vue-advanced-cropper": "2.8.9",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "9.13.1",
"vue-router": "4.4.0",
"vue-i18n": "10.0.1",
"vue-router": "4.4.5",
"vuemoji-picker": "0.2.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/lodash.clonedeep": "4.5.9",
"@types/lodash.debounce": "4.0.9",
"@types/node": "20.14.11",
"@types/node": "20.16.5",
"@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.23.3",
"caniuse-lite": "1.0.30001660",
"csstype": "3.1.3",
"cypress": "13.13.1",
"esbuild": "0.23.0",
"eslint": "8.57.0",
"eslint-plugin-vue": "9.27.0",
"cypress": "13.14.2",
"esbuild": "0.23.1",
"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.21.3",
"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.1",
"start-server-and-test": "2.0.8",
"typescript": "5.6.2",
"unplugin-inject-preload": "2.0.5",
"vite": "5.4.6",
"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"
}
}
}
-16
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",
+18
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",
+2709 -2418
View File
File diff suppressed because it is too large Load Diff
@@ -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
@@ -69,6 +69,10 @@ const shouldShowMessage = computed(() => {
@media screen and (min-width: $tablet) {
display: none;
}
@media print {
display: none;
}
&.has-update-available {
bottom: 5rem;
+4 -1
View File
@@ -48,7 +48,10 @@
class="task-detail-view-modal"
@close="closeModal()"
>
<component :is="currentModal" />
<component
:is="currentModal"
@close="closeModal()"
/>
</Modal>
<BaseButton
@@ -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>
+22 -4
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 {
+3 -16
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() {
@@ -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,
+2 -2
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)
+52 -17
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,13 +33,6 @@
'is-wide': wide
}"
>
<BaseButton
class="close"
@click="$emit('close')"
>
<Icon icon="times" />
</BaseButton>
<slot>
<div class="header">
<slot name="header" />
@@ -137,8 +136,8 @@ $modal-width: 1024px;
@media screen and (max-width: $tablet) {
margin: 0;
top: 25%;
transform: translate(-50%, -25%);
position: static;
transform: none;
}
.header {
@@ -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,42 @@ $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;
}
}
</style>
<style lang="scss">
@@ -219,4 +248,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>
@@ -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
})
}
@@ -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
@@ -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>
@@ -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;
}
}
}
}
@@ -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,17 +566,18 @@ async function addTaskToBucket(bucketId: IBucket['id']) {
bucketId,
projectId: project.value.id,
})
toggleShowNewTaskInput(bucketId)
newTaskText.value = ''
kanbanStore.addTaskToBucket(task)
scrollTaskContainerToBottom(bucketId)
scrollTaskContainerToTop(bucketId)
}
function scrollTaskContainerToBottom(bucketId: IBucket['id']) {
function scrollTaskContainerToTop(bucketId: IBucket['id']) {
const bucketEl = taskContainerRefs.value[bucketId]
if (!bucketEl) {
return
}
bucketEl.scrollTop = bucketEl.scrollHeight
bucketEl.scrollTop = 0
}
async function createNewBucket() {
@@ -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);
@@ -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,
@@ -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
@@ -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
+42 -4
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
@@ -180,7 +180,7 @@ import ProgressBar from '@/components/misc/ProgressBar.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import {canPreview} from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask'
@@ -274,10 +274,6 @@ async function viewOrDownload(attachment: IAttachment) {
}
}
function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
const copy = useCopyToClipboard()
function copyUrl(attachment: IAttachment) {
@@ -22,6 +22,7 @@
</span>
<div
v-for="c in comments"
:id="`comment-${c.id}`"
:key="c.id"
class="media comment"
>
@@ -56,6 +57,13 @@
>
· {{ $t('task.comment.edited', {date: formatDateSince(c.updated)}) }}
</span>
<a
v-tooltip="$t('task.comment.permalink')"
:href="`#comment-${c.id}`"
class="comment-permalink"
:title="$t('task.comment.permalink')"
@click.prevent.stop="copy(getCommentUrl(`${c.id}`))"
><Icon icon="link" /></a>
<CustomTransition name="fade">
<span
v-if="
@@ -199,6 +207,7 @@ import {getAvatarUrl, getDisplayName} from '@/models/user'
import {useConfigStore} from '@/stores/config'
import {useAuthStore} from '@/stores/auth'
import Reactions from '@/components/input/Reactions.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
const props = withDefaults(defineProps<{
taskId: number,
@@ -207,6 +216,8 @@ const props = withDefaults(defineProps<{
canWrite: true,
})
const copy = useCopyToClipboard()
const {t} = useI18n({useScope: 'global'})
const configStore = useConfigStore()
const authStore = useAuthStore()
@@ -242,6 +253,8 @@ const actions = computed(() => {
])))
})
const frontendUrl = computed(() => configStore.frontendUrl)
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
const uploadPromises: Promise<string>[] = []
@@ -364,6 +377,10 @@ async function deleteComment(commentToDelete: ITaskComment) {
showDeleteModal.value = false
}
}
function getCommentUrl(commentId: string) {
return `${frontendUrl.value}${location.pathname}${location.search}#comment-${commentId}`
}
</script>
<style lang="scss" scoped>
@@ -391,10 +408,23 @@ async function deleteComment(commentToDelete: ITaskComment) {
}
span {
span,
.comment-permalink {
font-size: .75rem;
line-height: 1;
}
.comment-permalink {
font-size: 1rem;
border: 1px solid transparent;
padding: 0.25rem;
border-radius: 1rem;
color: var(--grey, hsl(0, 0%, 48%));
}
.comment-permalink:hover {
color: var(--grey-dark, hsl(0, 0%, 29%));
border-color: var(--grey-dark, hsl(0, 0%, 29%));
}
}
.image.is-avatar {
@@ -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;
@@ -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)"
@@ -20,9 +20,9 @@
<script setup lang="ts">
import {ref, shallowReactive, watchEffect} from 'vue'
import AttachmentService from '@/services/attachment'
import AttachmentService, {PREVIEW_SIZE} from '@/services/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import {canPreview} from '@/models/attachment'
const props = defineProps<{
modelValue?: IAttachment
@@ -33,13 +33,9 @@ const blobUrl = ref<string | undefined>(undefined)
watchEffect(async () => {
if (props.modelValue && canPreview(props.modelValue)) {
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue)
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue, PREVIEW_SIZE.MD)
}
})
function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
</script>
<style scoped lang="scss">
@@ -1,20 +1,26 @@
<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
class="close"
@click="$emit('close')"
>
<Icon icon="times" />
</BaseButton>
</div>
<h1
class="title input"
@@ -26,6 +32,12 @@
>
{{ task.title.trim() }}
</h1>
<BaseButton
class="close"
@click="$emit('close')"
>
<Icon icon="times" />
</BaseButton>
<CustomTransition name="fade">
<span
v-if="loading && saving"
@@ -71,7 +83,8 @@ const props = withDefaults(defineProps<{
})
const emit = defineEmits<{
'update:task': [task: ITask]
'update:task': [task: ITask],
'close': [],
}>()
const router = useRouter()
@@ -138,14 +151,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 +163,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>
@@ -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">
@@ -93,7 +93,7 @@
</template>
<script lang="ts" setup>
import {ref, computed, watch} from 'vue'
import {computed, ref, watch} from 'vue'
import {useRouter} from 'vue-router'
import PriorityLabel from '@/components/tasks/partials/PriorityLabel.vue'
@@ -105,9 +105,9 @@ import ChecklistSummary from './ChecklistSummary.vue'
import {getHexColor} from '@/models/task'
import type {ITask} from '@/modelTypes/ITask'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import AttachmentService, {PREVIEW_SIZE} from '@/services/attachment'
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
import {formatDateLong, formatDateSince, formatISO} from '@/helpers/time/formatDate'
import {colorIsDark} from '@/helpers/color/colorIsDark'
import {useTaskStore} from '@/stores/tasks'
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
@@ -165,7 +165,7 @@ async function maybeDownloadCoverImage() {
}
const attachmentService = new AttachmentService()
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment, PREVIEW_SIZE.LG)
}
watch(
@@ -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>
@@ -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) {
@@ -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 } }"
@@ -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,
})
}
@@ -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,32 @@
: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>
<DeferTask
v-model="task"
/>
</template>
</Popup>
<span>
<span
@@ -138,6 +140,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 +148,7 @@
<BaseButton
:class="{'is-favorite': task.isFavorite}"
class="favorite"
@click="toggleFavorite"
@click.stop="toggleFavorite"
>
<Icon
v-if="task.isFavorite"
@@ -176,7 +179,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 +194,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 +241,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 +255,6 @@ watch(
},
)
onMounted(() => {
document.addEventListener('click', hideDeferDueDatePopup)
})
onBeforeUnmount(() => {
document.removeEventListener('click', hideDeferDueDatePopup)
})
const baseStore = useBaseStore()
const projectStore = useProjectStore()
const taskStore = useTaskStore()
@@ -350,35 +343,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, input, .favorite, [role="button"]')
if (isInteractiveElement || hasTextSelected()) {
return
}
}
}
function focusTaskLink() {
if (!hasTextSelected()) {
taskContainerRef.value.focus()
}
taskLinkRef.value?.$el.click()
}
</script>
@@ -397,7 +377,7 @@ function focusTaskLink() {
background-color: var(--grey-100);
}
&:focus-within, &:focus {
&:focus-within {
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
a.task-link {
@@ -405,6 +385,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;
@@ -542,4 +532,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>
@@ -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
}
+60
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(
+63 -84
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
}
+15
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
}
+2 -1
View File
@@ -24,7 +24,8 @@ export const SUPPORTED_LOCALES = {
'sl-SI': 'Slovenščina',
'pt-BR': 'Português Brasileiro',
'hr-HR': 'Hrvatski',
'uk-UA': 'українська',
'uk-UA': 'Українська',
'lt-LT': 'Lietuvių Kalba',
// IMPORTANT: Also add new languages to useDayjsLanguageSync
} as const
+6 -4
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": "ليس لديك الصلاحية لعرض المهمة.",
+6 -4
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.",
+6 -4
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.",
+6 -4
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.",
+5 -3
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.",
+5 -3
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.",
+5 -4
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?",
@@ -848,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",
@@ -1154,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.",
+6 -4
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.",
+6 -4
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.",
+6 -4
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 n’existe pas.",
"3007": "Un projet avec cet identifiant existe déjà.",
"3008": "Le projet est archivé et ne peut donc être consulté qu’en 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 n’existe 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 d’au moins une tâche lors de la modification en bloc de tâches.",
"4005": "Vous n’avez pas le droit de voir la tâche.",
+6 -4
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.",
+6 -4
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.",
+6 -4
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à.",
+8 -6
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.",
+6 -4
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
+6 -4
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.",
+6 -4
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.",
+6 -4
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.",
+6 -4
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.",
+6 -4
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.",
+6 -4
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.",
+6 -4
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": "У вас нет прав для просмотра задачи.",
+6 -4
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.",
+7 -5
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.",
+6 -4
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.",
+16 -14
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.",
+6 -4
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.",
+19 -17
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": "У Вас немає прав дивитися завдання.",
+6 -4
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.",
+6 -4
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": "你没有权限查看此任务。",
+6 -4
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.",
@@ -25,6 +25,7 @@ export const DAYJS_LOCALE_MAPPING = {
'pt-BR': 'pt',
'hr-HR': 'hr',
'uk-UA': 'uk',
'lt-LT': 'lt',
} as Record<SupportedLocale, ISOLanguage>
export const DAYJS_LANGUAGE_IMPORTS = {
@@ -48,6 +49,7 @@ export const DAYJS_LANGUAGE_IMPORTS = {
'sl-si': () => import('dayjs/locale/sl'),
'pt-br': () => import('dayjs/locale/pt-br'),
'uk-ua': () => import('dayjs/locale/uk'),
'lt-lt': () => import('dayjs/locale/lt'),
} as Record<SupportedLocale, () => Promise<ILocale>>
export function useDayjsLanguageSync(dayjsGlobal: typeof dayjs) {
+4 -3
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,
}
+4
View File
@@ -7,6 +7,10 @@ import type { IAttachment } from '@/modelTypes/IAttachment'
export const SUPPORTED_IMAGE_SUFFIX = ['.jpg', '.png', '.bmp', '.gif']
export function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment {
id = 0
taskId = 0
+8 -3
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)
}
+14 -2
View File
@@ -5,6 +5,13 @@ import type { IAttachment } from '@/modelTypes/IAttachment'
import {downloadBlob} from '@/helpers/downloadBlob'
export enum PREVIEW_SIZE {
SM = 'sm',
MD = 'md',
LG = 'lg',
XL = 'xl',
}
export default class AttachmentService extends AbstractService<IAttachment> {
constructor() {
super({
@@ -37,8 +44,13 @@ export default class AttachmentService extends AbstractService<IAttachment> {
return data
}
getBlobUrl(model: IAttachment) {
return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id)
getBlobUrl(model: IAttachment, size?: PREVIEW_SIZE) {
let mainUrl = '/tasks/' + model.taskId + '/attachments/' + model.id
if (size !== undefined) {
mainUrl += `?preview_size=${size}`
}
return AbstractService.prototype.getBlobUrl.call(this, mainUrl)
}
async download(model: IAttachment) {
+1 -23
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),
+8
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') {
+1 -1
View File
@@ -194,8 +194,8 @@ export const useKanbanStore = defineStore('kanban', () => {
...oldBucket,
count: (oldBucket?.count || 0) + 1,
tasks: [
...oldBucket.tasks,
task,
...oldBucket.tasks,
],
}
buckets.value[bucketIndex] = newBucket
+24 -20
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,
+24 -22
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