Compare commits

...

130 Commits

Author SHA1 Message Date
renovate 450cc7df77 chore(deps): update golangci/golangci-lint docker tag to v1.58.0 2024-05-03 19:05:22 +00:00
renovate 9260b3f1d3 chore(deps): update dependency vite to v5.2.11 2024-05-03 00:06:00 +00:00
renovate ab74b08314 fix(deps): update sentry-javascript monorepo to v7.113.0 2024-05-02 12:05:42 +00:00
renovate 9637db5a6b chore(deps): update dev-dependencies 2024-05-02 05:05:36 +00:00
renovate 08645d38a0 fix(deps): update dependency dompurify to v3.1.2 2024-05-01 18:47:25 +00:00
renovate f155d6bb60 fix(deps): update tiptap to v2.3.1 2024-04-30 16:05:36 +00:00
renovate 9a3d63a713 fix(deps): update dependency vue to v3.4.26 2024-04-29 13:05:49 +00:00
renovate 050f4313c8 chore(deps): update dev-dependencies 2024-04-29 05:05:40 +00:00
renovate e917323d91 fix(deps): update dependency dayjs to v1.11.11 2024-04-28 12:05:38 +00:00
renovate 8ad7d00559 chore(deps): update dev-dependencies 2024-04-28 00:06:00 +00:00
renovate fd126fa234 fix(deps): update dependency dompurify to v3.1.1 2024-04-26 12:06:39 +00:00
renovate 0c39a3dd38 chore(deps): update dependency vitest to v1.5.2 2024-04-26 00:12:26 +00:00
kolaente 66e96322ea
fix: do not remove empty openid teams when none are present
Maybe resolves https://community.vikunja.io/t/empty-openid-team-cleanup-cron-error-removing-empty-openid-team-database-is-locked-error-when-exporting-data/2306/3
2024-04-25 14:21:31 +02:00
kolaente 00a96663ba
fix(caldav): check if vtodo contains any components
Resolves https://vikunja.sentry.io/share/issue/1ae2fd1601aa40dea4aee41927cfcf78/
2024-04-25 13:40:23 +02:00
kolaente 741370b613
fix(caldav): return more than 1000 tasks
Resolves https://kolaente.dev/vikunja/vikunja/issues/2302
2024-04-25 13:37:04 +02:00
renovate 70183dd7c6 chore(deps): update pnpm to v9.0.6 2024-04-25 06:59:45 +00:00
renovate 760bec5e76 chore(deps): update dependency vitest to v1.5.1 2024-04-25 01:10:55 +00:00
renovate 78f03373b8 fix(deps): update dependency vue to v3.4.25 2024-04-24 13:09:36 +00:00
renovate 09c6d095df fix(deps): update sentry-javascript monorepo to v7.112.2 2024-04-24 12:06:21 +00:00
renovate b102fe8188 chore(deps): update dev-dependencies 2024-04-24 00:11:24 +00:00
renovate f7c367b5bb fix(deps): update dependency workbox-precaching to v7.1.0 2024-04-23 21:08:15 +00:00
renovate b94053e42e fix(deps): update sentry-javascript monorepo to v7.112.1 2024-04-23 14:06:37 +00:00
renovate 6e98a6d7ff fix(deps): update sentry-javascript monorepo to v7.112.0 2024-04-23 10:18:38 +00:00
renovate 42bfe107ae chore(deps): update dev-dependencies 2024-04-23 06:10:30 +00:00
Frederick [Bot] a1892ea10b chore(i18n): update translations via Crowdin 2024-04-23 00:07:19 +00:00
renovate 899f8f9bc1 fix(deps): update github.com/dustinkirkland/golang-petname digest to 76c06c4 2024-04-22 22:11:33 +00:00
renovate 40f0ca6670 fix(deps): update dependency vue-i18n to v9.13.1 2024-04-22 17:06:27 +00:00
renovate f8d35396dc fix(deps): update dependency vue to v3.4.24 2024-04-22 16:36:18 +00:00
kolaente 409822442b
fix(backgrounds): return full project after uploading image 2024-04-22 18:33:43 +02:00
kolaente aec60f3591
feat(backgrounds): resize images to a maximum of 4K
Resolves https://kolaente.dev/vikunja/vikunja/issues/1373#issuecomment-43491
2024-04-22 18:29:58 +02:00
renovate 9b5ae38784 chore(deps): update dev-dependencies 2024-04-22 02:08:31 +00:00
renovate 3e40a43d56 chore(deps): update pnpm to v9.0.5 2024-04-21 22:07:37 +00:00
kolaente 15e0c716ad
fix(reminders): do not show relative reminders as minutes when they round to hours
Regression from fd520dab0a
2024-04-22 00:05:12 +02:00
kolaente 26ada628a2
fix(editor): use colors from color scheme to render table cells
Resolves https://github.com/go-vikunja/vikunja/issues/253
2024-04-21 23:57:07 +02:00
kolaente d86fdcb756
fix(table view): do not sort table column fields when the field in question is hidden
Resolves https://kolaente.dev/vikunja/vikunja/issues/2272
2024-04-21 23:48:40 +02:00
kolaente 84197dd9c1
fix: correctly return error and bubble up when the api could not be reached 2024-04-21 23:33:50 +02:00
kolaente 324df991ce
chore(desktop): switch from yarn to pnpm 2024-04-21 21:04:07 +02:00
kolaente 1f6a1f8ad4
fix(kanban): fetch project and view when checking permissions 2024-04-21 19:44:47 +02:00
kolaente ea7527a3cf
fix(test): cast result before comparing 2024-04-21 19:43:57 +02:00
kolaente 574c7f218e
fix(labels): allow link shares to add existing labels to a task
Resolves https://github.com/go-vikunja/vikunja/issues/252
2024-04-21 15:12:27 +02:00
kolaente 1074a8d916
fix(views): only allow project admins to manage views
Resolves https://community.vikunja.io/t/manage-views-only-for-project-admins/2279
2024-04-21 14:36:09 +02:00
kolaente e88f95e501
fix(migration): remove buckets table name when dropping index
Related to https://kolaente.dev/vikunja/vikunja/issues/2243
2024-04-21 13:50:03 +02:00
kolaente 0962aa4262
fix(restore): transform json fields during restore
Resolves https://community.vikunja.io/t/unable-to-restore-after-dump-and-export-also-not-working/2263/5
2024-04-21 13:45:49 +02:00
renovate a48ad6c9e1 chore(deps): update dev-dependencies 2024-04-21 05:09:05 +00:00
renovate bc8fe05e9e chore(deps): update pnpm to v9.0.4 2024-04-19 06:59:31 +00:00
renovate b4c12273af chore(deps): update dev-dependencies 2024-04-19 00:07:11 +00:00
renovate be004793aa chore(deps): update dependency node to v20.12.2 (#2238)
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2238
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-04-18 20:46:48 +00:00
renovate a080400d3e chore(deps): update pnpm to v9 2024-04-18 19:05:45 +00:00
renovate d4ec0978ee fix(deps): update dependency vue-i18n to v9.13.0 2024-04-18 17:20:39 +00:00
renovate c37e08635a fix(deps): update module github.com/labstack/echo/v4 to v4.12.0 2024-04-18 17:20:18 +00:00
renovate 31f448e50f fix(deps): update sentry-javascript monorepo to v7.111.0 2024-04-18 17:06:02 +00:00
renovate 00c323891a fix(deps): update module golang.org/x/oauth2 to v0.19.0 2024-04-18 15:56:46 +00:00
renovate ff06bb202b fix(deps): update module github.com/tkuchiki/go-timezone to v0.2.3 2024-04-18 15:56:02 +00:00
renovate e806cbaf22 fix(deps): update dependency vue-router to v4.3.2 2024-04-18 10:09:09 +00:00
Frederick [Bot] d35ff0b380 chore(i18n): update translations via Crowdin 2024-04-18 00:06:32 +00:00
renovate 982884ee05 fix(deps): update module golang.org/x/sync to v0.7.0 (#2258)
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2258
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-04-17 20:37:47 +00:00
renovate e43d9e9bbd fix(deps): update dependency @intlify/unplugin-vue-i18n to v4 2024-04-17 20:06:11 +00:00
renovate da8cee0ba5 fix(deps): update dependency vue to v3.4.23 2024-04-17 10:07:15 +00:00
renovate 352381f377 chore(deps): update pnpm to v8.15.7 2024-04-17 07:06:42 +00:00
renovate 61455b8795 fix(deps): update sentry-javascript monorepo to v7.110.1 2024-04-17 06:21:21 +00:00
treysullivent aceaccbf11 docs: fix typo in README.md (#2271)
Fixed "exausted" to "exhausted"

Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2271
Reviewed-by: konrad <k@knt.li>
Co-authored-by: treysullivent <trey.sullivent@gmail.com>
Co-committed-by: treysullivent <trey.sullivent@gmail.com>
2024-04-17 06:20:44 +00:00
kolaente 392ce66edb
chore(deps): update github.com/adlio/trello to v1.12.0 2024-04-16 23:14:51 +02:00
kolaente ecbefdb921
fix(buckets): return correct task count for tasks in buckets 2024-04-14 17:21:53 +02:00
kolaente d8ca1a2de1
fix(favorites): make favorites work with configurable views 2024-04-14 17:12:16 +02:00
kolaente 2d084c091e
feat: new login image 2024-04-14 12:43:22 +02:00
kolaente 5a84d37fca
fix(kanban): do not focus on task list in bucket when clicking on a task 2024-04-14 11:21:59 +02:00
kolaente fd520dab0a
fix(reminders): do not fall back to hours when the reminder interval is minutes
Resolves https://github.com/go-vikunja/vikunja/issues/225
2024-04-14 11:20:20 +02:00
kolaente 144a6e4140
fix(kanban): do not add bottom spacing to view 2024-04-14 11:15:53 +02:00
kolaente a7aa74227a
fix(kanban): do not focus kanban board 2024-04-14 11:12:26 +02:00
kolaente d2adbc53c6
fix(test): add task to bucket in test 2024-04-14 11:00:41 +02:00
kolaente 422e4371f8
fix(project): add more spacing between filter button and view switcher on mobile 2024-04-14 00:06:26 +02:00
kolaente 6e5b31f1e0
fix(filters): always persist filter or search in query path and load it correctly into filter query input when loading the page
Previously, when using the filter query as a search input, it would load the search as requested but the filter query parameter in the url would be empty, which meant the search would not be loaded correctly when reloading (or otherwise newly accessing) the page. We're now persisting the filter and search in the task loading logic, to make sure they are always populated correctly.
2024-04-13 23:34:25 +02:00
kolaente 5756da412b
fix(project): return full project after duplicating it 2024-04-13 22:39:40 +02:00
kolaente 4e05b8e97c
fix(project): do not crash when duplicating a project with no tasks 2024-04-13 22:36:41 +02:00
kolaente 5177f516c4
fix(views): make sure view changes are reflected in switcher 2024-04-13 22:24:12 +02:00
kolaente 637c8f6ba5
fix(views): make sure the view is saved properly in localStorage 2024-04-13 22:15:41 +02:00
kolaente 1460d212ee
fix: do not push nil errors to sentry 2024-04-13 21:46:07 +02:00
kolaente e9de7d8a24
fix(project): delete all related entities when deleting a project 2024-04-13 21:43:44 +02:00
kolaente ce1d7778c7
fix(export): make export work with project views and new task positions 2024-04-13 21:07:06 +02:00
kolaente 9a16f6f817
fix: license in cmd help text 2024-04-13 20:13:24 +02:00
kolaente 7d755fcb89
fix: lint 2024-04-13 17:58:53 +02:00
kolaente 77e95642a9
fix(tasks): make fetching tasks in buckets via typesense work 2024-04-13 17:52:47 +02:00
kolaente a5d02380a3
fix(typesense): make fetching task positions per view more efficient 2024-04-13 17:26:38 +02:00
kolaente 3519b8b2fe
fix(tasks): index and order by task position when using typesense 2024-04-13 17:19:27 +02:00
kolaente cb648e5ad8
fix(typesense): fix reindexing views and positions in typesense 2024-04-13 16:38:45 +02:00
kolaente 75f830457b
fix(comments): order comments by created timestamp instead of id
Partially resolves https://community.vikunja.io/t/trello-import-comments-and-assignments/2174/14
2024-04-13 14:45:12 +02:00
kolaente 6e2b540394
fix(migration): import task comments with original timestamps
Partially resolves https://community.vikunja.io/t/trello-import-comments-and-assignments/2174/14
2024-04-13 14:44:55 +02:00
kolaente bf3c8ac9da
fix(views): check if bucket index already exists before creating new index
Resolves https://kolaente.dev/vikunja/vikunja/issues/2243
2024-04-13 14:20:27 +02:00
kolaente 3e7225ebee
fix(editor): do not prevent shift+enter to add a line break in text
Resolves https://github.com/go-vikunja/vikunja/issues/250
2024-04-13 14:08:27 +02:00
kolaente 9eb19e0362
fix(project): do not crash when views were not loaded yet
The project view crashed when accessing a task from /projects because the currentProject in store was not set, hence the views weren't set either. This change adds a fallback to it.

Related to https://kolaente.dev/vikunja/vikunja/issues/2246
Related to https://community.vikunja.io/t/vikunja-freezes/2246/5
2024-04-13 13:18:14 +02:00
kolaente 73bf119409
docs: clarify version checkout when building from source
Related to https://kolaente.dev/vikunja/vikunja/issues/2270#issuecomment-62038
2024-04-12 23:39:27 +02:00
kolaente 500b761fe6
fix(projects): do not return parent project id when authenticating as link share
Related to https://community.vikunja.io/t/vikunja-freezes/2246
Related to https://github.com/go-vikunja/vikunja/issues/233
2024-04-12 18:02:39 +02:00
kolaente 0bc9a670d7
fix(task): do not crash when loading a task if parent projects are not loaded
Related to https://community.vikunja.io/t/vikunja-freezes/2246
Related to https://github.com/go-vikunja/vikunja/issues/233
2024-04-12 17:56:19 +02:00
renovate a3e5e98c64 fix(deps): update module github.com/arran4/golang-ical to v0.2.8 2024-04-11 16:07:49 +00:00
Elscrux a3a4d05e89 feat(editor): checklist visual improvements (#2264)
This makes task lists (especially big ones) easier to read. I've set a margin so there is a distance between task items which makes them easier to stand out.
I've also changed the visuals of the checked elements (strike through + grey font color) so the unchecked ones stand out more. Note that this currently seems to be a big bugged outside of edit mode as `data-checked` doesn't seem to be updating correctly in this state which seems to be an issue that is already noted for the TipTap editor.

Co-authored-by: Elscrux <nickposer2102@gmail.com>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2264
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Elscrux <elscrux@gmail.com>
Co-committed-by: Elscrux <elscrux@gmail.com>
2024-04-11 15:46:10 +00:00
renovate 72c3e1a03f chore(deps): update dev-dependencies 2024-04-11 00:09:10 +00:00
Elscrux 61ee0bd5e2 feat(migration): include non upload attachments from Trello (#2261)
This makes the Trello migrator include attachments that are not file uploads. To include them in Vikunja without missing data, their text (usually links) will be appended to the Vikunja description.

Co-authored-by: Elscrux <nickposer2102@gmail.com>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2261
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Elscrux <elscrux@gmail.com>
Co-committed-by: Elscrux <elscrux@gmail.com>
2024-04-10 22:12:06 +00:00
kolaente 423558f58a
fix(migration): invalid field in organization struct 2024-04-10 23:52:10 +02:00
kolaente 75fd17c750
docs: clarify vikunja cli usage in docker 2024-04-10 23:05:45 +02:00
kolaente 4e49ec9e16
docs: clarify automatic openid team creation 2024-04-10 23:05:45 +02:00
renovate 58e0ec3d35 fix(deps): update tiptap to v2.3.0 2024-04-10 20:59:51 +00:00
kolaente ed4be389ab
fix(navigation): scrolling when many projects are present
Regression from ee3d20e1d2
Resolves https://github.com/go-vikunja/vikunja/issues/249
2024-04-10 22:54:36 +02:00
renovate cb2c2eeae8 fix(deps): update dependency vue-i18n to v9.11.1 2024-04-10 17:06:05 +00:00
renovate e19ac57130 chore(deps): update dev-dependencies 2024-04-10 15:06:07 +00:00
kolaente 0557d4b5bb
docs: clarify transitioning from unstable to release 2024-04-09 22:43:27 +02:00
kolaente bc19a2fb78
fix(migration): import card comments from Trello when migrating
Related: https://community.vikunja.io/t/trello-import-comments-and-assignments/2174/3
2024-04-09 13:56:17 +02:00
kolaente 994aaeb920
fix(migration): trello: only fetch attachments when the card actually has attachments 2024-04-09 13:25:03 +02:00
kolaente ee3d20e1d2
fix(navigation): do not hide shadows of dropdown menu 2024-04-09 13:07:01 +02:00
Elscrux 8458e77341 feat(migration): Trello organization based migration (#2211)
Migrate Trello organization after organization to limit total memory allocation.
Related discussion: https://community.vikunja.io/t/trello-import-issues/2110

Co-authored-by: Elscrux <nickposer2102@gmail.com>
Co-authored-by: konrad <k@knt.li>
Co-authored-by: kolaente <k@knt.li>
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2211
Reviewed-by: konrad <k@knt.li>
Co-authored-by: Elscrux <elscrux@gmail.com>
Co-committed-by: Elscrux <elscrux@gmail.com>
2024-04-09 10:54:38 +00:00
kolaente af3b0bbea1
fix: lint 2024-04-08 13:23:15 +02:00
renovate d2317b9531 fix(deps): update dependency vue-i18n to v9.11.0 2024-04-08 10:35:09 +00:00
renovate 552f8580a4 fix(deps): update dependency dompurify to v3.1.0 2024-04-08 10:27:31 +00:00
renovate c842cb27b2 fix(deps): update tiptap to v2.2.6 2024-04-08 10:19:11 +00:00
kolaente e10cd368bf
feat(migration): notify the user when a migration failed
This change introduces notifications via mail when a migration fails. It will contain the error message and a hint to post it in the forum when Sentry is disabled, otherwise the error message will be sent directly to sentry and the notification will inform accordingly.
I've tried to balance "this thing failed, go figure it out" with "here is what we know and how you can get help", we'll see how well that approach works.
2024-04-08 12:15:24 +02:00
renovate 61322d2e2e fix(deps): update module github.com/yuin/goldmark to v1.7.1 2024-04-08 09:28:22 +00:00
renovate a41e248e5f fix(deps): update font awesome to v6.5.2 2024-04-08 09:10:31 +00:00
kolaente 6e37934b61
chore(deps): update goreleaser/nfpm docker tag to v2.36.1 2024-04-08 11:09:39 +02:00
renovate d64322bb7a fix(deps): update dependency @infectoone/vue-ganttastic to v2.3.2 2024-04-08 08:58:21 +00:00
renovate fa3b657e7e chore(deps): update pnpm to v8.15.6 2024-04-08 08:06:35 +00:00
Raymi306 1adaa73141 docs: fix build-from-sources docs mistake (#2251)
While attempting to build on OpenBSD without having built the frontend, I ran into the following error:

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

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

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

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

Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2251
Co-authored-by: Raymi306 <raymi306@gmail.com>
Co-committed-by: Raymi306 <raymi306@gmail.com>
2024-04-08 07:48:12 +00:00
renovate 3e77e3043e chore(deps): update dev-dependencies 2024-04-08 07:09:20 +00:00
kolaente d082c0399d
fix(test): visit one more project in project history test 2024-04-07 22:36:09 +02:00
kolaente 0b9ef27d04
fix(migration): show correct message after starting a migration
Related to https://github.com/go-vikunja/vikunja/issues/238
2024-04-07 15:11:59 +02:00
kolaente 7acd1a7e51
fix(project): remove child projects from state when deleting a project 2024-04-07 15:03:18 +02:00
kolaente 8bee5aa806
fix(project): return the full project when setting a background
Related to https://kolaente.dev/vikunja/vikunja/issues/2246
2024-04-07 14:53:57 +02:00
kolaente 6641cbebc2
fix(project): save the last 6 projects in history, show only 5 on desktop
The project grid on the home page with the recently visited projects now contains an even number of projects which makes for a much nicer grid (because it's now uniform).
2024-04-07 14:34:18 +02:00
kolaente 5892622676
fix(notifications): rendering of plaintext mails 2024-04-07 14:12:44 +02:00
kolaente 191a476823
fix(notifications): only sanitze html content in notifications, do not convert it to markdown
Resolves https://community.vikunja.io/t/trello-import-html-mails/2197
2024-04-07 13:34:53 +02:00
renovate c146b72d64 chore(deps): update golangci/golangci-lint docker tag to v1.57.2 (#2225)
Reviewed-on: https://kolaente.dev/vikunja/vikunja/pulls/2225
Reviewed-by: konrad <k@knt.li>
Co-authored-by: renovate <renovatebot@kolaente.de>
Co-committed-by: renovate <renovatebot@kolaente.de>
2024-04-07 11:09:14 +00:00
kolaente ca33c0b2bc
fix: drop bucket index before recreating it
Resolves https://kolaente.dev/vikunja/vikunja/issues/2243
2024-04-07 12:50:42 +02:00
112 changed files with 12513 additions and 8968 deletions

View File

@ -139,7 +139,7 @@ steps:
event: [ push, tag, pull_request ]
- name: api-lint
image: golangci/golangci-lint:v1.56.2
image: golangci/golangci-lint:v1.58.0
pull: always
environment:
GOPROXY: 'https://goproxy.kolaente.de'
@ -364,7 +364,7 @@ steps:
- api-build
- name: frontend-dependencies
image: node:20.11.1-alpine
image: node:20.12.2-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -378,7 +378,7 @@ steps:
# - restore-cache
- name: frontend-lint
image: node:20.11.1-alpine
image: node:20.12.2-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -390,7 +390,7 @@ steps:
- frontend-dependencies
- name: frontend-build-prod
image: node:20.11.1-alpine
image: node:20.12.2-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -402,7 +402,7 @@ steps:
- frontend-dependencies
- name: frontend-test-unit
image: node:20.11.1-alpine
image: node:20.12.2-alpine
pull: always
commands:
- cd frontend
@ -413,7 +413,7 @@ steps:
- name: frontend-typecheck
failure: ignore
image: node:20.11.1-alpine
image: node:20.12.2-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -544,7 +544,7 @@ steps:
- git fetch --tags
- name: frontend-dependencies
image: node:20.11.1-alpine
image: node:20.12.2-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.11.1-alpine
image: node:20.12.2-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.35.3
image: goreleaser/nfpm:v2.36.1
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.35.3
image: goreleaser/nfpm:v2.36.1
pull: always
commands:
- apk add git go
@ -901,7 +901,7 @@ steps:
- git fetch --tags
- name: build
image: node:20.11.1-alpine
image: node:20.12.2-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -962,7 +962,7 @@ steps:
- git fetch --tags
- name: build
image: node:20.11.1-alpine
image: node:20.12.2-alpine
pull: always
environment:
PNPM_CACHE_FOLDER: .cache/pnpm
@ -1400,6 +1400,6 @@ steps:
- failure
---
kind: signature
hmac: c312afe632177a2d45f47c429bf6c7528af3c51a097430956558532ccdcc42b9
hmac: f2753482faf9e2a3d34a9111587a75dfb4519cb77002cc64a51266540fd2478e
...

View File

@ -18,6 +18,7 @@ linters:
- scopelint # Obsolete, using exportloopref instead
- durationcheck
- goconst
- musttag
presets:
- bugs
- unused
@ -103,3 +104,7 @@ issues:
text: "parameter 'tx' seems to be unused, consider removing or renaming it as"
linters:
- revive
- path: pkg/models/typesense.go
text: 'structtag: struct field Position repeats json tag "position" also at'
linters:
- govet

View File

@ -5,7 +5,7 @@
"eslint.packageManager": "pnpm",
"editor.formatOnSave": false,
"editor.codeActionsOnSave": {
"source.fixAll": true
"source.fixAll": "explicit"
},
"eslint.format.enable": true,
"[javascript]": {

View File

@ -1,5 +1,5 @@
# syntax=docker/dockerfile:1
FROM --platform=$BUILDPLATFORM node:20.11.1-alpine AS frontendbuilder
FROM --platform=$BUILDPLATFORM node:20.12.2-alpine AS frontendbuilder
WORKDIR /build

View File

@ -29,7 +29,7 @@ If you find any security-related issues you don't want to disclose publicly, ple
## Features
See [the features page](https://vikunja.io/features/) on our website for a more exaustive list or
See [the features page](https://vikunja.io/features/) on our website for a more exhaustive list or
try it on [try.vikunja.io](https://try.vikunja.io)!
## Docs

View File

@ -1,9 +0,0 @@
{ pkgs ? import <nixpkgs> {}
}:
pkgs.mkShell {
name="electron-dev";
buildInputs = [
pkgs.electron
];
}

View File

@ -51,7 +51,7 @@
}
},
"devDependencies": {
"electron": "29.2.0",
"electron": "29.3.1",
"electron-builder": "24.13.3"
},
"dependencies": {

2375
desktop/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -18,6 +18,7 @@ To fully build Vikunja from source files, you need to build the api and frontend
1. Make sure you have git installed
2. Clone the repo with `git clone https://code.vikunja.io/vikunja` and switch into the directory.
3. Check out the version you want to build with `git checkout VERSION` - replace `VERSION` with the version want to use. If you don't do this, you'll build the [latest unstable build]({{< ref "versions.md">}}), which might contain bugs.
## Frontend
@ -35,7 +36,7 @@ That means compiling it boils down to these steps:
1. Make sure [Go](https://golang.org/doc/install) is properly installed on your system. You'll need at least Go `1.21`.
2. Make sure [Mage](https://magefile.org) is properly installed on your system.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch index.html`.
3. If you did not build the frontend in the steps before, you need to either do that or create a dummy index file with `mkdir -p frontend/dist && touch frontend/dist/index.html`.
4. Run `mage build` in the source of the main repo. This will build a binary in the root of the repo which will be able to run on your system.
### Build for different architectures

View File

@ -225,10 +225,13 @@ go install github.com/magefile/mage
```
mkdir /mnt/GO/code.vikunja.io
cd /mnt/GO/code.vikunja.io
git clone https://code.vikunja.io/api
cd /mnt/GO/code.vikunja.io/api
git clone https://code.vikunja.io/vikunja
cd vikunja
```
**Note:** Ceck out the version you want to build with `git checkout VERSION` - replace `VERSION` with the version want to use.
If you don't do this, you'll build the [latest unstable build]({{< ref "versions.md">}}), which might contain bugs.
### Compile binaries
```

View File

@ -103,7 +103,7 @@ auth:
## Automatically assign users to teams
Vikunja is capable of automatically adding users to a team based on OIDC claims added by the identity provider.
Starting with version 0.24.0, Vikunja is capable of automatically adding users to a team based on OIDC claims added by the identity provider.
If configured, Vikunja will sync teams, automatically create new ones and make sure the members are part of the configured teams.
Teams which exist only because they were created from oidc attributes are not editable in Vikunja.

View File

@ -10,7 +10,7 @@ menu:
# Vikunja Versions
The Vikunja api and frontend are available in two different release flavors.
Vikunja api is available in two different release flavors.
{{< table_of_contents >}}
@ -30,6 +30,9 @@ There might be multiple new such builds a day.
Versions contain the last stable version, the number of commits since then and the commit the currently running binary was built from.
They look like this: `v0.18.1+269-5cc4927b9e`
Since a release is also cut from the main branch at some point, features from unstable will eventually become available in stable releases.
At the point in time of a new version release, the unstable build is the same exact thing.
The demo instance at [try.vikunja.io](https://try.vikunja.io) automatically updates and always runs the last unstable build.
## Switching between versions

View File

@ -28,13 +28,21 @@ All commands use the same standard [config file]({{< ref "../setup/config.md">}}
## Using the cli in docker
When running Vikunja in docker, you'll need to execute all commands in the `api` container.
When running Vikunja in docker, you'll need to execute all commands in the `vikunja` container.
Instead of running the `vikunja` binary directly, run it like this:
```sh
docker exec <name of the vikunja api container> /app/vikunja/vikunja <subcommand>
docker exec <name of the vikunja container> /app/vikunja/vikunja <subcommand>
```
If you need to run a bunch of Vikunja commands, you can also create a shell alias for it:
```sh
alias vikunja-docker='docker exec <name of the vikunja container> /app/vikunja/vikunja'
```
Then use it as `vikunja-docker <subcommand>`.
### `dump`
Creates a zip file with all vikunja-related files.

View File

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

View File

@ -12,6 +12,8 @@
nodePackages.pnpm cypress
# API tools
go golangci-lint mage
# Desktop
electron
];
};
};

View File

@ -1 +1 @@
20.11.1
20.12.2

View File

@ -12,7 +12,7 @@ describe('Project History', () => {
cy.intercept(Cypress.env('API_URL') + '/projects*').as('loadProjectArray')
cy.intercept(Cypress.env('API_URL') + '/projects/*').as('loadProject')
const projects = ProjectFactory.create(6)
const projects = ProjectFactory.create(7)
ProjectViewFactory.truncate()
projects.forEach(p => ProjectViewFactory.create(1, {
id: p.id,
@ -36,6 +36,8 @@ describe('Project History', () => {
cy.wait('@loadProject')
cy.visit(`/projects/${projects[5].id}/${projects[5].id}`)
cy.wait('@loadProject')
cy.visit(`/projects/${projects[6].id}/${projects[6].id}`)
cy.wait('@loadProject')
// cy.visit('/')
// Not using cy.visit here to work around the redirect issue fixed in #1337
@ -52,5 +54,6 @@ describe('Project History', () => {
.should('contain', projects[3].title)
.should('contain', projects[4].title)
.should('contain', projects[5].title)
.should('contain', projects[6].title)
})
})

View File

@ -13,6 +13,7 @@ import {BucketFactory} from '../../factories/bucket'
import {TaskAttachmentFactory} from '../../factories/task_attachments'
import {TaskReminderFactory} from '../../factories/task_reminders'
import {createDefaultViews} from "../project/prepareProjects";
import { TaskBucketFactory } from '../../factories/task_buckets'
function addLabelToTaskAndVerify(labelTitle: string) {
cy.get('.task-view .action-buttons .button')
@ -48,7 +49,7 @@ function uploadAttachmentAndVerify(taskId: number) {
describe('Task', () => {
createFakeUserAndLogin()
let projects
let projects: {}[]
let buckets
beforeEach(() => {
@ -470,6 +471,10 @@ describe('Task', () => {
})
const labels = LabelFactory.create(1)
LabelTaskFactory.truncate()
TaskBucketFactory.create(1, {
task_id: tasks[0].id,
bucket_id: buckets[0].id,
})
cy.visit(`/projects/${projects[0].id}/4`)

View File

@ -13,7 +13,7 @@
},
"homepage": "https://vikunja.io/",
"funding": "https://opencollective.com/vikunja",
"packageManager": "pnpm@8.15.5",
"packageManager": "pnpm@9.0.6",
"keywords": [
"todo",
"productivity",
@ -50,49 +50,49 @@
"story:preview": "histoire preview"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "6.5.1",
"@fortawesome/free-regular-svg-icons": "6.5.1",
"@fortawesome/free-solid-svg-icons": "6.5.1",
"@fortawesome/fontawesome-svg-core": "6.5.2",
"@fortawesome/free-regular-svg-icons": "6.5.2",
"@fortawesome/free-solid-svg-icons": "6.5.2",
"@fortawesome/vue-fontawesome": "3.0.6",
"@github/hotkey": "3.1.0",
"@infectoone/vue-ganttastic": "2.3.1",
"@intlify/unplugin-vue-i18n": "3.0.1",
"@infectoone/vue-ganttastic": "2.3.2",
"@intlify/unplugin-vue-i18n": "4.0.0",
"@kyvg/vue3-notification": "3.2.1",
"@sentry/tracing": "7.109.0",
"@sentry/vue": "7.109.0",
"@tiptap/core": "2.2.4",
"@tiptap/extension-blockquote": "2.2.4",
"@tiptap/extension-bold": "2.2.4",
"@tiptap/extension-bullet-list": "2.2.4",
"@tiptap/extension-code": "2.2.4",
"@tiptap/extension-code-block-lowlight": "2.2.4",
"@tiptap/extension-document": "2.2.4",
"@tiptap/extension-dropcursor": "2.2.4",
"@tiptap/extension-gapcursor": "2.2.4",
"@tiptap/extension-hard-break": "2.2.4",
"@tiptap/extension-heading": "2.2.4",
"@tiptap/extension-history": "2.2.4",
"@tiptap/extension-horizontal-rule": "2.2.4",
"@tiptap/extension-image": "2.2.4",
"@tiptap/extension-italic": "2.2.4",
"@tiptap/extension-link": "2.2.4",
"@tiptap/extension-list-item": "2.2.4",
"@tiptap/extension-ordered-list": "2.2.4",
"@tiptap/extension-paragraph": "2.2.4",
"@tiptap/extension-placeholder": "2.2.4",
"@tiptap/extension-strike": "2.2.4",
"@tiptap/extension-table": "2.2.4",
"@tiptap/extension-table-cell": "2.2.4",
"@tiptap/extension-table-header": "2.2.4",
"@tiptap/extension-table-row": "2.2.4",
"@tiptap/extension-task-item": "2.2.4",
"@tiptap/extension-task-list": "2.2.4",
"@tiptap/extension-text": "2.2.4",
"@tiptap/extension-typography": "2.2.4",
"@tiptap/extension-underline": "2.2.4",
"@tiptap/pm": "2.2.4",
"@tiptap/suggestion": "2.2.4",
"@tiptap/vue-3": "2.2.4",
"@sentry/tracing": "7.113.0",
"@sentry/vue": "7.113.0",
"@tiptap/core": "2.3.1",
"@tiptap/extension-blockquote": "2.3.1",
"@tiptap/extension-bold": "2.3.1",
"@tiptap/extension-bullet-list": "2.3.1",
"@tiptap/extension-code": "2.3.1",
"@tiptap/extension-code-block-lowlight": "2.3.1",
"@tiptap/extension-document": "2.3.1",
"@tiptap/extension-dropcursor": "2.3.1",
"@tiptap/extension-gapcursor": "2.3.1",
"@tiptap/extension-hard-break": "2.3.1",
"@tiptap/extension-heading": "2.3.1",
"@tiptap/extension-history": "2.3.1",
"@tiptap/extension-horizontal-rule": "2.3.1",
"@tiptap/extension-image": "2.3.1",
"@tiptap/extension-italic": "2.3.1",
"@tiptap/extension-link": "2.3.1",
"@tiptap/extension-list-item": "2.3.1",
"@tiptap/extension-ordered-list": "2.3.1",
"@tiptap/extension-paragraph": "2.3.1",
"@tiptap/extension-placeholder": "2.3.1",
"@tiptap/extension-strike": "2.3.1",
"@tiptap/extension-table": "2.3.1",
"@tiptap/extension-table-cell": "2.3.1",
"@tiptap/extension-table-header": "2.3.1",
"@tiptap/extension-table-row": "2.3.1",
"@tiptap/extension-task-item": "2.3.1",
"@tiptap/extension-task-list": "2.3.1",
"@tiptap/extension-text": "2.3.1",
"@tiptap/extension-typography": "2.3.1",
"@tiptap/extension-underline": "2.3.1",
"@tiptap/pm": "2.3.1",
"@tiptap/suggestion": "2.3.1",
"@tiptap/vue-3": "2.3.1",
"@types/is-touch-device": "1.0.2",
"@types/lodash.clonedeep": "4.5.9",
"@vueuse/core": "10.9.0",
@ -102,8 +102,8 @@
"bulma-css-variables": "0.9.33",
"camel-case": "4.1.2",
"date-fns": "3.6.0",
"dayjs": "1.11.10",
"dompurify": "3.0.11",
"dayjs": "1.11.11",
"dompurify": "3.1.2",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.31",
@ -118,13 +118,13 @@
"sortablejs": "1.15.2",
"tippy.js": "6.3.7",
"ufo": "1.5.3",
"vue": "3.4.21",
"vue": "3.4.26",
"vue-advanced-cropper": "2.8.8",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "9.10.2",
"vue-router": "4.3.0",
"vue-i18n": "9.13.1",
"vue-router": "4.3.2",
"vuemoji-picker": "0.2.1",
"workbox-precaching": "7.0.0",
"workbox-precaching": "7.1.0",
"zhyswan-vuedraggable": "4.1.3"
},
"devDependencies": {
@ -132,9 +132,9 @@
"@cypress/vite-dev-server": "5.0.7",
"@cypress/vue": "6.0.0",
"@faker-js/faker": "8.4.1",
"@histoire/plugin-screenshot": "0.17.15",
"@histoire/plugin-vue": "0.17.15",
"@rushstack/eslint-patch": "1.10.1",
"@histoire/plugin-screenshot": "0.17.17",
"@histoire/plugin-vue": "0.17.17",
"@rushstack/eslint-patch": "1.10.2",
"@tsconfig/node18": "18.2.4",
"@types/codemirror": "5.60.15",
"@types/dompurify": "3.0.5",
@ -142,11 +142,11 @@
"@types/is-touch-device": "1.0.2",
"@types/lodash.debounce": "4.0.9",
"@types/marked": "5.0.2",
"@types/node": "20.12.5",
"@types/node": "20.12.8",
"@types/postcss-preset-env": "7.7.0",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.5.0",
"@typescript-eslint/parser": "7.5.0",
"@typescript-eslint/eslint-plugin": "7.8.0",
"@typescript-eslint/parser": "7.8.0",
"@vitejs/plugin-legacy": "5.3.2",
"@vitejs/plugin-vue": "5.0.4",
"@vue/eslint-config-typescript": "13.0.0",
@ -154,34 +154,34 @@
"@vue/tsconfig": "0.5.1",
"autoprefixer": "10.4.19",
"browserslist": "4.23.0",
"caniuse-lite": "1.0.30001606",
"caniuse-lite": "1.0.30001615",
"css-has-pseudo": "6.0.3",
"csstype": "3.1.3",
"cypress": "13.7.2",
"cypress": "13.8.1",
"esbuild": "0.20.2",
"eslint": "8.57.0",
"eslint-plugin-vue": "9.24.0",
"happy-dom": "14.6.1",
"histoire": "0.17.15",
"eslint-plugin-vue": "9.25.0",
"happy-dom": "14.7.1",
"histoire": "0.17.17",
"postcss": "8.4.38",
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-focus-within": "8.0.1",
"postcss-preset-env": "9.5.4",
"rollup": "4.14.0",
"postcss-preset-env": "9.5.9",
"rollup": "4.17.2",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.74.1",
"sass": "1.76.0",
"start-server-and-test": "2.0.3",
"typescript": "5.4.4",
"vite": "5.2.8",
"typescript": "5.4.5",
"vite": "5.2.11",
"vite-plugin-inject-preload": "1.3.3",
"vite-plugin-pwa": "0.19.8",
"vite-plugin-pwa": "0.20.0",
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
"vitest": "1.4.0",
"vue-tsc": "2.0.10",
"vitest": "1.5.3",
"vue-tsc": "2.0.16",
"wait-on": "7.2.0",
"workbox-cli": "7.0.0"
"workbox-cli": "7.1.0"
},
"pnpm": {
"patchedDependencies": {

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 313 KiB

After

Width:  |  Height:  |  Size: 218 KiB

View File

@ -162,7 +162,7 @@ projectStore.loadAllProjects()
.app-content {
z-index: 10;
position: relative;
padding: 1.5rem 0.5rem 1rem;
padding: 1.5rem 0.5rem 0;
// TODO refactor: DRY `transition-timing-function` with `./navigation.vue`.
transition: margin-left $transition-duration;
@ -172,7 +172,7 @@ projectStore.loadAllProjects()
}
@media screen and (min-width: $tablet) {
padding: $navbar-height + 1.5rem 1.5rem 1rem 1.5rem;
padding: $navbar-height + 1.5rem 1.5rem 0 1.5rem;
}
&.is-menu-enabled {

View File

@ -34,6 +34,7 @@ import {useBaseStore} from '@/stores/base'
import Logo from '@/components/home/Logo.vue'
import PoweredByLink from './PoweredByLink.vue'
import {useProjectStore} from '@/stores/projects'
import {useLabelStore} from '@/stores/labels'
const baseStore = useBaseStore()
const currentProject = computed(() => baseStore.currentProject)
@ -42,6 +43,9 @@ const logoVisible = computed(() => baseStore.logoVisible)
const projectStore = useProjectStore()
projectStore.loadAllProjects()
const labelStore = useLabelStore()
labelStore.loadAllLabels()
</script>
<style lang="scss" scoped>

View File

@ -155,8 +155,8 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
bottom: 0;
left: 0;
transform: translateX(-100%);
overflow-x: auto;
width: $navbar-width;
overflow-y: auto;
@media screen and (max-width: $tablet) {
top: 0;

View File

@ -338,10 +338,12 @@ const editor = useEditor({
HardBreak.extend({
addKeyboardShortcuts() {
return {
'Shift-Enter': () => this.editor.commands.setHardBreak(),
'Mod-Enter': () => {
if (contentHasChanged.value) {
bubbleSave()
}
return true
},
}
},
@ -816,7 +818,7 @@ watch(
td,
th {
min-width: 1em;
border: 2px solid #ced4da;
border: 2px solid var(--grey-300) !important;
padding: 3px 5px;
vertical-align: top;
box-sizing: border-box;
@ -830,7 +832,7 @@ watch(
th {
font-weight: bold;
text-align: left;
background-color: #f1f3f5;
background-color: var(--grey-200);
}
.selectedCell:after {
@ -891,8 +893,14 @@ ul[data-type='taskList'] {
padding: 0;
margin-left: 0;
li[data-checked='true'] {
color: var(--grey-500);
text-decoration: line-through;
}
li {
display: flex;
margin-top: 0.25rem;
> label {
flex: 0 0 auto;

View File

@ -88,7 +88,7 @@ const currentProject = computed<IProject>(() => {
})
useTitle(() => currentProject.value?.id ? getProjectTitle(currentProject.value) : '')
const views = computed(() => currentProject.value?.views)
const views = computed(() => projectStore.projects[projectId]?.views)
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
// This resulted in loading and setting the project multiple times, even when navigating away from it.
@ -161,6 +161,7 @@ function getViewTitle(view: IProjectView) {
display: flex;
justify-content: space-between;
align-items: center;
gap: 1rem;
@media screen and (max-width: $tablet) {
justify-content: center;

View File

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

View File

@ -30,45 +30,27 @@ import {computed, ref, watch} from 'vue'
import Filters from '@/components/project/partials/filters.vue'
import {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection'
import {useRouteQuery} from '@vueuse/router'
import {type TaskFilterParams} from '@/services/taskCollection'
const modelValue = defineModel<TaskFilterParams>({})
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const value = ref<TaskFilterParams>({})
const filter = useRouteQuery('filter')
watch(
() => modelValue.value,
() => props.modelValue,
(modelValue: TaskFilterParams) => {
value.value = modelValue
if (value.value.filter !== '' && value.value.filter !== getDefaultTaskFilterParams().filter) {
filter.value = value.value.filter
}
},
{immediate: true},
)
watch(
() => filter.value,
val => {
if (modelValue.value?.filter === val || typeof val === 'undefined') {
return
}
modelValue.value.filter = val
},
{immediate: true},
)
function emitChanges(newValue: TaskFilterParams) {
filter.value = newValue.filter
if (modelValue.value?.filter === newValue.filter && modelValue.value?.s === newValue.s) {
return
}
modelValue.value.filter = newValue.filter
modelValue.value.s = newValue.s
emit('update:modelValue', {
...value.value,
filter: newValue.filter,
s: newValue.s,
})
}
const hasFilters = computed(() => {

View File

@ -5,7 +5,7 @@
role="search"
>
<FilterInput
v-model="params.filter"
v-model="filterQuery"
:project-id="projectId"
@blur="change()"
/>
@ -28,7 +28,7 @@
<x-button
variant="secondary"
class="mr-2"
:disabled="params.filter === ''"
:disabled="filterQuery === ''"
@click.prevent.stop="clearFiltersAndEmit"
>
{{ $t('filters.clear') }}
@ -87,6 +87,16 @@ const params = ref<TaskFilterParams>({
s: '',
})
const filterQuery = ref('')
watch(
() => [params.value.filter, params.value.s],
() => {
const filter = params.value.filter || ''
const s = params.value.s || ''
filterQuery.value = filter || s
},
)
// Using watchDebounced to prevent the filter re-triggering itself.
watch(
() => modelValue,
@ -107,7 +117,7 @@ const projectStore = useProjectStore()
function change() {
const filter = transformFilterStringForApi(
params.value.filter,
filterQuery.value,
labelTitle => labelStore.filterLabelsByQuery([], labelTitle)[0]?.id || null,
projectTitle => {
const found = projectStore.findProjectByExactname(projectTitle)
@ -142,7 +152,7 @@ function changeAndEmitButton() {
}
function clearFiltersAndEmit() {
params.value.filter = ''
filterQuery.value = ''
changeAndEmitButton()
}
</script>

View File

@ -773,11 +773,6 @@ $crazy-height-calculation: '100vh - 4.5rem - 1.5rem - 1rem - 1.5rem - 11px';
$crazy-height-calculation-tasks: '#{$crazy-height-calculation} - 1rem - 2.5rem - 2rem - #{$button-height} - 1rem';
$filter-container-height: '1rem - #{$switch-view-height}';
// FIXME:
.app-content.project\.kanban, .app-content.task\.detail {
padding-bottom: 0 !important;
}
.kanban {
overflow-x: auto;
overflow-y: hidden;
@ -785,6 +780,10 @@ $filter-container-height: '1rem - #{$switch-view-height}';
margin: 0 -1.5rem;
padding: 0 1.5rem;
&:focus, .bucket .tasks:focus {
box-shadow: none;
}
@media screen and (max-width: $tablet) {
height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
scroll-snap-type: x mandatory;

View File

@ -267,7 +267,7 @@
</template>
<script setup lang="ts">
import {computed, type Ref} from 'vue'
import {computed, type Ref, watch} from 'vue'
import {useStorage} from '@vueuse/core'
@ -337,6 +337,12 @@ Object.assign(params.value, {
filter: '',
})
watch(
() => activeColumns.value,
() => setActiveColumnsSortParam(),
{deep: true},
)
// FIXME: by doing this we can have multiple sort orders
function sort(property: keyof SortBy) {
const order = sortBy.value[property]
@ -347,7 +353,16 @@ function sort(property: keyof SortBy) {
} else {
delete sortBy.value[property]
}
sortByParam.value = sortBy.value
setActiveColumnsSortParam()
}
function setActiveColumnsSortParam() {
sortByParam.value = Object.keys(sortBy.value)
.filter(prop => activeColumns.value[prop])
.reduce((obj, key) => {
obj[key] = sortBy.value[key]
return obj
}, {})
}
// TODO: re-enable opening task detail in modal

View File

@ -1,5 +1,5 @@
import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
import {useRoute} from 'vue-router'
import {useRoute, useRouter} from 'vue-router'
import {useRouteQuery} from '@vueuse/router'
import TaskCollectionService, {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection'
@ -66,7 +66,6 @@ export function useTaskList(
const params = ref<TaskFilterParams>({...getDefaultTaskFilterParams()})
const search = ref('')
const page = useRouteQuery('page', '1', { transform: Number })
const sortBy = ref({ ...sortByDefault })
@ -74,10 +73,6 @@ export function useTaskList(
const allParams = computed(() => {
const loadParams = {...params.value}
if (search.value !== '') {
loadParams.s = search.value
}
return formatSortOrder(sortBy.value, loadParams)
})
@ -122,16 +117,38 @@ export function useTaskList(
const route = useRoute()
watch(() => route.query, (query) => {
const { page: pageQueryValue, search: searchQuery } = query
if (searchQuery !== undefined) {
search.value = searchQuery as string
const {
page: pageQueryValue,
s,
filter,
} = query
if (s !== undefined) {
params.value.s = s as string
}
if (pageQueryValue !== undefined) {
page.value = Number(pageQueryValue)
}
if (filter !== undefined) {
params.value.filter = filter
}
}, { immediate: true })
const router = useRouter()
watch(
() => [page.value, params.value.filter, params.value.s],
() => {
router.replace({
name: route.name,
params: route.params,
query: {
page: page.value,
filter: params.value.filter || undefined,
s: params.value.s || undefined,
},
})
},
{ deep: true },
)
// Only listen for query path changes
watch(() => JSON.stringify(getAllTasksParams.value), (newParams, oldParams) => {
@ -148,7 +165,6 @@ export function useTaskList(
totalPages,
currentPage: page,
loadTasks,
searchTerm: search,
params,
sortByParam: sortBy,
}

View File

@ -19,9 +19,16 @@ export function secondsToPeriod(seconds: number): { unit: PeriodUnit, amount: nu
}
}
if (seconds % SECONDS_A_HOUR === 0) {
return {
unit: 'hours',
amount: seconds / SECONDS_A_HOUR,
}
}
return {
unit: 'hours',
amount: seconds / SECONDS_A_HOUR,
unit: 'minutes',
amount: seconds / SECONDS_A_MINUTE,
}
}

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Nur Projektadministrator:innen können Ansichten bearbeiten."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Nur Projektadministrator:innen können Ansichten bearbeiten."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Kérjük, adjon meg egy címet.",
"delete": "Törölje ezt a nézetet",
"deleteText": "Biztosan eltávolítja ezt a nézetet? A továbbiakban nem lesz használható a projektben szereplő feladatok megtekintésére. Ez a művelet nem töröl semmilyen feladatot. Ezt nem lehet visszacsinálni!",
"deleteSuccess": "A nézet sikeresen törölve"
"deleteSuccess": "A nézet sikeresen törölve",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Proszę podać tytuł.",
"delete": "Usuń ten widok",
"deleteText": "Czy na pewno chcesz usunąć ten widok? Nie będzie już możliwe wyświetlanie zadań w tym projekcie. Ta akcja nie usunie żadnych zadań. Tej operacji nie można cofnąć!",
"deleteSuccess": "Widok został pomyślnie usunięty"
"deleteSuccess": "Widok został pomyślnie usunięty",
"onlyAdminsCanEdit": "Tylko administratorzy projektu mogą edytować widoki."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Prosim navedite naslov.",
"delete": "Izbriši pogled",
"deleteText": "Ali ste prepričani, da želite odstraniti ta pogled? Ne bo ga več mogoče uporabljati za ogled nalog v tem projektu. To dejanje ne bo izbrisalo nobenih opravil. Tega ni mogoče razveljaviti!",
"deleteSuccess": "Pogled je bil uspešno izbrisan"
"deleteSuccess": "Pogled je bil uspešno izbrisan",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -385,7 +385,7 @@
"views": {
"header": "Edit views",
"title": "Title",
"actions": "Actions",
"actions": "Åtgärder",
"kind": "Kind",
"bucketConfigMode": "Bucket configuration mode",
"bucketConfig": "Bucket configuration",
@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {
@ -455,7 +456,7 @@
},
"operators": {
"intro": "The available operators for filtering include:",
"notEqual": "Not equal to",
"notEqual": "Inte lika med",
"equal": "Lika med",
"greaterThan": "Större än",
"greaterThanOrEqual": "Större än eller lika med",
@ -777,7 +778,7 @@
"moveProject": "Flytta",
"color": "Set Color",
"delete": "Radera",
"favorite": "Add to Favorites",
"favorite": "Lägg till i favoriter",
"unfavorite": "Remove from Favorites"
}
},

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "请提供标题。",
"delete": "删除此视图",
"deleteText": "您确定要删除此视图吗?它将不再可能使用它来查看此项目中的任务。 此操作不会删除任何任务。此操作不能撤销!",
"deleteSuccess": "视图已成功删除"
"deleteSuccess": "视图已成功删除",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -396,7 +396,8 @@
"titleRequired": "Please provide a title.",
"delete": "Delete this view",
"deleteText": "Are you sure you want to remove this view? It will no longer be possible to use it to view tasks in this project. This action won't delete any tasks. This cannot be undone!",
"deleteSuccess": "The view was successfully deleted"
"deleteSuccess": "The view was successfully deleted",
"onlyAdminsCanEdit": "Only project admins can edit views."
}
},
"filters": {

View File

@ -26,7 +26,7 @@ test('store project in history', () => {
expect(saved).toBe('[{"id":1}]')
})
test('store only the last 5 projects in history', () => {
test('store only the last 6 projects in history', () => {
let saved: string | null = null
vi.spyOn(localStorage, 'getItem').mockImplementation(() => saved)
vi.spyOn(localStorage, 'setItem').mockImplementation((key: string, projects: string) => {
@ -39,7 +39,8 @@ test('store only the last 5 projects in history', () => {
saveProjectToHistory({id: 4})
saveProjectToHistory({id: 5})
saveProjectToHistory({id: 6})
expect(saved).toBe('[{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
saveProjectToHistory({id: 7})
expect(saved).toBe('[{"id":7},{"id":6},{"id":5},{"id":4},{"id":3},{"id":2}]')
})
test('don\'t store the same project twice', () => {

View File

@ -20,6 +20,8 @@ function saveHistory(history: ProjectHistory[]) {
localStorage.setItem('projectHistory', JSON.stringify(history))
}
const MAX_SAVED_PROJECTS = 6
export function saveProjectToHistory(project: ProjectHistory) {
const history: ProjectHistory[] = getHistory()
@ -33,7 +35,7 @@ export function saveProjectToHistory(project: ProjectHistory) {
// Add the new project to the beginning of the project
history.unshift(project)
if (history.length > 5) {
if (history.length > MAX_SAVED_PROJECTS) {
history.pop()
}
saveHistory(history)

View File

@ -2,7 +2,7 @@ import { createRouter, createWebHistory } from 'vue-router'
import type { RouteLocation } from 'vue-router'
import {saveLastVisited} from '@/helpers/saveLastVisited'
import {saveProjectView, getProjectViewId} from '@/helpers/projectView'
import {getProjectViewId} from '@/helpers/projectView'
import {parseDateOrString} from '@/helpers/time/parseDateOrString'
import {getNextWeekDate} from '@/helpers/time/getNextWeekDate'
import {LINK_SHARE_HASH_PREFIX} from '@/constants/linkShareHash'
@ -366,7 +366,6 @@ const router = createRouter({
path: '/projects/:projectId/:viewId',
name: 'project.view',
component: ProjectView,
beforeEnter: (to) => saveProjectView(parseInt(to.params.projectId as string), parseInt(to.params.viewId as string)),
props: route => ({
projectId: parseInt(route.params.projectId as string),
viewId: route.params.viewId ? parseInt(route.params.viewId as string): undefined,

View File

@ -1,5 +1,5 @@
import {computed, reactive, toRefs} from 'vue'
import {defineStore, acceptHMRUpdate} from 'pinia'
import {acceptHMRUpdate, defineStore} from 'pinia'
import {parseURL} from 'ufo'
import {HTTPFactory} from '@/helpers/fetcher'
@ -7,6 +7,7 @@ import {objectToCamelCase} from '@/helpers/case'
import type {IProvider} from '@/types/IProvider'
import type {MIGRATORS} from '@/views/migrate/migrators'
import {InvalidApiUrlProvidedError} from '@/helpers/checkAndSetApiUrl'
export interface ConfigState {
version: string,
@ -83,15 +84,17 @@ export const useConfigStore = defineStore('config', () => {
function setConfig(config: ConfigState) {
Object.assign(state, config)
}
async function update(): Promise<boolean> {
const HTTP = HTTPFactory()
const {data: config} = await HTTP.get('info')
if (typeof config.version === 'undefined') {
return false
throw new InvalidApiUrlProvidedError()
}
setConfig(objectToCamelCase(config))
const success = !!config
return success
return !!config
}
return {

View File

@ -160,7 +160,6 @@ export const useLabelStore = defineStore('label', () => {
deleteLabel,
updateLabel,
createLabel,
}
})

View File

@ -106,6 +106,12 @@ export const useProjectStore = defineStore('project', () => {
}
function removeProjectById(project: IProject) {
// Remove child projects from state as well
projectsArray.value
.filter(p => p.parentProjectId === project.id)
.forEach(p => removeProjectById(p))
remove(project)
delete projects.value[project.id]
}
@ -201,13 +207,17 @@ export const useProjectStore = defineStore('project', () => {
}
function getAncestors(project: IProject): IProject[] {
if (typeof project === 'undefined') {
return []
}
if (!project?.parentProjectId) {
return [project]
}
const parentProject = projects.value[project.parentProjectId]
return [
...getAncestors(parentProject),
...(parentProject ? getAncestors(parentProject) : []),
project,
]
}

View File

@ -3,7 +3,7 @@
<h1>{{ $t('migrate.titleService', {name: migrator.name}) }}</h1>
<p>{{ $t('migrate.descriptionDo') }}</p>
<template v-if="message === '' && lastMigrationStartedAt === null">
<template v-if="message === '' && lastMigrationStartedAt === null && !migrationJustStarted">
<template v-if="isMigrating === false">
<template v-if="migrator.isFileMigrator">
<p>{{ $t('migrate.importUpload', {name: migrator.name}) }}</p>

View File

@ -2,6 +2,7 @@
import {computed, watch} from 'vue'
import {useProjectStore} from '@/stores/projects'
import {useRoute, useRouter} from 'vue-router'
import {saveProjectView} from '@/helpers/projectView'
import ProjectList from '@/components/project/views/ProjectList.vue'
import ProjectGantt from '@/components/project/views/ProjectGantt.vue'
@ -53,6 +54,13 @@ watch(
redirectToFirstViewIfNecessary,
)
// using a watcher instead of beforeEnter because beforeEnter is not called when only the viewId changes
watch(
() => [projectId, viewId],
() => saveProjectView(projectId, viewId),
{immediate: true},
)
const route = useRoute()
</script>

View File

@ -1,6 +1,6 @@
<script setup lang="ts">
import CreateEdit from '@/components/misc/create-edit.vue'
import {computed, ref} from 'vue'
import {watch, ref, computed} from 'vue'
import {useProjectStore} from '@/stores/projects'
import ProjectViewModel from '@/models/projectView'
import type {IProjectView} from '@/modelTypes/IProjectView'
@ -9,6 +9,10 @@ import ProjectViewService from '@/services/projectViews'
import XButton from '@/components/input/button.vue'
import {error, success} from '@/message'
import {useI18n} from 'vue-i18n'
import ProjectService from '@/services/project'
import {RIGHTS} from '@/constants/rights'
import ProjectModel from '@/models/project'
import Message from '@/components/misc/message.vue'
const {
projectId,
@ -28,6 +32,17 @@ const viewIdToDelete = ref<number | null>(null)
const showDeleteModal = ref(false)
const viewToEdit = ref<IProjectView | null>(null)
const isAdmin = ref<boolean>(false)
watch(
() => projectId,
async () => {
const projectService = new ProjectService()
const project = await projectService.get(new ProjectModel({id: projectId}))
isAdmin.value = project.maxRight === RIGHTS.ADMIN
},
{immediate: true},
)
async function createView() {
if (!showCreateForm.value) {
showCreateForm.value = true
@ -83,13 +98,17 @@ async function saveView() {
<CreateEdit
:title="$t('project.views.header')"
:primary-label="$t('misc.save')"
:has-primary-action="false"
>
<ViewEditForm
v-if="showCreateForm"
v-model="newView"
class="mb-4"
/>
<div class="is-flex is-justify-content-end mb-4">
<div
v-if="isAdmin"
class="is-flex is-justify-content-end mb-4"
>
<XButton
:loading="projectViewService.loading"
@click="createView"
@ -97,6 +116,10 @@ async function saveView() {
{{ $t('project.views.create') }}
</XButton>
</div>
<Message v-if="!isAdmin">
{{ $t('project.views.onlyAdminsCanEdit') }}
</Message>
<table
v-if="views?.length > 0"
@ -144,6 +167,7 @@ async function saveView() {
<td>{{ v.viewKind }}</td>
<td class="has-text-right">
<XButton
v-if="isAdmin"
class="is-danger mr-2"
icon="trash-alt"
@click="() => {
@ -152,6 +176,7 @@ async function saveView() {
}"
/>
<XButton
v-if="isAdmin"
icon="pen"
@click="viewToEdit = {...v}"
/>

View File

@ -309,6 +309,7 @@
v-model="task.labels"
:disabled="!canWrite"
:task-id="taskId"
:creatable="!authStore.isLinkShareAuth"
/>
</div>
@ -653,6 +654,7 @@ const projectStore = useProjectStore()
const attachmentStore = useAttachmentStore()
const taskStore = useTaskStore()
const kanbanStore = useKanbanStore()
const authStore = useAuthStore()
const task = ref<ITask>(new TaskModel())
const taskTitle = computed(() => task.value.title)
@ -877,7 +879,7 @@ function toggleTaskDone() {
done: !task.value.done,
}
if (newTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
if (newTask.done && authStore.settings.frontendSettings.playSoundWhenDone) {
playPopSound()
}

View File

@ -17,11 +17,11 @@ import postcssEasings from 'postcss-easings'
import postcssEasingGradients from 'postcss-easing-gradients'
const pathSrc = fileURLToPath(new URL('./src', import.meta.url))
const pathSrc = fileURLToPath(new URL('./src', import.meta.url)).replaceAll('\\', '/')
// the @use rules have to be the first in the compiled stylesheets
const PREFIXED_SCSS_STYLES = `@use "sass:math";
@import "${pathSrc}/styles/common-imports";`
@import "${pathSrc}/styles/common-imports.scss";`
const isModernBuild = Boolean(process.env.BUILD_MODERN_ONLY)
const legacy = isModernBuild

30
go.mod
View File

@ -20,8 +20,8 @@ require (
code.vikunja.io/web v0.0.0-20210706160506-d85def955bd3
dario.cat/mergo v1.0.0
github.com/ThreeDotsLabs/watermill v1.3.5
github.com/adlio/trello v1.11.0
github.com/arran4/golang-ical v0.2.7
github.com/adlio/trello v1.12.0
github.com/arran4/golang-ical v0.2.8
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/bbrks/go-blurhash v1.1.1
github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500
@ -29,7 +29,7 @@ require (
github.com/cweill/gotests v1.6.0
github.com/d4l3k/messagediff v1.2.1
github.com/disintegration/imaging v1.6.2
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
github.com/gabriel-vasile/mimetype v1.4.3
github.com/ganigeorgiev/fexpr v0.4.0
github.com/getsentry/sentry-go v0.27.0
@ -45,11 +45,12 @@ require (
github.com/jinzhu/copier v0.4.0
github.com/jszwedko/go-datemath v0.1.1-0.20230526204004-640a500621d6
github.com/labstack/echo-jwt/v4 v4.2.0
github.com/labstack/echo/v4 v4.11.4
github.com/labstack/echo/v4 v4.12.0
github.com/labstack/gommon v0.4.2
github.com/lib/pq v1.10.9
github.com/magefile/mage v1.15.0
github.com/mattn/go-sqlite3 v1.14.22
github.com/microcosm-cc/bluemonday v1.0.26
github.com/olekukonko/tablewriter v0.0.5
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pquerna/otp v1.4.0
@ -62,17 +63,17 @@ require (
github.com/spf13/viper v1.18.2
github.com/stretchr/testify v1.9.0
github.com/swaggo/swag v1.16.3
github.com/tkuchiki/go-timezone v0.2.2
github.com/tkuchiki/go-timezone v0.2.3
github.com/typesense/typesense-go v1.0.0
github.com/ulule/limiter/v3 v3.11.2
github.com/wneessen/go-mail v0.4.0
github.com/yuin/goldmark v1.7.0
golang.org/x/crypto v0.21.0
github.com/yuin/goldmark v1.7.1
golang.org/x/crypto v0.22.0
golang.org/x/image v0.15.0
golang.org/x/oauth2 v0.18.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.18.0
golang.org/x/term v0.18.0
golang.org/x/oauth2 v0.19.0
golang.org/x/sync v0.7.0
golang.org/x/sys v0.19.0
golang.org/x/term v0.19.0
golang.org/x/text v0.14.0
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
@ -90,6 +91,7 @@ require (
github.com/KyleBanks/depth v1.2.1 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/apapsch/go-jsonmerge/v2 v2.0.0 // indirect
github.com/aymerick/douceur v0.2.0 // indirect
github.com/beevik/etree v1.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc // indirect
@ -108,7 +110,6 @@ require (
github.com/go-chi/chi/v5 v5.0.10 // indirect
github.com/go-faster/city v1.0.1 // indirect
github.com/go-faster/errors v0.6.1 // indirect
github.com/go-jose/go-jose/v3 v3.0.3 // indirect
github.com/go-jose/go-jose/v4 v4.0.1 // indirect
github.com/go-openapi/jsonpointer v0.20.1 // indirect
github.com/go-openapi/jsonreference v0.20.3 // indirect
@ -119,8 +120,8 @@ require (
github.com/go-playground/validator/v10 v10.15.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt v3.2.2+incompatible // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/golang/snappy v0.0.4 // indirect
github.com/gorilla/css v1.0.1 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
@ -176,10 +177,9 @@ require (
golang.org/x/arch v0.4.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.13.0 // indirect
google.golang.org/appengine v1.6.8 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect

77
go.sum
View File

@ -24,17 +24,19 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
github.com/ThreeDotsLabs/watermill v1.3.5 h1:50JEPEhMGZQMh08ct0tfO1PsgMOAOhV3zxK2WofkbXg=
github.com/ThreeDotsLabs/watermill v1.3.5/go.mod h1:O/u/Ptyrk5MPTxSeWM5vzTtZcZfxXfO9PK9eXTYiFZY=
github.com/adlio/trello v1.11.0 h1:PGpwpRZcRiVhsG7VEHb2GWKw4R2ZxB9nc6cMI/7mLD8=
github.com/adlio/trello v1.11.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
github.com/adlio/trello v1.12.0 h1:JqOE2GFHQ9YtEviRRRSnicSxPbt4WFOxhqXzjMOw8lw=
github.com/adlio/trello v1.12.0/go.mod h1:I4Lti4jf2KxjTNgTqs5W3lLuE78QZZdYbbPnQQGwjOo=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
github.com/andybalholm/cascadia v1.1.0/go.mod h1:GsXiBklL0woXo1j/WYWtSYYC4ouU9PqHO0sqidkEA4Y=
github.com/apapsch/go-jsonmerge/v2 v2.0.0 h1:axGnT1gRIfimI7gJifB699GoE/oq+F2MU7Dml6nw9rQ=
github.com/apapsch/go-jsonmerge/v2 v2.0.0/go.mod h1:lvDnEdqiQrp0O42VQGgmlKpxL1AP2+08jFMw88y4klk=
github.com/arran4/golang-ical v0.2.7 h1:VO7YlVaGupZE15aj6NhUhte/MIfZuoIzkoI71VsG6Gg=
github.com/arran4/golang-ical v0.2.7/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
github.com/arran4/golang-ical v0.2.8 h1:8lsFcfQqzg0gBpIxq7fWr4RV+8SVENLMXpSic5xsFUs=
github.com/arran4/golang-ical v0.2.8/go.mod h1:RqMuPGmwRRwjkb07hmm+JBqcWa1vF1LvVmPtSZN2OhQ=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/bbrks/go-blurhash v1.1.1 h1:uoXOxRPDca9zHYabUTwvS4KnY++KKUbwFo+Yxb8ME4M=
github.com/bbrks/go-blurhash v1.1.1/go.mod h1:lkAsdyXp+EhARcUo85yS2G1o+Sh43I2ebF5togC4bAY=
github.com/beevik/etree v1.1.0 h1:T0xke/WvNtMoCqgzPhkX2r4rjY3GDZFi+FjpRZY2Jbs=
@ -68,8 +70,6 @@ github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4M
github.com/chzyer/readline v1.5.0/go.mod h1:x22KAscuvRqlLoK9CsoYsmxoXZMMFVyOl86cAH8qUic=
github.com/chzyer/test v0.0.0-20210722231415-061457976a23/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/coreos/go-oidc/v3 v3.9.0 h1:0J/ogVOd4y8P0f0xUh8l9t07xRP/d8tccvjHl2dcsSo=
github.com/coreos/go-oidc/v3 v3.9.0/go.mod h1:rTKz2PYwftcrtoCzV5g5kvfJoWcm0Mk8AF8y1iAQro4=
github.com/coreos/go-oidc/v3 v3.10.0 h1:tDnXHnLyiTVyT/2zLDGj09pFPkhND8Gl8lnTRhoEaJU=
github.com/coreos/go-oidc/v3 v3.10.0/go.mod h1:5j11xcw0D3+SGxn6Z/WFADsgcWVMyNAlSQupk0KK3ac=
github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4=
@ -103,6 +103,8 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2 h1:S6Dco8FtAhEI/qkg/00H6RdEGC+MCy5GPiQ+xweNRFE=
github.com/dustinkirkland/golang-petname v0.0.0-20231002161417-6a283f1aaaf2/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b h1:+0Xqob+onh+4l9TSWmFyZ4JHqGUiCy5P1muyH8Evfpw=
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b/go.mod h1:8AuBTZBRSFqEYBPYULd+NN474/zZBLP+6WeT5S9xlAc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
@ -127,8 +129,6 @@ github.com/go-faster/city v1.0.1 h1:4WAxSZ3V2Ws4QRDrscLEDcibJY8uf41H6AhXDrNDcGw=
github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6vO3VcTw=
github.com/go-faster/errors v0.6.1 h1:nNIPOBkprlKzkThvS/0YaX8Zs9KewLCOSFQS5BU06FI=
github.com/go-faster/errors v0.6.1/go.mod h1:5MGV2/2T9yvlrbhe9pD9LO5Z/2zCSq2T8j+Jpi2LAyY=
github.com/go-jose/go-jose/v3 v3.0.3 h1:fFKWeig/irsp7XD2zBxvnmA/XaRWp5V3CBsZXJF7G7k=
github.com/go-jose/go-jose/v3 v3.0.3/go.mod h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
github.com/go-jose/go-jose/v4 v4.0.1 h1:QVEPDE3OluqXBQZDcnNvQrInro2h0e4eqNbnZSWqS6U=
github.com/go-jose/go-jose/v4 v4.0.1/go.mod h1:WVf9LFMHh/QVrmqrOfqun0C45tMe3RoiKJMPvgWwLfY=
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
@ -156,8 +156,6 @@ github.com/go-playground/validator/v10 v10.15.1 h1:BSe8uhN+xQ4r5guV/ywQI4gO59C2r
github.com/go-playground/validator/v10 v10.15.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
github.com/go-sql-driver/mysql v1.8.0 h1:UtktXaU2Nb64z/pLiGIxY4431SJ4/dR5cjMmlVHgnT4=
github.com/go-sql-driver/mysql v1.8.0/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
@ -193,8 +191,6 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
@ -215,6 +211,8 @@ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8=
github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
@ -320,8 +318,8 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/labstack/echo-jwt/v4 v4.2.0 h1:odSISV9JgcSCuhgQSV/6Io3i7nUmfM/QkBeR5GVJj5c=
github.com/labstack/echo-jwt/v4 v4.2.0/go.mod h1:MA2RqdXdEn4/uEglx0HcUOgQSyBaTh5JcaHIan3biwU=
github.com/labstack/echo/v4 v4.1.16/go.mod h1:awO+5TzAjvL8XpibdsfXxPgHr+orhtXZJZIQCVjogKI=
github.com/labstack/echo/v4 v4.11.4 h1:vDZmA+qNeh1pd/cCkEicDMrjtrnMGQ1QFI9gWN1zGq8=
github.com/labstack/echo/v4 v4.11.4/go.mod h1:noh7EvLwqDsmh/X/HWKPUl1AjzJrhyptRyEbQJfxen8=
github.com/labstack/echo/v4 v4.12.0 h1:IKpw49IMryVB2p1a4dzwlhP1O2Tf2E0Ir/450lH+kI0=
github.com/labstack/echo/v4 v4.12.0/go.mod h1:UP9Cr2DJXbOK3Kr9ONYzNowSh7HP0aG0ShAyycHSJvM=
github.com/labstack/gommon v0.3.0/go.mod h1:MULnywXg0yavhxWKc+lOruYdAhDwPK9wf0OL7NoOu+k=
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
@ -372,6 +370,8 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/microcosm-cc/bluemonday v1.0.26 h1:xbqSvqzQMeEHCqMi64VAs4d8uy6Mequs3rQ0k/Khz58=
github.com/microcosm-cc/bluemonday v1.0.26/go.mod h1:JyzOCs9gkyQyjs+6h10UEVSe02CGwkhd72Xdqh78TWs=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -501,8 +501,8 @@ github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpP
github.com/tidwall/pretty v1.0.0/go.mod h1:XNkn88O1ChpSDQmQeStsy+sBenx6DDtFZJxhVysOjyk=
github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk=
github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk=
github.com/tkuchiki/go-timezone v0.2.2 h1:MdHR65KwgVTwWFQrota4SKzc4L5EfuH5SdZZGtk/P2Q=
github.com/tkuchiki/go-timezone v0.2.2/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
github.com/tkuchiki/go-timezone v0.2.3 h1:D3TVdIPrFsu9lxGxqNX2wsZwn1MZtTqTW0mdevMozHc=
github.com/tkuchiki/go-timezone v0.2.3/go.mod h1:oFweWxYl35C/s7HMVZXiA19Jr9Y0qJHMaG/J2TES4LY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/typesense/typesense-go v1.0.0 h1:/8Lr1yf9YjmUKdn/xbTNy+OhwOvBd0noBTRkcB22Uhw=
@ -531,8 +531,8 @@ github.com/youmark/pkcs8 v0.0.0-20181117223130-1be2e3e5546d/go.mod h1:rHwXgn7Jul
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yuin/goldmark v1.7.0 h1:EfOIvIMZIzHdB/R/zVrikYLPPwJlfMcNczJFMs1m6sA=
github.com/yuin/goldmark v1.7.0/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/yuin/goldmark v1.7.1 h1:3bajkSilaCbjdKVsKdZjZCLBNPL9pYzrCakKaf4U49U=
github.com/yuin/goldmark v1.7.1/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
github.com/ziutek/mymysql v1.5.4/go.mod h1:LMSpPZ6DbqWFxNCHW77HeMg9I646SAhApZ/wKdgO/C0=
go.mongodb.org/mongo-driver v1.11.4/go.mod h1:PTSz5yu21bkT/wXpkS7WR5f0ddqw5quethTUn9WM+2g=
@ -572,9 +572,8 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -586,7 +585,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20180218175443-cbe0f9307d01/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -608,20 +606,18 @@ golang.org/x/net v0.0.0-20210610132358-84b48f89b13b/go.mod h1:9nx3DQGgdP8bBQD5qx
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc=
golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/oauth2 v0.18.0 h1:09qnuIAgzdx1XplqJvW6CQqMCtGZykZWcXzPMPUusvI=
golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi8=
golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w=
golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8=
golang.org/x/oauth2 v0.19.0 h1:9+E/EZBCbTLNrbN35fHv/a/d/mOBatymz1zbtQrXpIg=
golang.org/x/oauth2 v0.19.0/go.mod h1:vYi7skDa1x015PmRRYZ7+s1cWyPgrPiSYRe4rnsexc8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -650,27 +646,21 @@ golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o=
golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8=
golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58=
golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q=
golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -691,7 +681,6 @@ golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ=
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -700,8 +689,6 @@ golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM=
google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
@ -786,8 +773,6 @@ nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYm
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
sigs.k8s.io/yaml v1.3.0/go.mod h1:GeOyir5tyXNByN85N/dRIT9es5UQNerPYEKK56eTBm8=
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec h1:ICDp83UjJvLcOFWHAxr7vmziKIHJkE4jsIF1mbT9Bwk=
src.techknowlogick.com/xgo v1.7.1-0.20240305180710-770b8eae9cec/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884 h1:Ttvt8FCpUXfC8r3+LgSPrBUIr/JkHmYQtvmOwEET8qE=
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xormigrate v1.7.1 h1:RKGLLUAqJ+zO8iZ7eOc7oLH7f0cs2gfXSZSvBRBHnlY=
@ -798,7 +783,5 @@ xorm.io/builder v0.3.13 h1:a3jmiVVL19psGeXx8GIurTp7p0IIgqeDmwhcR6BAOAo=
xorm.io/builder v0.3.13/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
xorm.io/xorm v1.0.5/go.mod h1:uF9EtbhODq5kNWxMbnBEj8hRRZnlcNSz2t2N7HW/+A4=
xorm.io/xorm v1.3.3/go.mod h1:qFJGFoVYbbIdnz2vaL5OxSQ2raleMpyRRalnq3n9OJo=
xorm.io/xorm v1.3.8 h1:CJmplmWqfSRpLWSPMmqz+so8toBp3m7ehuRehIWedZo=
xorm.io/xorm v1.3.8/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=
xorm.io/xorm v1.3.9 h1:TUovzS0ko+IQ1XnNLfs5dqK1cJl1H5uHpWbWqAQ04nU=
xorm.io/xorm v1.3.9/go.mod h1:LsCCffeeYp63ssk0pKumP6l96WZcHix7ChpurcLNuMw=

View File

@ -95,6 +95,9 @@ func ParseTaskFromVTODO(content string) (vTask *models.Task, err error) {
if err != nil {
return nil, err
}
if len(parsed.Components) == 0 {
return nil, errors.New("VTODO element does seem not contain any components")
}
vTodo, ok := parsed.Components[0].(*ics.VTodo)
if !ok {
return nil, errors.New("VTODO element not found")

View File

@ -32,9 +32,9 @@ The to-do app to organize your life.
Also one of the two wild South American camelids which live in the high
alpine areas of the Andes and a relative of the llama.
Vikunja is a self-hosted To-Do list application with a web app and mobile apps for all platforms. It is licensed under the GPLv3.
Vikunja is a self-hosted To-Do list application with a web app and mobile apps for all platforms. It is licensed under the AGPLv3.
Find more info at vikunja.io.`,
Find out more at vikunja.io.`,
PreRun: webCmd.PreRun,
Run: webCmd.Run,
}

View File

@ -349,82 +349,135 @@ func TestBucket(t *testing.T) {
})
t.Run("Create", func(t *testing.T) {
t.Run("Normal", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "1"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "1",
"view": "3",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Nonexisting", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9999"}, `{"title":"Lorem Ipsum"}`)
t.Run("Nonexistent project", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "9999",
"view": "1",
}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectDoesNotExist)
assertHandlerErrorCode(t, err, models.ErrCodeProjectViewDoesNotExist)
})
t.Run("Nonexistent view", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "1",
"view": "9999",
}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assertHandlerErrorCode(t, err, models.ErrCodeProjectViewDoesNotExist)
})
t.Run("Rights check", func(t *testing.T) {
t.Run("Forbidden", func(t *testing.T) {
// Owned by user13
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "20"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "20",
"view": "80",
}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "6"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "6",
"view": "24",
}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "7"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "7",
"view": "28",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "8"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "8",
"view": "32",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "9"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "9",
"view": "36",
}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "10"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "10",
"view": "40",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "11"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "11",
"view": "44",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Parent Project Team readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "12"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "12",
"view": "48",
}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Parent Project Team write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "13"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "13",
"view": "52",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Parent Project Team admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "14"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "14",
"view": "56",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Parent Project User readonly", func(t *testing.T) {
_, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "15"}, `{"title":"Lorem Ipsum"}`)
_, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "15",
"view": "60",
}, `{"title":"Lorem Ipsum"}`)
require.Error(t, err)
assert.Contains(t, err.(*echo.HTTPError).Message, `Forbidden`)
})
t.Run("Shared Via Parent Project User write", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "16"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "16",
"view": "64",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})
t.Run("Shared Via Parent Project User admin", func(t *testing.T) {
rec, err := testHandler.testCreateWithUser(nil, map[string]string{"project": "17"}, `{"title":"Lorem Ipsum"}`)
rec, err := testHandler.testCreateWithUser(nil, map[string]string{
"project": "17",
"view": "68",
}, `{"title":"Lorem Ipsum"}`)
require.NoError(t, err)
assert.Contains(t, rec.Body.String(), `"title":"Lorem Ipsum"`)
})

View File

@ -99,12 +99,14 @@ insert into buckets_dg_tmp(id, title, "limit", position, created, updated, creat
select id, title, "limit", position, created, updated, created_by_id, project_view_id
from buckets;
drop index if exists UQE_buckets_id;
drop table buckets;
alter table buckets_dg_tmp
rename to buckets;
create unique index UQE_buckets_id
create unique index if not exists UQE_buckets_id
on buckets (id);
`)
return err

View File

@ -155,6 +155,21 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (task
projectIDs = append(projectIDs, p.ID)
}
views := map[int64]*ProjectView{}
err = s.In("project_id", projectIDs).Find(&views)
if err != nil {
return
}
viewIDs := []int64{}
for _, v := range views {
if projectsMap[v.ProjectID].Views == nil {
projectsMap[v.ProjectID].Views = []*ProjectView{}
}
projectsMap[v.ProjectID].Views = append(projectsMap[v.ProjectID].Views, v)
viewIDs = append(viewIDs, v.ID)
}
tasks, _, _, err := getTasksForProjects(s, rawProjects, u, &taskSearchOptions{
page: 0,
perPage: -1,
@ -194,17 +209,75 @@ func exportProjectsAndTasks(s *xorm.Session, u *user.User, wr *zip.Writer) (task
}
buckets := []*Bucket{}
err = s.In("project_id", projectIDs).Find(&buckets)
err = s.In("project_view_id", viewIDs).Find(&buckets)
if err != nil {
return
}
bucketIDs := []int64{}
for _, b := range buckets {
if _, exists := projectsMap[b.ProjectID]; !exists {
log.Debugf("[User Data Export] Project %d does not exist for bucket %d, omitting", b.ProjectID, b.ID)
view, exists := views[b.ProjectViewID]
if !exists {
log.Debugf("[User Data Export] Project view %d does not exist for bucket %d, omitting", b.ProjectViewID, b.ID)
continue
}
projectsMap[b.ProjectID].Buckets = append(projectsMap[b.ProjectID].Buckets, b)
_, exists = projectsMap[view.ProjectID]
if !exists {
log.Debugf("[User Data Export] Project %d does not exist for bucket %d, omitting", view.ProjectID, b.ID)
continue
}
projectsMap[view.ProjectID].Buckets = append(projectsMap[view.ProjectID].Buckets, b)
bucketIDs = append(bucketIDs, b.ID)
}
taskBuckets := []*TaskBucket{}
err = s.In("bucket_id", bucketIDs).Find(&taskBuckets)
if err != nil {
return
}
for _, tb := range taskBuckets {
view, exists := views[tb.ProjectViewID]
if !exists {
log.Debugf("[User Data Export] Project view %d does not exist, omitting", tb.ProjectViewID)
continue
}
_, exists = projectsMap[view.ProjectID]
if !exists {
log.Debugf("[User Data Export] Project %d does not exist, omitting", view.ProjectID)
continue
}
if projectsMap[view.ProjectID].TaskBuckets == nil {
projectsMap[view.ProjectID].TaskBuckets = []*TaskBucket{}
}
projectsMap[view.ProjectID].TaskBuckets = append(projectsMap[view.ProjectID].TaskBuckets, tb)
}
taskPositions := []*TaskPosition{}
err = s.In("project_view_id", viewIDs).Find(&taskPositions)
if err != nil {
return
}
for _, p := range taskPositions {
view, exists := views[p.ProjectViewID]
if !exists {
log.Debugf("[User Data Export] Project view %d does not exist, omitting", p.ProjectViewID)
continue
}
_, exists = projectsMap[view.ProjectID]
if !exists {
log.Debugf("[User Data Export] Project %d does not exist, omitting", view.ProjectID)
continue
}
if projectsMap[view.ProjectID].Positions == nil {
projectsMap[view.ProjectID].Positions = []*TaskPosition{}
}
projectsMap[view.ProjectID].Positions = append(projectsMap[view.ProjectID].Positions, p)
}
data, err := json.Marshal(projects)

View File

@ -23,11 +23,13 @@ import (
// CanCreate checks if a user can create a new bucket
func (b *Bucket) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
pv := &ProjectView{
ID: b.ProjectViewID,
ProjectID: b.ProjectID,
pv, err := GetProjectViewByIDAndProject(s, b.ProjectViewID, b.ProjectID)
if err != nil {
return false, err
}
return pv.CanUpdate(s, a)
p := &Project{ID: pv.ProjectID}
return p.CanUpdate(s, a)
}
// CanUpdate checks if a user can update an existing bucket
@ -46,9 +48,11 @@ func (b *Bucket) canDoBucket(s *xorm.Session, a web.Auth) (bool, error) {
if err != nil {
return false, err
}
pv := &ProjectView{
ID: bb.ProjectViewID,
ProjectID: b.ProjectID,
pv, err := GetProjectViewByIDAndProject(s, bb.ProjectViewID, b.ProjectID)
if err != nil {
return false, err
}
return pv.CanUpdate(s, a)
p := &Project{ID: pv.ProjectID}
return p.CanUpdate(s, a)
}

View File

@ -148,23 +148,14 @@ func (l *Label) Delete(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 500 {object} models.Message "Internal error"
// @Router /labels [get]
func (l *Label) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (ls interface{}, resultCount int, numberOfEntries int64, err error) {
if _, is := a.(*LinkSharing); is {
return nil, 0, 0, ErrGenericForbidden{}
}
u, err := user.GetUserByID(s, a.GetID())
if err != nil {
return nil, 0, 0, err
}
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
Search: []string{search},
User: u,
GetForUser: u.ID,
User: a,
Page: page,
PerPage: perPage,
GetUnusedLabels: true,
GroupByLabelIDsOnly: true,
GetForUser: true,
})
}

View File

@ -17,7 +17,6 @@
package models
import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/web"
"xorm.io/builder"
"xorm.io/xorm"
@ -64,27 +63,29 @@ func (l *Label) isLabelOwner(s *xorm.Session, a web.Auth) (bool, error) {
// Helper method to check if a user can see a specific label
func (l *Label) hasAccessToLabel(s *xorm.Session, a web.Auth) (has bool, maxRight int, err error) {
if _, is := a.(*LinkSharing); is {
return false, 0, nil
}
linkShare, isLinkShare := a.(*LinkSharing)
u, err := user.GetUserByID(s, a.GetID())
if err != nil {
return false, 0, err
var where builder.Cond
var createdByID int64
if isLinkShare {
where = builder.Eq{"project_id": linkShare.ProjectID}
} else {
where = builder.In("project_id", getUserProjectsStatement(a.GetID(), "", false).Select("l.id"))
createdByID = a.GetID()
}
cond := builder.In("label_tasks.task_id",
builder.
Select("id").
From("tasks").
Where(builder.In("project_id", getUserProjectsStatement(u.ID, "", false).Select("l.id"))),
Where(where),
)
ll := &LabelTask{}
has, err = s.Table("labels").
Select("label_tasks.*").
Join("LEFT", "label_tasks", "label_tasks.label_id = labels.id").
Where("label_tasks.label_id is not null OR labels.created_by_id = ?", u.ID).
Where("label_tasks.label_id is not null OR labels.created_by_id = ?", createdByID).
Or(cond).
And("labels.id = ?", l.ID).
Exist(ll)

View File

@ -144,7 +144,7 @@ func (lt *LabelTask) ReadAll(s *xorm.Session, a web.Auth, search string, page in
}
return GetLabelsByTaskIDs(s, &LabelByTaskIDsOptions{
User: &user.User{ID: a.GetID()},
User: a,
Search: []string{search},
Page: page,
TaskIDs: []int64{lt.TaskID},
@ -159,23 +159,26 @@ type LabelWithTaskID struct {
// LabelByTaskIDsOptions is a struct to not clutter the function with too many optional parameters.
type LabelByTaskIDsOptions struct {
User *user.User
User web.Auth
Search []string
Page int
PerPage int
TaskIDs []int64
GetUnusedLabels bool
GroupByLabelIDsOnly bool
GetForUser int64
GetForUser bool
}
// GetLabelsByTaskIDs is a helper function to get all labels for a set of tasks
// Used when getting all labels for one task as well when getting all lables
func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*LabelWithTaskID, resultCount int, totalEntries int64, err error) {
linkShare, isLinkShareAuth := opts.User.(*LinkSharing)
// We still need the task ID when we want to get all labels for a task, but because of this, we get the same label
// multiple times when it is associated to more than one task.
// Because of this whole thing, we need this extra switch here to only group by Task IDs if needed.
// Probably not the most ideal solution.
// Probably not the most ideataskdetaill solution.
var groupBy = "labels.id,label_tasks.task_id"
var selectStmt = "labels.*, label_tasks.task_id"
if opts.GroupByLabelIDsOnly {
@ -186,20 +189,25 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
// Get all labels associated with these tasks
var labels []*LabelWithTaskID
cond := builder.And(builder.NotNull{"label_tasks.label_id"})
if len(opts.TaskIDs) > 0 && opts.GetForUser == 0 {
if len(opts.TaskIDs) > 0 && !opts.GetForUser {
cond = builder.And(builder.In("label_tasks.task_id", opts.TaskIDs), cond)
}
if opts.GetForUser != 0 {
if opts.GetForUser {
projects, _, _, err := getRawProjectsForUser(s, &projectOptions{
user: &user.User{ID: opts.GetForUser},
})
if err != nil {
return nil, 0, 0, err
}
projectIDs := make([]int64, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
var projectIDs []int64
if isLinkShareAuth {
projectIDs = []int64{linkShare.ProjectID}
} else {
projects, _, _, err := getRawProjectsForUser(s, &projectOptions{
user: &user.User{ID: opts.User.GetID()},
})
if err != nil {
return nil, 0, 0, err
}
projectIDs = make([]int64, 0, len(projects))
for _, project := range projects {
projectIDs = append(projectIDs, project.ID)
}
}
cond = builder.And(builder.In("label_tasks.task_id",
@ -209,8 +217,8 @@ func GetLabelsByTaskIDs(s *xorm.Session, opts *LabelByTaskIDsOptions) (ls []*Lab
Where(builder.In("project_id", projectIDs)),
), cond)
}
if opts.GetUnusedLabels {
cond = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.ID})
if opts.GetUnusedLabels && !isLinkShareAuth {
cond = builder.Or(cond, builder.Eq{"labels.created_by_id": opts.User.GetID()})
}
ids := []int64{}

View File

@ -62,7 +62,7 @@ func TestLabel_ReadAll(t *testing.T) {
name string
fields fields
args args
wantLs interface{}
wantLs []*LabelWithTaskID
wantErr bool
}{
{
@ -143,7 +143,10 @@ func TestLabel_ReadAll(t *testing.T) {
t.Errorf("Label.ReadAll() error = %v, wantErr %v", err, tt.wantErr)
return
}
if diff, equal := messagediff.PrettyDiff(gotLs, tt.wantLs); !equal {
got := gotLs.([]*LabelWithTaskID)
if diff, equal := messagediff.PrettyDiff(got, tt.wantLs); !equal {
t.Errorf("Label.ReadAll() = %v, want %v, diff: %v", gotLs, tt.wantLs, diff)
}
s.Close()

View File

@ -529,7 +529,18 @@ func (l *AddTaskToTypesense) Handle(msg *message.Message) (err error) {
s := db.NewSession()
defer s.Close()
ttask, err := getTypesenseTaskForTask(s, event.Task, nil)
positionsMap, err := getPositionsForTask(s, event)
if err != nil {
return err
}
bucketsMap, err := getBucketsForTask(s, event)
if err != nil {
return err
}
ttask, err := getTypesenseTaskForTask(s, event.Task, nil, positionsMap, bucketsMap)
if err != nil {
return err
}
@ -540,6 +551,36 @@ func (l *AddTaskToTypesense) Handle(msg *message.Message) (err error) {
return
}
func getPositionsForTask(s *xorm.Session, event *TaskCreatedEvent) (positionsMap map[int64][]*TaskPositionWithView, err error) {
positions := []*TaskPositionWithView{}
err = s.
Table("project_views").
Where("project_views.project_id = ?", event.Task.ProjectID).
Join("LEFT", "task_positions", "project_views.id = task_positions.project_view_id AND task_positions.task_id = ?", event.Task.ID).
Find(&positions)
if err != nil {
return
}
positionsMap = make(map[int64][]*TaskPositionWithView, 1)
positionsMap[event.Task.ID] = positions
return
}
func getBucketsForTask(s *xorm.Session, event *TaskCreatedEvent) (bucketsMap map[int64][]*TaskBucket, err error) {
buckets := []*TaskBucket{}
err = s.
Where("task_id = ?", event.Task.ID).
Find(&buckets)
if err != nil {
return
}
bucketsMap = make(map[int64][]*TaskBucket, 1)
bucketsMap[event.Task.ID] = buckets
return
}
// UpdateTaskInTypesense represents a listener
type UpdateTaskInTypesense struct {
}

View File

@ -17,10 +17,8 @@
package models
import (
"bufio"
"sort"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/utils"
@ -77,20 +75,16 @@ func (n *TaskCommentNotification) SubjectID() int64 {
func (n *TaskCommentNotification) ToMail() *notifications.Mail {
mail := notifications.NewMail().
From(n.Doer.GetNameAndFromEmail())
From(n.Doer.GetNameAndFromEmail()).
Subject("Re: " + n.Task.Title)
subject := "Re: " + n.Task.Title
if n.Mentioned {
subject = n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`
mail.Line("**" + n.Doer.GetName() + "** mentioned you in a comment:")
mail.
Line("**" + n.Doer.GetName() + "** mentioned you in a comment:").
Subject(n.Doer.GetName() + ` mentioned you in a comment in "` + n.Task.Title + `"`)
}
mail.Subject(subject)
lines := bufio.NewScanner(strings.NewReader(n.Comment.Comment))
for lines.Scan() {
mail.Line(lines.Text())
}
mail.HTML(n.Comment.Comment)
return mail.
Action("View Task", n.Task.GetFrontendURL())
@ -306,12 +300,8 @@ func (n *UserMentionedInTaskNotification) ToMail() *notifications.Mail {
mail := notifications.NewMail().
From(n.Doer.GetNameAndFromEmail()).
Subject(subject).
Line("**" + n.Doer.GetName() + "** mentioned you in a task:")
lines := bufio.NewScanner(strings.NewReader(n.Task.Description))
for lines.Scan() {
mail.Line(lines.Text())
}
Line("**" + n.Doer.GetName() + "** mentioned you in a task:").
HTML(n.Task.Description)
return mail.
Action("View Task", n.Task.GetFrontendURL())

View File

@ -93,8 +93,10 @@ type ProjectWithTasksAndBuckets struct {
// An array of tasks which belong to the project.
Tasks []*TaskWithComments `xorm:"-" json:"tasks"`
// Only used for migration.
Buckets []*Bucket `xorm:"-" json:"buckets"`
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
Buckets []*Bucket `xorm:"-" json:"buckets"`
TaskBuckets []*TaskBucket `xorm:"-" json:"task_buckets"`
Positions []*TaskPosition `xorm:"-" json:"positions"`
BackgroundFileID int64 `xorm:"null" json:"background_file_id"`
}
// TableName returns a better name for the projects table
@ -110,15 +112,43 @@ type ProjectBackgroundType struct {
// ProjectBackgroundUpload represents the project upload background type
const ProjectBackgroundUpload string = "upload"
const FavoritesPseudoProjectID = -1
// FavoritesPseudoProject holds all tasks marked as favorites
var FavoritesPseudoProject = Project{
ID: -1,
ID: FavoritesPseudoProjectID,
Title: "Favorites",
Description: "This project has all tasks marked as favorites.",
IsFavorite: true,
Position: -1,
Created: time.Now(),
Updated: time.Now(),
Views: []*ProjectView{
{
ID: -1,
ProjectID: FavoritesPseudoProjectID,
Title: "List",
ViewKind: ProjectViewKindList,
Position: 100,
Filter: "done = false",
},
{
ID: -2,
ProjectID: FavoritesPseudoProjectID,
Title: "Gantt",
ViewKind: ProjectViewKindGantt,
Position: 200,
},
{
ID: -3,
ProjectID: FavoritesPseudoProjectID,
Title: "Table",
ViewKind: ProjectViewKindTable,
Position: 300,
},
},
Created: time.Now(),
Updated: time.Now(),
}
// ReadAll gets all projects a user has access to
@ -146,6 +176,9 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
}
projects := []*Project{project}
err = addProjectDetails(s, projects, a)
if err == nil && len(projects) > 0 {
projects[0].ParentProjectID = 0
}
return projects, 0, 0, err
}
@ -207,6 +240,7 @@ func (p *Project) ReadAll(s *xorm.Session, a web.Auth, search string, page int,
func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
if p.ID == FavoritesPseudoProject.ID {
p.Views = FavoritesPseudoProject.Views
// Already "built" the project in CanRead
return nil
}
@ -226,6 +260,11 @@ func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
p.OwnerID = sf.OwnerID
}
_, isShareAuth := a.(*LinkSharing)
if isShareAuth {
p.ParentProjectID = 0
}
// Get project owner
p.Owner, err = user.GetUserByID(s, p.OwnerID)
if err != nil {
@ -1073,6 +1112,46 @@ func (p *Project) Delete(s *xorm.Session, a web.Auth) (err error) {
}
}
// Delete related project entities
views, err := getViewsForProject(s, p.ID)
if err != nil {
return
}
viewIDs := []int64{}
for _, v := range views {
viewIDs = append(viewIDs, v.ID)
}
_, err = s.In("project_view_id", viewIDs).Delete(&Bucket{})
if err != nil {
return
}
_, err = s.In("id", viewIDs).Delete(&ProjectView{})
if err != nil {
return
}
err = removeFromFavorite(s, p.ID, a, FavoriteKindProject)
if err != nil {
return
}
_, err = s.Where("project_id = ?", p.ID).Delete(&LinkSharing{})
if err != nil {
return
}
_, err = s.Where("project_id = ?", p.ID).Delete(&ProjectUser{})
if err != nil {
return
}
_, err = s.Where("project_id = ?", p.ID).Delete(&TeamProject{})
if err != nil {
return
}
// Delete the project
_, err = s.ID(p.ID).Delete(&Project{})
if err != nil {

View File

@ -159,6 +159,7 @@ func (pd *ProjectDuplicate) Create(s *xorm.Session, doer web.Auth) (err error) {
log.Debugf("Duplicated all link shares from project %d into %d", pd.ProjectID, pd.Project.ID)
err = pd.Project.ReadOne(s, doer)
return
}
@ -226,9 +227,11 @@ func duplicateViews(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth, taskMa
})
}
_, err = s.Insert(&taskBuckets)
if err != nil {
return err
if len(taskBuckets) > 0 {
_, err = s.Insert(&taskBuckets)
if err != nil {
return err
}
}
oldTaskPositions := []*TaskPosition{}
@ -246,7 +249,9 @@ func duplicateViews(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth, taskMa
})
}
_, err = s.Insert(&taskPositions)
if len(taskPositions) > 0 {
_, err = s.Insert(&taskPositions)
}
return
}

View File

@ -347,10 +347,22 @@ func (p *ProjectView) Update(s *xorm.Session, _ web.Auth) (err error) {
return
}
func GetProjectViewByIDAndProject(s *xorm.Session, id, projectID int64) (view *ProjectView, err error) {
func GetProjectViewByIDAndProject(s *xorm.Session, viewID, projectID int64) (view *ProjectView, err error) {
if projectID == FavoritesPseudoProjectID && viewID < 0 {
for _, v := range FavoritesPseudoProject.Views {
if v.ID == viewID {
return v, nil
}
}
return nil, &ErrProjectViewDoesNotExist{
ProjectViewID: viewID,
}
}
view = &ProjectView{}
exists, err := s.
Where("id = ? AND project_id = ?", id, projectID).
Where("id = ? AND project_id = ?", viewID, projectID).
NoAutoCondition().
Get(view)
if err != nil {
@ -359,7 +371,7 @@ func GetProjectViewByIDAndProject(s *xorm.Session, id, projectID int64) (view *P
if !exists {
return nil, &ErrProjectViewDoesNotExist{
ProjectViewID: id,
ProjectViewID: viewID,
}
}

View File

@ -28,17 +28,17 @@ func (p *ProjectView) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
func (p *ProjectView) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
pp := p.getProject()
return pp.CanUpdate(s, a)
return pp.IsAdmin(s, a)
}
func (p *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
pp := p.getProject()
return pp.CanUpdate(s, a)
return pp.IsAdmin(s, a)
}
func (p *ProjectView) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
pp := p.getProject()
return pp.CanUpdate(s, a)
return pp.IsAdmin(s, a)
}
func (p *ProjectView) getProject() (pp *Project) {

View File

@ -100,6 +100,10 @@ func getTaskFilterOptsFromCollection(tf *TaskCollection, projectView *ProjectVie
param.orderBy = getSortOrderFromString(tf.OrderBy[i])
}
if s == taskPropertyPosition && projectView != nil && projectView.ID < 0 {
continue
}
if s == taskPropertyPosition && projectView != nil {
param.projectViewID = projectView.ID
}
@ -249,11 +253,20 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
opts.perPage = perPage
if view != nil {
opts.sortby = append(opts.sortby, &sortParam{
projectViewID: view.ID,
sortBy: taskPropertyPosition,
orderBy: orderAscending,
})
var hasOrderByPosition bool
for _, param := range opts.sortby {
if param.sortBy == taskPropertyPosition {
hasOrderByPosition = true
break
}
}
if !hasOrderByPosition {
opts.sortby = append(opts.sortby, &sortParam{
projectViewID: view.ID,
sortBy: taskPropertyPosition,
orderBy: orderAscending,
})
}
}
shareAuth, is := a.(*LinkSharing)

View File

@ -65,6 +65,14 @@ func (tc *TaskComment) TableName() string {
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{taskID}/comments [put]
func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
tc.Created = time.Time{}
tc.Updated = time.Time{}
return tc.CreateWithTimestamps(s, a)
}
func (tc *TaskComment) CreateWithTimestamps(s *xorm.Session, a web.Auth) (err error) {
// Check if the task exists
task, err := GetTaskSimple(s, &Task{ID: tc.TaskID})
if err != nil {
@ -77,9 +85,16 @@ func (tc *TaskComment) Create(s *xorm.Session, a web.Auth) (err error) {
}
tc.AuthorID = tc.Author.ID
_, err = s.Insert(tc)
if err != nil {
return
if !tc.Created.IsZero() && !tc.Updated.IsZero() {
_, err = s.NoAutoTime().Insert(tc)
if err != nil {
return
}
} else {
_, err = s.Insert(tc)
if err != nil {
return
}
}
return events.Dispatch(&TaskCommentCreatedEvent{
@ -255,7 +270,7 @@ func (tc *TaskComment) ReadAll(s *xorm.Session, auth web.Auth, search string, pa
query := s.
Where(builder.And(where...)).
Join("LEFT", "users", "users.id = task_comments.author_id").
OrderBy("task_comments.id asc")
OrderBy("task_comments.created asc")
if limit > 0 {
query = query.Limit(limit, start)
}

View File

@ -297,6 +297,7 @@ func (d *dbTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task, totalCo
queryCount = queryCount.Join("LEFT", "task_buckets", "task_buckets.task_id = tasks.id")
}
totalCount, err = queryCount.
Select("count(DISTINCT tasks.id)").
Count(&Task{})
return
}
@ -379,6 +380,10 @@ func convertParsedFilterToTypesense(rawFilters []*taskFilter) (filterBy string,
f.field = "project_id"
}
if f.field == "bucket_id" {
f.field = "buckets"
}
filter := f.field
switch f.comparator {
@ -450,6 +455,7 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
"(" + filter + ")",
}
var projectViewIDForPosition int64
var sortbyFields []string
for i, param := range opts.sortby {
// Validate the params
@ -457,17 +463,19 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
return nil, totalCount, err
}
sortBy := param.sortBy
// Typesense does not allow sorting by ID, so we sort by created timestamp instead
if param.sortBy == taskPropertyID {
param.sortBy = taskPropertyCreated
sortBy = taskPropertyCreated
}
if param.sortBy == taskPropertyPosition {
param.sortBy = "positions.view_" + strconv.FormatInt(param.projectViewID, 10)
continue
sortBy = "positions.view_" + strconv.FormatInt(param.projectViewID, 10)
projectViewIDForPosition = param.projectViewID
}
sortbyFields = append(sortbyFields, param.sortBy+"(missing_values:last):"+param.orderBy.String())
sortbyFields = append(sortbyFields, sortBy+"(missing_values:last):"+param.orderBy.String())
if i == 2 {
// Typesense supports up to 3 sorting parameters
@ -525,9 +533,14 @@ func (t *typesenseTaskSearcher) Search(opts *taskSearchOptions) (tasks []*Task,
return nil, 0, err
}
err = t.s.
query := t.s.
In("id", taskIDs).
OrderBy(orderby).
Find(&tasks)
OrderBy(orderby)
if projectViewIDForPosition != 0 {
query = query.Join("LEFT", "task_positions", "task_positions.task_id = tasks.id AND task_positions.project_view_id = ?", projectViewIDForPosition)
}
err = query.Find(&tasks)
return tasks, int64(*result.Found), err
}

View File

@ -1486,6 +1486,18 @@ func (t *Task) Delete(s *xorm.Session, a web.Auth) (err error) {
return
}
// Delete all positions
_, err = s.Where("task_id = ?", t.ID).Delete(&TaskPosition{})
if err != nil {
return
}
// Delete all bucket relations
_, err = s.Where("task_id = ?", t.ID).Delete(&TaskBucket{})
if err != nil {
return
}
// Actually delete the task
_, err = s.ID(t.ID).Delete(Task{})
if err != nil {

View File

@ -146,22 +146,10 @@ func CreateTypesenseCollections() error {
Name: "updated",
Type: "int64", // unix timestamp
},
{
Name: "bucket_id",
Type: "int64",
},
{
Name: "position",
Type: "float",
},
{
Name: "created_by_id",
Type: "int64",
},
{
Name: "project_view_id",
Type: "int64",
},
{
Name: "reminders",
Type: "object[]", // TODO
@ -192,6 +180,14 @@ func CreateTypesenseCollections() error {
Type: "object[]", // TODO
Optional: pointer.True(),
},
{
Name: "positions",
Type: "object",
},
{
Name: "buckets",
Type: "int64[]",
},
},
}
@ -248,14 +244,8 @@ func ReindexAllTasks() (err error) {
return
}
func getTypesenseTaskForTask(s *xorm.Session, task *Task, projectsCache map[int64]*Project) (ttask *typesenseTask, err error) {
positions := []*TaskPosition{}
err = s.Where("task_id = ?", task.ID).Find(&positions)
if err != nil {
return
}
ttask = convertTaskToTypesenseTask(task, positions)
func getTypesenseTaskForTask(s *xorm.Session, task *Task, projectsCache map[int64]*Project, taskPositionCache map[int64][]*TaskPositionWithView, taskBucketCache map[int64][]*TaskBucket) (ttask *typesenseTask, err error) {
ttask = convertTaskToTypesenseTask(task, taskPositionCache[task.ID], taskBucketCache[task.ID])
var p *Project
if projectsCache == nil {
@ -299,9 +289,19 @@ func reindexTasksInTypesense(s *xorm.Session, tasks map[int64]*Task) (err error)
projects := make(map[int64]*Project)
typesenseTasks := []interface{}{}
positionsByTask, err := getPositionsByTask(s)
if err != nil {
return err
}
bucketsByTask, err := getBucketsByTask(s)
if err != nil {
return err
}
for _, task := range tasks {
ttask, err := getTypesenseTaskForTask(s, task, projects)
ttask, err := getTypesenseTaskForTask(s, task, projects, positionsByTask, bucketsByTask)
if err != nil {
return err
}
@ -320,11 +320,55 @@ func reindexTasksInTypesense(s *xorm.Session, tasks map[int64]*Task) (err error)
return err
}
log.Debugf("Indexed tasks %v into Typesense", tasks)
log.Debugf("Indexed %d tasks into Typesense", len(tasks))
return nil
}
type TaskPositionWithView struct {
ProjectView `xorm:"extends"`
TaskPosition `xorm:"extends"`
}
func getPositionsByTask(s *xorm.Session) (positionsByTask map[int64][]*TaskPositionWithView, err error) {
rawPositions := []*TaskPositionWithView{}
err = s.
Table("project_views").
Join("LEFT", "task_positions", "project_views.id = task_positions.project_view_id").
Find(&rawPositions)
if err != nil {
return
}
positionsByTask = make(map[int64][]*TaskPositionWithView, len(rawPositions))
for _, p := range rawPositions {
_, has := positionsByTask[p.TaskID]
if !has {
positionsByTask[p.TaskID] = []*TaskPositionWithView{}
}
positionsByTask[p.TaskID] = append(positionsByTask[p.TaskID], p)
}
return positionsByTask, nil
}
func getBucketsByTask(s *xorm.Session) (positionsByTask map[int64][]*TaskBucket, err error) {
rawBuckets := []*TaskBucket{}
err = s.Find(&rawBuckets)
if err != nil {
return
}
positionsByTask = make(map[int64][]*TaskBucket, len(rawBuckets))
for _, p := range rawBuckets {
_, has := positionsByTask[p.TaskID]
if !has {
positionsByTask[p.TaskID] = []*TaskBucket{}
}
positionsByTask[p.TaskID] = append(positionsByTask[p.TaskID], p)
}
return positionsByTask, nil
}
func indexDummyTask() (err error) {
// The initial sync should contain one dummy task with all related fields populated so that typesense
// creates the indexes properly. A little hacky, but gets the job done.
@ -386,6 +430,13 @@ func indexDummyTask() (err error) {
},
},
},
Positions: map[string]float64{
"view_1": 10,
"view_2": 30,
"view_3": 5450,
"view_4": 42,
},
Buckets: []int64{42},
}
_, err = typesenseClient.Collection("tasks").
@ -430,9 +481,10 @@ type typesenseTask struct {
Attachments interface{} `json:"attachments"`
Comments interface{} `json:"comments"`
Positions map[string]float64 `json:"positions"`
Buckets []int64 `json:"buckets"`
}
func convertTaskToTypesenseTask(task *Task, positions []*TaskPosition) *typesenseTask {
func convertTaskToTypesenseTask(task *Task, positions []*TaskPositionWithView, buckets []*TaskBucket) *typesenseTask {
tt := &typesenseTask{
ID: fmt.Sprintf("%d", task.ID),
@ -461,6 +513,8 @@ func convertTaskToTypesenseTask(task *Task, positions []*TaskPosition) *typesens
Labels: task.Labels,
//RelatedTasks: task.RelatedTasks,
Attachments: task.Attachments,
Positions: make(map[string]float64, len(positions)),
Buckets: make([]int64, 0, len(buckets)),
}
if task.DoneAt.IsZero() {
@ -477,7 +531,15 @@ func convertTaskToTypesenseTask(task *Task, positions []*TaskPosition) *typesens
}
for _, position := range positions {
tt.Positions["view_"+strconv.FormatInt(position.ProjectViewID, 10)] = position.Position
pos := position.TaskPosition.Position
if pos == 0 {
pos = float64(task.ID)
}
tt.Positions["view_"+strconv.FormatInt(position.ProjectView.ID, 10)] = pos
}
for _, bucket := range buckets {
tt.Buckets = append(tt.Buckets, bucket.BucketID)
}
return tt

View File

@ -38,6 +38,10 @@ func RemoveEmptySSOTeams(s *xorm.Session) (err error) {
return err
}
if len(teams) == 0 {
return nil
}
teamIDs := make([]int64, 0, len(teams))
for _, team := range teams {
teamIDs = append(teamIDs, team.ID)
@ -63,6 +67,6 @@ func RegisterEmptyOpenIDTeamCleanupCron() {
}
})
if err != nil {
log.Fatalf("Could not empty openid teams cleanup cron: %s", err)
log.Fatalf("Could not register empty openid teams cleanup cron: %s", err)
}
}

View File

@ -32,6 +32,8 @@ type Image struct {
Info interface{} `json:"info,omitempty"`
}
const MaxBackgroundImageHeight = 3840
// Provider represents something that is able to get a project of images and set one of them as background
type Provider interface {
// Search is used to either return a pre-defined project of Image or let the user search for an image

View File

@ -17,10 +17,13 @@
package handler
import (
"bytes"
_ "image/gif" // To make sure the decoder used for generating blurHashes recognizes gifs
_ "image/jpeg" // To make sure the decoder used for generating blurHashes recognizes jpgs
_ "image/png" // To make sure the decoder used for generating blurHashes recognizes pngs
"github.com/disintegration/imaging"
_ "golang.org/x/image/bmp" // To make sure the decoder used for generating blurHashes recognizes bmps
_ "golang.org/x/image/tiff" // To make sure the decoder used for generating blurHashes recognizes tiffs
_ "golang.org/x/image/webp" // To make sure the decoder used for generating blurHashes recognizes tiffs
@ -143,6 +146,13 @@ func (bp *BackgroundProvider) SetBackground(c echo.Context) error {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
err = project.ReadOne(s, auth)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
return c.JSON(http.StatusOK, project)
}
@ -203,6 +213,12 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
return handler.HandleHTTPError(err, c)
}
err = project.ReadOne(s, auth)
if err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
}
if err := s.Commit(); err != nil {
_ = s.Rollback()
return handler.HandleHTTPError(err, c)
@ -213,7 +229,30 @@ func (bp *BackgroundProvider) UploadBackground(c echo.Context) error {
func SaveBackgroundFile(s *xorm.Session, auth web.Auth, project *models.Project, srcf io.ReadSeeker, filename string, filesize uint64) (err error) {
_, _ = srcf.Seek(0, io.SeekStart)
f, err := files.Create(srcf, filename, filesize, auth)
src, err := imaging.Decode(srcf)
if err != nil {
return err
}
_, _ = srcf.Seek(0, io.SeekStart)
imgConfig, _, err := image.DecodeConfig(srcf)
if err != nil {
return err
}
height := imgConfig.Height
if imgConfig.Height > background.MaxBackgroundImageHeight {
height = background.MaxBackgroundImageHeight
}
buf := bytes.Buffer{}
dst := imaging.Resize(src, 0, height, imaging.Lanczos)
err = imaging.Encode(&buf, dst, imaging.JPEG, imaging.JPEGQuality(80))
if err != nil {
return err
}
f, err := files.Create(&buf, filename, filesize, auth)
if err != nil {
return err
}

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