Compare commits

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

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

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

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

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

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

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

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

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

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

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

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

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

Resolved https://community.vikunja.io/t/preview-size-is-not-defined/2843
2024-09-29 11:59:18 +02:00
kolaente 17618301bc Revert "chore(attachments): refactor building image preview"
This reverts commit e70f5bcce3.
2024-09-29 11:54:20 +02:00
33 changed files with 2890 additions and 2475 deletions
+80 -82
View File
@@ -580,7 +580,7 @@ steps:
event: [ push, tag, pull_request ]
- name: before-static-build
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
commands:
- export PATH=$PATH:$GOPATH/bin
@@ -589,7 +589,7 @@ steps:
depends_on: [ fetch-tags, mage ]
- name: static-build-windows
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
@@ -605,7 +605,7 @@ steps:
- frontend-build
- name: static-build-linux
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
@@ -621,7 +621,7 @@ steps:
- frontend-build
- name: static-build-darwin
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
environment:
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
@@ -647,7 +647,7 @@ steps:
- ./mage-static release:compress
- name: after-build-static
image: techknowlogick/xgo:latest
image: ghcr.io/techknowlogick/xgo:go-1.23.x
pull: always
depends_on:
- after-build-compress
@@ -676,13 +676,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: dist/zip/
source: dist/zip/*
@@ -698,13 +698,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: dist/zip/
source: dist/zip/*
@@ -750,13 +750,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: dist/os-packages/
source: dist/os-packages/*
@@ -772,13 +772,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: dist/os-packages/
source: dist/os-packages/*
@@ -837,7 +837,6 @@ steps:
repo: vikunja/vikunja
tags: unstable
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
@@ -869,7 +868,6 @@ steps:
from_secret: docker_password
repo: vikunja/vikunja
platforms:
- linux/386
- linux/amd64
- linux/arm/v6
- linux/arm/v7
@@ -931,13 +929,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
source: frontend/vikunja-frontend-unstable.zip
target: /
@@ -990,13 +988,13 @@ steps:
image: plugins/s3
pull: always
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
source: frontend/vikunja-frontend-${DRONE_TAG##v}.zip
target: /
@@ -1095,8 +1093,8 @@ steps:
# settings:
# restore: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# endpoint: https://fsn1.your-objectstorage.com
# region: fsn1
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
@@ -1133,8 +1131,8 @@ steps:
# settings:
# rebuild: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# endpoint: https://fsn1.your-objectstorage.com
# region: fsn1
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
@@ -1176,8 +1174,8 @@ steps:
# settings:
# restore: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# endpoint: https://fsn1.your-objectstorage.com
# region: fsn1
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
@@ -1200,30 +1198,30 @@ steps:
- unzip vikunja-frontend-$$VERSION.zip -d frontend
- sed -i 's/\\/api\\/v1//g' frontend/index.html
- ./bumpp.sh
- yarn install
- cat package.json
- yarn dist --linux --windows
# - name: rebuild-cache
# image: meltwater/drone-cache:dev
# pull: true
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
# AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key
# settings:
# rebuild: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://s3.fr-par.scw.cloud
# region: fr-par
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
# - '.cache'
# depends_on:
# - build
- corepack enable && pnpm config set store-dir .cache/pnpm
- pnpm install --fetch-timeout 100000
- pnpm dist --linux --windows
# - name: rebuild-cache
# image: meltwater/drone-cache:dev
# pull: true
# environment:
# AWS_ACCESS_KEY_ID:
# from_secret: cache_aws_access_key_id
# AWS_SECRET_ACCESS_KEY:
# from_secret: cache_aws_secret_access_key
# settings:
# rebuild: true
# bucket: kolaente.dev-drone-dependency-cache
# endpoint: https://fsn1.your-objectstorage.com
# region: fsn1
# path_style: true
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
# mount:
# - '.cache'
# depends_on:
# - build
- name: rename-unstable
image: bash
pull: true
@@ -1241,13 +1239,13 @@ steps:
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: desktop/dist/
source: desktop/dist/Vikunja-Desktop*
@@ -1263,13 +1261,13 @@ steps:
image: plugins/s3
pull: true
settings:
bucket: vikunja-releases
bucket: vikunja
access_key:
from_secret: aws_access_key_id
from_secret: hetzner_access_key_id
secret_key:
from_secret: aws_secret_access_key
endpoint: https://s3.fr-par.scw.cloud
region: fr-par
from_secret: hetzner_secret_access_key
endpoint: https://fsn1.your-objectstorage.com
region: fsn1
path_style: true
strip_prefix: desktop/dist/
source: desktop/dist/*
@@ -1298,9 +1296,9 @@ steps:
# - name: build
# environment:
# ACCESS_KEY:
# from_secret: aws_access_key_id
# from_secret: hetzner_access_key_id
# SECRET_KEY:
# from_secret: aws_secret_access_key
# from_secret: hetzner_secret_access_key
# commands:
# - git fetch --tags
# - export VERSION=${DRONE_TAG##v}
@@ -1313,9 +1311,9 @@ steps:
# - sed -i '' "s/\$${version}/$$VERSION/g" package.json
# - yarn install
# - yarn dist --mac
# - mc config host add scw-fr-par https://s3.fr-par.scw.cloud $ACCESS_KEY $SECRET_KEY --api S3v4
# - mc cp ./dist/*.dmg scw-fr-par/vikunja-releases/desktop/$VERSION/
# - mc cp ./dist/*.dmg.blockmap scw-fr-par/vikunja-releases/desktop/$VERSION/
# - mc config host add scw-fsn1 https://fsn1.your-objectstorage.com $ACCESS_KEY $SECRET_KEY --api S3v4
# - mc cp ./dist/*.dmg scw-fsn1/vikunja/desktop/$VERSION/
# - mc cp ./dist/*.dmg.blockmap scw-fsn1/vikunja/desktop/$VERSION/
---
kind: pipeline
@@ -1352,6 +1350,6 @@ steps:
- failure
---
kind: signature
hmac: eff6e40dc763fa1395078daaabcbef2aaa74fe0a36818a87e89015dbc4e9dae4
hmac: 02fb3097f3e50facb2579de5c3b94e513b97e535a049a28ddb67f85e494d65b1
...
+73
View File
@@ -7,6 +7,79 @@ to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
All releases can be found on https://code.vikunja.io/vikunja/releases.
## [0.24.6] - 2024-12-22
### Bug Fixes
* *(export)* Update only current user export file id
### Features
* Use hetzner object storage for releases ([3f98f47](3f98f47256628ac559a947f6c9541c0bf03001b6))
### Miscellaneous Tasks
* Release preparation ([17b2810](17b281072f095730a0fbb28458d713fa883f43ec))
* Sign drone config ([b91a5d9](b91a5d9adfb7144126f5824224970ad4015dcd6a))
* Do not generate config yaml ([8c28596](8c285968afff7ebd087ce824b52b0a9f28a8ec20))
## [0.24.5] - 2024-11-21
### Bug Fixes
* *(attachments)* Check permissions when accessing all attachments
* *(saved filters)* Check permissions when accessing tasks of a filter
* Pin xgo to 1.22.x ([87b2aac](87b2aaccb8cdcbe1ecb6092951a0bfe224ad7006))
* Upgrade xgo ([19b63c8](19b63c86c51f67614b867c75a58cda1774685edd))
* Upgrade xgo docker image everywhere ([04b40f8](04b40f8a7dcd01a86ddb8b27596073d1e50f9e97))
* *(ci)* Do not build linux 368 docker images
* Disable 368 releases ([73db10f](73db10fb02268e07d29842493df55f4d645ac503))
- **BREAKING**: disable 368 releases
### Miscellaneous Tasks
* Sign drone config ([17c4878](17c487875b5771c0971ee8bf030807171de2dddc))
* Go mod tidy ([9639025](96390257e0911089ae33a9565e8be7fa954c772c))
## [0.24.4] - 2024-09-29
### Bug Fixes
* *(attachment)* Do not use image previews
* *(checkbox)* Use sibling css selector instead of has
* *(files)* Only use service rootpath for files when the files path is not absolute
* *(filters)* Explicitly search in json when using postgres
* *(task)* Paginate task comments
* *(task)* Do not show close button when the task was not opened via modal
* *(task)* Improve task delete modal on mobile
* *(test)* Use correct selector for modal header
* Partial fix to allow list tasks in ios reminders app (#2717)
* *(attachments)* Revert "chore(attachments): refactor building image preview"
### Dependencies
* *(deps)* Update desktop lockfile
* *(deps)* Update dependency vue to v3.5.7
* *(deps)* Update dev-dependencies
* *(deps)* Update dependency vue-i18n to v10.0.2
* *(deps)* Update dev-dependencies
* *(deps)* Update dependency vue to v3.5.8
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v5.1.0
* *(deps)* Update dependency vue-i18n to v10.0.3
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v5.2.0
* *(deps)* Update dependency @sentry/vue to v8.31.0
* *(deps)* Update dependency tailwindcss to v3.4.13
* *(deps)* Update dev-dependencies
* *(deps)* Update dev-dependencies
* *(deps)* Update dependency @sentry/vue to v8.32.0
* *(deps)* Update tiptap to v2.7.3
* *(deps)* Update dev-dependencies
* *(deps)* Update dependency vue to v3.5.9
* *(deps)* Update dependency dompurify to v3.1.7
* *(deps)* Update tiptap to v2.7.4
* *(deps)* Update dependency vue to v3.5.10
* *(deps)* Update dev-dependencies
## [0.24.3] - 2024-09-20
### Bug Fixes
+1 -1
View File
@@ -13,7 +13,7 @@ RUN corepack enable && \
pnpm install && \
pnpm run build
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.21.x AS apibuilder
FROM --platform=$BUILDPLATFORM ghcr.io/techknowlogick/xgo:go-1.23.x AS apibuilder
RUN go install github.com/magefile/mage@latest && \
mv /go/bin/mage /usr/local/go/bin
+1 -1
View File
@@ -2,7 +2,7 @@
[![Build Status](https://drone.kolaente.de/api/badges/vikunja/vikunjaa/status.svg)](https://drone.kolaente.de/vikunja/vikunja)
[![License: AGPL v3](https://img.shields.io/badge/License-AGPL%20v3-blue.svg)](LICENSE)
[![Install](https://img.shields.io/badge/download-v0.24.3,brightgreen.svg)](https://vikunja.io/docs/installing)
[![Install](https://img.shields.io/badge/download-v0.24.6,brightgreen.svg)](https://vikunja.io/docs/installing)
[![Docker Pulls](https://img.shields.io/docker/pulls/vikunja/vikunja.svg)](https://hub.docker.com/r/vikunja/vikunja/)
[![Swagger Docs](https://img.shields.io/badge/swagger-docs-brightgreen.svg)](https://try.vikunja.io/api/v1/docs)
[![Go Report Card](https://goreportcard.com/badge/kolaente.dev/vikunja/vikunja)](https://goreportcard.com/report/kolaente.dev/vikunja/vikunja)
+1541 -1242
View File
File diff suppressed because it is too large Load Diff
@@ -148,7 +148,7 @@ describe('Project View Kanban', () => {
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
.contains('Delete')
.click()
cy.get('.modal-mask .modal-container .modal-content .header')
cy.get('.modal-mask .modal-container .modal-content .modal-header')
.should('contain', 'Delete the bucket')
cy.get('.modal-mask .modal-container .modal-content .actions .button')
.contains('Do it!')
@@ -251,7 +251,7 @@ describe('Project View Kanban', () => {
.should('be.visible')
.contains('Delete')
.click()
cy.get('.modal-mask .modal-container .modal-content .header')
cy.get('.modal-mask .modal-container .modal-content .modal-header')
.should('contain', 'Delete this task')
cy.get('.modal-mask .modal-container .modal-content .actions .button')
.contains('Do it!')
+1 -1
View File
@@ -356,7 +356,7 @@ describe('Task', () => {
.should('be.visible')
.contains('Delete')
.click()
cy.get('.modal-mask .modal-container .modal-content .header')
cy.get('.modal-mask .modal-container .modal-content .modal-header')
.should('contain', 'Delete this task')
cy.get('.modal-mask .modal-container .modal-content .actions .button')
.contains('Do it!')
+49 -49
View File
@@ -56,44 +56,44 @@
"@fortawesome/vue-fontawesome": "3.0.8",
"@github/hotkey": "3.1.1",
"@infectoone/vue-ganttastic": "2.3.2",
"@intlify/unplugin-vue-i18n": "5.0.0",
"@intlify/unplugin-vue-i18n": "5.2.0",
"@kyvg/vue3-notification": "3.3.0",
"@sentry/tracing": "7.114.0",
"@sentry/vue": "8.30.0",
"@tiptap/core": "2.7.2",
"@tiptap/extension-blockquote": "2.7.2",
"@tiptap/extension-bold": "2.7.2",
"@tiptap/extension-bullet-list": "2.7.2",
"@tiptap/extension-code": "2.7.2",
"@tiptap/extension-code-block": "2.7.2",
"@tiptap/extension-code-block-lowlight": "2.7.2",
"@tiptap/extension-document": "2.7.2",
"@tiptap/extension-dropcursor": "2.7.2",
"@tiptap/extension-gapcursor": "2.7.2",
"@tiptap/extension-hard-break": "2.7.2",
"@tiptap/extension-heading": "2.7.2",
"@tiptap/extension-history": "2.7.2",
"@tiptap/extension-horizontal-rule": "2.7.2",
"@tiptap/extension-image": "2.7.2",
"@tiptap/extension-italic": "2.7.2",
"@tiptap/extension-link": "2.7.2",
"@tiptap/extension-list-item": "2.7.2",
"@tiptap/extension-ordered-list": "2.7.2",
"@tiptap/extension-paragraph": "2.7.2",
"@tiptap/extension-placeholder": "2.7.2",
"@tiptap/extension-strike": "2.7.2",
"@tiptap/extension-table": "2.7.2",
"@tiptap/extension-table-cell": "2.7.2",
"@tiptap/extension-table-header": "2.7.2",
"@tiptap/extension-table-row": "2.7.2",
"@tiptap/extension-task-item": "2.7.2",
"@tiptap/extension-task-list": "2.7.2",
"@tiptap/extension-text": "2.7.2",
"@tiptap/extension-typography": "2.7.2",
"@tiptap/extension-underline": "2.7.2",
"@tiptap/pm": "2.7.2",
"@tiptap/suggestion": "2.7.2",
"@tiptap/vue-3": "2.7.2",
"@sentry/vue": "8.32.0",
"@tiptap/core": "2.7.4",
"@tiptap/extension-blockquote": "2.7.4",
"@tiptap/extension-bold": "2.7.4",
"@tiptap/extension-bullet-list": "2.7.4",
"@tiptap/extension-code": "2.7.4",
"@tiptap/extension-code-block": "2.7.4",
"@tiptap/extension-code-block-lowlight": "2.7.4",
"@tiptap/extension-document": "2.7.4",
"@tiptap/extension-dropcursor": "2.7.4",
"@tiptap/extension-gapcursor": "2.7.4",
"@tiptap/extension-hard-break": "2.7.4",
"@tiptap/extension-heading": "2.7.4",
"@tiptap/extension-history": "2.7.4",
"@tiptap/extension-horizontal-rule": "2.7.4",
"@tiptap/extension-image": "2.7.4",
"@tiptap/extension-italic": "2.7.4",
"@tiptap/extension-link": "2.7.4",
"@tiptap/extension-list-item": "2.7.4",
"@tiptap/extension-ordered-list": "2.7.4",
"@tiptap/extension-paragraph": "2.7.4",
"@tiptap/extension-placeholder": "2.7.4",
"@tiptap/extension-strike": "2.7.4",
"@tiptap/extension-table": "2.7.4",
"@tiptap/extension-table-cell": "2.7.4",
"@tiptap/extension-table-header": "2.7.4",
"@tiptap/extension-table-row": "2.7.4",
"@tiptap/extension-task-item": "2.7.4",
"@tiptap/extension-task-list": "2.7.4",
"@tiptap/extension-text": "2.7.4",
"@tiptap/extension-typography": "2.7.4",
"@tiptap/extension-underline": "2.7.4",
"@tiptap/pm": "2.7.4",
"@tiptap/suggestion": "2.7.4",
"@tiptap/vue-3": "2.7.4",
"@vueuse/core": "11.1.0",
"@vueuse/router": "11.1.0",
"axios": "1.7.7",
@@ -102,7 +102,7 @@
"change-case": "5.4.4",
"date-fns": "4.1.0",
"dayjs": "1.11.13",
"dompurify": "3.1.6",
"dompurify": "3.1.7",
"fast-deep-equal": "3.1.3",
"flatpickr": "4.6.13",
"flexsearch": "0.7.43",
@@ -113,13 +113,13 @@
"pinia": "2.2.2",
"register-service-worker": "1.7.2",
"sortablejs": "1.15.3",
"tailwindcss": "3.4.12",
"tailwindcss": "3.4.13",
"tippy.js": "6.3.7",
"ufo": "1.5.4",
"vue": "3.5.6",
"vue": "3.5.10",
"vue-advanced-cropper": "2.8.9",
"vue-flatpickr-component": "11.0.5",
"vue-i18n": "10.0.1",
"vue-i18n": "10.0.3",
"vue-router": "4.4.5",
"vuemoji-picker": "0.3.1",
"workbox-precaching": "7.1.0",
@@ -137,9 +137,9 @@
"@types/codemirror": "5.60.15",
"@types/dompurify": "3.0.5",
"@types/flexsearch": "0.7.6",
"@types/is-touch-device": "1.0.2",
"@types/is-touch-device": "1.0.3",
"@types/lodash.clonedeep": "4.5.9",
"@types/node": "20.16.5",
"@types/node": "20.16.10",
"@types/sortablejs": "1.15.8",
"@typescript-eslint/eslint-plugin": "7.18.0",
"@typescript-eslint/parser": "7.18.0",
@@ -149,11 +149,11 @@
"@vue/test-utils": "2.4.6",
"@vue/tsconfig": "0.5.1",
"autoprefixer": "10.4.20",
"browserslist": "4.23.3",
"caniuse-lite": "1.0.30001662",
"browserslist": "4.24.0",
"caniuse-lite": "1.0.30001664",
"csstype": "3.1.3",
"cypress": "13.14.2",
"esbuild": "0.23.1",
"cypress": "13.15.0",
"esbuild": "0.24.0",
"eslint": "8.57.1",
"eslint-plugin-vue": "9.28.0",
"happy-dom": "14.12.3",
@@ -162,13 +162,13 @@
"postcss-easing-gradients": "3.0.1",
"postcss-easings": "4.0.0",
"postcss-preset-env": "9.6.0",
"rollup": "4.22.0",
"rollup": "4.22.5",
"rollup-plugin-visualizer": "5.12.0",
"sass": "1.79.2",
"sass": "1.79.4",
"start-server-and-test": "2.0.8",
"typescript": "5.6.2",
"unplugin-inject-preload": "2.0.5",
"vite": "5.4.6",
"vite": "5.4.8",
"vite-plugin-pwa": "0.20.5",
"vite-plugin-sentry": "1.4.0",
"vite-svg-loader": "5.1.0",
+794 -806
View File
File diff suppressed because it is too large Load Diff
+12 -25
View File
@@ -3,33 +3,22 @@
v-cy="'checkbox'"
class="base-checkbox"
>
<input
:id="checkboxId"
type="checkbox"
class="is-sr-only"
:checked="modelValue"
:disabled="disabled || undefined"
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
<label
class="base-checkbox__label"
>
<slot
name="label"
:checkbox-id="checkboxId"
>
<label
:for="checkboxId"
class="base-checkbox__label"
<input
type="checkbox"
class="is-sr-only"
:checked="modelValue"
:disabled="disabled || undefined"
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
>
<slot />
</label>
</slot>
<slot />
</label>
</div>
</template>
<script setup lang="ts">
import {ref} from 'vue'
import {createRandomID} from '@/helpers/randomId'
withDefaults(defineProps<{
modelValue?: boolean,
disabled: boolean,
@@ -38,10 +27,8 @@ withDefaults(defineProps<{
})
const emit = defineEmits<{
(event: 'update:modelValue', value: boolean): void
(event: 'update:modelValue', value: boolean): void
}>()
const checkboxId = ref(`checkbox_${createRandomID()}`)
</script>
<style lang="scss" scoped>
@@ -53,7 +40,7 @@ const checkboxId = ref(`checkbox_${createRandomID()}`)
}
.base-checkbox:has(input:disabled) .base-checkbox__label {
cursor:not-allowed;
cursor: not-allowed;
pointer-events: none;
}
</style>
@@ -0,0 +1,97 @@
<template>
<nav
v-if="totalPages > 1"
aria-label="pagination"
class="pagination is-centered p-4"
role="navigation"
>
<slot
name="previous"
:disabled="currentPage === 1"
>
{{ $t('misc.previous') }}
</slot>
<slot
name="next"
:disabled="currentPage === totalPages"
>
{{ $t('misc.next') }}
</slot>
<ul class="pagination-list">
<li
v-for="(p, i) in pages"
:key="`page-${i}`"
>
<span
v-if="p.isEllipsis"
class="pagination-ellipsis"
>&hellip;</span>
<slot
v-else
name="page-link"
:page="p"
:is-current="p.number === currentPage"
>
{{ p.number }}
</slot>
</li>
</ul>
</nav>
</template>
<script lang="ts" setup>
import {computed} from 'vue'
const props = defineProps<{
totalPages: number,
currentPage: number
}>()
function createPagination(totalPages: number, currentPage: number) {
const pages = []
for (let i = 0; i < totalPages; i++) {
if (
i > 0 &&
(i + 1) < totalPages &&
((i + 1) > currentPage + 1 || (i + 1) < currentPage - 1)
) {
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
pages.push({
number: 0,
isEllipsis: true,
})
}
continue
}
pages.push({
number: i + 1,
isEllipsis: false,
})
}
return pages
}
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
</script>
<style lang="scss" scoped>
.pagination {
padding-bottom: 1rem;
}
.pagination-previous,
.pagination-next {
&:not(:disabled):hover {
background: $scheme-main;
cursor: pointer;
}
}
.pagination-list {
&, & li {
margin-top: 0;
}
}
</style>
@@ -38,7 +38,6 @@ const emit = defineEmits<{
}>()
</script>
<style lang="scss" scoped>
.fancy-checkbox {
display: inline-block;
@@ -70,8 +69,8 @@ const emit = defineEmits<{
}
}
.fancy-checkbox:not(:has(input:disabled)):hover .fancy-checkbox__icon,
.fancy-checkbox:has(input:checked) .fancy-checkbox__icon {
.fancy-checkbox:hover input:not(:disabled) + .fancy-checkbox__icon,
.fancy-checkbox input:checked + .fancy-checkbox__icon {
--stroke-color: var(--primary);
}
</style>
@@ -80,13 +79,13 @@ const emit = defineEmits<{
// Since css-has-pseudo doesn't work with deep classes,
// the following rules can't be scoped
.fancy-checkbox:has(:not(input:checked)) .fancy-checkbox__icon {
.fancy-checkbox :not(input:checked) + .fancy-checkbox__icon {
path {
transition-delay: 0.05s;
}
}
.fancy-checkbox:has(input:checked) .fancy-checkbox__icon {
.fancy-checkbox input:checked + .fancy-checkbox__icon {
path {
stroke-dashoffset: 60;
}
+10 -2
View File
@@ -34,7 +34,7 @@
}"
>
<slot>
<div class="header">
<div class="modal-header">
<slot name="header" />
</div>
<div class="content">
@@ -140,7 +140,7 @@ $modal-width: 1024px;
transform: none;
}
.header {
.modal-header {
font-size: 2rem;
font-weight: 700;
}
@@ -241,6 +241,14 @@ $modal-width: 1024px;
margin-bottom: 0 !important;
}
}
.modal-content:has(.modal-header) {
display: flex;
flex-direction: column;
justify-content: center;
padding: 0 1rem;
min-height: 100vh
}
</style>
<style lang="scss">
+34 -90
View File
@@ -1,89 +1,49 @@
<template>
<nav
v-if="totalPages > 1"
aria-label="pagination"
class="pagination is-centered p-4"
role="navigation"
<BasePagination
:total-pages="totalPages"
:current-page="currentPage"
>
<RouterLink
:disabled="currentPage === 1 || undefined"
:to="getRouteForPagination(currentPage - 1)"
class="pagination-previous"
>
{{ $t('misc.previous') }}
</RouterLink>
<RouterLink
:disabled="currentPage === totalPages || undefined"
:to="getRouteForPagination(currentPage + 1)"
class="pagination-next"
>
{{ $t('misc.next') }}
</RouterLink>
<ul class="pagination-list">
<li
v-for="(p, i) in pages"
:key="`page-${i}`"
<template #previous="{ disabled }">
<RouterLink
:disabled="disabled || undefined"
:to="getRouteForPagination(currentPage - 1)"
class="pagination-previous"
>
<span
v-if="p.isEllipsis"
class="pagination-ellipsis"
>&hellip;</span>
<RouterLink
v-else
class="pagination-link"
:aria-label="'Goto page ' + p.number"
:class="{ 'is-current': p.number === currentPage }"
:to="getRouteForPagination(p.number)"
>
{{ p.number }}
</RouterLink>
</li>
</ul>
</nav>
{{ $t('misc.previous') }}
</RouterLink>
</template>
<template #next="{ disabled }">
<RouterLink
:disabled="disabled || undefined"
:to="getRouteForPagination(currentPage + 1)"
class="pagination-next"
>
{{ $t('misc.next') }}
</RouterLink>
</template>
<template #page-link="{ page, isCurrent }">
<RouterLink
class="pagination-link"
:aria-label="'Goto page ' + page.number"
:class="{ 'is-current': isCurrent }"
:to="getRouteForPagination(page.number)"
>
{{ page.number }}
</RouterLink>
</template>
</BasePagination>
</template>
<script lang="ts" setup>
import {computed} from 'vue'
import BasePagination from '@/components/base/BasePagination.vue'
const props = withDefaults(defineProps<{
withDefaults(defineProps<{
totalPages: number,
currentPage?: number
}>(), {
currentPage: 0,
})
function createPagination(totalPages: number, currentPage: number) {
const pages = []
for (let i = 0; i < totalPages; i++) {
// Show ellipsis instead of all pages
if (
i > 0 && // Always at least the first page
(i + 1) < totalPages && // And the last page
(
// And the current with current + 1 and current - 1
(i + 1) > currentPage + 1 ||
(i + 1) < currentPage - 1
)
) {
// Only add an ellipsis if the last page isn't already one
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
pages.push({
number: 0,
isEllipsis: true,
})
}
continue
}
pages.push({
number: i + 1,
isEllipsis: false,
})
}
return pages
}
function getRouteForPagination(page = 1, type = null) {
return {
name: type,
@@ -95,20 +55,4 @@ function getRouteForPagination(page = 1, type = null) {
},
}
}
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
</script>
<style lang="scss" scoped>
.pagination {
padding-bottom: 1rem;
}
.pagination-previous,
.pagination-next {
&:not(:disabled):hover {
background: $scheme-main;
cursor: pointer;
}
}
</style>
</script>
@@ -0,0 +1,55 @@
<template>
<BasePagination
:total-pages="totalPages"
:current-page="currentPage"
>
<template #previous="{ disabled }">
<BaseButton
:disabled="disabled"
class="pagination-previous"
@click="changePage(currentPage - 1)"
>
{{ $t('misc.previous') }}
</BaseButton>
</template>
<template #next="{ disabled }">
<BaseButton
:disabled="disabled"
class="pagination-next"
@click="changePage(currentPage + 1)"
>
{{ $t('misc.next') }}
</BaseButton>
</template>
<template #page-link="{ page, isCurrent }">
<BaseButton
class="pagination-link"
:aria-label="'Goto page ' + page.number"
:class="{ 'is-current': isCurrent }"
@click="changePage(page.number)"
>
{{ page.number }}
</BaseButton>
</template>
</BasePagination>
</template>
<script lang="ts" setup>
import BasePagination from '@/components/base/BasePagination.vue'
import BaseButton from '@/components/base/BaseButton.vue'
const props = withDefaults(defineProps<{
totalPages: number,
currentPage: number
}>(), {
currentPage: 1,
})
const emit = defineEmits(['pageChanged'])
function changePage(page: number) {
if (page >= 1 && page <= props.totalPages) {
emit('pageChanged', page)
}
}
</script>
@@ -180,7 +180,7 @@ import ProgressBar from '@/components/misc/ProgressBar.vue'
import BaseButton from '@/components/base/BaseButton.vue'
import AttachmentService from '@/services/attachment'
import {canPreview} from '@/models/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import type {ITask} from '@/modelTypes/ITask'
@@ -274,6 +274,10 @@ async function viewOrDownload(attachment: IAttachment) {
}
}
function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
const copy = useCopyToClipboard()
function copyUrl(attachment: IAttachment) {
@@ -1,7 +1,8 @@
<template>
<div
v-if="enabled"
class="content details"
ref="commentsRef"
class="content details comments-container"
>
<h3
v-if="canWrite || comments.length > 0"
@@ -15,7 +16,7 @@
<div class="comments">
<span
v-if="taskCommentService.loading && saving === null && !creating"
class="is-inline-flex is-align-items-center"
class="is-flex is-align-items-center my-4 ml-2"
>
<span class="loader is-inline-block mr-2" />
{{ $t('task.comment.loading') }}
@@ -107,6 +108,14 @@
/>
</div>
</div>
<PaginationEmit
v-if="taskCommentService.totalPages > 1"
:total-pages="taskCommentService.totalPages"
:current-page="currentPage"
@pageChanged="changePage"
/>
<div
v-if="canWrite"
class="media comment d-print-none"
@@ -160,6 +169,7 @@
</div>
</div>
<Modal
:enabled="showDeleteModal"
@close="showDeleteModal = false"
@@ -185,6 +195,7 @@ import {useI18n} from 'vue-i18n'
import CustomTransition from '@/components/misc/CustomTransition.vue'
import Editor from '@/components/input/AsyncEditor'
import PaginationEmit from '@/components/misc/PaginationEmit.vue'
import TaskCommentService from '@/services/taskComment'
import TaskCommentModel from '@/models/taskComment'
@@ -242,6 +253,12 @@ const actions = computed(() => {
])))
})
const frontendUrl = computed(() => configStore.frontendUrl)
const currentPage = ref(1)
const commentsRef = ref<HTMLElement | null>(null)
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
const uploadPromises: Promise<string>[] = []
@@ -267,12 +284,21 @@ async function loadComments(taskId: ITask['id']) {
newComment.taskId = taskId
commentEdit.taskId = taskId
commentToDelete.taskId = taskId
comments.value = await taskCommentService.getAll({taskId})
comments.value = await taskCommentService.getAll({taskId}, {}, currentPage.value)
}
async function changePage(page: number) {
commentsRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
currentPage.value = page
await loadComments(props.taskId)
}
watch(
() => props.taskId,
loadComments,
() => {
currentPage.value = 1 // Reset to first page when task changes
loadComments(props.taskId)
},
{immediate: true},
)
@@ -404,4 +430,8 @@ async function deleteComment(commentToDelete: ITaskComment) {
.media-content {
width: calc(100% - 48px - 2rem);
}
.comments-container {
scroll-margin-top: 4rem;
}
</style>
@@ -22,7 +22,7 @@
import {ref, shallowReactive, watchEffect} from 'vue'
import AttachmentService from '@/services/attachment'
import type {IAttachment} from '@/modelTypes/IAttachment'
import {canPreview} from '@/models/attachment'
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
const props = defineProps<{
modelValue?: IAttachment
@@ -33,9 +33,13 @@ const blobUrl = ref<string | undefined>(undefined)
watchEffect(async () => {
if (props.modelValue && canPreview(props.modelValue)) {
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue, PREVIEW_SIZE.MD)
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue)
}
})
function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
</script>
<style scoped lang="scss">
@@ -16,6 +16,7 @@
:is-done="task.done"
/>
<BaseButton
v-if="hasClose"
class="close"
@click="$emit('close')"
>
@@ -33,6 +34,7 @@
{{ task.title.trim() }}
</h1>
<BaseButton
v-if="hasClose"
class="close"
@click="$emit('close')"
>
@@ -78,8 +80,10 @@ import {getHexColor, getTaskIdentifier} from '@/models/task'
const props = withDefaults(defineProps<{
task: ITask,
canWrite: boolean | undefined,
hasClose: boolean | undefined,
}>(), {
canWrite: false,
hasClose: false,
})
const emit = defineEmits<{
@@ -165,7 +165,7 @@ async function maybeDownloadCoverImage() {
}
const attachmentService = new AttachmentService()
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment, PREVIEW_SIZE.LG)
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
}
watch(
-4
View File
@@ -7,10 +7,6 @@ import type { IAttachment } from '@/modelTypes/IAttachment'
export const SUPPORTED_IMAGE_SUFFIX = ['.jpg', '.png', '.bmp', '.gif']
export function canPreview(attachment: IAttachment): boolean {
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
}
export default class AttachmentModel extends AbstractModel<IAttachment> implements IAttachment {
id = 0
taskId = 0
+2 -7
View File
@@ -37,13 +37,8 @@ export default class AttachmentService extends AbstractService<IAttachment> {
return data
}
getBlobUrl(model: IAttachment, size?: PREVIEW_SIZE) {
let mainUrl = '/tasks/' + model.taskId + '/attachments/' + model.id
if (size !== undefined) {
mainUrl += `?preview_size=${size}`
}
return AbstractService.prototype.getBlobUrl.call(this, mainUrl)
getBlobUrl(model: IAttachment) {
return AbstractService.prototype.getBlobUrl.call(this, '/tasks/' + model.taskId + '/attachments/' + model.id)
}
async download(model: IAttachment) {
+5 -2
View File
@@ -15,6 +15,7 @@
ref="heading"
:task="task"
:can-write="canWrite"
:has-close="isModal"
@update:task="Object.assign(task, $event)"
@close="$emit('close')"
/>
@@ -572,8 +573,10 @@
</template>
<template #text>
<p>
{{ $t('task.detail.delete.text1') }}<br>
<p class="tw-text-balance !tw-mb-0">
{{ $t('task.detail.delete.text1') }}
</p>
<p class="tw-text-balance">
{{ $t('task.detail.delete.text2') }}
</p>
</template>
+1 -1
View File
@@ -78,7 +78,7 @@ require (
gopkg.in/d4l3k/messagediff.v1 v1.2.1
gopkg.in/yaml.v3 v3.0.1
mvdan.cc/xurls/v2 v2.5.0
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884
src.techknowlogick.com/xgo v1.8.1-0.20241105013731-313dedef864f
src.techknowlogick.com/xormigrate v1.7.1
xorm.io/builder v0.3.13
xorm.io/xorm v1.3.9
+2 -2
View File
@@ -723,8 +723,8 @@ mvdan.cc/xurls/v2 v2.5.0 h1:lyBNOm8Wo71UknhUs4QTFUNNMyxy2JEIaKKo0RWOh+8=
mvdan.cc/xurls/v2 v2.5.0/go.mod h1:yQgaGQ1rFtJUzkmKiHYSSfuQxqfYmd//X6PxvholpeE=
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.20240403232151-e01c4fbef884 h1:Ttvt8FCpUXfC8r3+LgSPrBUIr/JkHmYQtvmOwEET8qE=
src.techknowlogick.com/xgo v1.7.1-0.20240403232151-e01c4fbef884/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xgo v1.8.1-0.20241105013731-313dedef864f h1:Dy7qQ31o3z4EV+0ISDH1IldkPxzubOAvV7DW+9HnbNg=
src.techknowlogick.com/xgo v1.8.1-0.20241105013731-313dedef864f/go.mod h1:31CE1YKtDOrKTk9PSnjTpe6YbO6W/0LTYZ1VskL09oU=
src.techknowlogick.com/xormigrate v1.7.1 h1:RKGLLUAqJ+zO8iZ7eOc7oLH7f0cs2gfXSZSvBRBHnlY=
src.techknowlogick.com/xormigrate v1.7.1/go.mod h1:YGNBdj8prENlySwIKmfoEXp7ILGjAltyKFXD0qLgD7U=
xorm.io/builder v0.3.11-0.20220531020008-1bd24a7dc978/go.mod h1:aUW0S9eb9VCaPohFCH3j7czOx1PMW3i1HrSzbLYGBSE=
+13 -1
View File
@@ -547,7 +547,19 @@ func (Release) Windows() error {
// Builds binaries for linux
func (Release) Linux() error {
return runXgo("linux/*")
targets := []string{
"linux/amd64",
"linux/arm-5",
"linux/arm-6",
"linux/arm-7",
"linux/arm64",
"linux/mips",
"linux/mipsle",
"linux/mips64",
"linux/mips64le",
"linux/riscv64",
}
return runXgo(strings.Join(targets, ","))
}
// Builds binaries for darwin
+6 -1
View File
@@ -22,6 +22,7 @@ import (
"os"
"path/filepath"
"strconv"
"strings"
"time"
"code.vikunja.io/api/pkg/config"
@@ -58,8 +59,12 @@ func (*File) TableName() string {
}
func (f *File) getAbsoluteFilePath() string {
base := ""
if !strings.HasPrefix(config.FilesBasePath.GetString(), "/") {
base = config.ServiceRootpath.GetString()
}
return filepath.Join(
config.ServiceRootpath.GetString(),
base,
config.FilesBasePath.GetString(),
strconv.FormatInt(f.ID, 10),
)
+5 -1
View File
@@ -36,6 +36,7 @@ import (
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/utils"
"code.vikunja.io/api/pkg/version"
"xorm.io/xorm"
)
@@ -106,7 +107,10 @@ func ExportUserData(s *xorm.Session, u *user.User) (err error) {
// Save the file id with the user
u.ExportFileID = exportFile.ID
_, err = s.Cols("export_file_id").Update(u)
_, err = s.
Where("id = ?", u.ID).
Cols("export_file_id").
Update(u)
if err != nil {
return
}
+6 -1
View File
@@ -360,8 +360,13 @@ func RegisterAddTaskToFilterViewCron() {
defer s.Close()
// Get all filters with a date clause and a manual kanban view
where := "filters LIKE '%_date%'"
if db.GetDialect() == builder.POSTGRES {
where = "filters::jsonb ?| array['due_date', 'start_date', 'end_date']"
}
filters := map[int64]*SavedFilter{}
err := s.Where("filters LIKE '%_date%'").Find(&filters)
err := s.Where(where).Find(&filters)
if err != nil {
log.Errorf("%sError fetching filters: %s", logPrefix, err)
return
+11 -113
View File
@@ -17,21 +17,14 @@
package models
import (
"bytes"
"image"
"image/png"
"io"
"strconv"
"time"
"code.vikunja.io/api/pkg/events"
"code.vikunja.io/api/pkg/files"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/modules/keyvalue"
"code.vikunja.io/api/pkg/user"
"code.vikunja.io/api/pkg/web"
"github.com/disintegration/imaging"
"xorm.io/xorm"
)
@@ -139,7 +132,16 @@ func (ta *TaskAttachment) ReadOne(s *xorm.Session, _ web.Auth) (err error) {
// @Failure 404 {object} models.Message "The task does not exist."
// @Failure 500 {object} models.Message "Internal error"
// @Router /tasks/{id}/attachments [get]
func (ta *TaskAttachment) ReadAll(s *xorm.Session, _ web.Auth, _ string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
func (ta *TaskAttachment) ReadAll(s *xorm.Session, a web.Auth, _ string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
task := Task{ID: ta.TaskID}
canRead, _, err := task.CanRead(s, a)
if err != nil {
return nil, 0, 0, err
}
if !canRead {
return nil, 0, 0, ErrGenericForbidden{}
}
attachments := []*TaskAttachment{}
limit, start := getLimitFromPageIndex(page, perPage)
@@ -192,110 +194,6 @@ func (ta *TaskAttachment) ReadAll(s *xorm.Session, _ web.Auth, _ string, page in
return attachments, len(attachments), numberOfTotalItems, err
}
func cacheKeyForTaskAttachmentPreview(id int64, size PreviewSize) string {
return "task_attachment_preview_" + strconv.FormatInt(id, 10) + "_size_" + string(size)
}
func (ta *TaskAttachment) GetPreviewFromCache(previewSize PreviewSize) []byte {
cacheKey := cacheKeyForTaskAttachmentPreview(ta.ID, previewSize)
var cached []byte
exists, err := keyvalue.GetWithValue(cacheKey, &cached)
// If the preview is not cached, return nil
if err != nil || !exists || cached == nil {
return nil
}
return cached
}
type PreviewSize string
const (
PreviewSizeUnknown PreviewSize = "unknown"
PreviewSmall PreviewSize = "sm"
PreviewMedium PreviewSize = "md"
PreviewLarge PreviewSize = "lg"
PreviewExtraLarge PreviewSize = "xl"
)
func (previewSize PreviewSize) GetSize() int {
switch previewSize {
case PreviewSmall:
return 100
case PreviewMedium:
return 200
case PreviewLarge:
return 400
case PreviewExtraLarge:
return 800
case PreviewSizeUnknown:
return 0
default:
return 200
}
}
func GetPreviewSizeFromString(size string) PreviewSize {
switch size {
case "sm":
return PreviewSmall
case "md":
return PreviewMedium
case "lg":
return PreviewLarge
case "xl":
return PreviewExtraLarge
}
return PreviewSizeUnknown
}
func resizeImage(img image.Image, width int) *image.NRGBA {
resizedImg := imaging.Resize(img, width, 0, imaging.Lanczos)
log.Debugf(
"Resized attachment image from %vx%v to %vx%v for a preview",
img.Bounds().Size().X,
img.Bounds().Size().Y,
resizedImg.Bounds().Size().X,
resizedImg.Bounds().Size().Y,
)
return resizedImg
}
func (ta *TaskAttachment) GenerateAndSavePreviewToCache(previewSize PreviewSize) []byte {
img, _, err := image.Decode(ta.File.File)
if err != nil {
return nil
}
// Scale down the image to a minimum size
resizedImg := resizeImage(img, previewSize.GetSize())
// Get the raw bytes of the resized image
buf := &bytes.Buffer{}
if err := png.Encode(buf, resizedImg); err != nil {
return nil
}
previewImage, err := io.ReadAll(buf)
if err != nil {
return nil
}
// Store the preview image in the cache
cacheKey := cacheKeyForTaskAttachmentPreview(ta.ID, previewSize)
err = keyvalue.Put(cacheKey, previewImage)
if err != nil {
return nil
}
log.Infof("Attachment image preview for task attachment %v of size %v created and cached", ta.ID, previewSize)
return previewImage
}
// Delete removes an attachment
// @Summary Delete an attachment
// @Description Delete an attachment.
+8
View File
@@ -233,6 +233,14 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
return nil, 0, 0, err
}
canRead, _, err := sf.CanRead(s, a)
if err != nil {
return nil, 0, 0, err
}
if !canRead {
return nil, 0, 0, ErrGenericForbidden{}
}
// By prepending sort options before the saved ones from the filter, we make sure the supplied sort
// options via query take precedence over the rest.
+1 -24
View File
@@ -17,14 +17,11 @@
package v1
import (
"net/http"
"strings"
"code.vikunja.io/api/pkg/db"
"code.vikunja.io/api/pkg/log"
"code.vikunja.io/api/pkg/models"
auth2 "code.vikunja.io/api/pkg/modules/auth"
"code.vikunja.io/api/pkg/web/handler"
"net/http"
"github.com/labstack/echo/v4"
)
@@ -117,7 +114,6 @@ func UploadTaskAttachment(c echo.Context) error {
// @Produce octet-stream
// @Param id path int true "Task ID"
// @Param attachmentID path int true "Attachment ID"
// @Param preview_size query string false "The size of the preview image. Can be sm = 100px, md = 200px, lg = 400px or xl = 800px. If provided, a preview image will be returned if the attachment is an image."
// @Security JWTKeyAuth
// @Success 200 {file} blob "The attachment file."
// @Failure 403 {object} models.Message "No access to this task."
@@ -156,17 +152,6 @@ func GetTaskAttachment(c echo.Context) error {
return handler.HandleHTTPError(err)
}
// If the preview query parameter is set and the preview was already generated and cached, return the cached preview image
previewSize := models.GetPreviewSizeFromString(c.QueryParam("preview_size"))
if previewSize != models.PreviewSizeUnknown && strings.HasPrefix(taskAttachment.File.Mime, "image") {
previewFileBytes := taskAttachment.GetPreviewFromCache(previewSize)
if previewFileBytes != nil {
log.Debugf("Cached attachment image preview found for task attachment %v", taskAttachment.ID)
return c.Blob(http.StatusOK, "image/png", previewFileBytes)
}
}
// Open and send the file to the client
err = taskAttachment.File.LoadFileByID()
if err != nil {
@@ -179,14 +164,6 @@ func GetTaskAttachment(c echo.Context) error {
return handler.HandleHTTPError(err)
}
// If a preview is requested and the preview was not cached, we create the preview and cache it
if previewSize != models.PreviewSizeUnknown {
previewFileBytes := taskAttachment.GenerateAndSavePreviewToCache(previewSize)
if previewFileBytes != nil {
return c.Blob(http.StatusOK, "image/png", previewFileBytes)
}
}
http.ServeContent(c.Response(), c.Request(), taskAttachment.File.Name, taskAttachment.File.Created, taskAttachment.File.File)
return nil
}
+26 -4
View File
@@ -52,7 +52,7 @@ type VikunjaCaldavProjectStorage struct {
}
// GetResources returns either all projects, links to the principal, or only one project, depending on the request
func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, _ bool) ([]data.Resource, error) {
func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, withChildren bool) ([]data.Resource, error) {
// It looks like we need to have the same handler for returning both the calendar home set and the user principal
// Since the client seems to ignore the whatever is being returned in the first request and just makes a second one
@@ -92,6 +92,24 @@ func (vcls *VikunjaCaldavProjectStorage) GetResources(rpath string, _ bool) ([]d
}
r := data.NewResource(rpath, &rr)
r.Name = vcls.project.Title
// If the request is withChildren (Depth: 1), we need to return all tasks of the project
if withChildren {
resources := []data.Resource{r}
// Check if there are tasks to iterate over
if vcls.project.Tasks != nil {
for i := range vcls.project.Tasks {
taskResource := VikunjaProjectResourceAdapter{
project: vcls.project,
task: &vcls.project.Tasks[i].Task,
isCollection: false,
}
addTaskResource(&vcls.project.Tasks[i].Task, &taskResource, &resources)
}
}
return resources, nil
}
return []data.Resource{r}, nil
}
@@ -163,9 +181,7 @@ func (vcls *VikunjaCaldavProjectStorage) GetResourcesByList(rpaths []string) (re
rr := VikunjaProjectResourceAdapter{
task: t,
}
r := data.NewResource(getTaskURL(t), &rr)
r.Name = t.Title
resources = append(resources, r)
addTaskResource(t, &rr, &resources)
}
return
@@ -671,3 +687,9 @@ func (vcls *VikunjaCaldavProjectStorage) getProjectRessource(isCollection bool)
return
}
func addTaskResource(task *models.Task, rr *VikunjaProjectResourceAdapter, resources *[]data.Resource) {
taskResourceInstance := data.NewResource(getTaskURL(task), rr)
taskResourceInstance.Name = task.Title
*resources = append(*resources, taskResourceInstance)
}