Compare commits
187
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da695de78e | ||
|
|
2783df1918 | ||
|
|
f520881538 | ||
|
|
bfb7cf9403 | ||
|
|
9c5f35a0d8 | ||
|
|
2185d36d36 | ||
|
|
4afc77488e | ||
|
|
cbee46b403 | ||
|
|
9eb5c62b01 | ||
|
|
1387f1c497 | ||
|
|
f76970b5a3 | ||
|
|
98be0d6f67 | ||
|
|
d046599192 | ||
|
|
e8ca2a9ab8 | ||
|
|
bdc3dae4f3 | ||
|
|
2412d29a40 | ||
|
|
703a88e99f | ||
|
|
336b3448cf | ||
|
|
4359819faf | ||
|
|
3f97414497 | ||
|
|
47ff7d8ad3 | ||
|
|
dc379b7a7e | ||
|
|
6a2a8c106b | ||
|
|
28a58303f0 | ||
|
|
01c84be945 | ||
|
|
b579041185 | ||
|
|
e3260772ca | ||
|
|
a65955b409 | ||
|
|
9a9abe53da | ||
|
|
b3040b8466 | ||
|
|
b3c93ae811 | ||
|
|
a1cf2b8bc7 | ||
|
|
6f9b0ddfe7 | ||
|
|
c9a68d3a63 | ||
|
|
7ac2c42e4d | ||
|
|
5abca0927b | ||
|
|
4512045cbf | ||
|
|
e56a01f42d | ||
|
|
cff602c246 | ||
|
|
13d52c721d | ||
|
|
98c10acb50 | ||
|
|
9b5ee28a69 | ||
|
|
8f5be72104 | ||
|
|
07df606c68 | ||
|
|
e8a07fc8e0 | ||
|
|
a800cbed5f | ||
|
|
2ac11b13a1 | ||
|
|
9aca4ca0f6 | ||
|
|
2c9625b26b | ||
|
|
25fd0f6108 | ||
|
|
942c2e4af6 | ||
|
|
5c93d0c57c | ||
|
|
ed5474a28a | ||
|
|
57e43a9d51 | ||
|
|
3de532ed50 | ||
|
|
84309701d3 | ||
|
|
e02ebbcba1 | ||
|
|
1f55e3f866 | ||
|
|
1563c462a2 | ||
|
|
63732a37c0 | ||
|
|
80f8570e22 | ||
|
|
a325b27051 | ||
|
|
f0ebc3542e | ||
|
|
69b619144d | ||
|
|
01f126bb43 | ||
|
|
a38fafefaa | ||
|
|
8691919d68 | ||
|
|
33f82a6d1f | ||
|
|
075ea98ccd | ||
|
|
4d0ab0ef7f | ||
|
|
5daa1c6808 | ||
|
|
522db23183 | ||
|
|
c52b7efc97 | ||
|
|
f77f03efbe | ||
|
|
f8fe3b7e87 | ||
|
|
90951d4003 | ||
|
|
03eff35245 | ||
|
|
727b69266a | ||
|
|
de1ad619cf | ||
|
|
d8ac7003c6 | ||
|
|
575df84476 | ||
|
|
de6079cc86 | ||
|
|
3dd42555b5 | ||
|
|
8d8406df05 | ||
|
|
e57f04ec23 | ||
|
|
91d603dc9e | ||
|
|
d9215b1a1b | ||
|
|
069cc5144f | ||
|
|
388abe97ce | ||
|
|
e8576577a3 | ||
|
|
f1311b74c6 | ||
|
|
5cbb06c226 | ||
|
|
affa0c465c | ||
|
|
92571d9998 | ||
|
|
a136161478 | ||
|
|
bab96a8686 | ||
|
|
43ad449ebf | ||
|
|
72d3a9ebf8 | ||
|
|
da9f36d568 | ||
|
|
2f78e0e22d | ||
|
|
8dfb34c863 | ||
|
|
ed071e504e | ||
|
|
d9c1f3d51a | ||
|
|
174522e63d | ||
|
|
e8426c0390 | ||
|
|
89f3caaa52 | ||
|
|
28b1bb824a | ||
|
|
eecb96a683 | ||
|
|
ee04423c4c | ||
|
|
5edccba028 | ||
|
|
8732837596 | ||
|
|
85823b3e97 | ||
|
|
3c13d3b635 | ||
|
|
3659b7b58d | ||
|
|
bbbd936868 | ||
|
|
80219c34b9 | ||
|
|
0124d10391 | ||
|
|
675985b26c | ||
|
|
a5cc5e3783 | ||
|
|
e124ad4df9 | ||
|
|
26f02d2e6a | ||
|
|
42e01637ef | ||
|
|
0af1accbb4 | ||
|
|
6bba2b2dc3 | ||
|
|
7a10a49c5c | ||
|
|
f2eac4623d | ||
|
|
a58c5de657 | ||
|
|
0b9fd2c485 | ||
|
|
940b450a56 | ||
|
|
d616bab76d | ||
|
|
92c58ab32f | ||
|
|
624907ad6a | ||
|
|
f9a6b4f1bb | ||
|
|
394c9432a8 | ||
|
|
3bafaa7101 | ||
|
|
b8c4e0a91e | ||
|
|
feb6b708e3 | ||
|
|
bd3eab8c34 | ||
|
|
1db1333d8a | ||
|
|
05349ddb5c | ||
|
|
3479fa1228 | ||
|
|
fef19c5c1d | ||
|
|
8ccb2cceea | ||
|
|
194a3239af | ||
|
|
8a91e30e12 | ||
|
|
0df69f6998 | ||
|
|
3f1cf817d6 | ||
|
|
d727e1ffb5 | ||
|
|
99499ad470 | ||
|
|
8c2728f62f | ||
|
|
6357d5eb02 | ||
|
|
436fcc2532 | ||
|
|
25173a57e5 | ||
|
|
1705d9de8d | ||
|
|
a22033acd3 | ||
|
|
44c659aa34 | ||
|
|
da0e110ee4 | ||
|
|
4d79bab8f1 | ||
|
|
88eef87a8c | ||
|
|
46c09d3e56 | ||
|
|
525367f2f4 | ||
|
|
bf5672bf36 | ||
|
|
9b85f3bd0c | ||
|
|
bd64778a6a | ||
|
|
bf6875533b | ||
|
|
5550b69144 | ||
|
|
5181ba587c | ||
|
|
742774dc6c | ||
|
|
6fe22aba39 | ||
|
|
cc82f3043f | ||
|
|
c3d2a92835 | ||
|
|
513f773041 | ||
|
|
8f69ce0e6b | ||
|
|
d6194b8f10 | ||
|
|
f826fb9a91 | ||
|
|
6ba55d4de0 | ||
|
|
22812199dd | ||
|
|
c7914bc245 | ||
|
|
4556cfb057 | ||
|
|
9e987a89d4 | ||
|
|
3bfde5a34d | ||
|
|
64ffba2813 | ||
|
|
2693419959 | ||
|
|
24fc7be8c4 | ||
|
|
452cc66b32 | ||
|
|
cf6836f857 | ||
|
|
de1eac5d36 |
+8
-10
@@ -626,7 +626,7 @@ steps:
|
||||
depends_on: [ generate-config-yaml ]
|
||||
|
||||
- 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
|
||||
@@ -635,7 +635,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.
|
||||
@@ -651,7 +651,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.
|
||||
@@ -667,7 +667,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.
|
||||
@@ -693,7 +693,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
|
||||
@@ -884,7 +884,6 @@ steps:
|
||||
repo: vikunja/vikunja
|
||||
tags: unstable
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
@@ -916,7 +915,6 @@ steps:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/vikunja
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
@@ -1150,7 +1148,7 @@ steps:
|
||||
# - '.cache'
|
||||
|
||||
- name: build
|
||||
image: electronuserland/builder:wine-mono
|
||||
image: electronuserland/builder:wine
|
||||
pull: true
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -1232,7 +1230,7 @@ steps:
|
||||
# - '.cache'
|
||||
|
||||
- name: build
|
||||
image: electronuserland/builder:wine-mono
|
||||
image: electronuserland/builder:wine
|
||||
pull: true
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -1400,6 +1398,6 @@ steps:
|
||||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: beadda6de8c397224c60fcac921a2883983383ac564c3a6c7e6d4bf5901112bd
|
||||
hmac: 7fc1cb2b0cfdc84d7eda25ce75dd56446fc01060c03af89c91043f095e92a2b3
|
||||
|
||||
...
|
||||
|
||||
@@ -7,6 +7,24 @@ 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.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
|
||||
|
||||
+1
-1
@@ -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
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://drone.kolaente.de/vikunja/vikunja)
|
||||
[](LICENSE)
|
||||
[](https://vikunja.io/docs/installing)
|
||||
[](https://vikunja.io/docs/installing)
|
||||
[](https://hub.docker.com/r/vikunja/vikunja/)
|
||||
[](https://try.vikunja.io/api/v1/docs)
|
||||
[](https://goreportcard.com/report/kolaente.dev/vikunja/vikunja)
|
||||
|
||||
+1
-1
@@ -17,6 +17,7 @@ body = """
|
||||
{% raw %} {% endraw %}- **BREAKING**: {{commit.breaking_description}}
|
||||
{%- endif -%}
|
||||
{%- endfor -%}
|
||||
{% raw %}\n{% endraw %}\
|
||||
{%- for commit in commits %}
|
||||
{%- if commit.scope -%}
|
||||
{% else -%}
|
||||
@@ -56,4 +57,3 @@ commit_parsers = [
|
||||
{ body = ".*security", group = "Security"},
|
||||
{ message = ".*", group = "Other", default_scope = "other"}, # Everything that's not a conventional commit goes into the "Other" category
|
||||
]
|
||||
|
||||
|
||||
+3
-2
@@ -251,7 +251,7 @@
|
||||
{
|
||||
"key": "url",
|
||||
"default_value": "",
|
||||
"comment": "The url to the Typesense instance you want to use. Can be hosted locally or in Typesense Cloud as long\nas Vikunja is able to reach it."
|
||||
"comment": "The url to the Typesense instance you want to use. Can be hosted locally or in Typesense Cloud as long as Vikunja is able to reach it. Must be a http(s) url."
|
||||
},
|
||||
{
|
||||
"key": "apikey",
|
||||
@@ -665,9 +665,10 @@
|
||||
},
|
||||
{
|
||||
"key": "providers",
|
||||
"comment": "A list of enabled providers. This expects an array, which means you cannot use enviroment variables.",
|
||||
"comment": "A list of enabled providers. You can freely choose the `<provider key>`. Note that you must add at least one key to a config file if you want to read values from an environment variable as the provider won't be known to Vikunja otherwise.",
|
||||
"children": [
|
||||
{
|
||||
"key": "<provider key>",
|
||||
"children": [
|
||||
{
|
||||
"key": "name",
|
||||
|
||||
@@ -51,11 +51,11 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "33.0.2",
|
||||
"electron": "33.2.1",
|
||||
"electron-builder": "25.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"express": "4.21.1"
|
||||
"express": "4.21.2"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+188
-72
@@ -12,12 +12,12 @@ importers:
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
express:
|
||||
specifier: 4.21.1
|
||||
version: 4.21.1
|
||||
specifier: 4.21.2
|
||||
version: 4.21.2
|
||||
devDependencies:
|
||||
electron:
|
||||
specifier: 33.0.2
|
||||
version: 33.0.2
|
||||
specifier: 33.2.1
|
||||
version: 33.2.1
|
||||
electron-builder:
|
||||
specifier: 25.1.8
|
||||
version: 25.1.8(electron-builder-squirrel-windows@24.13.3)
|
||||
@@ -31,6 +31,11 @@ packages:
|
||||
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
|
||||
engines: {node: '>= 8.9.0'}
|
||||
|
||||
'@electron/asar@3.2.17':
|
||||
resolution: {integrity: sha512-OcWImUI686w8LkghQj9R2ynZ2ME693Ek6L1SiaAgqGKzBaTIZw3fHDqN82Rcl+EU1Gm9EgkJ5KLIY/q5DCRbbA==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
hasBin: true
|
||||
|
||||
'@electron/asar@3.2.9':
|
||||
resolution: {integrity: sha512-Vu2P3X2gcZ3MY9W7yH72X9+AMXwUQZEJBrsPIbX0JsdllLtoh62/Q8Wg370/DawIEVKOyfD6KtTLo645ezqxUA==}
|
||||
engines: {node: '>=10.12.0'}
|
||||
@@ -260,6 +265,9 @@ packages:
|
||||
async@3.2.5:
|
||||
resolution: {integrity: sha512-baNZyqaaLhyLVKm/DlvdW051MSgO6b8eVfIezl9E5PqWxFgzLm/wQntEW4zOytVburDEr0JlALEpdOFwvErLsg==}
|
||||
|
||||
async@3.2.6:
|
||||
resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==}
|
||||
|
||||
asynckit@0.4.0:
|
||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||
|
||||
@@ -446,6 +454,9 @@ packages:
|
||||
core-util-is@1.0.2:
|
||||
resolution: {integrity: sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
crc-32@1.2.2:
|
||||
resolution: {integrity: sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==}
|
||||
engines: {node: '>=0.8'}
|
||||
@@ -458,8 +469,8 @@ packages:
|
||||
crc@3.8.0:
|
||||
resolution: {integrity: sha512-iX3mfgcTMIq3ZKLIsVFAbv7+Mc10kxabAGQb8HvjA1o3T1PIYprbakQ65d3I+2HGHt6nSKkM9PYjgoJO2KcFBQ==}
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==}
|
||||
cross-spawn@7.0.6:
|
||||
resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
debug@2.6.9:
|
||||
@@ -470,8 +481,8 @@ packages:
|
||||
supports-color:
|
||||
optional: true
|
||||
|
||||
debug@4.3.4:
|
||||
resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==}
|
||||
debug@4.3.7:
|
||||
resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==}
|
||||
engines: {node: '>=6.0'}
|
||||
peerDependencies:
|
||||
supports-color: '*'
|
||||
@@ -575,8 +586,8 @@ packages:
|
||||
electron-publish@25.1.7:
|
||||
resolution: {integrity: sha512-+jbTkR9m39eDBMP4gfbqglDd6UvBC7RLh5Y0MhFSsc6UkGHj9Vj9TWobxevHYMMqmoujL11ZLjfPpMX+Pt6YEg==}
|
||||
|
||||
electron@33.0.2:
|
||||
resolution: {integrity: sha512-C2WksfP0COsMHbYXSJG68j6S3TjuGDrw/YT42B526yXalIlNQZ2GeAYKryg6AEMkIp3p8TUfDRD0+HyiyCt/nw==}
|
||||
electron@33.2.1:
|
||||
resolution: {integrity: sha512-SG/nmSsK9Qg1p6wAW+ZfqU+AV8cmXMTIklUL18NnOKfZLlum4ZsDoVdmmmlL39ZmeCaq27dr7CgslRPahfoVJg==}
|
||||
engines: {node: '>= 12.20.55'}
|
||||
hasBin: true
|
||||
|
||||
@@ -636,8 +647,8 @@ packages:
|
||||
exponential-backoff@3.1.1:
|
||||
resolution: {integrity: sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==}
|
||||
|
||||
express@4.21.1:
|
||||
resolution: {integrity: sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==}
|
||||
express@4.21.2:
|
||||
resolution: {integrity: sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
extract-zip@2.0.1:
|
||||
@@ -669,10 +680,18 @@ packages:
|
||||
resolution: {integrity: sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
foreground-child@3.3.0:
|
||||
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
form-data@4.0.1:
|
||||
resolution: {integrity: sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
@@ -732,6 +751,10 @@ packages:
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
hasBin: true
|
||||
|
||||
glob@10.4.5:
|
||||
resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==}
|
||||
hasBin: true
|
||||
|
||||
glob@7.2.3:
|
||||
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==}
|
||||
deprecated: Glob versions prior to v9 are no longer supported
|
||||
@@ -887,6 +910,10 @@ packages:
|
||||
resolution: {integrity: sha512-GvcjojwonMjWbTkfMpnVHVqXW/wKMYDfEpY94/8zy8HFMOqb/VL6oeONq9v87q4ttVlaTLnGXnJD4B5B1OTGIg==}
|
||||
engines: {node: '>= 18.0.0'}
|
||||
|
||||
isbinaryfile@5.0.4:
|
||||
resolution: {integrity: sha512-YKBKVkKhty7s8rxddb40oOkuP0NbaeXrQvLin6QMHL7Ypiy2RW9LwOVrVgZRyOrhQlayMd9t+D8yDy8MKFTSDQ==}
|
||||
engines: {node: '>= 18.0.0'}
|
||||
|
||||
isexe@2.0.0:
|
||||
resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
|
||||
|
||||
@@ -894,6 +921,9 @@ packages:
|
||||
resolution: {integrity: sha512-N3yCS/NegsOBokc8GAdM8UcmfsKiSS8cipheD/nivzr700H+nsMOxJjQnvwOcRYVuFkdH0wGUvW2WbXGmrZGbQ==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
jackspeak@3.4.3:
|
||||
resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==}
|
||||
|
||||
jake@10.8.7:
|
||||
resolution: {integrity: sha512-ZDi3aP+fG/LchyBzUM804VjddnwfSfsdeYkwt8NcbKRvo4rFkjhs456iLFn3k2ZUWvNe4i48WACDbza8fhq2+w==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -966,6 +996,9 @@ packages:
|
||||
resolution: {integrity: sha512-2bIM8x+VAf6JT4bKAljS1qUWgMsqZRPGJS6FSahIMPVvctcNhyVp7AJu7quxOW9jwkryBReKZY5tY5JYv2n/7Q==}
|
||||
engines: {node: 14 || >=16.14}
|
||||
|
||||
lru-cache@10.4.3:
|
||||
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
|
||||
|
||||
lru-cache@6.0.0:
|
||||
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1038,6 +1071,10 @@ packages:
|
||||
resolution: {integrity: sha512-KqWh+VchfxcMNRAJjj2tnsSJdNbHsVgnkBhTNrW7AjVo6OvLtxw8zfT9oLw1JSohlFzJ8jCoTgaoXvJ+kHt6fw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimatch@9.0.5:
|
||||
resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minimist@1.2.8:
|
||||
resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==}
|
||||
|
||||
@@ -1073,6 +1110,10 @@ packages:
|
||||
resolution: {integrity: sha512-jYofLM5Dam9279rdkWzqHozUo4ybjdZmCsDHePy5V/PbBcVMiSZR97gmAy45aqi8CK1lG2ECd356FU86avfwUQ==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minipass@7.1.2:
|
||||
resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
minizlib@2.1.2:
|
||||
resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==}
|
||||
engines: {node: '>= 8'}
|
||||
@@ -1085,9 +1126,6 @@ packages:
|
||||
ms@2.0.0:
|
||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||
|
||||
ms@2.1.2:
|
||||
resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==}
|
||||
|
||||
ms@2.1.3:
|
||||
resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==}
|
||||
|
||||
@@ -1162,6 +1200,9 @@ packages:
|
||||
resolution: {integrity: sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
@@ -1178,8 +1219,12 @@ packages:
|
||||
resolution: {integrity: sha512-7xTavNy5RQXnsjANvVvMkEjvloOinkAjv/Z6Ildz9v2RinZ4SBKTWFOVRbaF8p0vpHnyjV/UwNDdKuUv6M5qcA==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
||||
path-to-regexp@0.1.10:
|
||||
resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==}
|
||||
path-scurry@1.11.1:
|
||||
resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==}
|
||||
engines: {node: '>=16 || 14 >=14.18'}
|
||||
|
||||
path-to-regexp@0.1.12:
|
||||
resolution: {integrity: sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==}
|
||||
|
||||
pe-library@0.4.1:
|
||||
resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==}
|
||||
@@ -1302,6 +1347,9 @@ packages:
|
||||
sax@1.3.0:
|
||||
resolution: {integrity: sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==}
|
||||
|
||||
sax@1.4.1:
|
||||
resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==}
|
||||
|
||||
semver-compare@1.0.0:
|
||||
resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==}
|
||||
|
||||
@@ -1314,6 +1362,11 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
semver@7.6.3:
|
||||
resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
send@0.19.0:
|
||||
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
@@ -1469,6 +1522,11 @@ packages:
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
typescript@5.6.3:
|
||||
resolution: {integrity: sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==}
|
||||
engines: {node: '>=14.17'}
|
||||
hasBin: true
|
||||
|
||||
undici-types@5.26.5:
|
||||
resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==}
|
||||
|
||||
@@ -1574,6 +1632,12 @@ snapshots:
|
||||
ajv: 6.12.6
|
||||
ajv-keywords: 3.5.2(ajv@6.12.6)
|
||||
|
||||
'@electron/asar@3.2.17':
|
||||
dependencies:
|
||||
commander: 5.1.0
|
||||
glob: 7.2.3
|
||||
minimatch: 3.1.2
|
||||
|
||||
'@electron/asar@3.2.9':
|
||||
dependencies:
|
||||
commander: 5.1.0
|
||||
@@ -1582,7 +1646,7 @@ snapshots:
|
||||
|
||||
'@electron/get@2.0.3':
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
env-paths: 2.2.1
|
||||
fs-extra: 8.1.0
|
||||
got: 11.8.6
|
||||
@@ -1596,7 +1660,7 @@ snapshots:
|
||||
|
||||
'@electron/notarize@2.2.1':
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
fs-extra: 9.1.0
|
||||
promise-retry: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
@@ -1604,7 +1668,7 @@ snapshots:
|
||||
|
||||
'@electron/notarize@2.5.0':
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
fs-extra: 9.1.0
|
||||
promise-retry: 2.0.1
|
||||
transitivePeerDependencies:
|
||||
@@ -1613,7 +1677,7 @@ snapshots:
|
||||
'@electron/osx-sign@1.0.5':
|
||||
dependencies:
|
||||
compare-version: 0.1.2
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
fs-extra: 10.1.0
|
||||
isbinaryfile: 4.0.10
|
||||
minimist: 1.2.8
|
||||
@@ -1624,7 +1688,7 @@ snapshots:
|
||||
'@electron/osx-sign@1.3.1':
|
||||
dependencies:
|
||||
compare-version: 0.1.2
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
fs-extra: 10.1.0
|
||||
isbinaryfile: 4.0.10
|
||||
minimist: 1.2.8
|
||||
@@ -1636,7 +1700,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@malept/cross-spawn-promise': 2.0.0
|
||||
chalk: 4.1.2
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
detect-libc: 2.0.3
|
||||
fs-extra: 10.1.0
|
||||
got: 11.8.6
|
||||
@@ -1645,7 +1709,7 @@ snapshots:
|
||||
node-gyp: 9.4.1
|
||||
ora: 5.4.1
|
||||
read-binary-file-arch: 1.0.6
|
||||
semver: 7.6.0
|
||||
semver: 7.6.3
|
||||
tar: 6.2.1
|
||||
yargs: 17.7.2
|
||||
transitivePeerDependencies:
|
||||
@@ -1654,9 +1718,9 @@ snapshots:
|
||||
|
||||
'@electron/universal@1.5.1':
|
||||
dependencies:
|
||||
'@electron/asar': 3.2.9
|
||||
'@electron/asar': 3.2.17
|
||||
'@malept/cross-spawn-promise': 1.1.1
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
dir-compare: 3.3.0
|
||||
fs-extra: 9.1.0
|
||||
minimatch: 3.1.2
|
||||
@@ -1668,7 +1732,7 @@ snapshots:
|
||||
dependencies:
|
||||
'@electron/asar': 3.2.9
|
||||
'@malept/cross-spawn-promise': 2.0.0
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
dir-compare: 4.2.0
|
||||
fs-extra: 11.2.0
|
||||
minimatch: 9.0.4
|
||||
@@ -1689,15 +1753,15 @@ snapshots:
|
||||
|
||||
'@malept/cross-spawn-promise@1.1.1':
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
'@malept/cross-spawn-promise@2.0.0':
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
cross-spawn: 7.0.6
|
||||
|
||||
'@malept/flatpak-bundler@0.4.0':
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
fs-extra: 9.1.0
|
||||
lodash: 4.17.21
|
||||
tmp-promise: 3.0.3
|
||||
@@ -1707,7 +1771,7 @@ snapshots:
|
||||
'@npmcli/fs@2.1.2':
|
||||
dependencies:
|
||||
'@gar/promisify': 1.1.3
|
||||
semver: 7.6.0
|
||||
semver: 7.6.3
|
||||
|
||||
'@npmcli/move-file@2.0.1':
|
||||
dependencies:
|
||||
@@ -1781,13 +1845,13 @@ snapshots:
|
||||
|
||||
agent-base@6.0.2:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
agent-base@7.1.1:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -1838,22 +1902,22 @@ snapshots:
|
||||
builder-util: 24.13.1
|
||||
builder-util-runtime: 9.2.4
|
||||
chromium-pickle-js: 0.2.0
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
dmg-builder: 25.1.8(electron-builder-squirrel-windows@24.13.3)
|
||||
ejs: 3.1.10
|
||||
electron-builder-squirrel-windows: 24.13.3(dmg-builder@25.1.8)
|
||||
electron-publish: 24.13.1
|
||||
form-data: 4.0.0
|
||||
form-data: 4.0.1
|
||||
fs-extra: 10.1.0
|
||||
hosted-git-info: 4.1.0
|
||||
is-ci: 3.0.1
|
||||
isbinaryfile: 5.0.2
|
||||
isbinaryfile: 5.0.4
|
||||
js-yaml: 4.1.0
|
||||
lazy-val: 1.0.5
|
||||
minimatch: 5.1.6
|
||||
read-config-file: 6.3.2
|
||||
sanitize-filename: 1.6.3
|
||||
semver: 7.6.0
|
||||
semver: 7.6.3
|
||||
tar: 6.2.1
|
||||
temp-file: 3.4.0
|
||||
transitivePeerDependencies:
|
||||
@@ -1874,7 +1938,7 @@ snapshots:
|
||||
builder-util-runtime: 9.2.10
|
||||
chromium-pickle-js: 0.2.0
|
||||
config-file-ts: 0.2.8-rc1
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
dmg-builder: 25.1.8(electron-builder-squirrel-windows@24.13.3)
|
||||
dotenv: 16.4.5
|
||||
dotenv-expand: 11.0.6
|
||||
@@ -1930,7 +1994,7 @@ snapshots:
|
||||
archiver@5.3.2:
|
||||
dependencies:
|
||||
archiver-utils: 2.1.0
|
||||
async: 3.2.5
|
||||
async: 3.2.6
|
||||
buffer-crc32: 0.2.13
|
||||
readable-stream: 3.6.2
|
||||
readdir-glob: 1.1.3
|
||||
@@ -1956,6 +2020,8 @@ snapshots:
|
||||
|
||||
async@3.2.5: {}
|
||||
|
||||
async@3.2.6: {}
|
||||
|
||||
asynckit@0.4.0: {}
|
||||
|
||||
at-least-node@1.0.0: {}
|
||||
@@ -2018,15 +2084,15 @@ snapshots:
|
||||
|
||||
builder-util-runtime@9.2.10:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
sax: 1.3.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
builder-util-runtime@9.2.4:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
sax: 1.3.0
|
||||
debug: 4.3.7
|
||||
sax: 1.4.1
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2038,8 +2104,8 @@ snapshots:
|
||||
bluebird-lst: 1.0.9
|
||||
builder-util-runtime: 9.2.4
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.3
|
||||
debug: 4.3.4
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.3.7
|
||||
fs-extra: 10.1.0
|
||||
http-proxy-agent: 5.0.0
|
||||
https-proxy-agent: 5.0.1
|
||||
@@ -2059,8 +2125,8 @@ snapshots:
|
||||
bluebird-lst: 1.0.9
|
||||
builder-util-runtime: 9.2.10
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.3
|
||||
debug: 4.3.4
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.3.7
|
||||
fs-extra: 10.1.0
|
||||
http-proxy-agent: 7.0.2
|
||||
https-proxy-agent: 7.0.5
|
||||
@@ -2181,8 +2247,8 @@ snapshots:
|
||||
|
||||
config-file-ts@0.2.6:
|
||||
dependencies:
|
||||
glob: 10.3.12
|
||||
typescript: 5.4.5
|
||||
glob: 10.4.5
|
||||
typescript: 5.6.3
|
||||
|
||||
config-file-ts@0.2.8-rc1:
|
||||
dependencies:
|
||||
@@ -2203,7 +2269,10 @@ snapshots:
|
||||
|
||||
cookie@0.7.1: {}
|
||||
|
||||
core-util-is@1.0.2: {}
|
||||
core-util-is@1.0.2:
|
||||
optional: true
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
crc-32@1.2.2: {}
|
||||
|
||||
@@ -2217,7 +2286,7 @@ snapshots:
|
||||
buffer: 5.7.1
|
||||
optional: true
|
||||
|
||||
cross-spawn@7.0.3:
|
||||
cross-spawn@7.0.6:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
shebang-command: 2.0.0
|
||||
@@ -2227,9 +2296,9 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.0.0
|
||||
|
||||
debug@4.3.4:
|
||||
debug@4.3.7:
|
||||
dependencies:
|
||||
ms: 2.1.2
|
||||
ms: 2.1.3
|
||||
|
||||
decompress-response@6.0.0:
|
||||
dependencies:
|
||||
@@ -2373,7 +2442,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron@33.0.2:
|
||||
electron@33.2.1:
|
||||
dependencies:
|
||||
'@electron/get': 2.0.3
|
||||
'@types/node': 20.12.7
|
||||
@@ -2422,7 +2491,7 @@ snapshots:
|
||||
|
||||
exponential-backoff@3.1.1: {}
|
||||
|
||||
express@4.21.1:
|
||||
express@4.21.2:
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
array-flatten: 1.1.1
|
||||
@@ -2443,7 +2512,7 @@ snapshots:
|
||||
methods: 1.1.2
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 0.1.10
|
||||
path-to-regexp: 0.1.12
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.13.0
|
||||
range-parser: 1.2.1
|
||||
@@ -2460,7 +2529,7 @@ snapshots:
|
||||
|
||||
extract-zip@2.0.1:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
get-stream: 5.2.0
|
||||
yauzl: 2.10.0
|
||||
optionalDependencies:
|
||||
@@ -2497,7 +2566,12 @@ snapshots:
|
||||
|
||||
foreground-child@3.1.1:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.3
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
foreground-child@3.3.0:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
signal-exit: 4.1.0
|
||||
|
||||
form-data@4.0.0:
|
||||
@@ -2506,6 +2580,12 @@ snapshots:
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
form-data@4.0.1:
|
||||
dependencies:
|
||||
asynckit: 0.4.0
|
||||
combined-stream: 1.0.8
|
||||
mime-types: 2.1.35
|
||||
|
||||
forwarded@0.2.0: {}
|
||||
|
||||
fresh@0.5.2: {}
|
||||
@@ -2578,6 +2658,15 @@ snapshots:
|
||||
minipass: 7.0.4
|
||||
path-scurry: 1.10.2
|
||||
|
||||
glob@10.4.5:
|
||||
dependencies:
|
||||
foreground-child: 3.3.0
|
||||
jackspeak: 3.4.3
|
||||
minimatch: 9.0.5
|
||||
minipass: 7.1.2
|
||||
package-json-from-dist: 1.0.1
|
||||
path-scurry: 1.11.1
|
||||
|
||||
glob@7.2.3:
|
||||
dependencies:
|
||||
fs.realpath: 1.0.0
|
||||
@@ -2601,7 +2690,7 @@ snapshots:
|
||||
es6-error: 4.1.1
|
||||
matcher: 3.0.0
|
||||
roarr: 2.15.4
|
||||
semver: 7.6.0
|
||||
semver: 7.6.3
|
||||
serialize-error: 7.0.1
|
||||
optional: true
|
||||
|
||||
@@ -2664,14 +2753,14 @@ snapshots:
|
||||
dependencies:
|
||||
'@tootallnate/once': 2.0.0
|
||||
agent-base: 6.0.2
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
http-proxy-agent@7.0.2:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2683,14 +2772,14 @@ snapshots:
|
||||
https-proxy-agent@5.0.1:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
https-proxy-agent@7.0.5:
|
||||
dependencies:
|
||||
agent-base: 7.1.1
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -2752,6 +2841,8 @@ snapshots:
|
||||
|
||||
isbinaryfile@5.0.2: {}
|
||||
|
||||
isbinaryfile@5.0.4: {}
|
||||
|
||||
isexe@2.0.0: {}
|
||||
|
||||
jackspeak@2.3.6:
|
||||
@@ -2760,6 +2851,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jackspeak@3.4.3:
|
||||
dependencies:
|
||||
'@isaacs/cliui': 8.0.2
|
||||
optionalDependencies:
|
||||
'@pkgjs/parseargs': 0.11.0
|
||||
|
||||
jake@10.8.7:
|
||||
dependencies:
|
||||
async: 3.2.5
|
||||
@@ -2823,6 +2920,8 @@ snapshots:
|
||||
|
||||
lru-cache@10.2.0: {}
|
||||
|
||||
lru-cache@10.4.3: {}
|
||||
|
||||
lru-cache@6.0.0:
|
||||
dependencies:
|
||||
yallist: 4.0.0
|
||||
@@ -2894,6 +2993,10 @@ snapshots:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
minimatch@9.0.5:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.1
|
||||
|
||||
minimist@1.2.8: {}
|
||||
|
||||
minipass-collect@1.0.2:
|
||||
@@ -2928,6 +3031,8 @@ snapshots:
|
||||
|
||||
minipass@7.0.4: {}
|
||||
|
||||
minipass@7.1.2: {}
|
||||
|
||||
minizlib@2.1.2:
|
||||
dependencies:
|
||||
minipass: 3.3.6
|
||||
@@ -2937,22 +3042,20 @@ snapshots:
|
||||
|
||||
ms@2.0.0: {}
|
||||
|
||||
ms@2.1.2: {}
|
||||
|
||||
ms@2.1.3: {}
|
||||
|
||||
negotiator@0.6.3: {}
|
||||
|
||||
node-abi@3.71.0:
|
||||
dependencies:
|
||||
semver: 7.6.0
|
||||
semver: 7.6.3
|
||||
|
||||
node-addon-api@1.7.2:
|
||||
optional: true
|
||||
|
||||
node-api-version@0.2.0:
|
||||
dependencies:
|
||||
semver: 7.6.0
|
||||
semver: 7.6.3
|
||||
|
||||
node-gyp@9.4.1:
|
||||
dependencies:
|
||||
@@ -2964,7 +3067,7 @@ snapshots:
|
||||
nopt: 6.0.0
|
||||
npmlog: 6.0.2
|
||||
rimraf: 3.0.2
|
||||
semver: 7.6.0
|
||||
semver: 7.6.3
|
||||
tar: 6.2.1
|
||||
which: 2.0.2
|
||||
transitivePeerDependencies:
|
||||
@@ -3025,6 +3128,8 @@ snapshots:
|
||||
dependencies:
|
||||
aggregate-error: 3.1.0
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
parseurl@1.3.3: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
@@ -3036,7 +3141,12 @@ snapshots:
|
||||
lru-cache: 10.2.0
|
||||
minipass: 7.0.4
|
||||
|
||||
path-to-regexp@0.1.10: {}
|
||||
path-scurry@1.11.1:
|
||||
dependencies:
|
||||
lru-cache: 10.4.3
|
||||
minipass: 7.1.2
|
||||
|
||||
path-to-regexp@0.1.12: {}
|
||||
|
||||
pe-library@0.4.1: {}
|
||||
|
||||
@@ -3088,7 +3198,7 @@ snapshots:
|
||||
|
||||
read-binary-file-arch@1.0.6:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3103,7 +3213,7 @@ snapshots:
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.2
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
@@ -3166,6 +3276,8 @@ snapshots:
|
||||
|
||||
sax@1.3.0: {}
|
||||
|
||||
sax@1.4.1: {}
|
||||
|
||||
semver-compare@1.0.0:
|
||||
optional: true
|
||||
|
||||
@@ -3175,6 +3287,8 @@ snapshots:
|
||||
dependencies:
|
||||
lru-cache: 6.0.0
|
||||
|
||||
semver@7.6.3: {}
|
||||
|
||||
send@0.19.0:
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
@@ -3256,7 +3370,7 @@ snapshots:
|
||||
socks-proxy-agent@7.0.0:
|
||||
dependencies:
|
||||
agent-base: 6.0.2
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
socks: 2.8.3
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -3313,7 +3427,7 @@ snapshots:
|
||||
|
||||
sumchecker@3.0.1:
|
||||
dependencies:
|
||||
debug: 4.3.4
|
||||
debug: 4.3.7
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -3365,6 +3479,8 @@ snapshots:
|
||||
|
||||
typescript@5.4.5: {}
|
||||
|
||||
typescript@5.6.3: {}
|
||||
|
||||
undici-types@5.26.5: {}
|
||||
|
||||
unique-filename@2.0.1:
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
let
|
||||
pkgs-unstable = import inputs.nixpkgs-unstable { system = pkgs.stdenv.system; };
|
||||
in {
|
||||
scripts.patch-sass-embedded.exec = ''
|
||||
find node_modules/.pnpm/sass-embedded-linux-*/node_modules/sass-embedded-linux-*/dart-sass/src -name dart -print0 | xargs -I {} -0 patchelf --set-interpreter "$(<$NIX_CC/nix-support/dynamic-linker)" {}
|
||||
'';
|
||||
|
||||
packages = with pkgs-unstable; [
|
||||
# General tools
|
||||
git-cliff
|
||||
|
||||
@@ -59,7 +59,7 @@ describe('Projects', () => {
|
||||
cy.get('.menu-container .menu-list li:first-child .dropdown .dropdown-content')
|
||||
.contains('Edit')
|
||||
.click()
|
||||
cy.get('#title')
|
||||
cy.get('#title:not(:disabled)')
|
||||
.type(`{selectall}${newProjectName}`)
|
||||
cy.get('footer.card-footer .button')
|
||||
.contains('Save')
|
||||
|
||||
Vendored
-5
@@ -1,8 +1,3 @@
|
||||
declare module 'postcss-easings' {
|
||||
import postcssEasings from 'postcss-easings'
|
||||
export default postcssEasings
|
||||
}
|
||||
|
||||
declare module 'postcss-easing-gradients' {
|
||||
import postcssEasingGradients from 'postcss-easing-gradients'
|
||||
export default postcssEasingGradients
|
||||
|
||||
Vendored
+2
@@ -16,6 +16,8 @@ interface ImportMetaEnv {
|
||||
readonly SENTRY_PROJECT?: string
|
||||
|
||||
readonly VITE_IS_ONLINE: boolean
|
||||
|
||||
readonly VUE_DEVTOOLS_LAUNCH_EDITOR: VitePluginVueDevToolsOptions.launchEditor
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
||||
+76
-80
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@9.12.3",
|
||||
"packageManager": "pnpm@9.15.0",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
@@ -31,7 +31,6 @@
|
||||
"preview:test": "vite preview --port 4173 --outDir dist-test",
|
||||
"build": "vite build && workbox copyLibraries dist/",
|
||||
"build:test": "vite build --outDir dist-test && workbox copyLibraries dist-dev/",
|
||||
"build:modern-only": "BUILD_MODERN_ONLY=true vite build && workbox copyLibraries dist/",
|
||||
"build:dev": "vite build --mode development --outDir dist-dev/",
|
||||
"lint": "eslint 'src/**/*.{js,ts,vue}'",
|
||||
"lint:fix": "pnpm run lint --fix",
|
||||
@@ -50,59 +49,59 @@
|
||||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/vue-fontawesome": "3.0.8",
|
||||
"@github/hotkey": "3.1.1",
|
||||
"@infectoone/vue-ganttastic": "2.3.2",
|
||||
"@intlify/unplugin-vue-i18n": "5.2.0",
|
||||
"@intlify/unplugin-vue-i18n": "6.0.1",
|
||||
"@kyvg/vue3-notification": "3.4.1",
|
||||
"@sentry/tracing": "7.114.0",
|
||||
"@sentry/vue": "8.36.0",
|
||||
"@tiptap/core": "2.9.1",
|
||||
"@tiptap/extension-blockquote": "2.9.1",
|
||||
"@tiptap/extension-bold": "2.9.1",
|
||||
"@tiptap/extension-bullet-list": "2.9.1",
|
||||
"@tiptap/extension-code": "2.9.1",
|
||||
"@tiptap/extension-code-block": "2.9.1",
|
||||
"@tiptap/extension-code-block-lowlight": "2.9.1",
|
||||
"@tiptap/extension-document": "2.9.1",
|
||||
"@tiptap/extension-dropcursor": "2.9.1",
|
||||
"@tiptap/extension-gapcursor": "2.9.1",
|
||||
"@tiptap/extension-hard-break": "2.9.1",
|
||||
"@tiptap/extension-heading": "2.9.1",
|
||||
"@tiptap/extension-history": "2.9.1",
|
||||
"@tiptap/extension-horizontal-rule": "2.9.1",
|
||||
"@tiptap/extension-image": "2.9.1",
|
||||
"@tiptap/extension-italic": "2.9.1",
|
||||
"@tiptap/extension-link": "2.9.1",
|
||||
"@tiptap/extension-list-item": "2.9.1",
|
||||
"@tiptap/extension-ordered-list": "2.9.1",
|
||||
"@tiptap/extension-paragraph": "2.9.1",
|
||||
"@tiptap/extension-placeholder": "2.9.1",
|
||||
"@tiptap/extension-strike": "2.9.1",
|
||||
"@tiptap/extension-table": "2.9.1",
|
||||
"@tiptap/extension-table-cell": "2.9.1",
|
||||
"@tiptap/extension-table-header": "2.9.1",
|
||||
"@tiptap/extension-table-row": "2.9.1",
|
||||
"@tiptap/extension-task-item": "2.9.1",
|
||||
"@tiptap/extension-task-list": "2.9.1",
|
||||
"@tiptap/extension-text": "2.9.1",
|
||||
"@tiptap/extension-typography": "2.9.1",
|
||||
"@tiptap/extension-underline": "2.9.1",
|
||||
"@tiptap/pm": "2.9.1",
|
||||
"@tiptap/suggestion": "2.9.1",
|
||||
"@tiptap/vue-3": "2.9.1",
|
||||
"@vueuse/core": "11.2.0",
|
||||
"@vueuse/router": "11.2.0",
|
||||
"axios": "1.7.7",
|
||||
"@sentry/vue": "8.47.0",
|
||||
"@tiptap/core": "2.10.4",
|
||||
"@tiptap/extension-blockquote": "2.10.4",
|
||||
"@tiptap/extension-bold": "2.10.4",
|
||||
"@tiptap/extension-bullet-list": "2.10.4",
|
||||
"@tiptap/extension-code": "2.10.4",
|
||||
"@tiptap/extension-code-block": "2.10.4",
|
||||
"@tiptap/extension-code-block-lowlight": "2.10.4",
|
||||
"@tiptap/extension-document": "2.10.4",
|
||||
"@tiptap/extension-dropcursor": "2.10.4",
|
||||
"@tiptap/extension-gapcursor": "2.10.4",
|
||||
"@tiptap/extension-hard-break": "2.10.4",
|
||||
"@tiptap/extension-heading": "2.10.4",
|
||||
"@tiptap/extension-history": "2.10.4",
|
||||
"@tiptap/extension-horizontal-rule": "2.10.4",
|
||||
"@tiptap/extension-image": "2.10.4",
|
||||
"@tiptap/extension-italic": "2.10.4",
|
||||
"@tiptap/extension-link": "2.10.4",
|
||||
"@tiptap/extension-list-item": "2.10.4",
|
||||
"@tiptap/extension-ordered-list": "2.10.4",
|
||||
"@tiptap/extension-paragraph": "2.10.4",
|
||||
"@tiptap/extension-placeholder": "2.10.4",
|
||||
"@tiptap/extension-strike": "2.10.4",
|
||||
"@tiptap/extension-table": "2.10.4",
|
||||
"@tiptap/extension-table-cell": "2.10.4",
|
||||
"@tiptap/extension-table-header": "2.10.4",
|
||||
"@tiptap/extension-table-row": "2.10.4",
|
||||
"@tiptap/extension-task-item": "2.10.4",
|
||||
"@tiptap/extension-task-list": "2.10.4",
|
||||
"@tiptap/extension-text": "2.10.4",
|
||||
"@tiptap/extension-typography": "2.10.4",
|
||||
"@tiptap/extension-underline": "2.10.4",
|
||||
"@tiptap/pm": "2.10.4",
|
||||
"@tiptap/suggestion": "2.10.4",
|
||||
"@tiptap/vue-3": "2.10.4",
|
||||
"@vueuse/core": "12.0.0",
|
||||
"@vueuse/router": "12.0.0",
|
||||
"axios": "1.7.9",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"change-case": "5.4.4",
|
||||
"date-fns": "4.1.0",
|
||||
"dayjs": "1.11.13",
|
||||
"dompurify": "3.1.7",
|
||||
"dompurify": "3.2.3",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.43",
|
||||
@@ -110,68 +109,65 @@
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lowlight": "2.9.0",
|
||||
"pinia": "2.2.5",
|
||||
"pinia": "2.3.0",
|
||||
"register-service-worker": "1.7.2",
|
||||
"sortablejs": "1.15.3",
|
||||
"tailwindcss": "3.4.14",
|
||||
"sortablejs": "1.15.6",
|
||||
"tailwindcss": "3.4.17",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.5.4",
|
||||
"vue": "3.5.12",
|
||||
"vue": "3.5.13",
|
||||
"vue-advanced-cropper": "2.8.9",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "10.0.4",
|
||||
"vue-router": "4.4.5",
|
||||
"vuemoji-picker": "0.3.1",
|
||||
"vue-i18n": "10.0.5",
|
||||
"vue-router": "4.5.0",
|
||||
"vuemoji-picker": "0.3.2",
|
||||
"workbox-precaching": "7.3.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.2.0",
|
||||
"@cypress/vite-dev-server": "5.2.1",
|
||||
"@cypress/vue": "6.0.1",
|
||||
"@faker-js/faker": "9.1.0",
|
||||
"@faker-js/faker": "9.3.0",
|
||||
"@histoire/plugin-screenshot": "0.17.17",
|
||||
"@histoire/plugin-vue": "0.17.17",
|
||||
"@tsconfig/node20": "20.1.4",
|
||||
"@tsconfig/node22": "22.0.0",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/flexsearch": "0.7.6",
|
||||
"@types/is-touch-device": "1.0.3",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@types/node": "22.8.6",
|
||||
"@types/node": "22.10.2",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "8.12.2",
|
||||
"@typescript-eslint/parser": "8.12.2",
|
||||
"@vitejs/plugin-legacy": "5.4.3",
|
||||
"@vitejs/plugin-vue": "5.1.4",
|
||||
"@vue/eslint-config-typescript": "14.1.3",
|
||||
"@typescript-eslint/eslint-plugin": "8.18.1",
|
||||
"@typescript-eslint/parser": "8.18.1",
|
||||
"@vitejs/plugin-vue": "5.2.1",
|
||||
"@vue/eslint-config-typescript": "14.1.4",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"@vue/tsconfig": "0.5.1",
|
||||
"@vue/tsconfig": "0.7.0",
|
||||
"autoprefixer": "10.4.20",
|
||||
"browserslist": "4.24.2",
|
||||
"caniuse-lite": "1.0.30001676",
|
||||
"browserslist": "4.24.3",
|
||||
"caniuse-lite": "1.0.30001689",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.15.1",
|
||||
"cypress": "13.17.0",
|
||||
"esbuild": "0.24.0",
|
||||
"eslint": "9.14.0",
|
||||
"eslint-plugin-vue": "9.30.0",
|
||||
"happy-dom": "15.8.0",
|
||||
"eslint": "9.17.0",
|
||||
"eslint-plugin-vue": "9.32.0",
|
||||
"happy-dom": "15.11.7",
|
||||
"histoire": "0.17.17",
|
||||
"postcss": "8.4.47",
|
||||
"postcss": "8.4.49",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-preset-env": "10.0.9",
|
||||
"rollup": "4.24.3",
|
||||
"postcss-preset-env": "10.1.2",
|
||||
"rollup": "4.28.1",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass-embedded": "^1.80.6",
|
||||
"start-server-and-test": "2.0.8",
|
||||
"typescript": "5.6.3",
|
||||
"sass-embedded": "1.83.0",
|
||||
"start-server-and-test": "2.0.9",
|
||||
"typescript": "5.7.2",
|
||||
"unplugin-inject-preload": "3.0.0",
|
||||
"vite": "5.4.10",
|
||||
"vite-plugin-pwa": "0.20.5",
|
||||
"vite": "6.0.3",
|
||||
"vite-plugin-pwa": "0.21.1",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-plugin-vue-devtools": "7.6.8",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "2.1.4",
|
||||
"vitest": "2.1.8",
|
||||
"vue-tsc": "2.1.10",
|
||||
"wait-on": "8.0.1",
|
||||
"workbox-cli": "7.3.0"
|
||||
|
||||
Generated
+2382
-1938
File diff suppressed because it is too large
Load Diff
@@ -134,11 +134,16 @@ function removeHeight(el: HTMLElement) {
|
||||
<style lang="scss" scoped>
|
||||
$transition-time: 300ms;
|
||||
|
||||
// https://easings.net/#easeInQuint
|
||||
$ease-in-quint: cubic-bezier(0.64, 0, 0.78, 0);
|
||||
// https://easings.net/#easeInOutQuint
|
||||
$ease-in-out-quint: cubic-bezier(0.83, 0, 0.17, 1);
|
||||
|
||||
.expandable-slide-enter-active,
|
||||
.expandable-slide-leave-active {
|
||||
transition:
|
||||
opacity $transition-time ease-in-quint,
|
||||
height $transition-time ease-in-out-quint;
|
||||
opacity $transition-time $ease-in-quint,
|
||||
height $transition-time $ease-in-out-quint;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ const enabled = computed(() => configStore.demoModeEnabled && !hide.value)
|
||||
|
||||
<style scoped lang="scss">
|
||||
.demo-mode-banner {
|
||||
position: sticky;
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
inset-inline: 0;
|
||||
background: var(--danger);
|
||||
|
||||
@@ -76,38 +76,32 @@
|
||||
v-if="projectStore.isLoading"
|
||||
variant="small"
|
||||
/>
|
||||
<template v-else>
|
||||
<nav
|
||||
v-if="favoriteProjects"
|
||||
class="menu"
|
||||
>
|
||||
<ProjectsNavigation
|
||||
:model-value="favoriteProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="false"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<nav
|
||||
v-if="savedFilterProjects"
|
||||
class="menu"
|
||||
>
|
||||
<ProjectsNavigation
|
||||
:model-value="savedFilterProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="false"
|
||||
/>
|
||||
</nav>
|
||||
<nav
|
||||
v-else
|
||||
class="menu"
|
||||
>
|
||||
<ProjectsNavigation
|
||||
v-if="favoriteProjects?.length"
|
||||
:model-value="favoriteProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="false"
|
||||
/>
|
||||
|
||||
<nav class="menu">
|
||||
<ProjectsNavigation
|
||||
:model-value="projects"
|
||||
:can-edit-order="true"
|
||||
:can-collapse="true"
|
||||
:level="1"
|
||||
/>
|
||||
</nav>
|
||||
</template>
|
||||
<ProjectsNavigation
|
||||
v-if="savedFilterProjects?.length"
|
||||
:model-value="savedFilterProjects"
|
||||
:can-edit-order="false"
|
||||
:can-collapse="false"
|
||||
/>
|
||||
|
||||
<ProjectsNavigation
|
||||
v-if="projects?.length"
|
||||
:model-value="projects"
|
||||
:can-edit-order="true"
|
||||
:can-collapse="true"
|
||||
:level="1"
|
||||
/>
|
||||
</nav>
|
||||
|
||||
<PoweredByLink
|
||||
class="mt-auto"
|
||||
@@ -117,7 +111,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import { storeToRefs } from 'pinia'
|
||||
|
||||
import PoweredByLink from '@/components/home/PoweredByLink.vue'
|
||||
import Logo from '@/components/home/Logo.vue'
|
||||
@@ -129,16 +123,15 @@ import ProjectsNavigation from '@/components/home/ProjectsNavigation.vue'
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const projects = computed(() => projectStore.notArchivedRootProjects)
|
||||
const favoriteProjects = computed(() => projectStore.favoriteProjects)
|
||||
const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
||||
const {
|
||||
notArchivedRootProjects: projects,
|
||||
favoriteProjects,
|
||||
savedFilterProjects,
|
||||
} = storeToRefs(projectStore)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.logo {
|
||||
display: block;
|
||||
|
||||
padding-left: 1rem;
|
||||
margin-right: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
@@ -192,7 +185,7 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
||||
}
|
||||
}
|
||||
|
||||
.menu + .menu {
|
||||
.menu-list + .menu-list {
|
||||
padding-top: math.div($navbar-padding, 2);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -84,12 +84,14 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {storeToRefs} from 'pinia'
|
||||
import {useStorage} from '@vueuse/core'
|
||||
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import ProjectSettingsDropdown from '@/components/project/ProjectSettingsDropdown.vue'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
@@ -105,18 +107,17 @@ const props = defineProps<{
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const baseStore = useBaseStore()
|
||||
const currentProject = computed(() => baseStore.currentProject)
|
||||
const {currentProject} = storeToRefs(baseStore)
|
||||
|
||||
// Persist open state across browser reloads. Using a separate ref for the state
|
||||
// allows us to use only one entry in local storage instead of one for every project id.
|
||||
type OpenState = { [key: number]: boolean }
|
||||
const childProjectsOpenState = useStorage<OpenState>('navigation-child-projects-open', {})
|
||||
const childProjectsCollapsed = useStorage<{ [key: number]: boolean }>('navigation-child-projects-collapsed', {})
|
||||
const childProjectsOpen = computed({
|
||||
get() {
|
||||
return childProjectsOpenState.value[props.project.id] ?? true
|
||||
return childProjectsCollapsed.value[props.project.id] ?? true
|
||||
},
|
||||
set(open) {
|
||||
childProjectsOpenState.value[props.project.id] = open
|
||||
childProjectsCollapsed.value[props.project.id] = open
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -159,6 +159,7 @@ function to(n, index) {
|
||||
case names.TASK_COMMENT:
|
||||
case names.TASK_ASSIGNED:
|
||||
case names.TASK_REMINDER:
|
||||
case names.TASK_MENTIONED:
|
||||
to.name = 'task.detail'
|
||||
to.params.id = n.notification.task.id
|
||||
break
|
||||
|
||||
@@ -87,7 +87,7 @@ const filterFromView = computed(() => {
|
||||
return
|
||||
}
|
||||
const view = project.views.find(v => v.id === props.viewId)
|
||||
return view?.filter
|
||||
return view?.filter?.filter
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -65,7 +65,11 @@ import {useRoute} from 'vue-router'
|
||||
import type {TaskFilterParams} from '@/services/taskCollection'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {FILTER_OPERATORS, transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
|
||||
import {
|
||||
hasFilterQuery,
|
||||
transformFilterStringForApi,
|
||||
transformFilterStringFromApi,
|
||||
} from '@/helpers/filters'
|
||||
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
@@ -161,8 +165,7 @@ function change(event: 'blur' | 'modelValue' | 'always') {
|
||||
let s = ''
|
||||
|
||||
// When the filter does not contain any filter tokens, assume a simple search and redirect the input
|
||||
const hasFilterQueries = FILTER_OPERATORS.find(o => filter.includes(o)) || false
|
||||
if (!hasFilterQueries) {
|
||||
if (!hasFilterQuery(filter)) {
|
||||
s = filter
|
||||
}
|
||||
|
||||
|
||||
@@ -58,7 +58,7 @@ import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import {useProjectBackground} from './useProjectBackground'
|
||||
import {useProjectBackground} from '@/composables/useProjectBackground'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
|
||||
|
||||
@@ -191,10 +191,12 @@
|
||||
</div>
|
||||
<x-button
|
||||
v-else
|
||||
v-tooltip="bucket.limit > 0 && bucket.count >= bucket.limit ? $t('project.kanban.bucketLimitReached') : ''"
|
||||
class="is-fullwidth has-text-centered"
|
||||
:shadow="false"
|
||||
icon="plus"
|
||||
variant="secondary"
|
||||
:disabled="bucket.limit > 0 && bucket.count >= bucket.limit"
|
||||
@click="toggleShowNewTaskInput(bucket.id)"
|
||||
>
|
||||
{{
|
||||
@@ -414,10 +416,7 @@ watch(
|
||||
collapsedBuckets.value = getCollapsedBucketState(projectId)
|
||||
kanbanStore.loadBucketsForProject(projectId, viewId, params)
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
function setTaskContainerRef(id: IBucket['id'], el: HTMLElement) {
|
||||
@@ -569,7 +568,6 @@ async function addTaskToBucket(bucketId: IBucket['id']) {
|
||||
bucketId,
|
||||
projectId: project.value.id,
|
||||
})
|
||||
toggleShowNewTaskInput(bucketId)
|
||||
newTaskText.value = ''
|
||||
kanbanStore.addTaskToBucket(task)
|
||||
scrollTaskContainerToTop(bucketId)
|
||||
@@ -975,4 +973,4 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
||||
.move-card-leave-active {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -37,14 +37,13 @@
|
||||
<Nothing v-if="ctaVisible && tasks.length === 0 && !loading">
|
||||
{{ $t('project.list.empty') }}
|
||||
<ButtonLink
|
||||
v-if="project.id > 0"
|
||||
v-if="project.id > 0 && canWrite"
|
||||
@click="focusNewTaskInput()"
|
||||
>
|
||||
{{ $t('project.list.newTaskCta') }}
|
||||
</ButtonLink>
|
||||
</Nothing>
|
||||
|
||||
|
||||
<draggable
|
||||
v-if="tasks && tasks.length > 0"
|
||||
v-model="tasks"
|
||||
@@ -140,7 +139,14 @@ const {
|
||||
loadTasks,
|
||||
params,
|
||||
sortByParam,
|
||||
} = useTaskList(() => props.projectId, () => props.viewId, {position: 'asc'})
|
||||
} = useTaskList(
|
||||
() => props.projectId,
|
||||
() => props.viewId,
|
||||
{position: 'asc'},
|
||||
() => props.projectId === -1
|
||||
? null
|
||||
: 'subtasks',
|
||||
)
|
||||
|
||||
const taskPositionService = ref(new TaskPositionService())
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
<script setup lang="ts">
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import type {IFilter} from '@/modelTypes/ISavedFilter'
|
||||
import XButton from '@/components/input/Button.vue'
|
||||
import FilterInput from '@/components/project/partials/FilterInput.vue'
|
||||
import {ref, onBeforeMount} from 'vue'
|
||||
import {transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
|
||||
import {onBeforeMount, ref} from 'vue'
|
||||
import {hasFilterQuery, transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
|
||||
import {useLabelStore} from '@/stores/labels'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
|
||||
@@ -28,18 +29,24 @@ const labelStore = useLabelStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
onBeforeMount(() => {
|
||||
const transform = (filterString: string) => transformFilterStringFromApi(
|
||||
filterString,
|
||||
labelId => labelStore.getLabelById(labelId)?.title || null,
|
||||
projectId => projectStore.projects[projectId]?.title || null,
|
||||
)
|
||||
const transformFilterToString = (filter: IFilter): string => {
|
||||
if (filter.s !== '') {
|
||||
return filter.s
|
||||
}
|
||||
|
||||
return transformFilterStringFromApi(
|
||||
filter.filter,
|
||||
labelId => labelStore.getLabelById(labelId)?.title || null,
|
||||
projectId => projectStore.projects[projectId]?.title || null,
|
||||
)
|
||||
}
|
||||
|
||||
const transformed = {
|
||||
...props.modelValue,
|
||||
filter: transform(props.modelValue.filter),
|
||||
filter: transformFilterToString(props.modelValue.filter),
|
||||
bucketConfiguration: props.modelValue.bucketConfiguration.map(bc => ({
|
||||
title: bc.title,
|
||||
filter: transform(bc.filter),
|
||||
filter: transformFilterToString(bc.filter),
|
||||
})),
|
||||
}
|
||||
|
||||
@@ -49,21 +56,31 @@ onBeforeMount(() => {
|
||||
})
|
||||
|
||||
function save() {
|
||||
const transformFilter = (filterQuery: string) => transformFilterStringForApi(
|
||||
filterQuery,
|
||||
labelTitle => labelStore.getLabelByExactTitle(labelTitle)?.id || null,
|
||||
projectTitle => {
|
||||
const found = projectStore.findProjectByExactname(projectTitle)
|
||||
return found?.id || null
|
||||
},
|
||||
)
|
||||
const transformFilterForApi = (filterQuery: string): IFilter => {
|
||||
const filterString = transformFilterStringForApi(
|
||||
filterQuery,
|
||||
labelTitle => labelStore.getLabelByExactTitle(labelTitle)?.id || null,
|
||||
projectTitle => {
|
||||
const found = projectStore.findProjectByExactname(projectTitle)
|
||||
return found?.id || null
|
||||
},
|
||||
)
|
||||
const filter: IFilter = {}
|
||||
if (hasFilterQuery(filterString)) {
|
||||
filter.filter = filterString
|
||||
} else {
|
||||
filter.s = filterString
|
||||
}
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
emit('update:modelValue', {
|
||||
...view.value,
|
||||
filter: transformFilter(view.value?.filter),
|
||||
filter: transformFilterForApi(view.value?.filter || ''),
|
||||
bucketConfiguration: view.value?.bucketConfiguration.map(bc => ({
|
||||
title: bc.title,
|
||||
filter: transformFilter(bc.filter),
|
||||
filter: transformFilterForApi(bc.filter || ''),
|
||||
})),
|
||||
})
|
||||
}
|
||||
@@ -231,7 +248,7 @@ function handleBubbleSave() {
|
||||
<XButton
|
||||
variant="secondary"
|
||||
icon="plus"
|
||||
@click="() => view.bucketConfiguration.push({title: '', filter: ''})"
|
||||
@click="() => view.bucketConfiguration.push({title: '', filter: {filter: ''}})"
|
||||
>
|
||||
{{ $t('project.kanban.addBucket') }}
|
||||
</XButton>
|
||||
|
||||
@@ -100,7 +100,9 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th />
|
||||
<th>{{ $t('project.share.links.view') }}</th>
|
||||
<th v-if="availableViews.length > 0">
|
||||
{{ $t('project.share.links.view') }}
|
||||
</th>
|
||||
<th>{{ $t('project.share.attributes.delete') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -150,7 +152,7 @@
|
||||
<div class="field has-addons no-input-mobile">
|
||||
<div class="control">
|
||||
<input
|
||||
:value="getShareLink(s.hash, selectedView[s.id])"
|
||||
:value="shareLinks[s.id]"
|
||||
class="input"
|
||||
readonly
|
||||
type="text"
|
||||
@@ -160,7 +162,7 @@
|
||||
<x-button
|
||||
v-tooltip="$t('misc.copy')"
|
||||
:shadow="false"
|
||||
@click="copy(getShareLink(s.hash, selectedView[s.id]))"
|
||||
@click="copy(shareLinks[s.id])"
|
||||
>
|
||||
<span class="icon">
|
||||
<Icon icon="paste" />
|
||||
@@ -169,9 +171,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<td v-if="availableViews.length > 0">
|
||||
<div class="select">
|
||||
<select v-model="selectedView[s.id]">
|
||||
<select v-model="selectedViews[s.id]">
|
||||
<option
|
||||
v-for="(view) in availableViews"
|
||||
:key="view.id"
|
||||
@@ -251,10 +253,6 @@ const showDeleteModal = ref(false)
|
||||
const linkIdToDelete = ref(0)
|
||||
const showNewForm = ref(false)
|
||||
|
||||
type SelectedViewMapper = Record<IProject['id'], IProjectView['id']>
|
||||
|
||||
const selectedView = ref<SelectedViewMapper>({})
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const availableViews = computed<IProjectView[]>(() => projectStore.projects[props.projectId]?.views || [])
|
||||
@@ -274,13 +272,28 @@ async function load(projectId: IProject['id']) {
|
||||
return
|
||||
}
|
||||
|
||||
const links = await linkShareService.getAll({projectId})
|
||||
links.forEach((l: ILinkShare) => {
|
||||
selectedView.value[l.id] = availableViews.value[0].id
|
||||
})
|
||||
linkShares.value = links
|
||||
linkShares.value = await linkShareService.getAll({projectId})
|
||||
}
|
||||
|
||||
type SelectedViewMapper = Record<IProject['id'], IProjectView['id']>
|
||||
|
||||
const selectedViews = ref<SelectedViewMapper>({})
|
||||
|
||||
watch(() => ([linkShares.value, availableViews.value]), ([newLinkShares, newProjectViews]) => {
|
||||
if (!newLinkShares?.length || !newProjectViews?.length) {
|
||||
selectedViews.value = {}
|
||||
return
|
||||
}
|
||||
|
||||
newLinkShares.forEach((linkShare) => {
|
||||
selectedViews.value[linkShare.id] = newProjectViews.length > 0 ? newProjectViews[0].id : null
|
||||
})
|
||||
}, {
|
||||
immediate:true,
|
||||
deep: true,
|
||||
})
|
||||
|
||||
|
||||
async function add(projectId: IProject['id']) {
|
||||
const newLinkShare = new LinkShareModel({
|
||||
right: selectedRight.value,
|
||||
@@ -310,9 +323,17 @@ async function remove(projectId: IProject['id']) {
|
||||
}
|
||||
}
|
||||
|
||||
function getShareLink(hash: string, viewId: IProjectView['id']) {
|
||||
return frontendUrl.value + 'share/' + hash + '/auth?view=' + viewId
|
||||
function getShareLink(hash: string, viewId: IProjectView['id']|null) {
|
||||
return frontendUrl.value + 'share/' + hash + '/auth' + (viewId ? '?view=' + viewId : '')
|
||||
}
|
||||
|
||||
const shareLinks = computed(() => {
|
||||
return linkShares.value.reduce((links, linkShare) => {
|
||||
links[linkShare.id] = getShareLink(linkShare.hash, selectedViews.value[linkShare.id] ?? null)
|
||||
return links
|
||||
}, {} as {[id: string]: string },
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -320,4 +341,4 @@ function getShareLink(hash: string, viewId: IProjectView['id']) {
|
||||
.sharables-project:not(.card-content) {
|
||||
overflow-y: auto
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
:class="{'textarea-empty': newTaskTitle === ''}"
|
||||
:placeholder="$t('project.list.addPlaceholder')"
|
||||
rows="1"
|
||||
@keyup="resetEmptyTitleError"
|
||||
@keydown="resetEmptyTitleError"
|
||||
@keydown.enter="handleEnter"
|
||||
/>
|
||||
<span class="icon is-small is-left">
|
||||
@@ -99,13 +99,10 @@ const taskAddHovered = useElementHover(taskAdd)
|
||||
|
||||
const errorMessage = ref('')
|
||||
|
||||
function resetEmptyTitleError(e: KeyboardEvent) {
|
||||
if (
|
||||
(e.which <= 90 && e.which >= 48 || e.which >= 96 && e.which <= 105)
|
||||
&& newTaskTitle.value !== ''
|
||||
) {
|
||||
errorMessage.value = ''
|
||||
}
|
||||
function resetEmptyTitleError() {
|
||||
if (!newTaskTitle.value) {
|
||||
errorMessage.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
const loading = computed(() => taskStore.isLoading)
|
||||
@@ -135,7 +132,7 @@ async function addTask() {
|
||||
|
||||
const taskCollectionService = new TaskService()
|
||||
const projectIndices = new Map<number, number>()
|
||||
|
||||
|
||||
let currentProjectId = authStore.settings.defaultProjectId
|
||||
if (typeof router.currentRoute.value.params.projectId !== 'undefined') {
|
||||
currentProjectId = Number(router.currentRoute.value.params.projectId)
|
||||
@@ -143,22 +140,22 @@ async function addTask() {
|
||||
|
||||
// Create a map of project indices before creating tasks
|
||||
if (tasksToCreate.length > 1) {
|
||||
for (const {project} of tasksToCreate) {
|
||||
const projectId = project !== null
|
||||
? await taskStore.findProjectId({project, projectId: 0})
|
||||
: currentProjectId
|
||||
for (const {project} of tasksToCreate) {
|
||||
const projectId = project !== null
|
||||
? await taskStore.findProjectId({project, projectId: 0})
|
||||
: currentProjectId
|
||||
|
||||
if (!projectIndices.has(projectId)) {
|
||||
const newestTask = await taskCollectionService.getAll(new TaskModel({}), {
|
||||
sort_by: ['id'],
|
||||
order_by: ['desc'],
|
||||
per_page: 1,
|
||||
filter: `project_id = ${projectId}`,
|
||||
})
|
||||
projectIndices.set(projectId, newestTask[0]?.index || 0)
|
||||
if (!projectIndices.has(projectId)) {
|
||||
const newestTask = await taskCollectionService.getAll(new TaskModel({}), {
|
||||
sort_by: ['id'],
|
||||
order_by: ['desc'],
|
||||
per_page: 1,
|
||||
filter: `project_id = ${projectId}`,
|
||||
})
|
||||
projectIndices.set(projectId, newestTask[0]?.index || 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newTasks = tasksToCreate.map(async ({title, project}, index) => {
|
||||
if (title === '') {
|
||||
@@ -169,7 +166,7 @@ async function addTask() {
|
||||
const projectId = project !== null
|
||||
? await taskStore.findProjectId({project, projectId: 0})
|
||||
: currentProjectId
|
||||
|
||||
|
||||
// Calculate new index for this task per project
|
||||
let taskIndex: number | undefined
|
||||
if (tasksToCreate.length > 1) {
|
||||
@@ -201,7 +198,6 @@ async function addTask() {
|
||||
|
||||
const isParent = allParentTasks.includes(t.title)
|
||||
if (t.parent === null && !isParent) {
|
||||
emit('taskAdded', createdTask)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -215,23 +211,37 @@ async function addTask() {
|
||||
otherTaskId: createdParentTask.id,
|
||||
relationKind: RELATION_KIND.PARENTTASK,
|
||||
}))
|
||||
|
||||
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = [{
|
||||
|
||||
if (typeof createdTask.relatedTasks === 'undefined') {
|
||||
createdTask.relatedTasks = {}
|
||||
}
|
||||
if (typeof createdTask.relatedTasks[RELATION_KIND.PARENTTASK] === 'undefined') {
|
||||
createdTask.relatedTasks[RELATION_KIND.PARENTTASK] = []
|
||||
}
|
||||
createdTask.relatedTasks[RELATION_KIND.PARENTTASK].push({
|
||||
...createdParentTask,
|
||||
relatedTasks: {}, // To avoid endless references
|
||||
}]
|
||||
// we're only emitting here so that the relation shows up in the project
|
||||
emit('taskAdded', createdTask)
|
||||
})
|
||||
|
||||
createdParentTask.relatedTasks[RELATION_KIND.SUBTASK] = [{
|
||||
if (typeof createdParentTask.relatedTasks === 'undefined') {
|
||||
createdParentTask.relatedTasks = {}
|
||||
}
|
||||
if (typeof createdParentTask.relatedTasks[RELATION_KIND.SUBTASK] === 'undefined') {
|
||||
createdParentTask.relatedTasks[RELATION_KIND.SUBTASK] = []
|
||||
}
|
||||
createdParentTask.relatedTasks[RELATION_KIND.SUBTASK].push({
|
||||
...createdTask,
|
||||
relatedTasks: {}, // To avoid endless references
|
||||
}]
|
||||
emit('taskAdded', createdParentTask)
|
||||
})
|
||||
|
||||
return rel
|
||||
})
|
||||
await Promise.all(relations)
|
||||
|
||||
// We're emitting all tasks at once at the end to avoid the same task showing up multiple times
|
||||
Object.values(createdTasks).forEach(task => {
|
||||
emit('taskAdded', task)
|
||||
})
|
||||
} catch (e) {
|
||||
newTaskTitle.value = taskTitleBackup
|
||||
if (e?.message === 'NO_PROJECT') {
|
||||
@@ -264,7 +274,7 @@ defineExpose({
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.task-add,
|
||||
// overwrite bulma styles
|
||||
// overwrite bulma styles
|
||||
.task-add .add-task__field {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
<ul>
|
||||
<!-- Not localized because these only work in english -->
|
||||
<li>Today</li>
|
||||
<li>Tonight</li>
|
||||
<li>Tomorrow</li>
|
||||
<li>Next monday</li>
|
||||
<li>This weekend</li>
|
||||
|
||||
@@ -126,7 +126,7 @@
|
||||
@update:modelValue="toggleTaskDone(task)"
|
||||
/>
|
||||
<RouterLink
|
||||
:to="{ name: route.name as string, params: { id: task.id } }"
|
||||
:to="{ name: route.name as string, params: { id: task.id }, state: { backdropView: route.fullPath } }"
|
||||
:class="{ 'is-strikethrough': task.done}"
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -144,7 +144,7 @@ watch(
|
||||
if (newReminder) {
|
||||
reminder.value = newReminder
|
||||
|
||||
if (newReminder.relativeTo === null) {
|
||||
if (newReminder.relativeTo === null && newReminder.reminder !== null) {
|
||||
reminderDate.value = new Date(newReminder.reminder)
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ function updateData() {
|
||||
})
|
||||
}
|
||||
|
||||
function addNewReminder(newReminder: ITaskReminder) {
|
||||
function addNewReminder(newReminder: ITaskReminder|null) {
|
||||
if (newReminder === null) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
import { ref, reactive, computed, type MaybeRefOrGetter, toValue, watch } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { klona } from 'klona/lite'
|
||||
|
||||
import type { ITask } from '@/modelTypes/ITask'
|
||||
import type { IProject } from '@/modelTypes/IProject'
|
||||
import type { Priority } from '@/constants/priorities'
|
||||
import type {Action as MessageAction} from '@/message'
|
||||
|
||||
import TaskModel from '@/models/task'
|
||||
|
||||
import { useTaskStore } from '@/stores/tasks'
|
||||
import { useProjectStore } from '@/stores/projects'
|
||||
import { useKanbanStore } from '@/stores/kanban'
|
||||
import { useBaseStore } from '@/stores/base'
|
||||
|
||||
import { RIGHTS } from '@/constants/rights'
|
||||
import { success } from '@/message'
|
||||
import { playPopSound } from '@/helpers/playPop'
|
||||
import { TASK_REPEAT_MODES } from '@/types/IRepeatMode'
|
||||
import { shallowReactive } from 'vue'
|
||||
import TaskService from '@/services/task'
|
||||
import { useAttachmentStore } from '@/stores/attachments'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { getProjectTitle } from '@/helpers/getProjectTitle'
|
||||
import { uploadFile } from '@/helpers/attachments'
|
||||
|
||||
export function useTask(taskId: MaybeRefOrGetter<ITask['id']>) {
|
||||
const {t} = useI18n()
|
||||
const router = useRouter()
|
||||
const taskStore = useTaskStore()
|
||||
|
||||
const taskTitle = ref('')
|
||||
|
||||
// We doubled the task color property here because verte does not have a real change property, leading
|
||||
// to the color property change being triggered when the # is removed from it, leading to an update,
|
||||
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
|
||||
// updated, changed, updated and so on.
|
||||
// To prevent this, we put the task color property in a seperate value which is set to the task color
|
||||
// when it is saved and loaded.
|
||||
const taskColor = ref<ITask['hexColor']>('')
|
||||
|
||||
const task = reactive({
|
||||
...new TaskModel(),
|
||||
title: taskTitle,
|
||||
}) as ITask
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
|
||||
/** Avoid flashing of empty elements if the task content is not yet loaded. */
|
||||
const isReady = ref(false)
|
||||
|
||||
const isLoading = computed(() => taskService.loading)
|
||||
|
||||
// load task
|
||||
watch(
|
||||
() => toValue(taskId),
|
||||
async (newTaskId) => {
|
||||
if (newTaskId === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const loaded = await taskService.get({ id: newTaskId })
|
||||
Object.assign(task, loaded)
|
||||
attachmentStore.set(task.attachments)
|
||||
taskColor.value = task.hexColor
|
||||
setActiveFields()
|
||||
} finally {
|
||||
isReady.value = true
|
||||
}
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
const canWrite = computed(() => (
|
||||
task.maxRight !== null &&
|
||||
task.maxRight > RIGHTS.READ
|
||||
))
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const project = computed(() => projectStore.projects[task.projectId])
|
||||
|
||||
const ancestorProjects = computed(() => projectStore.getAncestors(project.value))
|
||||
|
||||
const ancestorProjectTitles = computed(() => ancestorProjects.value.map(project => getProjectTitle(project)))
|
||||
|
||||
const attachmentStore = useAttachmentStore()
|
||||
const {hasAttachments} = storeToRefs(attachmentStore)
|
||||
|
||||
async function saveTask(
|
||||
currentTask: ITask | null = null,
|
||||
undoCallback?: () => void,
|
||||
) {
|
||||
if (currentTask === null) {
|
||||
currentTask = klona(task)
|
||||
}
|
||||
|
||||
if (!canWrite.value) {
|
||||
return
|
||||
}
|
||||
|
||||
currentTask.hexColor = taskColor.value
|
||||
|
||||
// If no end date is being set, but a start date and due date,
|
||||
// use the due date as the end date
|
||||
if (
|
||||
currentTask.endDate === null &&
|
||||
currentTask.startDate !== null &&
|
||||
currentTask.dueDate !== null
|
||||
) {
|
||||
currentTask.endDate = currentTask.dueDate
|
||||
}
|
||||
|
||||
const updatedTask = await taskStore.update(currentTask) // TODO: markraw ?
|
||||
Object.assign(task, updatedTask)
|
||||
setActiveFields()
|
||||
|
||||
let actions: MessageAction[] = []
|
||||
if (undoCallback) {
|
||||
actions = [{
|
||||
title: t('task.undo'),
|
||||
callback: undoCallback,
|
||||
}]
|
||||
}
|
||||
success({message: t('task.detail.updateSuccess')}, actions)
|
||||
}
|
||||
|
||||
async function deleteTask() {
|
||||
await taskStore.delete(task)
|
||||
success({message: t('task.detail.deleteSuccess')})
|
||||
return router.push({name: 'project.index', params: {projectId: task.projectId}})
|
||||
}
|
||||
|
||||
function uploadAttachment(file: File, onSuccess?: (url: string) => void) {
|
||||
return uploadFile(toValue(taskId), file, onSuccess)
|
||||
}
|
||||
|
||||
async function toggleTaskDone() {
|
||||
const newTask = {
|
||||
...klona(task),
|
||||
done: !task.done,
|
||||
}
|
||||
|
||||
if (newTask.done) {
|
||||
playPopSound()
|
||||
}
|
||||
|
||||
await saveTask(
|
||||
newTask,
|
||||
toggleTaskDone,
|
||||
)
|
||||
}
|
||||
|
||||
async function changeProject(project: IProject) {
|
||||
const kanbanStore = useKanbanStore()
|
||||
const baseStore = useBaseStore()
|
||||
kanbanStore.removeTaskInBucket(task)
|
||||
await saveTask({
|
||||
...task,
|
||||
projectId: project.id,
|
||||
})
|
||||
baseStore.setCurrentProject(project)
|
||||
}
|
||||
|
||||
async function toggleFavorite() {
|
||||
const newTask = await taskStore.toggleFavorite(task)
|
||||
Object.assign(task, newTask)
|
||||
}
|
||||
|
||||
async function setPriority(priority: Priority) {
|
||||
const newTask: ITask = {
|
||||
...task,
|
||||
priority,
|
||||
}
|
||||
|
||||
return saveTask(newTask)
|
||||
}
|
||||
|
||||
async function setPercentDone(percentDone: number) {
|
||||
const newTask: ITask = {
|
||||
...task,
|
||||
percentDone,
|
||||
}
|
||||
|
||||
return saveTask(newTask)
|
||||
}
|
||||
|
||||
async function removeRepeatAfter() {
|
||||
task.repeatAfter.amount = 0
|
||||
task.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||
await saveTask()
|
||||
}
|
||||
|
||||
return {
|
||||
project,
|
||||
ancestorProjects,
|
||||
ancestorProjectTitles,
|
||||
|
||||
isReady,
|
||||
isLoading,
|
||||
|
||||
task,
|
||||
taskTitle,
|
||||
taskColor,
|
||||
|
||||
canWrite,
|
||||
hasAttachments,
|
||||
|
||||
saveTask,
|
||||
deleteTask,
|
||||
uploadAttachment,
|
||||
|
||||
toggleTaskDone,
|
||||
changeProject,
|
||||
toggleFavorite,
|
||||
setPriority,
|
||||
setPercentDone,
|
||||
removeRepeatAfter,
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,11 @@ import {ref, shallowReactive, watch, computed, type ComputedGetter} from 'vue'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
import {useRouteQuery} from '@vueuse/router'
|
||||
|
||||
import TaskCollectionService, {getDefaultTaskFilterParams, type TaskFilterParams} from '@/services/taskCollection'
|
||||
import TaskCollectionService, {
|
||||
type ExpandTaskFilterParam,
|
||||
getDefaultTaskFilterParams,
|
||||
type TaskFilterParams,
|
||||
} from '@/services/taskCollection'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {error} from '@/message'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
@@ -59,6 +63,7 @@ export function useTaskList(
|
||||
projectIdGetter: ComputedGetter<IProject['id']>,
|
||||
projectViewIdGetter: ComputedGetter<IProjectView['id']>,
|
||||
sortByDefault: SortBy = SORT_BY_DEFAULT,
|
||||
expandGetter: ComputedGetter<ExpandTaskFilterParam> = () => 'subtasks',
|
||||
) {
|
||||
|
||||
const projectId = computed(() => projectIdGetter())
|
||||
@@ -95,6 +100,7 @@ export function useTaskList(
|
||||
{
|
||||
...allParams.value,
|
||||
filter_timezone: authStore.settings.timezone,
|
||||
expand: expandGetter(),
|
||||
},
|
||||
page.value,
|
||||
]
|
||||
|
||||
@@ -51,10 +51,9 @@ export const checkAndSetApiUrl = (url: string | undefined | null): Promise<strin
|
||||
window.API_URL = urlToCheck.toString()
|
||||
|
||||
const configStore = useConfigStore()
|
||||
const updateConfig = () => configStore.update()
|
||||
|
||||
// Check if the api is reachable at the provided url
|
||||
return updateConfig()
|
||||
return configStore.update()
|
||||
.catch(e => {
|
||||
// Check if it is reachable at /api/v1 and http
|
||||
if (
|
||||
@@ -63,7 +62,7 @@ export const checkAndSetApiUrl = (url: string | undefined | null): Promise<strin
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
return configStore.update()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
@@ -76,7 +75,7 @@ export const checkAndSetApiUrl = (url: string | undefined | null): Promise<strin
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
return configStore.update()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
@@ -85,7 +84,7 @@ export const checkAndSetApiUrl = (url: string | undefined | null): Promise<strin
|
||||
if (urlToCheck.port !== API_DEFAULT_PORT) {
|
||||
urlToCheck.port = API_DEFAULT_PORT
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
return configStore.update()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
@@ -98,7 +97,7 @@ export const checkAndSetApiUrl = (url: string | undefined | null): Promise<strin
|
||||
) {
|
||||
urlToCheck.pathname = `${urlToCheck.pathname}api/v1`
|
||||
window.API_URL = urlToCheck.toString()
|
||||
return updateConfig()
|
||||
return configStore.update()
|
||||
}
|
||||
throw e
|
||||
})
|
||||
|
||||
@@ -60,6 +60,10 @@ export const FILTER_JOIN_OPERATOR = [
|
||||
|
||||
export const FILTER_OPERATORS_REGEX = '(<|>|<=|>=|=|!=|not in|in)'
|
||||
|
||||
export function hasFilterQuery(filter: string): boolean {
|
||||
return FILTER_OPERATORS.find(o => filter.includes(o)) || false
|
||||
}
|
||||
|
||||
export function getFilterFieldRegexPattern(field: string): RegExp {
|
||||
return new RegExp('(' + field + '\\s*' + FILTER_OPERATORS_REGEX + '\\s*)([\'"]?)([^\'"&|()<]+\\1?)?', 'ig')
|
||||
}
|
||||
|
||||
@@ -22,6 +22,11 @@ export const parseDate = (text: string, now: Date = new Date()): dateParseResult
|
||||
if (matchesDateExpr(text, 'today')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('today')), 'today')
|
||||
}
|
||||
if (matchesDateExpr(text, 'tonight')) {
|
||||
const taskDate = getDateFromInterval(calculateDayInterval('today'))
|
||||
taskDate.setHours(21)
|
||||
return addTimeToDate(text, taskDate, 'tonight')
|
||||
}
|
||||
if (matchesDateExpr(text, 'tomorrow')) {
|
||||
return addTimeToDate(text, getDateFromInterval(calculateDayInterval('tomorrow')), 'tomorrow')
|
||||
}
|
||||
|
||||
@@ -365,7 +365,8 @@
|
||||
"deleteBucketSuccess": "The bucket has been deleted successfully.",
|
||||
"bucketTitleSavedSuccess": "The bucket title has been saved successfully.",
|
||||
"bucketLimitSavedSuccess": "The bucket limit been saved successfully.",
|
||||
"collapse": "Collapse this bucket"
|
||||
"collapse": "Collapse this bucket",
|
||||
"bucketLimitReached": "You have reached the bucket limit. Remove tasks or increase the limit to add new tasks."
|
||||
},
|
||||
"pseudo": {
|
||||
"favorites": {
|
||||
@@ -1219,4 +1220,4 @@
|
||||
"years": "year|years"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@
|
||||
"title": "Filtrai",
|
||||
"clear": "Anuliuoti filtrus",
|
||||
"showResults": "Rodyti rezultatus",
|
||||
"fromView": "The current view has a filter set as well:",
|
||||
"fromView": "Dabartinis vaizdas turi nustatytą filtrą taip pat:",
|
||||
"fromViewBoth": "It will be used in combination with what you enter here.",
|
||||
"attributes": {
|
||||
"title": "Pavadinimas",
|
||||
@@ -463,7 +463,7 @@
|
||||
"project": "The project the task belongs to (only available for saved filters, not on a project level)",
|
||||
"reminders": "The reminders of the task as a date field, will return all tasks with at least one reminder matching the query",
|
||||
"created": "The time and date when the task was created",
|
||||
"updated": "The time and date when the task was last changed"
|
||||
"updated": "Data ir laikas, kada užduotis buvo pakeista"
|
||||
},
|
||||
"operators": {
|
||||
"intro": "The available operators for filtering include:",
|
||||
@@ -486,7 +486,7 @@
|
||||
"examples": {
|
||||
"intro": "Čia pateikiami keli filtravimo užklausos pavyzdžiai:",
|
||||
"priorityEqual": "Atitinka užduotis su 4 lygio prioritetu",
|
||||
"dueDatePast": "Matches tasks with a due date in the past",
|
||||
"dueDatePast": "Sutampa su užduotimis su praeities data",
|
||||
"undoneHighPriority": "Matches undone tasks with priority level 3 or higher",
|
||||
"assigneesIn": "Matches tasks assigned to either \"user1\" or \"user2\"",
|
||||
"priorityOneOrTwoPastDue": "Matches tasks with priority level 1 or 2 and a due date in the past"
|
||||
@@ -538,7 +538,7 @@
|
||||
}
|
||||
},
|
||||
"sharing": {
|
||||
"authenticating": "Authenticating…",
|
||||
"authenticating": "Identifikuojama…",
|
||||
"passwordRequired": "Šis pasidalintas projektas reikalauja slaptažodžio. Įveskite jį žemiau:",
|
||||
"error": "Įvyko klaida.",
|
||||
"invalidPassword": "Slaptažodis neteisingas."
|
||||
@@ -548,7 +548,7 @@
|
||||
"upcoming": "Būsimos",
|
||||
"settings": "Nustatymai",
|
||||
"imprint": "Įspaudas",
|
||||
"privacy": "Privacy Policy"
|
||||
"privacy": "Privatumo politika"
|
||||
},
|
||||
"misc": {
|
||||
"loading": "Kraunama…",
|
||||
@@ -559,18 +559,18 @@
|
||||
"refresh": "Atnaujinti",
|
||||
"disable": "Išjungti",
|
||||
"copy": "Kopijuoti į iškarpinę",
|
||||
"copyError": "Copy to clipboard failed",
|
||||
"copyError": "Kopijavimas į iškarpinę nepavyko",
|
||||
"search": "Paieška",
|
||||
"searchPlaceholder": "Rašykite čia norėdami ieškoti…",
|
||||
"previous": "Ankstesnis",
|
||||
"next": "Toliau",
|
||||
"poweredBy": "Sukurta Vikunja",
|
||||
"info": "Info",
|
||||
"info": "Informacija",
|
||||
"create": "Sukurti",
|
||||
"doit": "Atlikt!",
|
||||
"saving": "Išsaugoma…",
|
||||
"saved": "Išsaugota!",
|
||||
"default": "Default",
|
||||
"default": "Numatytasis",
|
||||
"close": "Uždaryti",
|
||||
"download": "Atsisiųsti",
|
||||
"showMenu": "Rodyti meniu",
|
||||
@@ -593,10 +593,10 @@
|
||||
"thisWeekend": "Šį savaitgalį",
|
||||
"laterThisWeek": "Vėliau šią savaitę",
|
||||
"nextWeek": "Kitą savaitę",
|
||||
"chooseDate": "Choose a date"
|
||||
"chooseDate": "Pasirinkite datą"
|
||||
},
|
||||
"editor": {
|
||||
"edit": "Edit",
|
||||
"edit": "Keisti",
|
||||
"done": "Atlikta",
|
||||
"heading1": "Heading 1",
|
||||
"heading1Tooltip": "Big section heading.",
|
||||
@@ -605,18 +605,18 @@
|
||||
"heading3": "Heading 3",
|
||||
"heading3Tooltip": "Smaller section header.",
|
||||
"headingSmaller": "Heading Smaller",
|
||||
"headingBigger": "Heading Bigger",
|
||||
"bold": "Bold",
|
||||
"italic": "Italic",
|
||||
"strikethrough": "Strikethrough",
|
||||
"headingBigger": "Antraštė Didesnė",
|
||||
"bold": "Paryškintas",
|
||||
"italic": "Pasviręs",
|
||||
"strikethrough": "Perbrauktas",
|
||||
"underline": "Underline",
|
||||
"code": "Code",
|
||||
"codeTooltip": "Capture a code snippet.",
|
||||
"codeTooltip": "Atvaizduoti kodo ištrauką.",
|
||||
"quote": "Quote",
|
||||
"quoteTooltip": "Capture a quote.",
|
||||
"bulletList": "Bullet list",
|
||||
"bulletListTooltip": "Create a simple bullet list.",
|
||||
"unorderedList": "Unordered list",
|
||||
"bulletListTooltip": "Sukurti paprastą sąrašą.",
|
||||
"unorderedList": "Nerikiuojamas sąrašas",
|
||||
"orderedList": "Ordered list",
|
||||
"orderedListTooltip": "Create a list with numbering.",
|
||||
"cleanBlock": "Clean Block",
|
||||
@@ -626,31 +626,31 @@
|
||||
"horizontalRule": "Horizontal Rule",
|
||||
"horizontalRuleTooltip": "Divide a section.",
|
||||
"sideBySide": "Vienas šalia kito",
|
||||
"guide": "Guide",
|
||||
"guide": "Vadovas",
|
||||
"text": "Text",
|
||||
"textTooltip": "Just start typing with plain text.",
|
||||
"taskList": "Task list",
|
||||
"taskListTooltip": "Track tasks with a to-do list.",
|
||||
"textTooltip": "Pradėkite rašyti paprastą tekstą.",
|
||||
"taskList": "Užduočių sąrašas",
|
||||
"taskListTooltip": "Sekti užduotis su veiklų sąrašu.",
|
||||
"undo": "Undo",
|
||||
"redo": "Redo",
|
||||
"placeholder": "Type some text or hit '/' to see more options…",
|
||||
"redo": "Grąžinti",
|
||||
"placeholder": "Rašykite tekstą arba spauskite '/' norėdami matyti daugiau pasirinkčių…",
|
||||
"table": {
|
||||
"title": "Table",
|
||||
"insert": "Insert table",
|
||||
"addColumnBefore": "Add column before",
|
||||
"addColumnAfter": "Add column after",
|
||||
"deleteColumn": "Delete column",
|
||||
"addRowBefore": "Add row before",
|
||||
"addRowAfter": "Add row after",
|
||||
"addRowBefore": "Pridėti eilutę prieš tai",
|
||||
"addRowAfter": "Pridėti eilutę po",
|
||||
"deleteRow": "Ištrinti eilutę",
|
||||
"deleteTable": "Delete table",
|
||||
"mergeCells": "Merge cells",
|
||||
"splitCell": "Split cell",
|
||||
"deleteTable": "Ištrinti lentelę",
|
||||
"mergeCells": "Sujungti langelius",
|
||||
"splitCell": "Skelti langelį",
|
||||
"toggleHeaderColumn": "Toggle header column",
|
||||
"toggleHeaderRow": "Toggle header row",
|
||||
"toggleHeaderCell": "Toggle header cell",
|
||||
"mergeOrSplit": "Merge or split",
|
||||
"fixTables": "Fix tables"
|
||||
"mergeOrSplit": "Sujungti arba padalinti",
|
||||
"fixTables": "Pataisyti lenteles"
|
||||
}
|
||||
},
|
||||
"multiselect": {
|
||||
@@ -658,16 +658,16 @@
|
||||
"selectPlaceholder": "Click or press enter to select"
|
||||
},
|
||||
"datepickerRange": {
|
||||
"to": "To",
|
||||
"to": "Į",
|
||||
"from": "From",
|
||||
"fromto": "{from} to {to}",
|
||||
"date": "Date",
|
||||
"fromto": "{from}{to}",
|
||||
"date": "Data",
|
||||
"ranges": {
|
||||
"today": "Today",
|
||||
"thisWeek": "This Week",
|
||||
"thisWeek": "Šią savaitę",
|
||||
"restOfThisWeek": "The Rest of This Week",
|
||||
"nextWeek": "Next Week",
|
||||
"next7Days": "Next 7 Days",
|
||||
"nextWeek": "Kitą savaitę",
|
||||
"next7Days": "Artimiausios 7 dienos",
|
||||
"lastWeek": "Praeitą savaitę",
|
||||
"thisMonth": "Šį mėnesį",
|
||||
"restOfThisMonth": "Šį mėnesį",
|
||||
@@ -682,9 +682,9 @@
|
||||
"startOfToday": "Pradedant šiandien",
|
||||
"endOfToday": "Iki dienos pabaigos",
|
||||
"beginningOflastWeek": "Praeitos savaitės pradžios",
|
||||
"endOfLastWeek": "End of last week",
|
||||
"endOfLastWeek": "Praeitos savaitės pabaiga",
|
||||
"beginningOfThisWeek": "Beginning of this week",
|
||||
"endOfThisWeek": "End of this week",
|
||||
"endOfThisWeek": "Šios savaaitės pabaaiga",
|
||||
"startOfNextWeek": "Start of next week",
|
||||
"endOfNextWeek": "Iki kitos savaitės pabaigos",
|
||||
"in7Days": "Už 7 dienų",
|
||||
@@ -693,10 +693,10 @@
|
||||
"startOfThisMonth": "Start of this month",
|
||||
"endOfThisMonth": "End of this month",
|
||||
"startOfNextMonth": "Start of next month",
|
||||
"endOfNextMonth": "End of next month",
|
||||
"endOfNextMonth": "Kito mėnesio pabaiga",
|
||||
"in30Days": "In 30 days",
|
||||
"startOfThisYear": "Beginning of this year",
|
||||
"endOfThisYear": "End of this year"
|
||||
"endOfThisYear": "Šių metų pabaiga"
|
||||
}
|
||||
},
|
||||
"datemathHelp": {
|
||||
@@ -706,7 +706,7 @@
|
||||
"intro": "Specify relative dates which are resolved on the fly by Vikunja when applying the filter.",
|
||||
"expression": "Each Date Math expression starts with an anchor date, which can either be {0}, or a date string ending with {1}. This anchor date can optionally be followed by one or more maths expressions.",
|
||||
"similar": "These expressions are similar to the ones provided by {0} and {1}.",
|
||||
"add1Day": "Add one day",
|
||||
"add1Day": "Pridėti vieną dieną",
|
||||
"minus1Day": "Subtract one day",
|
||||
"roundDay": "Round down to the nearest day",
|
||||
"supportedUnits": "Supported time units",
|
||||
@@ -716,41 +716,41 @@
|
||||
"minutes": "Minutes",
|
||||
"hours": "Hours",
|
||||
"days": "Days",
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years"
|
||||
"weeks": "Savaitės",
|
||||
"months": "Mėnesiai",
|
||||
"years": "Metai"
|
||||
},
|
||||
"examples": {
|
||||
"now": "Right now",
|
||||
"in24h": "In 24h",
|
||||
"today": "Today at 00:00",
|
||||
"now": "Dabar",
|
||||
"in24h": "24-ių valandų bėgyje",
|
||||
"today": "Šiandien 00:00",
|
||||
"beginningOfThisWeek": "The beginning of this week at 00:00",
|
||||
"endOfThisWeek": "The end of this week",
|
||||
"in30Days": "In 30 days",
|
||||
"in30Days": "Už 30 dienų",
|
||||
"datePlusMonth": "{0} plus one month at 00:00 of that day"
|
||||
}
|
||||
}
|
||||
},
|
||||
"task": {
|
||||
"task": "Task",
|
||||
"new": "Create a task",
|
||||
"delete": "Delete this task",
|
||||
"task": "Užduotis",
|
||||
"new": "Sukurti užduotį",
|
||||
"delete": "Ištrinti šią užduotį",
|
||||
"createSuccess": "The task was successfully created.",
|
||||
"addReminder": "Add a reminder…",
|
||||
"doneSuccess": "The task was successfully marked as done.",
|
||||
"undoneSuccess": "The task was successfully un-marked as done.",
|
||||
"revertSuccess": "The task change was successfully undone.",
|
||||
"undo": "Undo",
|
||||
"openDetail": "Open task detail view",
|
||||
"openDetail": "Atidaryti užduoties duomenų vaizdą",
|
||||
"checklistTotal": "{checked} of {total} tasks",
|
||||
"checklistAllDone": "{total} tasks",
|
||||
"checklistAllDone": "{total} užduotys",
|
||||
"show": {
|
||||
"titleCurrent": "Current Tasks",
|
||||
"titleDates": "Tasks from {from} until {to}",
|
||||
"noDates": "Show tasks without dates",
|
||||
"overdue": "Show overdue tasks",
|
||||
"fromuntil": "Tasks from {from} until {until}",
|
||||
"select": "Select a date range",
|
||||
"titleCurrent": "Dabartinės Užduotys",
|
||||
"titleDates": "Užduotys nuo {from} iki {to}",
|
||||
"noDates": "Rodyti užduotis be datų",
|
||||
"overdue": "Rodyti neišpildytas užduotis",
|
||||
"fromuntil": "Užduotys nuo {from} iki {until}",
|
||||
"select": "Pasirinkti datos rėžį",
|
||||
"noTasks": "Nothing to do — Have a nice day!"
|
||||
},
|
||||
"detail": {
|
||||
@@ -823,55 +823,55 @@
|
||||
"notSubscribedTask": "You are not subscribed to this task and won't receive notifications for changes.",
|
||||
"subscribe": "Subscribe",
|
||||
"unsubscribe": "Unsubscribe",
|
||||
"subscribeSuccessProject": "You are now subscribed to this project",
|
||||
"unsubscribeSuccessProject": "You are now unsubscribed to this project",
|
||||
"subscribeSuccessTask": "You are now subscribed to this task",
|
||||
"subscribeSuccessProject": "Užsiprenumeravai projekto naujienas",
|
||||
"unsubscribeSuccessProject": "Projekto naujienų atsisakyta",
|
||||
"subscribeSuccessTask": "Užsiprenumeravai užduoties naujienas",
|
||||
"unsubscribeSuccessTask": "Užsiprenumeravai užduoties naujienas"
|
||||
},
|
||||
"attachment": {
|
||||
"title": "Attachments",
|
||||
"createdBy": "created {0} by {1}",
|
||||
"downloadTooltip": "Download this attachment",
|
||||
"createdBy": "sukurta {0} {1}",
|
||||
"downloadTooltip": "Atsiųsti šį prisegtuką",
|
||||
"upload": "Upload attachment",
|
||||
"drop": "Drop files here to upload",
|
||||
"delete": "Delete attachment",
|
||||
"deleteTooltip": "Delete this attachment",
|
||||
"deleteTooltip": "Pašalinti šį priedą",
|
||||
"deleteText1": "Ar esi tikra -s, kad nori ištrinti šį prisegtuką {filename}?",
|
||||
"copyUrl": "Copy URL",
|
||||
"copyUrlTooltip": "Copy the url of this attachment for usage in text",
|
||||
"copyUrlTooltip": "Nukopijuoti nuorodą šio prisegtuko, kaip paprastą tekstą",
|
||||
"setAsCover": "Make cover",
|
||||
"unsetAsCover": "Remove cover",
|
||||
"successfullyChangedCoverImage": "The cover image was successfully changed.",
|
||||
"usedAsCover": "Cover image"
|
||||
"unsetAsCover": "Pašalinti viršelį",
|
||||
"successfullyChangedCoverImage": "Viršelio paveikslėlis buvo sėkmingai pakeistas.",
|
||||
"usedAsCover": "Viršelio nuotrauka"
|
||||
},
|
||||
"comment": {
|
||||
"title": "Comments",
|
||||
"loading": "Loading comments…",
|
||||
"edited": "edited {date}",
|
||||
"creating": "Creating comment…",
|
||||
"placeholder": "Add your comment, hit '/' for more options…",
|
||||
"edited": "pataisyta {date}",
|
||||
"creating": "Kraunami komentarai…",
|
||||
"placeholder": "Pridėk komentarą, spausk '/' dėl daugiau pasirinkčių…",
|
||||
"comment": "Comment",
|
||||
"delete": "Delete this comment",
|
||||
"deleteText1": "Are you sure you want to delete this comment?",
|
||||
"deleteSuccess": "The comment was deleted successfully.",
|
||||
"addedSuccess": "The comment was added successfully.",
|
||||
"permalink": "Copy permalink to this comment"
|
||||
"deleteText1": "Ar tikrai nori ištrinti šį komentarą?",
|
||||
"deleteSuccess": "Komentaras panaikintas sėkmingai.",
|
||||
"addedSuccess": "Komentaras sėkmigai pridėtas.",
|
||||
"permalink": "Nukopijuoti komentaro nuorodą"
|
||||
},
|
||||
"deferDueDate": {
|
||||
"title": "Defer due date",
|
||||
"1day": "1 day",
|
||||
"3days": "3 days",
|
||||
"1week": "1 week"
|
||||
"1day": "1 diena",
|
||||
"3days": "3 dienos",
|
||||
"1week": "1 savaitė"
|
||||
},
|
||||
"description": {
|
||||
"placeholder": "Įvesti aprašą, spausti '/' norint gauti daugiau pasirinkčių…",
|
||||
"empty": "Aprašo kol kas nėra."
|
||||
},
|
||||
"assignee": {
|
||||
"placeholder": "Type to assign a user…",
|
||||
"selectPlaceholder": "Assign this user",
|
||||
"assignSuccess": "The user has been assigned successfully.",
|
||||
"unassignSuccess": "The user has been unassigned successfully."
|
||||
"placeholder": "Rašykite norėdami priskirti naudotoją…",
|
||||
"selectPlaceholder": "Priskirti šį naudotoją",
|
||||
"assignSuccess": "Naudotojas buvo sėmingai priskirtas.",
|
||||
"unassignSuccess": "Naudotojas buvo sėkmingai atšauktas."
|
||||
},
|
||||
"label": {
|
||||
"placeholder": "Rašykite noredami pridėti etiketę…",
|
||||
@@ -898,15 +898,15 @@
|
||||
"add": "Add a New Task Relation",
|
||||
"new": "New Task Relation",
|
||||
"searchPlaceholder": "Type search for a task to add as related…",
|
||||
"createPlaceholder": "Add this as related task",
|
||||
"createPlaceholder": "Pridėti kaip susijusią užduotį",
|
||||
"differentProject": "This task belongs to a different project.",
|
||||
"noneYet": "No task relations yet.",
|
||||
"noneYet": "Kol kas užduoties ryšio nėra.",
|
||||
"delete": "Delete Task Relation",
|
||||
"deleteText1": "Are you sure you want to delete this task relation?",
|
||||
"select": "Select a relation kind",
|
||||
"select": "Pasirinkite ryšio tipą",
|
||||
"taskRequired": "Please select a task or enter a new task title.",
|
||||
"kinds": {
|
||||
"subtask": "Subtask | Subtasks",
|
||||
"subtask": "Sub užduotis | Sub užduotys",
|
||||
"parenttask": "Parent Task | Parent Tasks",
|
||||
"related": "Related Task | Related Tasks",
|
||||
"duplicateof": "Duplicate Of | Duplicates Of",
|
||||
@@ -944,7 +944,7 @@
|
||||
"weeks": "Weeks",
|
||||
"months": "Months",
|
||||
"years": "Years",
|
||||
"invalidAmount": "Please enter more than 0."
|
||||
"invalidAmount": "Prašome įvesti daugiau nei 0."
|
||||
},
|
||||
"quickAddMagic": {
|
||||
"hint": "Use magic prefixes to define due dates, assignees and other task properties.",
|
||||
@@ -965,7 +965,7 @@
|
||||
"dateAndTime": "Date and time",
|
||||
"date": "Any date will be used as the due date of the new task. You can use dates in any of these formats:",
|
||||
"dateWeekday": "any weekday, will use the next date with that date",
|
||||
"dateCurrentYear": "will use the current year",
|
||||
"dateCurrentYear": "naudoti šiuos metus",
|
||||
"dateNth": "will use the {day}th of the current month",
|
||||
"dateTime": "Combine any of the date formats with \"{time}\" (or {timePM}) to set a time.",
|
||||
"repeats": "Repeating tasks",
|
||||
@@ -982,7 +982,7 @@
|
||||
"edit": {
|
||||
"title": "Edit Team \"{team}\"",
|
||||
"members": "Team Members",
|
||||
"search": "Type to search a user…",
|
||||
"search": "Rašykite čia norėdami ieškoti naudotojo…",
|
||||
"addUser": "Add to team",
|
||||
"makeMember": "Make Member",
|
||||
"makeAdmin": "Make Admin",
|
||||
@@ -990,7 +990,7 @@
|
||||
"userAddedSuccess": "The team member was successfully added.",
|
||||
"madeMember": "The team member was successfully made member.",
|
||||
"madeAdmin": "The team member was successfully made admin.",
|
||||
"mustSelectUser": "Please select a user.",
|
||||
"mustSelectUser": "Prašome pasirinkti vartotoją.",
|
||||
"delete": {
|
||||
"header": "Delete the team",
|
||||
"text1": "Are you sure you want to delete this team and all of its members?",
|
||||
@@ -1001,13 +1001,13 @@
|
||||
"header": "Remove a user from the team",
|
||||
"text1": "Are you sure you want to remove this user from the team?",
|
||||
"text2": "They will lose access to all projects this team has access to. This CANNOT BE UNDONE!",
|
||||
"success": "The user was successfully deleted from the team."
|
||||
"success": "Naudotojas buvo sėkmingai ištrintas iš komandos."
|
||||
},
|
||||
"leave": {
|
||||
"title": "Leave team",
|
||||
"text1": "Are you sure you want to leave this team?",
|
||||
"text2": "You will lose access to all projects this team has access to. If you change your mind you'll need a team admin to add you again.",
|
||||
"success": "You have successfully left the team."
|
||||
"success": "Jūs sėkmingai palikote komandą."
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
@@ -1016,7 +1016,7 @@
|
||||
"nameRequired": "Prašome nurodyti vardą.",
|
||||
"description": "Aprašymas",
|
||||
"descriptionPlaceholder": "Aprašykite komandą čia, spauskite '/' dėl daugiau opcijų…",
|
||||
"admin": "Admin",
|
||||
"admin": "Administravimas",
|
||||
"member": "Member",
|
||||
"isPublic": "Public Team",
|
||||
"isPublicDescription": "Padaryk komandą viešai randamą. Kai įgalinta, bet kas gali pasidalinti projektu su komanda, net nebūdama -s tiesioginiu komandos nariu."
|
||||
@@ -1041,7 +1041,7 @@
|
||||
"related": "Modify related tasks of this task",
|
||||
"color": "Change the color of this task",
|
||||
"move": "Move this task to another project",
|
||||
"reminder": "Manage reminders of this task",
|
||||
"reminder": "Valdyti šios užduoties priminimus",
|
||||
"description": "Praskleisti redagavimą užduoties apraše",
|
||||
"delete": "Delete this task",
|
||||
"priority": "Change the priority of this task",
|
||||
@@ -1105,7 +1105,7 @@
|
||||
},
|
||||
"quickActions": {
|
||||
"commands": "Commands",
|
||||
"placeholder": "Type a command or search…",
|
||||
"placeholder": "Rašykite komandą arba ieškokite…",
|
||||
"hint": "Gai naudoti {project} sutrumpinti projektų paiešką. Sujungti {project} ar {label} (etiketes) paieškos užklausoje norint ieškoti užduoties su tomis etiketėmis ar tame projekte. Naudok {assignee} norėdama(-s) ieškoti komandu.",
|
||||
"tasks": "Tasks",
|
||||
"projects": "Projects",
|
||||
@@ -1165,7 +1165,7 @@
|
||||
"4002": "The task does not exist.",
|
||||
"4003": "All bulk editing tasks must belong to the same project.",
|
||||
"4004": "Need at least one task when bulk editing tasks.",
|
||||
"4005": "You do not have the right to see the task.",
|
||||
"4005": "Neturi prieigos prie šios užduoties.",
|
||||
"4006": "You can't set a parent task as the task itself.",
|
||||
"4007": "You can't create a task relation with an invalid kind of relation.",
|
||||
"4008": "You can't create a task relation which already exists.",
|
||||
@@ -1186,7 +1186,7 @@
|
||||
"6005": "The user is already a member of that team.",
|
||||
"6006": "Cannot delete the last team member.",
|
||||
"6007": "The team does not have access to the project to perform that action.",
|
||||
"7002": "The user already has access to that project.",
|
||||
"7002": "Naudotojas jau turi prieigą prie šio projekto.",
|
||||
"7003": "You do not have access to that project.",
|
||||
"8001": "Ši etiketė jau yra prie šios užduoties.",
|
||||
"8002": "Etiketė neegzistuoja.",
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
"usernameEmail": "Brukernavn / e-postadresse",
|
||||
"usernamePlaceholder": "e.g. frederick",
|
||||
"email": "E-postadresse",
|
||||
"emailPlaceholder": "e.g. frederic{'@'}vikunja.io",
|
||||
"emailPlaceholder": "f.eks. frederic{'@'}vikunja.io",
|
||||
"password": "Passord",
|
||||
"passwordPlaceholder": "f.eks. ••••••••••• •",
|
||||
"forgotPassword": "Glemt ditt passord?",
|
||||
@@ -73,8 +73,8 @@
|
||||
"title": "Innstillinger",
|
||||
"newPasswordTitle": "Oppdater passord",
|
||||
"newPassword": "New password",
|
||||
"newPasswordConfirm": "New password confirmation",
|
||||
"currentPassword": "Current password",
|
||||
"newPasswordConfirm": "Bekreft nytt passord",
|
||||
"currentPassword": "Gjeldende passord",
|
||||
"currentPasswordPlaceholder": "Ditt nåværende passord",
|
||||
"passwordsDontMatch": "Det nye passordet og bekreftelse samsvarer ikke.",
|
||||
"passwordUpdateSuccess": "Passordet ditt ble vellykket oppdatert.",
|
||||
@@ -83,13 +83,13 @@
|
||||
"updateEmailSuccess": "E-postadressen din ble oppdatert. Vi har sendt deg en lenke for å bekrefte den.",
|
||||
"general": {
|
||||
"title": "Generelle innstillinger",
|
||||
"name": "My Name",
|
||||
"name": "Mitt Navn",
|
||||
"newName": "The new name",
|
||||
"savedSuccess": "Innstillingene ble lagret.",
|
||||
"emailReminders": "Send me reminders for tasks via email",
|
||||
"emailReminders": "Send påminnelser om oppgaver på e-post",
|
||||
"overdueReminders": "Send meg et sammendrag av mine uferdige oppdager over dato hver dag",
|
||||
"discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
|
||||
"discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
|
||||
"discoverableByEmail": "Tillat andre brukere å legge meg til som medlem i grupper eller prosjekter når det søkes etter min e-postadresse",
|
||||
"playSoundWhenDone": "Spill av en lyd når merking av oppgaver som ferdig",
|
||||
"weekStart": "Uken starter på",
|
||||
"weekStartSunday": "Søndag",
|
||||
@@ -121,7 +121,7 @@
|
||||
"more": "Mer informasjon om CalDAV i Vikunja",
|
||||
"tokens": "CalDAV Tokens",
|
||||
"tokensHowTo": "Du kan bruke et CalDAV token til å bruke istedet for passord for å logge på det ovennevnte endepunktet.",
|
||||
"createToken": "Opprett Token",
|
||||
"createToken": "Opprett token",
|
||||
"tokenCreated": "Her er din token: {token}",
|
||||
"wontSeeItAgain": "Skriv den ned, du vil ikke kunne se den igjen.",
|
||||
"mustUseToken": "Du må opprette et CalDAV-token hvis du ønsker å bruke CalDAS med en tredjeparts klient. Bruk token som passord.",
|
||||
@@ -158,7 +158,7 @@
|
||||
"apiDocs": "Check out the api docs",
|
||||
"createAToken": "Create a token",
|
||||
"createToken": "Create token",
|
||||
"30d": "30 Days",
|
||||
"30d": "30 dager",
|
||||
"60d": "60 Days",
|
||||
"90d": "90 Days",
|
||||
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
|
||||
@@ -231,7 +231,7 @@
|
||||
"archive": "Arkiver dette prosjektet",
|
||||
"unarchive": "Av-arkivere dette prosjektet",
|
||||
"unarchiveText": "You will be able to create tasks or edit it.",
|
||||
"archiveText": "You won't be able to edit this project or create tasks until you un-archive it.",
|
||||
"archiveText": "Du kan ikke redigere dette prosjektet før du tar det ut av arkivet.",
|
||||
"success": "Prosjektet ble vellykket arkivert."
|
||||
},
|
||||
"background": {
|
||||
@@ -268,7 +268,7 @@
|
||||
"identifier": "Prosjekt identifikator",
|
||||
"identifierPlaceholder": "Prosjektidentifikatoren kommer her…",
|
||||
"description": "Beskrivelse",
|
||||
"descriptionPlaceholder": "Enter a description for this project, hit '/' for more options…",
|
||||
"descriptionPlaceholder": "Angi en beskrivelse til dette prosjektet. Trykk '/' for flere valg…",
|
||||
"color": "Farger",
|
||||
"success": "Prosjektet ble opprettet."
|
||||
},
|
||||
@@ -609,7 +609,7 @@
|
||||
"bold": "Fet",
|
||||
"italic": "Kursiv",
|
||||
"strikethrough": "Gjennomstrekning",
|
||||
"underline": "Underline",
|
||||
"underline": "Understrek",
|
||||
"code": "Kode",
|
||||
"codeTooltip": "Capture a code snippet.",
|
||||
"quote": "Sitat",
|
||||
@@ -768,8 +768,8 @@
|
||||
"belongsToProject": "Denne oppgaven tilhører listen '{project}'",
|
||||
"due": "Forfallsdato {at}",
|
||||
"closePopup": "Lukk popup",
|
||||
"organization": "Organization",
|
||||
"management": "Management",
|
||||
"organization": "Organisering",
|
||||
"management": "Behandling",
|
||||
"dateAndTime": "Date and time",
|
||||
"delete": {
|
||||
"header": "Slette denne oppgaven",
|
||||
@@ -904,7 +904,7 @@
|
||||
"delete": "Slett relasjon",
|
||||
"deleteText1": "Er du sikker på at du vil slette denne relasjonen?",
|
||||
"select": "Velg en relasjonstype",
|
||||
"taskRequired": "Velg en oppgave eller angi en ny oppgavetitle.",
|
||||
"taskRequired": "Velg en oppgave eller angi en ny oppgavetittel.",
|
||||
"kinds": {
|
||||
"subtask": "Underoppgave | Underoppgaver",
|
||||
"parenttask": "Foreldreoppgaven + Overordnede oppgaver",
|
||||
@@ -1061,7 +1061,7 @@
|
||||
"upcoming": "Gå til kommende oppgaver",
|
||||
"labels": "Naviger til etiketter",
|
||||
"teams": "Naviger til gruppe",
|
||||
"projects": "Navigate to projects"
|
||||
"projects": "Gå til prosjekter"
|
||||
}
|
||||
},
|
||||
"update": {
|
||||
@@ -1100,7 +1100,7 @@
|
||||
"title": "Varsler",
|
||||
"none": "Du har ingen varsler på dette tidspunktet!",
|
||||
"explainer": "Notifications will appear here when actions projects or tasks you subscribed to happen.",
|
||||
"markAllRead": "Mark all notifications as read",
|
||||
"markAllRead": "Marker alle varslinger som lest",
|
||||
"markAllReadSuccess": "Successfully marked all notifications as read."
|
||||
},
|
||||
"quickActions": {
|
||||
@@ -1211,7 +1211,7 @@
|
||||
"time": {
|
||||
"units": {
|
||||
"seconds": "second|seconds",
|
||||
"minutes": "minute|minutes",
|
||||
"minutes": "minutt|minutter",
|
||||
"hours": "hour|hours",
|
||||
"days": "day|days",
|
||||
"weeks": "week|weeks",
|
||||
|
||||
@@ -12,25 +12,27 @@ export const NOTIFICATION_NAMES = {
|
||||
'TASK_REMINDER': 'task.reminder',
|
||||
'PROJECT_CREATED': 'project.created',
|
||||
'TEAM_MEMBER_ADDED': 'team.member.added',
|
||||
'TASK_MENTIONED': 'task.mentioned',
|
||||
} as const
|
||||
|
||||
interface Notification {
|
||||
doer: IUser
|
||||
}
|
||||
interface NotificationTask extends Notification {
|
||||
|
||||
interface NotificationTaskComment extends Notification {
|
||||
task: ITask
|
||||
comment: ITaskComment
|
||||
}
|
||||
|
||||
interface NotificationTask extends Notification {
|
||||
task: ITask
|
||||
}
|
||||
|
||||
interface NotificationAssigned extends Notification {
|
||||
task: ITask
|
||||
assignee: IUser
|
||||
}
|
||||
|
||||
interface NotificationDeleted extends Notification {
|
||||
task: ITask
|
||||
}
|
||||
|
||||
interface NotificationCreated extends Notification {
|
||||
task: ITask
|
||||
project: IProject
|
||||
@@ -49,7 +51,7 @@ interface NotificationMemberAdded extends Notification {
|
||||
export interface INotification extends IAbstract {
|
||||
id: number
|
||||
name: string
|
||||
notification: NotificationTask | NotificationAssigned | NotificationDeleted | NotificationCreated | NotificationMemberAdded | NotificationTaskReminder
|
||||
notification: NotificationTaskComment | NotificationTask | NotificationAssigned | NotificationCreated | NotificationMemberAdded | NotificationTaskReminder
|
||||
read: boolean
|
||||
readAt: Date | null
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type {IAbstract} from './IAbstract'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {IFilters} from '@/modelTypes/ISavedFilter'
|
||||
|
||||
export const PROJECT_VIEW_KINDS = {
|
||||
LIST: 'list',
|
||||
@@ -20,7 +21,7 @@ export type ProjectViewBucketConfigurationMode = typeof PROJECT_VIEW_BUCKET_CONF
|
||||
|
||||
export interface IProjectViewBucketConfiguration {
|
||||
title: string
|
||||
filter: string
|
||||
filter: IFilters
|
||||
}
|
||||
|
||||
export interface IProjectView extends IAbstract {
|
||||
@@ -29,7 +30,7 @@ export interface IProjectView extends IAbstract {
|
||||
projectId: IProject['id']
|
||||
viewKind: ProjectViewKind
|
||||
|
||||
filter: string
|
||||
filter: IFilters
|
||||
position: number
|
||||
|
||||
bucketConfigurationMode: ProjectViewBucketConfigurationMode
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {IAbstract} from './IAbstract'
|
||||
import type {IUser} from './IUser'
|
||||
|
||||
// FIXME: what makes this different from TaskFilterParams?
|
||||
interface Filters {
|
||||
export interface IFilters {
|
||||
sort_by: ('start_date' | 'done' | 'id' | 'position')[],
|
||||
order_by: ('asc' | 'desc')[],
|
||||
filter: string,
|
||||
@@ -14,7 +14,7 @@ export interface ISavedFilter extends IAbstract {
|
||||
id: number
|
||||
title: string
|
||||
description: string
|
||||
filters: Filters
|
||||
filters: IFilters
|
||||
|
||||
owner: IUser
|
||||
created: Date
|
||||
|
||||
@@ -62,6 +62,12 @@ export default class NotificationModel extends AbstractModel<INotification> impl
|
||||
project: new ProjectModel(this.notification.project),
|
||||
}
|
||||
break
|
||||
case NOTIFICATION_NAMES.TASK_MENTIONED:
|
||||
this.notification = {
|
||||
doer: new UserModel(this.notification.doer),
|
||||
task: new TaskModel(this.notification.task),
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
this.created = new Date(this.created)
|
||||
@@ -96,6 +102,8 @@ export default class NotificationModel extends AbstractModel<INotification> impl
|
||||
return `added ${who} to the ${this.notification.team.name} team`
|
||||
case NOTIFICATION_NAMES.TASK_REMINDER:
|
||||
return `Reminder for ${this.notification.task.getTextIdentifier()} ${this.notification.task.title} (${this.notification.project.title})`
|
||||
case NOTIFICATION_NAMES.TASK_MENTIONED:
|
||||
return `${getDisplayName(this.notification.doer)} mentioned you on ${this.notification.task.getTextIdentifier()}`
|
||||
}
|
||||
|
||||
return ''
|
||||
|
||||
@@ -7,7 +7,13 @@ export default class ProjectViewModel extends AbstractModel<IProjectView> implem
|
||||
projectId = 0
|
||||
viewKind: ProjectViewKind = 'list'
|
||||
|
||||
filter = ''
|
||||
filter: IProjectView['filters'] = {
|
||||
sort_by: ['done', 'id'],
|
||||
order_by: ['asc', 'desc'],
|
||||
filter: 'done = false',
|
||||
filter_include_nulls: true,
|
||||
s: '',
|
||||
}
|
||||
position = 0
|
||||
|
||||
bucketConfiguration = []
|
||||
|
||||
@@ -75,6 +75,16 @@ describe('Parse Task Text', () => {
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
})
|
||||
it('should recognize tonight', () => {
|
||||
const result = parseTaskText('Lorem Ipsum tonight')
|
||||
|
||||
expect(result.text).toBe('Lorem Ipsum')
|
||||
const now = new Date()
|
||||
expect(result?.date?.getFullYear()).toBe(now.getFullYear())
|
||||
expect(result?.date?.getMonth()).toBe(now.getMonth())
|
||||
expect(result?.date?.getDate()).toBe(now.getDate())
|
||||
expect(result?.date?.getHours()).toBe(21)
|
||||
})
|
||||
describe('should recognize today with a time', () => {
|
||||
const cases = {
|
||||
'at 15:00': '15:0',
|
||||
@@ -87,7 +97,7 @@ describe('Parse Task Text', () => {
|
||||
'at 3:12 am': '3:12',
|
||||
'at 3:12 pm': '15:12',
|
||||
} as const
|
||||
|
||||
|
||||
for (const c in cases) {
|
||||
it(`should recognize today with a time ${c}`, () => {
|
||||
const result = parseTaskText(`Lorem Ipsum today ${c}`)
|
||||
|
||||
@@ -4,6 +4,8 @@ import TaskModel from '@/models/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import BucketModel from '@/models/bucket'
|
||||
|
||||
export type ExpandTaskFilterParam = 'subtasks' | null
|
||||
|
||||
export interface TaskFilterParams {
|
||||
sort_by: ('start_date' | 'end_date' | 'due_date' | 'done' | 'id' | 'position')[],
|
||||
order_by: ('asc' | 'desc')[],
|
||||
@@ -12,7 +14,7 @@ export interface TaskFilterParams {
|
||||
filter_timezone?: string,
|
||||
s: string,
|
||||
per_page?: number,
|
||||
expand?: 'subtasks' | null,
|
||||
expand?: ExpandTaskFilterParam,
|
||||
}
|
||||
|
||||
export function getDefaultTaskFilterParams(): TaskFilterParams {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {ref, readonly} from 'vue'
|
||||
import {ref, computed, readonly} from 'vue'
|
||||
import {defineStore, acceptHMRUpdate} from 'pinia'
|
||||
import {findIndexById} from '@/helpers/utils'
|
||||
|
||||
@@ -23,11 +23,14 @@ export const useAttachmentStore = defineStore('attachment', () => {
|
||||
console.debug('Remove attachement', id)
|
||||
}
|
||||
|
||||
const hasAttachments = computed(() => attachments.value.length > 0)
|
||||
|
||||
return {
|
||||
attachments: readonly(attachments),
|
||||
set,
|
||||
add,
|
||||
removeById,
|
||||
hasAttachments,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import {success} from '@/message'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import {getSavedFilterIdFromProjectId} from '@/services/savedFilter'
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import {RIGHTS} from '@/constants/rights.ts'
|
||||
|
||||
const {add, remove, search, update} = createNewIndexer('projects', ['title', 'description'])
|
||||
|
||||
@@ -43,6 +44,24 @@ export const useProjectStore = defineStore('project', () => {
|
||||
return (id: IProject['id']) => projectsArray.value.filter(p => p.parentProjectId === id)
|
||||
})
|
||||
|
||||
const getAncestors = computed(() => {
|
||||
return (project: IProject): IProject[] => {
|
||||
if (typeof project === 'undefined') {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!project?.parentProjectId) {
|
||||
return [project]
|
||||
}
|
||||
|
||||
const parentProject = projects.value[project.parentProjectId]
|
||||
return [
|
||||
...(parentProject ? getAncestors.value(parentProject) : []),
|
||||
project,
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
const findProjectByExactname = computed(() => {
|
||||
return (name: string) => {
|
||||
const project = projectsArray.value.find(l => {
|
||||
@@ -200,22 +219,6 @@ export const useProjectStore = defineStore('project', () => {
|
||||
|
||||
return loadedProjects
|
||||
}
|
||||
|
||||
function getAncestors(project: IProject): IProject[] {
|
||||
if (typeof project === 'undefined') {
|
||||
return []
|
||||
}
|
||||
|
||||
if (!project?.parentProjectId) {
|
||||
return [project]
|
||||
}
|
||||
|
||||
const parentProject = projects.value[project.parentProjectId]
|
||||
return [
|
||||
...(parentProject ? getAncestors(parentProject) : []),
|
||||
project,
|
||||
]
|
||||
}
|
||||
|
||||
function setProjectView(view: IProjectView) {
|
||||
const views = [...projects.value[view.projectId].views]
|
||||
@@ -306,6 +309,9 @@ export function useProject(projectId: MaybeRefOrGetter<IProject['id']>) {
|
||||
})
|
||||
|
||||
const duplicate = await projectDuplicateService.create(projectDuplicate)
|
||||
if (duplicate.duplicatedProject) {
|
||||
duplicate.duplicatedProject.maxRight = RIGHTS.ADMIN
|
||||
}
|
||||
|
||||
projectStore.setProject(duplicate.duplicatedProject)
|
||||
success({message: t('project.duplicate.success')})
|
||||
|
||||
@@ -28,7 +28,7 @@ import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
import ProjectUserService from '@/services/projectUsers'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {type TaskFilterParams} from '@/services/taskCollection'
|
||||
import TaskCollectionService, {type TaskFilterParams} from '@/services/taskCollection'
|
||||
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
||||
import {REPEAT_TYPES} from '@/types/IRepeatAfter'
|
||||
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
||||
@@ -134,15 +134,16 @@ export const useTaskStore = defineStore('task', () => {
|
||||
if (!params.filter_timezone || params.filter_timezone === '') {
|
||||
params.filter_timezone = authStore.settings.timezone
|
||||
}
|
||||
|
||||
if (projectId !== null) {
|
||||
params.filter = 'project = '+projectId+' && (' + params.filter +')'
|
||||
}
|
||||
|
||||
const cancel = setModuleLoading(setIsLoading)
|
||||
try {
|
||||
const taskService = new TaskService()
|
||||
tasks.value = await taskService.getAll({}, params)
|
||||
const model = {}
|
||||
let taskCollectionService = new TaskService()
|
||||
if (projectId !== null) {
|
||||
model.projectId = projectId
|
||||
taskCollectionService = new TaskCollectionService()
|
||||
}
|
||||
tasks.value = await taskCollectionService.getAll(model, params)
|
||||
baseStore.setHasTasks(tasks.value.length > 0)
|
||||
return tasks.value
|
||||
} finally {
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
class="is-max-width-desktop"
|
||||
@taskAdded="updateTaskKey"
|
||||
/>
|
||||
<template v-if="!hasTasks && !loading && migratorsEnabled">
|
||||
<template v-if="tasksLoaded && !hasTasks && !loading && migratorsEnabled">
|
||||
<p class="mt-4">
|
||||
{{ $t('home.project.importText') }}
|
||||
</p>
|
||||
@@ -49,6 +49,7 @@
|
||||
v-if="projectStore.hasProjects"
|
||||
:key="showTasksKey"
|
||||
class="show-tasks"
|
||||
@tasksLoaded="tasksLoaded = true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@@ -91,6 +92,8 @@ const projectHistory = computed(() => {
|
||||
.filter(l => Boolean(l))
|
||||
})
|
||||
|
||||
const tasksLoaded = ref(false)
|
||||
|
||||
const migratorsEnabled = computed(() => configStore.availableMigrators?.length > 0)
|
||||
const hasTasks = computed(() => baseStore.hasTasks)
|
||||
const loading = computed(() => taskStore.isLoading)
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, watchEffect} from 'vue'
|
||||
import {computed, shallowReactive, watchEffect} from 'vue'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useRoute, useRouter} from 'vue-router'
|
||||
@@ -46,38 +46,47 @@ const projectStore = useProjectStore()
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const totalTasks = ref<number | null>(null)
|
||||
const projectId = computed(() => Number(route.params.projectId))
|
||||
|
||||
const project = computed(() => projectStore.projects[route.params.projectId])
|
||||
const childProjectIds = ref<number[]>([])
|
||||
const project = computed(() => projectStore.projects[projectId.value])
|
||||
|
||||
const projectIdsToDelete = computed(() => {
|
||||
if (!projectId.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
return [
|
||||
...projectStore
|
||||
.getChildProjects(projectId.value)
|
||||
.map(p => p.id),
|
||||
projectId.value,
|
||||
]
|
||||
})
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
watchEffect(
|
||||
() => {
|
||||
if (!route.params.projectId) {
|
||||
async () => {
|
||||
if (!projectIdsToDelete.value.length) {
|
||||
return
|
||||
}
|
||||
|
||||
childProjectIds.value = projectStore.getChildProjects(parseInt(route.params.projectId)).map(p => p.id)
|
||||
if (childProjectIds.value.length === 0) {
|
||||
childProjectIds.value = [parseInt(route.params.projectId)]
|
||||
}
|
||||
|
||||
const taskService = new TaskService()
|
||||
taskService.getAll({}, {filter: `project in ${childProjectIds.value.join(',')}`}).then(() => {
|
||||
totalTasks.value = taskService.totalPages * taskService.resultCount
|
||||
})
|
||||
await taskService.getAll({}, {filter: `project in ${projectIdsToDelete.value.join(',')}`})
|
||||
},
|
||||
)
|
||||
const totalTasks = computed(() => taskService.totalPages * taskService.resultCount)
|
||||
|
||||
useTitle(() => t('project.delete.title', {project: project?.value?.title}))
|
||||
|
||||
const deleteNotice = computed(() => {
|
||||
if(totalTasks.value && totalTasks.value > 0 && childProjectIds.value.length <= 1) {
|
||||
return t('project.delete.tasksToDelete', {count: totalTasks.value})
|
||||
}
|
||||
|
||||
if(totalTasks.value && totalTasks.value > 0 && childProjectIds.value.length > 1) {
|
||||
return t('project.delete.tasksAndChildProjectsToDelete', {tasks: totalTasks.value, projects: childProjectIds.value.length})
|
||||
if (totalTasks.value && totalTasks.value > 0) {
|
||||
if (projectIdsToDelete.value.length <= 1) {
|
||||
return t('project.delete.tasksToDelete', {count: totalTasks.value})
|
||||
}
|
||||
|
||||
if (projectIdsToDelete.value.length > 1) {
|
||||
return t('project.delete.tasksAndChildProjectsToDelete', {tasks: totalTasks.value, projects: projectIdsToDelete.value.length})
|
||||
}
|
||||
}
|
||||
|
||||
return t('project.delete.noTasksToDelete')
|
||||
|
||||
@@ -98,8 +98,14 @@ const props = withDefaults(defineProps<{
|
||||
showOverdue: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'tasksLoaded': true,
|
||||
}>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const taskStore = useTaskStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
@@ -108,8 +114,6 @@ const tasks = ref<ITask[]>([])
|
||||
const showNothingToDo = ref<boolean>(false)
|
||||
const taskCollectionService = ref(new TaskCollectionService())
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
setTimeout(() => showNothingToDo.value = true, 100)
|
||||
|
||||
const showAll = computed(() => typeof props.dateFrom === 'undefined' || typeof props.dateTo === 'undefined')
|
||||
@@ -200,13 +204,14 @@ async function loadPendingTasks(from: Date|string, to: Date|string) {
|
||||
}
|
||||
}
|
||||
|
||||
let projectId = null
|
||||
const filterId = authStore.settings.frontendSettings.filterIdUsedOnOverview
|
||||
if (showAll.value && filterId && typeof projectStore.projects[filterId] !== 'undefined') {
|
||||
tasks.value = await taskCollectionService.value.getAll({projectId: filterId}, params)
|
||||
return
|
||||
projectId = filterId
|
||||
}
|
||||
|
||||
tasks.value = await taskStore.loadTasks(params)
|
||||
tasks.value = await taskStore.loadTasks(params, projectId)
|
||||
emit('tasksLoaded', true)
|
||||
}
|
||||
|
||||
// FIXME: this modification should happen in the store
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
<div
|
||||
class="loader-container task-view-container"
|
||||
:class="{
|
||||
'is-loading': taskService.loading || !visible,
|
||||
'is-loading': isLoading || !isReady,
|
||||
'is-modal': isModal,
|
||||
}"
|
||||
>
|
||||
<!-- Removing everything until the task is loaded to prevent empty initialization of other components -->
|
||||
<div
|
||||
v-if="visible"
|
||||
v-if="isReady"
|
||||
class="task-view"
|
||||
>
|
||||
<Heading
|
||||
@@ -24,7 +24,7 @@
|
||||
class="subtitle"
|
||||
>
|
||||
<template
|
||||
v-for="p in projectStore.getAncestors(project)"
|
||||
v-for="p in ancestorProjects"
|
||||
:key="p.id"
|
||||
>
|
||||
<a
|
||||
@@ -117,7 +117,7 @@
|
||||
:ref="e => setFieldRef('dueDate', e)"
|
||||
v-model="task.dueDate"
|
||||
:choose-date-label="$t('task.detail.chooseDueDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
:disabled="isLoading || !canWrite"
|
||||
@closeOnChange="saveTask()"
|
||||
/>
|
||||
<BaseButton
|
||||
@@ -171,7 +171,7 @@
|
||||
:ref="e => setFieldRef('startDate', e)"
|
||||
v-model="task.startDate"
|
||||
:choose-date-label="$t('task.detail.chooseStartDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
:disabled="isLoading || !canWrite"
|
||||
@closeOnChange="saveTask()"
|
||||
/>
|
||||
<BaseButton
|
||||
@@ -204,7 +204,7 @@
|
||||
:ref="e => setFieldRef('endDate', e)"
|
||||
v-model="task.endDate"
|
||||
:choose-date-label="$t('task.detail.chooseEndDate')"
|
||||
:disabled="taskService.loading || !canWrite"
|
||||
:disabled="isLoading || !canWrite"
|
||||
@closeOnChange="saveTask()"
|
||||
/>
|
||||
<BaseButton
|
||||
@@ -320,7 +320,7 @@
|
||||
<Description
|
||||
:model-value="task"
|
||||
:can-write="canWrite"
|
||||
:attachment-upload="attachmentUpload"
|
||||
:attachment-upload="uploadAttachment"
|
||||
@update:modelValue="Object.assign(task, $event)"
|
||||
/>
|
||||
</div>
|
||||
@@ -415,10 +415,9 @@
|
||||
{{ task.done ? $t('task.detail.undone') : $t('task.detail.done') }}
|
||||
</x-button>
|
||||
<TaskSubscription
|
||||
v-model="task.subscription"
|
||||
entity="task"
|
||||
:entity-id="task.id"
|
||||
:model-value="task.subscription"
|
||||
@update:modelValue="sub => task.subscription = sub"
|
||||
/>
|
||||
<x-button
|
||||
v-shortcut="'s'"
|
||||
@@ -585,21 +584,14 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, reactive, shallowReactive, computed, watch, nextTick, onMounted, onBeforeUnmount} from 'vue'
|
||||
import {ref, reactive, computed, nextTick, watchPostEffect, watch} from 'vue'
|
||||
import {useRouter, type RouteLocation} from 'vue-router'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {unrefElement} from '@vueuse/core'
|
||||
import {klona} from 'klona/lite'
|
||||
import {unrefElement, useEventListener} from '@vueuse/core'
|
||||
import {eventToHotkeyString} from '@github/hotkey'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel from '@/models/task'
|
||||
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import {PRIORITIES, type Priority} from '@/constants/priorities'
|
||||
import {RIGHTS} from '@/constants/rights'
|
||||
import {PRIORITIES} from '@/constants/priorities'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
@@ -625,23 +617,14 @@ import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
|
||||
import Reactions from '@/components/input/Reactions.vue'
|
||||
|
||||
import {uploadFile} from '@/helpers/attachments'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
import {scrollIntoView} from '@/helpers/scrollIntoView'
|
||||
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
||||
import {playPopSound} from '@/helpers/playPop'
|
||||
|
||||
import {useAttachmentStore} from '@/stores/attachments'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useKanbanStore} from '@/stores/kanban'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useBaseStore} from '@/stores/base'
|
||||
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
|
||||
import {success} from '@/message'
|
||||
import type {Action as MessageAction} from '@/message'
|
||||
import {useTask} from '@/composables/useTask'
|
||||
|
||||
const props = defineProps<{
|
||||
taskId: ITask['id'],
|
||||
@@ -653,99 +636,59 @@ defineEmits<{
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
const attachmentStore = useAttachmentStore()
|
||||
const taskStore = useTaskStore()
|
||||
const kanbanStore = useKanbanStore()
|
||||
const authStore = useAuthStore()
|
||||
const baseStore = useBaseStore()
|
||||
|
||||
const task = ref<ITask>(new TaskModel())
|
||||
const taskTitle = computed(() => task.value.title)
|
||||
const {
|
||||
project,
|
||||
ancestorProjects,
|
||||
|
||||
isReady,
|
||||
isLoading,
|
||||
|
||||
task,
|
||||
taskTitle,
|
||||
taskColor,
|
||||
|
||||
canWrite,
|
||||
hasAttachments,
|
||||
|
||||
saveTask,
|
||||
deleteTask,
|
||||
uploadAttachment,
|
||||
|
||||
toggleTaskDone,
|
||||
changeProject,
|
||||
toggleFavorite,
|
||||
setPriority,
|
||||
setPercentDone,
|
||||
removeRepeatAfter,
|
||||
} = useTask(() => props.taskId)
|
||||
|
||||
useTitle(taskTitle)
|
||||
|
||||
|
||||
// See https://github.com/github/hotkey/discussions/85#discussioncomment-5214660
|
||||
function saveTaskViaHotkey(event) {
|
||||
useEventListener('keydown', (event) => {
|
||||
const hotkeyString = eventToHotkeyString(event)
|
||||
if (!hotkeyString) return
|
||||
if (hotkeyString !== 'Control+s' && hotkeyString !== 'Meta+s') return
|
||||
event.preventDefault()
|
||||
|
||||
saveTask()
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
document.addEventListener('keydown', saveTaskViaHotkey)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('keydown', saveTaskViaHotkey)
|
||||
})
|
||||
|
||||
// We doubled the task color property here because verte does not have a real change property, leading
|
||||
// to the color property change being triggered when the # is removed from it, leading to an update,
|
||||
// which leads in turn to a change... This creates an infinite loop in which the task is updated, changed,
|
||||
// updated, changed, updated and so on.
|
||||
// To prevent this, we put the task color property in a seperate value which is set to the task color
|
||||
// when it is saved and loaded.
|
||||
const taskColor = ref<ITask['hexColor']>('')
|
||||
|
||||
// Used to avoid flashing of empty elements if the task content is not yet loaded.
|
||||
const visible = ref(false)
|
||||
|
||||
const project = computed(() => projectStore.projects[task.value.projectId])
|
||||
|
||||
const canWrite = computed(() => (
|
||||
task.value.maxRight !== null &&
|
||||
task.value.maxRight > RIGHTS.READ
|
||||
))
|
||||
|
||||
const color = computed(() => {
|
||||
const color = task.value.getHexColor
|
||||
? task.value.getHexColor()
|
||||
: undefined
|
||||
|
||||
return color
|
||||
})
|
||||
|
||||
const hasAttachments = computed(() => attachmentStore.attachments.length > 0)
|
||||
const color = computed(() => task?.getHexColor())
|
||||
|
||||
const isModal = computed(() => Boolean(props.backdropView))
|
||||
|
||||
function attachmentUpload(file: File, onSuccess?: (url: string) => void) {
|
||||
return uploadFile(props.taskId, file, onSuccess)
|
||||
}
|
||||
|
||||
const heading = ref<HTMLElement | null>(null)
|
||||
|
||||
async function scrollToHeading() {
|
||||
scrollIntoView(unrefElement(heading))
|
||||
}
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
|
||||
// load task
|
||||
watch(
|
||||
() => props.taskId,
|
||||
async (id) => {
|
||||
if (id === undefined) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
const loaded = await taskService.get({id})
|
||||
Object.assign(task.value, loaded)
|
||||
attachmentStore.set(task.value.attachments)
|
||||
taskColor.value = task.value.hexColor
|
||||
setActiveFields()
|
||||
} finally {
|
||||
await nextTick()
|
||||
scrollToHeading()
|
||||
visible.value = true
|
||||
}
|
||||
}, {immediate: true})
|
||||
watchPostEffect(() => {
|
||||
if (isReady.value) {
|
||||
scrollIntoView(unrefElement(heading))
|
||||
}
|
||||
})
|
||||
|
||||
type FieldType =
|
||||
| 'assignees'
|
||||
@@ -784,17 +727,19 @@ function setActiveFields() {
|
||||
// task.endDate = task.endDate || null
|
||||
|
||||
// Set all active fields based on values in the model
|
||||
activeFields.assignees = task.value.assignees.length > 0
|
||||
activeFields.attachments = task.value.attachments.length > 0
|
||||
activeFields.dueDate = task.value.dueDate !== null
|
||||
activeFields.endDate = task.value.endDate !== null
|
||||
activeFields.labels = task.value.labels.length > 0
|
||||
activeFields.percentDone = task.value.percentDone > 0
|
||||
activeFields.priority = task.value.priority !== PRIORITIES.UNSET
|
||||
activeFields.relatedTasks = Object.keys(task.value.relatedTasks).length > 0
|
||||
activeFields.reminders = task.value.reminders.length > 0
|
||||
activeFields.repeatAfter = task.value.repeatAfter?.amount > 0 || task.value.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||
activeFields.startDate = task.value.startDate !== null
|
||||
Object.assign(activeFields, {
|
||||
assignees: task.assignees.length > 0,
|
||||
attachments: task.attachments.length > 0,
|
||||
dueDate: task.dueDate !== null,
|
||||
endDate: task.endDate !== null,
|
||||
labels: task.labels.length > 0,
|
||||
percentDone: task.percentDone > 0,
|
||||
priority: task.priority !== PRIORITIES.UNSET,
|
||||
relatedTasks: Object.keys(task.relatedTasks).length > 0,
|
||||
reminders: task.reminders.length > 0,
|
||||
repeatAfter: task.repeatAfter?.amount > 0 || task.repeatMode !== TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT,
|
||||
startDate: task.startDate !== null,
|
||||
})
|
||||
}
|
||||
|
||||
const activeFieldElements: { [id in FieldType]: HTMLElement | null } = reactive({
|
||||
@@ -833,106 +778,8 @@ function setFieldActive(fieldName: keyof typeof activeFields) {
|
||||
})
|
||||
}
|
||||
|
||||
async function saveTask(
|
||||
currentTask: ITask | null = null,
|
||||
undoCallback?: () => void,
|
||||
) {
|
||||
if (currentTask === null) {
|
||||
currentTask = klona(task.value)
|
||||
}
|
||||
|
||||
if (!canWrite.value) {
|
||||
return
|
||||
}
|
||||
|
||||
currentTask.hexColor = taskColor.value
|
||||
|
||||
// If no end date is being set, but a start date and due date,
|
||||
// use the due date as the end date
|
||||
if (
|
||||
currentTask.endDate === null &&
|
||||
currentTask.startDate !== null &&
|
||||
currentTask.dueDate !== null
|
||||
) {
|
||||
currentTask.endDate = currentTask.dueDate
|
||||
}
|
||||
|
||||
const updatedTask = await taskStore.update(currentTask) // TODO: markraw ?
|
||||
Object.assign(task.value, updatedTask)
|
||||
setActiveFields()
|
||||
|
||||
let actions: MessageAction[] = []
|
||||
if (undoCallback) {
|
||||
actions = [{
|
||||
title: t('task.undo'),
|
||||
callback: undoCallback,
|
||||
}]
|
||||
}
|
||||
success({message: t('task.detail.updateSuccess')}, actions)
|
||||
}
|
||||
|
||||
const showDeleteModal = ref(false)
|
||||
|
||||
async function deleteTask() {
|
||||
await taskStore.delete(task.value)
|
||||
success({message: t('task.detail.deleteSuccess')})
|
||||
router.push({name: 'project.index', params: {projectId: task.value.projectId}})
|
||||
}
|
||||
|
||||
async function toggleTaskDone() {
|
||||
const newTask = {
|
||||
...task.value,
|
||||
done: !task.value.done,
|
||||
}
|
||||
|
||||
if (newTask.done) {
|
||||
playPopSound()
|
||||
}
|
||||
|
||||
await saveTask(
|
||||
newTask,
|
||||
toggleTaskDone,
|
||||
)
|
||||
}
|
||||
|
||||
async function changeProject(project: IProject) {
|
||||
kanbanStore.removeTaskInBucket(task.value)
|
||||
await saveTask({
|
||||
...task.value,
|
||||
projectId: project.id,
|
||||
})
|
||||
baseStore.setCurrentProject(project)
|
||||
}
|
||||
|
||||
async function toggleFavorite() {
|
||||
const newTask = await taskStore.toggleFavorite(task.value)
|
||||
Object.assign(task.value, newTask)
|
||||
}
|
||||
|
||||
async function setPriority(priority: Priority) {
|
||||
const newTask: ITask = {
|
||||
...task.value,
|
||||
priority,
|
||||
}
|
||||
|
||||
return saveTask(newTask)
|
||||
}
|
||||
|
||||
async function setPercentDone(percentDone: number) {
|
||||
const newTask: ITask = {
|
||||
...task.value,
|
||||
percentDone,
|
||||
}
|
||||
|
||||
return saveTask(newTask)
|
||||
}
|
||||
|
||||
async function removeRepeatAfter() {
|
||||
task.value.repeatAfter.amount = 0
|
||||
task.value.repeatMode = TASK_REPEAT_MODES.REPEAT_MODE_DEFAULT
|
||||
await saveTask()
|
||||
}
|
||||
|
||||
function setRelatedTasksActive() {
|
||||
setFieldActive('relatedTasks')
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": [
|
||||
"@tsconfig/node20/tsconfig.json",
|
||||
"@tsconfig/node22/tsconfig.json",
|
||||
"@vue/tsconfig/tsconfig.json"
|
||||
],
|
||||
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*"],
|
||||
|
||||
+9
-17
@@ -2,7 +2,6 @@
|
||||
import {defineConfig, type PluginOption, loadEnv} from 'vite'
|
||||
import {configDefaults} from 'vitest/config'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import legacyFn from '@vitejs/plugin-legacy'
|
||||
import {URL, fileURLToPath} from 'node:url'
|
||||
import {dirname, resolve} from 'node:path'
|
||||
|
||||
@@ -14,9 +13,9 @@ import {visualizer} from 'rollup-plugin-visualizer'
|
||||
import viteSentry, {type ViteSentryPluginOptions} from 'vite-plugin-sentry'
|
||||
import svgLoader from 'vite-svg-loader'
|
||||
import postcssPresetEnv from 'postcss-preset-env'
|
||||
import postcssEasings from 'postcss-easings'
|
||||
import postcssEasingGradients from 'postcss-easing-gradients'
|
||||
import tailwindcss from 'tailwindcss'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
const pathSrc = fileURLToPath(new URL('./src', import.meta.url)).replaceAll('\\', '/')
|
||||
|
||||
@@ -24,16 +23,6 @@ const pathSrc = fileURLToPath(new URL('./src', import.meta.url)).replaceAll('\\'
|
||||
const PREFIXED_SCSS_STYLES = `@use "sass:math";
|
||||
@import "${pathSrc}/styles/common-imports.scss";`
|
||||
|
||||
const isModernBuild = Boolean(process.env.BUILD_MODERN_ONLY)
|
||||
const legacy = isModernBuild
|
||||
? undefined
|
||||
: legacyFn()
|
||||
|
||||
console.log(isModernBuild
|
||||
? 'Building "modern-only" build'
|
||||
: 'Building "legacy" build with "@vitejs/plugin-legacy"',
|
||||
)
|
||||
|
||||
/*
|
||||
** Configure sentry plugin
|
||||
*/
|
||||
@@ -93,17 +82,18 @@ export default defineConfig(({mode}) => {
|
||||
},
|
||||
css: {
|
||||
preprocessorOptions: {
|
||||
sass: {
|
||||
quietDeps: true, // silence deprecation warnings
|
||||
},
|
||||
scss: {
|
||||
api: 'modern-compiler',
|
||||
additionalData: PREFIXED_SCSS_STYLES,
|
||||
charset: false, // fixes "@charset" must be the first rule in the file" warnings,
|
||||
quietDeps: true, // silence bulmas deprecation warnings
|
||||
quietDeps: true, // silence deprecation warnings
|
||||
},
|
||||
},
|
||||
postcss: {
|
||||
plugins: [
|
||||
tailwindcss(),
|
||||
postcssEasings(),
|
||||
postcssEasingGradients(),
|
||||
postcssPresetEnv(),
|
||||
],
|
||||
@@ -115,7 +105,6 @@ export default defineConfig(({mode}) => {
|
||||
propsDestructure: true,
|
||||
},
|
||||
}),
|
||||
legacy,
|
||||
svgLoader({
|
||||
// Since the svgs are already manually optimized via https://jakearchibald.github.io/svgomg/
|
||||
// we don't need to optimize them again.
|
||||
@@ -194,6 +183,9 @@ export default defineConfig(({mode}) => {
|
||||
],
|
||||
},
|
||||
}),
|
||||
vueDevTools({
|
||||
launchEditor: env.VUE_DEVTOOLS_LAUNCH_EDITOR || 'code'
|
||||
}),
|
||||
viteSentry(getSentryConfig(env)),
|
||||
],
|
||||
resolve: {
|
||||
@@ -219,7 +211,7 @@ export default defineConfig(({mode}) => {
|
||||
build: {
|
||||
target: 'esnext',
|
||||
// required for sentry debugging: tells vite to create source maps
|
||||
sourcemap: true,
|
||||
sourcemap: Boolean(env.SENTRY_AUTH_TOKEN),
|
||||
rollupOptions: {
|
||||
plugins: [
|
||||
visualizer({
|
||||
|
||||
@@ -29,9 +29,9 @@ require (
|
||||
github.com/d4l3k/messagediff v1.2.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
github.com/dustinkirkland/golang-petname v0.0.0-20240422154211-76c06c4bde6b
|
||||
github.com/gabriel-vasile/mimetype v1.4.6
|
||||
github.com/gabriel-vasile/mimetype v1.4.7
|
||||
github.com/ganigeorgiev/fexpr v0.4.1
|
||||
github.com/getsentry/sentry-go v0.29.1
|
||||
github.com/getsentry/sentry-go v0.30.0
|
||||
github.com/go-sql-driver/mysql v1.8.1
|
||||
github.com/go-testfixtures/testfixtures/v3 v3.11.0
|
||||
github.com/gocarina/gocsv v0.0.0-20231116093920-b87c2d0e983a
|
||||
@@ -44,8 +44,8 @@ require (
|
||||
github.com/iancoleman/strcase v0.3.0
|
||||
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.12.0
|
||||
github.com/labstack/echo-jwt/v4 v4.3.0
|
||||
github.com/labstack/echo/v4 v4.13.3
|
||||
github.com/labstack/gommon v0.4.2
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/magefile/mage v1.15.0
|
||||
@@ -61,24 +61,23 @@ require (
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/spf13/cobra v1.8.1
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/stretchr/testify v1.9.0
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/swaggo/swag v1.16.4
|
||||
github.com/tkuchiki/go-timezone v0.2.3
|
||||
github.com/typesense/typesense-go/v2 v2.0.0
|
||||
github.com/ulule/limiter/v3 v3.11.2
|
||||
github.com/wneessen/go-mail v0.5.1
|
||||
github.com/wneessen/go-mail v0.5.2
|
||||
github.com/yuin/goldmark v1.7.8
|
||||
golang.org/x/crypto v0.28.0
|
||||
golang.org/x/image v0.21.0
|
||||
golang.org/x/oauth2 v0.23.0
|
||||
golang.org/x/sync v0.8.0
|
||||
golang.org/x/sys v0.26.0
|
||||
golang.org/x/term v0.25.0
|
||||
golang.org/x/text v0.19.0
|
||||
golang.org/x/crypto v0.31.0
|
||||
golang.org/x/image v0.23.0
|
||||
golang.org/x/oauth2 v0.24.0
|
||||
golang.org/x/sync v0.10.0
|
||||
golang.org/x/sys v0.28.0
|
||||
golang.org/x/term v0.27.0
|
||||
golang.org/x/text v0.21.0
|
||||
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
|
||||
@@ -166,12 +165,13 @@ require (
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/mod v0.17.0 // indirect
|
||||
golang.org/x/net v0.30.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/time v0.8.0 // indirect
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect
|
||||
google.golang.org/protobuf v1.34.2 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
@@ -179,4 +179,4 @@ replace github.com/samedi/caldav-go => github.com/kolaente/caldav-go v3.0.1-0.20
|
||||
|
||||
go 1.22
|
||||
|
||||
toolchain go1.23.2
|
||||
toolchain go1.23.4
|
||||
|
||||
@@ -19,10 +19,6 @@ github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0
|
||||
github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/RaveNoX/go-jsoncommentstrip v1.0.0/go.mod h1:78ihd09MekBnJnxpICcwzCMzGrKSKYe4AqU6PDYYpjk=
|
||||
github.com/ThreeDotsLabs/watermill v1.3.7 h1:NV0PSTmuACVEOV4dMxRnmGXrmbz8U83LENOvpHekN7o=
|
||||
github.com/ThreeDotsLabs/watermill v1.3.7/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
|
||||
github.com/ThreeDotsLabs/watermill v1.4.0 h1:c8T4QHY/MuxSXYQ1Cxn93cCZB5lkGgqhYA6L2jh2ghA=
|
||||
github.com/ThreeDotsLabs/watermill v1.4.0/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
|
||||
github.com/ThreeDotsLabs/watermill v1.4.1 h1:gjP6yZH+otMPjV0KsV07pl9TeMm9UQV/gqiuiuG5Drs=
|
||||
github.com/ThreeDotsLabs/watermill v1.4.1/go.mod h1:lBnrLbxOjeMRgcJbv+UiZr8Ylz8RkJ4m6i/VN/Nk+to=
|
||||
github.com/adlio/trello v1.12.0 h1:JqOE2GFHQ9YtEviRRRSnicSxPbt4WFOxhqXzjMOw8lw=
|
||||
@@ -95,16 +91,14 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo
|
||||
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5 h1:J7wGKdGu33ocBOhGy0z653k/lFKLFDPJMG8Gql0kxn4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.5/go.mod h1:ibHel+/kbxn9x2407k1izTA1S81ku1z/DlgOW2QE0M4=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7 h1:SKFKl7kD0RiPdbht0s7hFtjl489WcQ1VyPW8ZzUMYCA=
|
||||
github.com/gabriel-vasile/mimetype v1.4.7/go.mod h1:GDlAgAyIRT27BhFl53XNAFtfjzOkLaF35JdEG0P7LtU=
|
||||
github.com/ganigeorgiev/fexpr v0.4.1 h1:hpUgbUEEWIZhSDBtf4M9aUNfQQ0BZkGRaMePy7Gcx5k=
|
||||
github.com/ganigeorgiev/fexpr v0.4.1/go.mod h1:RyGiGqmeXhEQ6+mlGdnUleLHgtzzu/VGO2WtJkF5drE=
|
||||
github.com/getsentry/sentry-go v0.29.0 h1:YtWluuCFg9OfcqnaujpY918N/AhCCwarIDWOYSBAjCA=
|
||||
github.com/getsentry/sentry-go v0.29.0/go.mod h1:jhPesDAL0Q0W2+2YEuVOvdWmVtdsr1+jtBrlDEVWwLY=
|
||||
github.com/getsentry/sentry-go v0.29.1 h1:DyZuChN8Hz3ARxGVV8ePaNXh1dQ7d76AiB117xcREwA=
|
||||
github.com/getsentry/sentry-go v0.29.1/go.mod h1:x3AtIzN01d6SiWkderzaH28Tm0lgkafpJ5Bm3li39O0=
|
||||
github.com/getsentry/sentry-go v0.30.0 h1:lWUwDnY7sKHaVIoZ9wYqRHJ5iEmoc0pqcRqFkosKzBo=
|
||||
github.com/getsentry/sentry-go v0.30.0/go.mod h1:WU9B9/1/sHDqeV8T+3VwwbjeR5MSXs/6aqG3mqZrezA=
|
||||
github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
|
||||
github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
|
||||
@@ -296,8 +290,20 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
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-jwt/v4 v4.2.1 h1:uzOP05+dcXcCsWkE3DPWv0mlDJ0nco9ORhbMYYieky8=
|
||||
github.com/labstack/echo-jwt/v4 v4.2.1/go.mod h1:oJ+cTmJWs6Fa5TVU5plL09Cs/dfigWeefPWNvnexfcA=
|
||||
github.com/labstack/echo-jwt/v4 v4.3.0 h1:8JcvVCrK9dRkPx/aWY3ZempZLO336Bebh4oAtBcxAv4=
|
||||
github.com/labstack/echo-jwt/v4 v4.3.0/go.mod h1:OlWm3wqfnq3Ma8DLmmH7GiEAz2S7Bj23im2iPMEAR+Q=
|
||||
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/echo/v4 v4.13.0 h1:8DjSi4H/k+RqoOmwXkxW14A2H1pdPdS95+qmdJ4q1Tg=
|
||||
github.com/labstack/echo/v4 v4.13.0/go.mod h1:61j7WN2+bp8V21qerqRs4yVlVTGyOagMBpF0vE7VcmM=
|
||||
github.com/labstack/echo/v4 v4.13.1 h1:q3+CpQlYhJwpRr9+08pX0IdlabTIpnIhrg2AKPSKhFE=
|
||||
github.com/labstack/echo/v4 v4.13.1/go.mod h1:61j7WN2+bp8V21qerqRs4yVlVTGyOagMBpF0vE7VcmM=
|
||||
github.com/labstack/echo/v4 v4.13.2 h1:9aAt4hstpH54qIcqkuUXRLTf+v7yOTfMPWzDtuqLmtA=
|
||||
github.com/labstack/echo/v4 v4.13.2/go.mod h1:uc9gDtHB8UWt3FfbYx0HyxcCuvR4YuPYOxF/1QjoV/c=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/laurent22/ical-go v0.1.1-0.20181107184520-7e5d6ade8eef h1:RZnRnSID1skF35j/15KJ6hKZkdIC/teQClJK5wP5LU4=
|
||||
@@ -337,8 +343,6 @@ github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZ
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/mattn/go-sqlite3 v1.14.23 h1:gbShiuAP1W5j9UOksQ06aiiqPMxYecovVGwmTxWtuw0=
|
||||
github.com/mattn/go-sqlite3 v1.14.23/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk=
|
||||
@@ -395,8 +399,6 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.4.0 h1:wZvl1TIVxKRThZIBiwOOHOGP/1+nZyWBil9Y2XNEDzg=
|
||||
github.com/pquerna/otp v1.4.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/prometheus/client_golang v1.20.4 h1:Tgh3Yr67PaOv/uTqloMsCEdeuFTatm5zIq5+qNN23vI=
|
||||
github.com/prometheus/client_golang v1.20.4/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
|
||||
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
|
||||
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||
@@ -405,10 +407,6 @@ github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G
|
||||
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
|
||||
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
|
||||
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
|
||||
github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4=
|
||||
github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
github.com/redis/go-redis/v9 v9.6.2 h1:w0uvkRbc9KpgD98zcvo5IrVUsn0lXpRMuhNgiHDJzdk=
|
||||
github.com/redis/go-redis/v9 v9.6.2/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA=
|
||||
github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E=
|
||||
github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 h1:OdAsTTz6OkFY5QxjkYwrChwuRruF69c169dPK26NUlk=
|
||||
@@ -476,10 +474,10 @@ github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/swaggo/swag v1.16.3 h1:PnCYjPCah8FK4I26l2F/KQ4yz3sILcVUN3cTlBFA9Pg=
|
||||
github.com/swaggo/swag v1.16.3/go.mod h1:DImHIuOFXKpMFAQjcC7FG4m3Dg4+QuUgUzJmKjI/gRk=
|
||||
github.com/swaggo/swag v1.16.4 h1:clWJtd9LStiG3VeijiCfOVODP6VpHtKdQy9ELFG3s1A=
|
||||
github.com/swaggo/swag v1.16.4/go.mod h1:VBsHJRsDvfYvqoiMKnsdwhNV9LEMHgEDZcyVYX0sxPg=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
@@ -499,10 +497,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wneessen/go-mail v0.4.4 h1:rI8wJzPYymUpUth87vFV3k313bmnid4v+FwhBAYYLFM=
|
||||
github.com/wneessen/go-mail v0.4.4/go.mod h1:zxOlafWCP/r6FEhAaRgH4IC1vg2YXxO0Nar9u0IScZ8=
|
||||
github.com/wneessen/go-mail v0.5.1 h1:3XIiVt4N3oZzHmACyLsp1OTq5/yQuSZWtHliPMD3KsI=
|
||||
github.com/wneessen/go-mail v0.5.1/go.mod h1:kRroJvEq2hOSEPFRiKjN7Csrz0G1w+RpiGR3b6yo+Ck=
|
||||
github.com/wneessen/go-mail v0.5.2 h1:MZKwgHJoRboLJ+EHMLuHpZc95wo+u1xViL/4XSswDT8=
|
||||
github.com/wneessen/go-mail v0.5.2/go.mod h1:kRroJvEq2hOSEPFRiKjN7Csrz0G1w+RpiGR3b6yo+Ck=
|
||||
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
|
||||
github.com/xdg-go/scram v1.1.1/go.mod h1:RaEWvsqvNKKvBPvcKeFjrG2cJqOkHTiyTpzz23ni57g=
|
||||
github.com/xdg-go/stringprep v1.0.3/go.mod h1:W3f5j4i+9rC0kuIEJL0ky1VpHXQU3ocBgklLGvcBnW8=
|
||||
@@ -512,14 +508,6 @@ 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.4 h1:BDXOHExt+A7gwPCJgPIIq7ENvceR7we7rOS9TNoLZeg=
|
||||
github.com/yuin/goldmark v1.7.4/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.5 h1:ekTA+pa72CEE23b+Cr+9Z7Xq8/rpKdRMqqyauGO5RNE=
|
||||
github.com/yuin/goldmark v1.7.5/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.6 h1:cZgJxVh5mL5cu8KOnwxvFJy5TFB0BHUskZZyq7TYbDg=
|
||||
github.com/yuin/goldmark v1.7.6/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.7 h1:5m9rrB1sW3JUMToKFQfb+FGt1U7r57IHu5GrYrG2nqU=
|
||||
github.com/yuin/goldmark v1.7.7/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
@@ -560,17 +548,20 @@ golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||
golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A=
|
||||
golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70=
|
||||
golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
|
||||
golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
|
||||
golang.org/x/crypto v0.29.0 h1:L5SG1JTTXupVV3n6sUqMTeWbjAyfPwoda2DLX8J8FrQ=
|
||||
golang.org/x/crypto v0.29.0/go.mod h1:+F4F4N5hv6v38hfeYwTdx20oUvLLc+QfrE9Ax9HtgRg=
|
||||
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
|
||||
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||
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=
|
||||
golang.org/x/image v0.20.0 h1:7cVCUjQwfL18gyBJOmYvptfSHS8Fb3YUDtfLIZ7Nbpw=
|
||||
golang.org/x/image v0.20.0/go.mod h1:0a88To4CYVBAHp5FXJm8o7QbUl37Vd85ply1vyD8auM=
|
||||
golang.org/x/image v0.21.0 h1:c5qV36ajHpdj4Qi0GnE0jUc/yuo33OLFaa0d+crTD5s=
|
||||
golang.org/x/image v0.21.0/go.mod h1:vUbsLavqK/W303ZroQQVKQ+Af3Yl6Uz1Ppu5J/cLz78=
|
||||
golang.org/x/image v0.22.0 h1:UtK5yLUzilVrkjMAZAZ34DXGpASN8i8pj8g+O+yd10g=
|
||||
golang.org/x/image v0.22.0/go.mod h1:9hPFhljd4zZ1GNSIZJ49sqbp45GKK9t6w+iXvGqZUz4=
|
||||
golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68=
|
||||
golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
@@ -601,12 +592,14 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
|
||||
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
|
||||
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
|
||||
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
|
||||
golang.org/x/oauth2 v0.23.0 h1:PbgcYx2W7i4LvjJWEbf0ngHV6qJYr86PkAV3bXdLEbs=
|
||||
golang.org/x/oauth2 v0.23.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
golang.org/x/net v0.31.0 h1:68CPQngjLL0r2AlUKiSxtQFKvzRVbnzLwMUn5SzcLHo=
|
||||
golang.org/x/net v0.31.0/go.mod h1:P4fl1q7dY2hnZFxEk4pPSkDHF+QqjitcnDjUQyMM+pM=
|
||||
golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
|
||||
golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE=
|
||||
golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI=
|
||||
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=
|
||||
@@ -617,8 +610,11 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
|
||||
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
|
||||
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
|
||||
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.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,10 +646,11 @@ golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
|
||||
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
|
||||
golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s=
|
||||
golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
|
||||
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||
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=
|
||||
@@ -663,10 +660,11 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
|
||||
golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||
golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM=
|
||||
golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8=
|
||||
golang.org/x/term v0.25.0 h1:WtHI/ltw4NvSUig5KARz9h521QvRC8RmF/cuYqifU24=
|
||||
golang.org/x/term v0.25.0/go.mod h1:RPyXicDX+6vLxogjjRxjgD2TKtmAO6NZBsBRfrOLu7M=
|
||||
golang.org/x/term v0.26.0 h1:WEQa6V3Gja/BhNxg540hBip/kkaYtRg3cxg4oXSw4AU=
|
||||
golang.org/x/term v0.26.0/go.mod h1:Si5m1o57C5nBNQo5z1iq+XDijt21BDBDp2bK0QI8e3E=
|
||||
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
|
||||
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
|
||||
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=
|
||||
@@ -678,13 +676,16 @@ golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
|
||||
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
|
||||
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
|
||||
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
|
||||
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
|
||||
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
|
||||
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
|
||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg=
|
||||
golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
@@ -792,8 +793,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
@@ -559,7 +559,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
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/initialize"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(healthcheckCmd)
|
||||
}
|
||||
|
||||
var healthcheckCmd = &cobra.Command{
|
||||
Use: "healthcheck",
|
||||
Short: "Preform a healthcheck on the Vikunja api server",
|
||||
PreRun: func(_ *cobra.Command, _ []string) {
|
||||
initialize.LightInit()
|
||||
},
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
client := &http.Client{
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
host := config.ServiceInterface.GetString()
|
||||
url := "http://%s/health"
|
||||
resp, err := client.Get(fmt.Sprintf(url, host))
|
||||
if err != nil {
|
||||
fmt.Printf("API server is not healthy: %v\n", err)
|
||||
os.Exit(1)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// Check the response status
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
fmt.Println("API server is healthy")
|
||||
os.Exit(0)
|
||||
return
|
||||
}
|
||||
fmt.Printf("API server is not healthy: HTTP %d\n", resp.StatusCode)
|
||||
os.Exit(1)
|
||||
},
|
||||
}
|
||||
@@ -421,6 +421,43 @@ func InitDefaultConfig() {
|
||||
AutoTLSRenewBefore.setDefault("720h") // 30days in hours
|
||||
}
|
||||
|
||||
func GetConfigValueFromFile(configKey string) string {
|
||||
if !strings.HasSuffix(configKey, ".file") {
|
||||
configKey += ".file"
|
||||
}
|
||||
var valuePath = viper.GetString(configKey)
|
||||
if valuePath == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(valuePath, "/") {
|
||||
valuePath = path.Join(ServiceRootpath.GetString(), valuePath)
|
||||
}
|
||||
|
||||
contents, err := os.ReadFile(valuePath)
|
||||
if err == nil {
|
||||
return strings.Trim(string(contents), "\n")
|
||||
}
|
||||
|
||||
log.Fatalf("Failed to read the config file at %s for key %s: %v", valuePath, configKey, err)
|
||||
return ""
|
||||
}
|
||||
|
||||
func readConfigValuesFromFiles() {
|
||||
keys := viper.AllKeys()
|
||||
for _, key := range keys {
|
||||
if strings.Contains(key, "auth.openid.providers") {
|
||||
// Setting openid provider values will remove everything but the value from file
|
||||
continue
|
||||
}
|
||||
// Env is evaluated manually at runtime, so we need to check this for each key
|
||||
value := GetConfigValueFromFile(key)
|
||||
if value != "" {
|
||||
viper.Set(strings.TrimSuffix(key, ".file"), value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// InitConfig initializes the config, sets defaults etc.
|
||||
func InitConfig() {
|
||||
|
||||
@@ -465,6 +502,8 @@ func InitConfig() {
|
||||
log.Info("No config file found, using default or config from environment variables.")
|
||||
}
|
||||
|
||||
readConfigValuesFromFiles()
|
||||
|
||||
if RateLimitStore.GetString() == "keyvalue" {
|
||||
RateLimitStore.Set(KeyvalueType.GetString())
|
||||
}
|
||||
|
||||
@@ -75,7 +75,10 @@ func init() {
|
||||
Title: bc.Title,
|
||||
})
|
||||
}
|
||||
_, err = tx.Where("id = ?", view.ID).Update(newView)
|
||||
_, err = tx.
|
||||
Where("id = ?", view.ID).
|
||||
Cols("id", "bucket_configuration").
|
||||
Update(newView)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type taskCollection20241118123644 struct {
|
||||
Filter string `query:"filter" json:"filter"`
|
||||
}
|
||||
|
||||
type projectViews20241118123644New struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||
Filter *taskCollection20241118123644 `xorm:"json null default null" query:"filter" json:"filter"`
|
||||
}
|
||||
|
||||
func (*projectViews20241118123644New) TableName() string {
|
||||
return "project_views"
|
||||
}
|
||||
|
||||
type projectViews20241118123644 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||
Filter string `xorm:"json null default null" query:"filter" json:"filter"`
|
||||
}
|
||||
|
||||
func (*projectViews20241118123644) TableName() string {
|
||||
return "project_views"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20241118123644",
|
||||
Description: "change filter format",
|
||||
Migrate: func(tx *xorm.Engine) (err error) {
|
||||
oldViews := []*projectViews20241118123644{}
|
||||
|
||||
err = tx.Where("filter != '' AND filter IS NOT NULL").Find(&oldViews)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = tx.Sync(projectViews20241118123644New{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, view := range oldViews {
|
||||
newView := &projectViews20241118123644New{
|
||||
ID: view.ID,
|
||||
Filter: &taskCollection20241118123644{
|
||||
Filter: view.Filter,
|
||||
},
|
||||
}
|
||||
|
||||
_, err = tx.Where("id = ?", view.ID).Update(newView)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
// Vikunja is a to-do list application to facilitate your life.
|
||||
// Copyright 2018-present Vikunja and contributors. All rights reserved.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public Licensee as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public Licensee for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public Licensee
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package migration
|
||||
|
||||
import (
|
||||
"src.techknowlogick.com/xormigrate"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
type projectView20241119115012BucketConfiguration struct {
|
||||
Title string `json:"title"`
|
||||
Filter string `json:"filter"`
|
||||
}
|
||||
|
||||
type projectView20241119115012 struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||
BucketConfiguration []*projectView20241119115012BucketConfiguration `xorm:"json" json:"bucket_configuration"`
|
||||
}
|
||||
|
||||
func (projectView20241119115012) TableName() string {
|
||||
return "project_views"
|
||||
}
|
||||
|
||||
type projectView20241119115012BucketConfigurationNew struct {
|
||||
Title string `json:"title"`
|
||||
Filter *taskCollection20241118123644 `json:"filter"`
|
||||
}
|
||||
|
||||
type projectView20241119115012New struct {
|
||||
ID int64 `xorm:"autoincr not null unique pk" json:"id" param:"view"`
|
||||
BucketConfiguration []*projectView20241119115012BucketConfigurationNew `xorm:"json" json:"bucket_configuration"`
|
||||
}
|
||||
|
||||
func (projectView20241119115012New) TableName() string {
|
||||
return "project_views"
|
||||
}
|
||||
|
||||
func init() {
|
||||
migrations = append(migrations, &xormigrate.Migration{
|
||||
ID: "20241119115012",
|
||||
Description: "change bucket filter format",
|
||||
Migrate: func(tx *xorm.Engine) (err error) {
|
||||
oldViews := []*projectView20241119115012{}
|
||||
|
||||
err = tx.Where("bucket_configuration_mode = 2").Find(&oldViews)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = tx.Sync(projectView20241119115012New{})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, view := range oldViews {
|
||||
newView := &projectView20241119115012New{
|
||||
ID: view.ID,
|
||||
}
|
||||
|
||||
for _, configuration := range view.BucketConfiguration {
|
||||
newView.BucketConfiguration = append(newView.BucketConfiguration, &projectView20241119115012BucketConfigurationNew{
|
||||
Title: configuration.Title,
|
||||
Filter: &taskCollection20241118123644{
|
||||
Filter: configuration.Filter,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
_, err = tx.Where("id = ?", view.ID).Update(newView)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
Rollback: func(tx *xorm.Engine) error {
|
||||
return nil
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -230,7 +230,14 @@ func GetTasksInBucketsForView(s *xorm.Session, view *ProjectView, projects []*Pr
|
||||
|
||||
var bucketFilter = taskPropertyBucketID + " = " + strconv.FormatInt(id, 10)
|
||||
if view.BucketConfigurationMode == BucketConfigurationModeFilter {
|
||||
bucketFilter = "(" + view.BucketConfiguration[id].Filter + ")"
|
||||
bucketFilter = ""
|
||||
if view.BucketConfiguration[id].Filter.Filter != "" {
|
||||
bucketFilter = "(" + view.BucketConfiguration[id].Filter.Filter + ")"
|
||||
}
|
||||
|
||||
if view.BucketConfiguration[id].Filter.Search != "" {
|
||||
opts.search = view.BucketConfiguration[id].Filter.Search
|
||||
}
|
||||
}
|
||||
|
||||
var filterString string
|
||||
|
||||
@@ -663,7 +663,7 @@ func (l *UpdateTaskInSavedFilterViews) Handle(msg *message.Message) (err error)
|
||||
|
||||
filterIDs := []int64{}
|
||||
for _, view := range kanbanFilterViews {
|
||||
filterIDs = append(filterIDs, getSavedFilterIDFromProjectID(view.ProjectID))
|
||||
filterIDs = append(filterIDs, GetSavedFilterIDFromProjectID(view.ProjectID))
|
||||
}
|
||||
|
||||
filters := map[int64]*SavedFilter{}
|
||||
@@ -688,7 +688,7 @@ func (l *UpdateTaskInSavedFilterViews) Handle(msg *message.Message) (err error)
|
||||
viewIDToCleanUp := []int64{}
|
||||
|
||||
for _, view := range kanbanFilterViews {
|
||||
filter, exists := filters[getSavedFilterIDFromProjectID(view.ProjectID)]
|
||||
filter, exists := filters[GetSavedFilterIDFromProjectID(view.ProjectID)]
|
||||
if !exists {
|
||||
log.Debugf("Did not find filter for view %d", view.ID)
|
||||
continue
|
||||
|
||||
@@ -207,6 +207,15 @@ func (n *TeamMemberAddedNotification) Name() string {
|
||||
return "team.member.added"
|
||||
}
|
||||
|
||||
func getOverdueSinceString(until time.Duration) (overdueSince string) {
|
||||
overdueSince = `overdue since ` + utils.HumanizeDuration(until)
|
||||
if until == 0 {
|
||||
overdueSince = `overdue now`
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// UndoneTaskOverdueNotification represents a UndoneTaskOverdueNotification notification
|
||||
type UndoneTaskOverdueNotification struct {
|
||||
User *user.User
|
||||
@@ -220,7 +229,7 @@ func (n *UndoneTaskOverdueNotification) ToMail() *notifications.Mail {
|
||||
return notifications.NewMail().
|
||||
Subject(`Task "`+n.Task.Title+`" (`+n.Project.Title+`) is overdue`).
|
||||
Greeting("Hi "+n.User.GetName()+",").
|
||||
Line(`This is a friendly reminder of the task "`+n.Task.Title+`" (`+n.Project.Title+`) which is overdue since `+utils.HumanizeDuration(until)+` and not yet done.`).
|
||||
Line(`This is a friendly reminder of the task "`+n.Task.Title+`" (`+n.Project.Title+`) which is `+getOverdueSinceString(until)+` and not yet done.`).
|
||||
Action("Open Task", config.ServicePublicURL.GetString()+"tasks/"+strconv.FormatInt(n.Task.ID, 10)).
|
||||
Line("Have a nice day!")
|
||||
}
|
||||
@@ -257,7 +266,7 @@ func (n *UndoneTasksOverdueNotification) ToMail() *notifications.Mail {
|
||||
overdueLine := ""
|
||||
for _, task := range sortedTasks {
|
||||
until := time.Until(task.DueDate).Round(1*time.Hour) * -1
|
||||
overdueLine += `* [` + task.Title + `](` + config.ServicePublicURL.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `) (` + n.Projects[task.ProjectID].Title + `), overdue since ` + utils.HumanizeDuration(until) + "\n"
|
||||
overdueLine += `* [` + task.Title + `](` + config.ServicePublicURL.GetString() + "tasks/" + strconv.FormatInt(task.ID, 10) + `) (` + n.Projects[task.ProjectID].Title + `), ` + getOverdueSinceString(until) + "\n"
|
||||
}
|
||||
|
||||
return notifications.NewMail().
|
||||
|
||||
+13
-13
@@ -136,7 +136,7 @@ var FavoritesPseudoProject = Project{
|
||||
Title: "List",
|
||||
ViewKind: ProjectViewKindList,
|
||||
Position: 100,
|
||||
Filter: "done = false",
|
||||
Filter: &TaskCollection{Filter: "done = false"},
|
||||
},
|
||||
{
|
||||
ID: -2,
|
||||
@@ -265,10 +265,10 @@ func (p *Project) ReadOne(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
// Check for saved filters
|
||||
filterID := getSavedFilterIDFromProjectID(p.ID)
|
||||
filterID := GetSavedFilterIDFromProjectID(p.ID)
|
||||
isFilter := filterID > 0
|
||||
if isFilter {
|
||||
sf, err := getSavedFilterSimpleByID(s, filterID)
|
||||
sf, err := GetSavedFilterSimpleByID(s, filterID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -351,8 +351,8 @@ func getProjectSimple(s *xorm.Session, cond builder.Cond) (project *Project, exi
|
||||
return
|
||||
}
|
||||
|
||||
// GetProjectSimplByTaskID gets a project by a task id
|
||||
func GetProjectSimplByTaskID(s *xorm.Session, taskID int64) (l *Project, err error) {
|
||||
// GetProjectSimpleByTaskID gets a project by a task id
|
||||
func GetProjectSimpleByTaskID(s *xorm.Session, taskID int64) (l *Project, err error) {
|
||||
// We need to re-init our project object, because otherwise xorm creates a "where for every item in that project object,
|
||||
// leading to not finding anything if the id is good, but for example the title is different.
|
||||
var project Project
|
||||
@@ -373,8 +373,8 @@ func GetProjectSimplByTaskID(s *xorm.Session, taskID int64) (l *Project, err err
|
||||
return &project, nil
|
||||
}
|
||||
|
||||
// GetProjectsMapSimplByTaskIDs gets a list of projects by a task ids
|
||||
func GetProjectsMapSimplByTaskIDs(s *xorm.Session, taskIDs []int64) (ps map[int64]*Project, err error) {
|
||||
// GetProjectsMapSimpleByTaskIDs gets a list of projects by a task ids
|
||||
func GetProjectsMapSimpleByTaskIDs(s *xorm.Session, taskIDs []int64) (ps map[int64]*Project, err error) {
|
||||
ps = make(map[int64]*Project)
|
||||
err = s.
|
||||
Select("projects.*").
|
||||
@@ -385,7 +385,7 @@ func GetProjectsMapSimplByTaskIDs(s *xorm.Session, taskIDs []int64) (ps map[int6
|
||||
return
|
||||
}
|
||||
|
||||
func GetProjectsSimplByTaskIDs(s *xorm.Session, taskIDs []int64) (ps []*Project, err error) {
|
||||
func GetProjectsSimpleByTaskIDs(s *xorm.Session, taskIDs []int64) (ps []*Project, err error) {
|
||||
err = s.
|
||||
Select("projects.*").
|
||||
Table(Project{}).
|
||||
@@ -591,7 +591,7 @@ func getSavedFilterProjects(s *xorm.Session, doer *user.User) (savedFiltersProje
|
||||
}
|
||||
|
||||
for _, filter := range savedFilters {
|
||||
filterProject := filter.toProject()
|
||||
filterProject := filter.ToProject()
|
||||
filterProject.Owner = doer
|
||||
savedFiltersProjects = append(savedFiltersProjects, filterProject)
|
||||
}
|
||||
@@ -735,7 +735,7 @@ func addProjectDetails(s *xorm.Session, projects []*Project, a web.Auth) (err er
|
||||
func addMaxRightToProjects(s *xorm.Session, projects []*Project, u *user.User) (err error) {
|
||||
projectIDs := make([]int64, 0, len(projects))
|
||||
for _, project := range projects {
|
||||
if getSavedFilterIDFromProjectID(project.ID) > 0 {
|
||||
if GetSavedFilterIDFromProjectID(project.ID) > 0 {
|
||||
project.MaxRight = RightAdmin
|
||||
continue
|
||||
}
|
||||
@@ -1039,9 +1039,9 @@ func recalculateProjectPositions(s *xorm.Session, parentProjectID int64) (err er
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{id} [post]
|
||||
func (p *Project) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
fid := getSavedFilterIDFromProjectID(p.ID)
|
||||
fid := GetSavedFilterIDFromProjectID(p.ID)
|
||||
if fid > 0 {
|
||||
f, err := getSavedFilterSimpleByID(s, fid)
|
||||
f, err := GetSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1054,7 +1054,7 @@ func (p *Project) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
*p = *f.toProject()
|
||||
*p = *f.ToProject()
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -216,6 +216,16 @@ func duplicateViews(s *xorm.Session, pd *ProjectDuplicate, doer web.Auth, taskMa
|
||||
bucketMap[oldBucketID] = b.ID
|
||||
}
|
||||
|
||||
for _, view := range views {
|
||||
if view.DoneBucketID != 0 {
|
||||
view.DoneBucketID = bucketMap[view.DoneBucketID]
|
||||
err = view.Update(s, doer)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldTaskBuckets := []*TaskBucket{}
|
||||
err = s.In("bucket_id", oldBucketIDs).Find(&oldTaskBuckets)
|
||||
if err != nil {
|
||||
|
||||
@@ -86,8 +86,8 @@ func (p *Project) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
}
|
||||
|
||||
// Saved Filter Projects need a special case
|
||||
if getSavedFilterIDFromProjectID(p.ID) > 0 {
|
||||
sf := &SavedFilter{ID: getSavedFilterIDFromProjectID(p.ID)}
|
||||
if GetSavedFilterIDFromProjectID(p.ID) > 0 {
|
||||
sf := &SavedFilter{ID: GetSavedFilterIDFromProjectID(p.ID)}
|
||||
return sf.CanRead(s, a)
|
||||
}
|
||||
|
||||
@@ -117,9 +117,9 @@ func (p *Project) CanUpdate(s *xorm.Session, a web.Auth) (canUpdate bool, err er
|
||||
return false, nil
|
||||
}
|
||||
|
||||
fid := getSavedFilterIDFromProjectID(p.ID)
|
||||
fid := GetSavedFilterIDFromProjectID(p.ID)
|
||||
if fid > 0 {
|
||||
sf, err := getSavedFilterSimpleByID(s, fid)
|
||||
sf, err := GetSavedFilterSimpleByID(s, fid)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@@ -115,8 +116,8 @@ func (p *BucketConfigurationModeKind) UnmarshalJSON(bytes []byte) error {
|
||||
}
|
||||
|
||||
type ProjectViewBucketConfiguration struct {
|
||||
Title string `json:"title"`
|
||||
Filter string `json:"filter"`
|
||||
Title string `json:"title"`
|
||||
Filter *TaskCollection `json:"filter"`
|
||||
}
|
||||
|
||||
type ProjectView struct {
|
||||
@@ -130,7 +131,7 @@ type ProjectView struct {
|
||||
ViewKind ProjectViewKind `xorm:"not null" json:"view_kind"`
|
||||
|
||||
// The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation.
|
||||
Filter string `xorm:"text null default null" query:"filter" json:"filter"`
|
||||
Filter *TaskCollection `xorm:"json null default null" query:"filter" json:"filter"`
|
||||
// The position of this view in the list. The list of all views will be sorted by this parameter.
|
||||
Position float64 `xorm:"double null" json:"position"`
|
||||
|
||||
@@ -273,6 +274,24 @@ func (pv *ProjectView) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
func createProjectView(s *xorm.Session, p *ProjectView, a web.Auth, createBacklogBucket bool, addExistingTasksToView bool) (err error) {
|
||||
if p.Filter != nil && p.Filter.Filter != "" {
|
||||
_, err = getTaskFiltersFromFilterString(p.Filter.Filter, p.Filter.FilterTimezone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if p.BucketConfigurationMode == BucketConfigurationModeFilter {
|
||||
for _, configuration := range p.BucketConfiguration {
|
||||
if configuration.Filter != nil && configuration.Filter.Filter != "" {
|
||||
_, err = getTaskFiltersFromFilterString(configuration.Filter.Filter, configuration.Filter.FilterTimezone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
p.ID = 0
|
||||
_, err = s.Insert(p)
|
||||
if err != nil {
|
||||
@@ -348,6 +367,13 @@ func addTasksToView(s *xorm.Session, a web.Auth, pv *ProjectView, b *Bucket) (er
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /projects/{project}/views/{id} [post]
|
||||
func (pv *ProjectView) Update(s *xorm.Session, _ web.Auth) (err error) {
|
||||
if pv.Filter != nil && pv.Filter.Filter != "" {
|
||||
_, err = getTaskFiltersFromFilterString(pv.Filter.Filter, pv.Filter.FilterTimezone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the project view exists
|
||||
_, err = GetProjectViewByIDAndProject(s, pv.ID, pv.ProjectID)
|
||||
if err != nil {
|
||||
@@ -428,7 +454,9 @@ func CreateDefaultViewsForProject(s *xorm.Session, project *Project, a web.Auth,
|
||||
Position: 100,
|
||||
}
|
||||
if createDefaultListFilter {
|
||||
list.Filter = "done = false"
|
||||
list.Filter = &TaskCollection{
|
||||
Filter: "done = false",
|
||||
}
|
||||
}
|
||||
err = createProjectView(s, list, a, createBacklogBucket, true)
|
||||
if err != nil {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
func (pv *ProjectView) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
filterID := getSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
filterID := GetSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
if filterID > 0 {
|
||||
sf := &SavedFilter{ID: filterID}
|
||||
return sf.CanRead(s, a)
|
||||
@@ -33,7 +33,7 @@ func (pv *ProjectView) CanRead(s *xorm.Session, a web.Auth) (bool, int, error) {
|
||||
}
|
||||
|
||||
func (pv *ProjectView) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
filterID := getSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
filterID := GetSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
if filterID > 0 {
|
||||
sf := &SavedFilter{ID: filterID}
|
||||
return sf.CanDelete(s, a)
|
||||
@@ -44,7 +44,7 @@ func (pv *ProjectView) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
}
|
||||
|
||||
func (pv *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
filterID := getSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
filterID := GetSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
if filterID > 0 {
|
||||
sf := &SavedFilter{ID: filterID}
|
||||
return sf.CanUpdate(s, a)
|
||||
@@ -55,7 +55,7 @@ func (pv *ProjectView) CanUpdate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
}
|
||||
|
||||
func (pv *ProjectView) CanCreate(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
filterID := getSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
filterID := GetSavedFilterIDFromProjectID(pv.ProjectID)
|
||||
if filterID > 0 {
|
||||
sf := &SavedFilter{ID: filterID}
|
||||
return sf.CanUpdate(s, a)
|
||||
|
||||
@@ -68,9 +68,9 @@ func (sf *SavedFilter) getTaskCollection() *TaskCollection {
|
||||
return sf.Filters
|
||||
}
|
||||
|
||||
// Returns the saved filter ID from a project ID. Will not check if the filter actually exists.
|
||||
// GetSavedFilterIDFromProjectID returns the saved filter ID from a project ID. Will not check if the filter actually exists.
|
||||
// If the returned ID is zero, means that it is probably invalid.
|
||||
func getSavedFilterIDFromProjectID(projectID int64) (filterID int64) {
|
||||
func GetSavedFilterIDFromProjectID(projectID int64) (filterID int64) {
|
||||
// We get the id of the saved filter by multiplying the ProjectID with -1 and subtracting one
|
||||
filterID = projectID*-1 - 1
|
||||
// FilterIDs from projectIDs are always positive
|
||||
@@ -99,7 +99,7 @@ func getSavedFiltersForUser(s *xorm.Session, auth web.Auth) (filters []*SavedFil
|
||||
return
|
||||
}
|
||||
|
||||
func (sf *SavedFilter) toProject() *Project {
|
||||
func (sf *SavedFilter) ToProject() *Project {
|
||||
return &Project{
|
||||
ID: getProjectIDFromSavedFilterID(sf.ID),
|
||||
Title: sf.Title,
|
||||
@@ -123,6 +123,11 @@ func (sf *SavedFilter) toProject() *Project {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters [put]
|
||||
func (sf *SavedFilter) Create(s *xorm.Session, auth web.Auth) (err error) {
|
||||
_, err = getTaskFiltersFromFilterString(sf.Filters.Filter, sf.Filters.FilterTimezone)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sf.OwnerID = auth.GetID()
|
||||
sf.ID = 0
|
||||
_, err = s.Insert(sf)
|
||||
@@ -134,7 +139,7 @@ func (sf *SavedFilter) Create(s *xorm.Session, auth web.Auth) (err error) {
|
||||
return err
|
||||
}
|
||||
|
||||
func getSavedFilterSimpleByID(s *xorm.Session, id int64) (sf *SavedFilter, err error) {
|
||||
func GetSavedFilterSimpleByID(s *xorm.Session, id int64) (sf *SavedFilter, err error) {
|
||||
sf = &SavedFilter{}
|
||||
exists, err := s.
|
||||
Where("id = ?", id).
|
||||
@@ -181,7 +186,7 @@ func (sf *SavedFilter) ReadOne(s *xorm.Session, _ web.Auth) error {
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /filters/{id} [post]
|
||||
func (sf *SavedFilter) Update(s *xorm.Session, _ web.Auth) error {
|
||||
origFilter, err := getSavedFilterSimpleByID(s, sf.ID)
|
||||
origFilter, err := GetSavedFilterSimpleByID(s, sf.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -190,6 +195,11 @@ func (sf *SavedFilter) Update(s *xorm.Session, _ web.Auth) error {
|
||||
sf.Filters = origFilter.Filters
|
||||
}
|
||||
|
||||
_, err = getTaskFiltersFromFilterString(sf.Filters.Filter, sf.Filters.FilterTimezone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("id = ?", sf.ID).
|
||||
Cols(
|
||||
@@ -423,7 +433,7 @@ func RegisterAddTaskToFilterViewCron() {
|
||||
deleteCond := []builder.Cond{}
|
||||
taskIDsToRemove := []int64{}
|
||||
for _, view := range kanbanFilterViews {
|
||||
filterID := getSavedFilterIDFromProjectID(view.ProjectID)
|
||||
filterID := GetSavedFilterIDFromProjectID(view.ProjectID)
|
||||
filter, exists := filters[filterID]
|
||||
if !exists {
|
||||
log.Debugf("%sDid not find filter for view %d", logPrefix, view.ID)
|
||||
|
||||
@@ -55,7 +55,7 @@ func (sf *SavedFilter) canDoFilter(s *xorm.Session, auth web.Auth) (can bool, er
|
||||
return false, ErrSavedFilterNotAvailableForLinkShare{LinkShareID: auth.GetID(), SavedFilterID: sf.ID}
|
||||
}
|
||||
|
||||
sff, err := getSavedFilterSimpleByID(s, sf.ID)
|
||||
sff, err := GetSavedFilterSimpleByID(s, sf.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -38,42 +38,64 @@ func TestSavedFilter_getProjectIDFromFilter(t *testing.T) {
|
||||
|
||||
func TestSavedFilter_getFilterIDFromProjectID(t *testing.T) {
|
||||
t.Run("normal", func(t *testing.T) {
|
||||
assert.Equal(t, int64(1), getSavedFilterIDFromProjectID(-2))
|
||||
assert.Equal(t, int64(1), GetSavedFilterIDFromProjectID(-2))
|
||||
})
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
assert.Equal(t, int64(0), getSavedFilterIDFromProjectID(2))
|
||||
assert.Equal(t, int64(0), GetSavedFilterIDFromProjectID(2))
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilter_Create(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
t.Run("empty filter", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sf := &SavedFilter{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Filters: &TaskCollection{}, // Empty filter
|
||||
}
|
||||
sf := &SavedFilter{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Filters: &TaskCollection{}, // Empty filter
|
||||
}
|
||||
|
||||
u := &user.User{ID: 1}
|
||||
err := sf.Create(s, u)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, u.ID, sf.OwnerID)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
vals := map[string]interface{}{
|
||||
"title": "'test'",
|
||||
"description": "'Lorem Ipsum dolor sit amet'",
|
||||
"filters": "'{\"sort_by\":null,\"order_by\":null,\"filter\":\"\",\"filter_include_nulls\":false}'",
|
||||
"owner_id": 1,
|
||||
}
|
||||
// Postgres can't compare json values directly, see https://dba.stackexchange.com/a/106290/210721
|
||||
if db.Type() == schemas.POSTGRES {
|
||||
vals["filters::jsonb"] = vals["filters"].(string) + "::jsonb"
|
||||
delete(vals, "filters")
|
||||
}
|
||||
db.AssertExists(t, "saved_filters", vals, true)
|
||||
u := &user.User{ID: 1}
|
||||
err := sf.Create(s, u)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, u.ID, sf.OwnerID)
|
||||
err = s.Commit()
|
||||
require.NoError(t, err)
|
||||
vals := map[string]interface{}{
|
||||
"title": "'test'",
|
||||
"description": "'Lorem Ipsum dolor sit amet'",
|
||||
"filters": `'{"s":"","sort_by":null,"order_by":null,"filter":"","filter_include_nulls":false}'`,
|
||||
"owner_id": 1,
|
||||
}
|
||||
// Postgres can't compare json values directly, see https://dba.stackexchange.com/a/106290/210721
|
||||
if db.Type() == schemas.POSTGRES {
|
||||
vals["filters::jsonb"] = vals["filters"].(string) + "::jsonb"
|
||||
delete(vals, "filters")
|
||||
}
|
||||
db.AssertExists(t, "saved_filters", vals, true)
|
||||
})
|
||||
t.Run("invalid filter string", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sf := &SavedFilter{
|
||||
Title: "test",
|
||||
Description: "Lorem Ipsum dolor sit amet",
|
||||
Filters: &TaskCollection{
|
||||
Filter: "foo = value",
|
||||
},
|
||||
}
|
||||
|
||||
u := &user.User{ID: 1}
|
||||
err := sf.Create(s, u)
|
||||
require.Error(t, err)
|
||||
db.AssertMissing(t, "saved_filters", map[string]interface{}{
|
||||
"title": "test",
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilter_ReadOne(t *testing.T) {
|
||||
@@ -134,6 +156,22 @@ func TestSavedFilter_Update(t *testing.T) {
|
||||
"is_favorite": true,
|
||||
}, false)
|
||||
})
|
||||
t.Run("invalid filter string", func(t *testing.T) {
|
||||
db.LoadAndAssertFixtures(t)
|
||||
s := db.NewSession()
|
||||
defer s.Close()
|
||||
|
||||
sf := &SavedFilter{
|
||||
ID: 1,
|
||||
Title: "NewTitle",
|
||||
Description: "", // Explicitly reset the description
|
||||
Filters: &TaskCollection{
|
||||
Filter: "foo = bar",
|
||||
},
|
||||
}
|
||||
err := sf.Update(s, &user.User{ID: 1})
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestSavedFilter_Delete(t *testing.T) {
|
||||
|
||||
@@ -24,7 +24,7 @@ import (
|
||||
// CanCreate checks if a user can subscribe to an entity
|
||||
func (sb *Subscription) CanCreate(s *xorm.Session, a web.Auth) (can bool, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, &ErrGenericForbidden{}
|
||||
return false, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
sb.EntityType = getEntityTypeFromString(sb.Entity)
|
||||
@@ -46,7 +46,7 @@ func (sb *Subscription) CanCreate(s *xorm.Session, a web.Auth) (can bool, err er
|
||||
// CanDelete checks if a user can delete a subscription
|
||||
func (sb *Subscription) CanDelete(s *xorm.Session, a web.Auth) (can bool, err error) {
|
||||
if _, is := a.(*LinkSharing); is {
|
||||
return false, &ErrGenericForbidden{}
|
||||
return false, ErrGenericForbidden{}
|
||||
}
|
||||
|
||||
sb.EntityType = getEntityTypeFromString(sb.Entity)
|
||||
|
||||
@@ -20,13 +20,11 @@ import (
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"xorm.io/builder"
|
||||
|
||||
"code.vikunja.io/api/pkg/events"
|
||||
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
@@ -219,7 +217,7 @@ func (la *TaskAssginee) Delete(s *xorm.Session, a web.Auth) (err error) {
|
||||
func (la *TaskAssginee) Create(s *xorm.Session, a web.Auth) (err error) {
|
||||
|
||||
// Get the project to perform later checks
|
||||
project, err := GetProjectSimplByTaskID(s, la.TaskID)
|
||||
project, err := GetProjectSimpleByTaskID(s, la.TaskID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -314,7 +312,7 @@ func (t *Task) addNewAssigneeByID(s *xorm.Session, newAssigneeID int64, project
|
||||
// @Failure 500 {object} models.Message "Internal error"
|
||||
// @Router /tasks/{taskID}/assignees [get]
|
||||
func (la *TaskAssginee) ReadAll(s *xorm.Session, a web.Auth, search string, page int, perPage int) (result interface{}, resultCount int, numberOfTotalItems int64, err error) {
|
||||
task, err := GetProjectSimplByTaskID(s, la.TaskID)
|
||||
task, err := GetProjectSimpleByTaskID(s, la.TaskID)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ func (la *TaskAssginee) CanDelete(s *xorm.Session, a web.Auth) (bool, error) {
|
||||
|
||||
func canDoTaskAssingee(s *xorm.Session, taskID int64, a web.Auth) (bool, error) {
|
||||
// Check if the current user can edit the project
|
||||
project, err := GetProjectSimplByTaskID(s, taskID)
|
||||
project, err := GetProjectSimpleByTaskID(s, taskID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
@@ -139,7 +139,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)
|
||||
|
||||
@@ -32,6 +32,8 @@ type TaskCollection struct {
|
||||
ProjectID int64 `param:"project" json:"-"`
|
||||
ProjectViewID int64 `param:"view" json:"-"`
|
||||
|
||||
Search string `query:"s" json:"s"`
|
||||
|
||||
// The query parameter to sort by. This is for ex. done, priority, etc.
|
||||
SortBy []string `query:"sort_by" json:"sort_by"`
|
||||
SortByArr []string `query:"sort_by[]" json:"-"`
|
||||
@@ -181,6 +183,10 @@ func getRelevantProjectsFromCollection(s *xorm.Session, a web.Auth, tf *TaskColl
|
||||
}
|
||||
|
||||
func getFilterValueForBucketFilter(filter string, view *ProjectView) (newFilter string, err error) {
|
||||
if view.BucketConfigurationMode != BucketConfigurationModeFilter {
|
||||
return filter, nil
|
||||
}
|
||||
|
||||
re := regexp.MustCompile(`bucket_id\s*=\s*(\d+)`)
|
||||
|
||||
match := re.FindStringSubmatch(filter)
|
||||
@@ -195,7 +201,7 @@ func getFilterValueForBucketFilter(filter string, view *ProjectView) (newFilter
|
||||
|
||||
for id, bucket := range view.BucketConfiguration {
|
||||
if id == bucketID {
|
||||
return re.ReplaceAllString(filter, `(`+bucket.Filter+`)`), nil
|
||||
return re.ReplaceAllString(filter, `(`+bucket.Filter.Filter+`)`), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,11 +234,19 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
||||
// If the project id is < -1 this means we're dealing with a saved filter - in that case we get and populate the filter
|
||||
// -1 is the favorites project which works as intended
|
||||
if !tf.isSavedFilter && tf.ProjectID < -1 {
|
||||
sf, err := getSavedFilterSimpleByID(s, getSavedFilterIDFromProjectID(tf.ProjectID))
|
||||
sf, err := GetSavedFilterSimpleByID(s, GetSavedFilterIDFromProjectID(tf.ProjectID))
|
||||
if err != nil {
|
||||
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.
|
||||
|
||||
@@ -263,7 +277,11 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
||||
tc.isSavedFilter = true
|
||||
|
||||
if tf.Filter != "" {
|
||||
tc.Filter = "(" + tf.Filter + ") && (" + tc.Filter + ")"
|
||||
if tc.Filter != "" {
|
||||
tc.Filter = "(" + tf.Filter + ") && (" + tc.Filter + ")"
|
||||
} else {
|
||||
tc.Filter = tf.Filter
|
||||
}
|
||||
}
|
||||
|
||||
return tc.ReadAll(s, a, search, page, perPage)
|
||||
@@ -277,21 +295,34 @@ func (tf *TaskCollection) ReadAll(s *xorm.Session, a web.Auth, search string, pa
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
|
||||
if view.Filter != "" {
|
||||
if tf.Filter != "" {
|
||||
tf.Filter = "(" + tf.Filter + ") && (" + view.Filter + ")"
|
||||
} else {
|
||||
tf.Filter = view.Filter
|
||||
if view.Filter != nil {
|
||||
if view.Filter.Filter != "" {
|
||||
if tf.Filter != "" {
|
||||
tf.Filter = "(" + tf.Filter + ") && (" + view.Filter.Filter + ")"
|
||||
} else {
|
||||
tf.Filter = view.Filter.Filter
|
||||
}
|
||||
tf.FilterIncludeNulls = view.Filter.FilterIncludeNulls
|
||||
}
|
||||
|
||||
if view.Filter.FilterTimezone != "" {
|
||||
tf.FilterTimezone = view.Filter.FilterTimezone
|
||||
}
|
||||
|
||||
if view.Filter.FilterIncludeNulls {
|
||||
tf.FilterIncludeNulls = view.Filter.FilterIncludeNulls
|
||||
}
|
||||
|
||||
if view.Filter.Search != "" {
|
||||
search = view.Filter.Search
|
||||
}
|
||||
}
|
||||
|
||||
if strings.Contains(tf.Filter, taskPropertyBucketID) {
|
||||
filteringForBucket = true
|
||||
if view.BucketConfigurationMode == BucketConfigurationModeFilter {
|
||||
tf.Filter, err = getFilterValueForBucketFilter(tf.Filter, view)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
tf.Filter, err = getFilterValueForBucketFilter(tf.Filter, view)
|
||||
if err != nil {
|
||||
return nil, 0, 0, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,7 +139,7 @@ func RegisterOverdueReminderCron() {
|
||||
}
|
||||
}
|
||||
|
||||
projects, err := GetProjectsMapSimplByTaskIDs(s, taskIDs)
|
||||
projects, err := GetProjectsMapSimpleByTaskIDs(s, taskIDs)
|
||||
if err != nil {
|
||||
log.Errorf("[Undone Overdue Tasks Reminder] Could not get projects for tasks: %s", err)
|
||||
return
|
||||
|
||||
@@ -99,7 +99,7 @@ func (tp *TaskPosition) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
}
|
||||
|
||||
_, err = s.
|
||||
Where("task_id = ?", tp.TaskID).
|
||||
Where("task_id = ? AND project_view_id = ?", tp.TaskID, tp.ProjectViewID).
|
||||
Cols("project_view_id", "position").
|
||||
Update(tp)
|
||||
if err != nil {
|
||||
@@ -137,6 +137,19 @@ func RecalculateTaskPositions(s *xorm.Session, view *ProjectView, a web.Auth) (e
|
||||
}
|
||||
if view.ProjectID < -1 {
|
||||
tc.ProjectID = 0
|
||||
|
||||
sf, err := GetSavedFilterSimpleByID(s, GetSavedFilterIDFromProjectID(view.ProjectID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts.filterIncludeNulls = sf.Filters.FilterIncludeNulls
|
||||
opts.filterTimezone = sf.Filters.FilterTimezone
|
||||
opts.filter = sf.Filters.Filter
|
||||
opts.parsedFilters, err = getTaskFiltersFromFilterString(opts.filter, opts.filterTimezone)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
projects, err := getRelevantProjectsFromCollection(s, a, tc)
|
||||
|
||||
@@ -206,7 +206,7 @@ func getTasksWithRemindersDueAndTheirUsers(s *xorm.Session, now time.Time) (remi
|
||||
|
||||
seen := make(map[int64]map[int64]bool)
|
||||
|
||||
projects, err := GetProjectsMapSimplByTaskIDs(s, taskIDs)
|
||||
projects, err := GetProjectsMapSimpleByTaskIDs(s, taskIDs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func getOrderByDBStatement(opts *taskSearchOptions) (orderby string, err error)
|
||||
prefix = "task_positions."
|
||||
}
|
||||
|
||||
if param.sortBy == taskPropertyID {
|
||||
if param.sortBy == taskPropertyID || param.sortBy == taskPropertyCreated || param.sortBy == taskPropertyUpdated {
|
||||
prefix = "tasks."
|
||||
}
|
||||
|
||||
|
||||
+5
-1
@@ -310,7 +310,7 @@ func getRawTasksForProjects(s *xorm.Session, projects []*Project, a web.Auth, op
|
||||
}
|
||||
origOpts := clone.Clone(opts)
|
||||
tasks, totalItems, err = tsSearcher.Search(opts)
|
||||
// It is possible that project views are not yet in Typesnse's index. This causes the query here to fail.
|
||||
// It is possible that project views are not yet in Typesense's index. This causes the query here to fail.
|
||||
// To avoid crashing everything, we fall back to the db search in that case.
|
||||
var tsErr = &typesense.HTTPError{}
|
||||
if err != nil && errors.As(err, &tsErr) && tsErr.Status == 404 {
|
||||
@@ -956,6 +956,10 @@ func (t *Task) Update(s *xorm.Session, a web.Auth) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = s.Where("task_id = ?", t.ID).Delete(&TaskPosition{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, view := range views {
|
||||
var bucketID = view.DoneBucketID
|
||||
|
||||
@@ -21,9 +21,8 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/web"
|
||||
|
||||
@@ -17,13 +17,13 @@
|
||||
package openid
|
||||
|
||||
import (
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
|
||||
"code.vikunja.io/api/pkg/cron"
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
|
||||
"xorm.io/builder"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func RemoveEmptySSOTeams(s *xorm.Session) (err error) {
|
||||
|
||||
@@ -24,14 +24,14 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/api/pkg/web/handler"
|
||||
|
||||
"code.vikunja.io/api/pkg/db"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/models"
|
||||
"code.vikunja.io/api/pkg/modules/auth"
|
||||
"code.vikunja.io/api/pkg/user"
|
||||
"code.vikunja.io/api/pkg/utils"
|
||||
"code.vikunja.io/api/pkg/web/handler"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
petname "github.com/dustinkirkland/golang-petname"
|
||||
"github.com/labstack/echo/v4"
|
||||
@@ -100,7 +100,6 @@ func HandleCallback(c echo.Context) error {
|
||||
provider, err := GetProvider(providerKey)
|
||||
log.Debugf("Provider: %v", provider)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return handler.HandleHTTPError(err)
|
||||
}
|
||||
if provider == nil {
|
||||
@@ -114,7 +113,6 @@ func HandleCallback(c echo.Context) error {
|
||||
if err != nil {
|
||||
var rerr *oauth2.RetrieveError
|
||||
if errors.As(err, &rerr) {
|
||||
log.Error(err)
|
||||
|
||||
details := make(map[string]interface{})
|
||||
if err := json.Unmarshal(rerr.Body, &details); err != nil {
|
||||
@@ -122,6 +120,7 @@ func HandleCallback(c echo.Context) error {
|
||||
return handler.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
log.Error(err)
|
||||
return c.JSON(http.StatusBadRequest, map[string]interface{}{
|
||||
"message": "Could not authenticate against third party.",
|
||||
"details": details,
|
||||
|
||||
@@ -17,14 +17,13 @@
|
||||
package openid
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"code.vikunja.io/api/pkg/log"
|
||||
"code.vikunja.io/api/pkg/modules/keyvalue"
|
||||
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"golang.org/x/oauth2"
|
||||
)
|
||||
@@ -43,14 +42,18 @@ func GetAllProviders() (providers []*Provider, err error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
rawProvider := rawProviders.([]interface{})
|
||||
rawProvider, is := rawProviders.(map[string]interface{})
|
||||
if !is {
|
||||
log.Criticalf("It looks like your openid configuration is in the wrong format. Please check the docs for the correct format.")
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range rawProvider {
|
||||
for key, p := range rawProvider {
|
||||
var pi map[string]interface{}
|
||||
var is bool
|
||||
pi, is = p.(map[string]interface{})
|
||||
// JSON config is a map[string]interface{}, other providers are not. Under the hood they are all strings so
|
||||
// it is save to cast.
|
||||
// it is safe to cast.
|
||||
if !is {
|
||||
pis := p.(map[interface{}]interface{})
|
||||
pi = make(map[string]interface{}, len(pis))
|
||||
@@ -59,21 +62,21 @@ func GetAllProviders() (providers []*Provider, err error) {
|
||||
}
|
||||
}
|
||||
|
||||
provider, err := getProviderFromMap(pi)
|
||||
provider, err := getProviderFromMap(pi, key)
|
||||
|
||||
if err != nil {
|
||||
if provider != nil {
|
||||
log.Errorf("Error while getting openid provider %s: %s", provider.Name, err)
|
||||
continue
|
||||
}
|
||||
log.Errorf("Error while getting openid provider: %s", err)
|
||||
log.Errorf("Error while getting openid provider %s: %s", key, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if provider == nil {
|
||||
log.Errorf("Could not openid provider %s, please check your config", key)
|
||||
continue
|
||||
}
|
||||
|
||||
providers = append(providers, provider)
|
||||
|
||||
k := getKeyFromName(pi["name"].(string))
|
||||
err = keyvalue.Put("openid_provider_"+k, provider)
|
||||
err = keyvalue.Put("openid_provider_"+key, provider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -107,31 +110,61 @@ func GetProvider(key string) (provider *Provider, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
func getKeyFromName(name string) string {
|
||||
reg := regexp.MustCompile("[^a-z0-9]+")
|
||||
return reg.ReplaceAllString(strings.ToLower(name), "")
|
||||
}
|
||||
func getProviderFromMap(pi map[string]interface{}, key string) (provider *Provider, err error) {
|
||||
|
||||
requiredKeys := []string{
|
||||
"name",
|
||||
"authurl",
|
||||
"clientsecret",
|
||||
"clientid",
|
||||
}
|
||||
|
||||
allKeys := append(
|
||||
[]string{
|
||||
"logouturl",
|
||||
"scope",
|
||||
},
|
||||
requiredKeys...,
|
||||
)
|
||||
|
||||
for _, configKey := range allKeys {
|
||||
valueFromFile := config.GetConfigValueFromFile("auth.openid.providers." + key + "." + configKey)
|
||||
if valueFromFile != "" {
|
||||
pi[configKey] = valueFromFile
|
||||
}
|
||||
}
|
||||
|
||||
for _, key := range requiredKeys {
|
||||
if _, exists := pi[key]; !exists {
|
||||
return nil, fmt.Errorf("required key '%s' is missing in the provider configuration", key)
|
||||
}
|
||||
}
|
||||
|
||||
func getProviderFromMap(pi map[string]interface{}) (provider *Provider, err error) {
|
||||
name, is := pi["name"].(string)
|
||||
if !is {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
k := getKeyFromName(name)
|
||||
|
||||
logoutURL, ok := pi["logouturl"].(string)
|
||||
if !ok {
|
||||
logoutURL = ""
|
||||
var logoutURL string
|
||||
logoutValue, exists := pi["logouturl"]
|
||||
if exists {
|
||||
url, ok := logoutValue.(string)
|
||||
if ok {
|
||||
logoutURL = url
|
||||
}
|
||||
}
|
||||
|
||||
scope, _ := pi["scope"].(string)
|
||||
var scope string
|
||||
if scopeValue, exists := pi["scope"]; exists {
|
||||
scope = scopeValue.(string)
|
||||
}
|
||||
if scope == "" {
|
||||
scope = "openid profile email"
|
||||
}
|
||||
|
||||
provider = &Provider{
|
||||
Name: pi["name"].(string),
|
||||
Key: k,
|
||||
Name: name,
|
||||
Key: key,
|
||||
AuthURL: pi["authurl"].(string),
|
||||
OriginalAuthURL: pi["authurl"].(string),
|
||||
ClientSecret: pi["clientsecret"].(string),
|
||||
|
||||
@@ -225,7 +225,7 @@ func convertMarkdownToHTML(input string) (output string, err error) {
|
||||
|
||||
// Converts all previously obtained data from trello into the vikunja format.
|
||||
// `trelloData` should contain all boards with their projects and cards respectively.
|
||||
func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Board, token string, currentMember *trello.Member) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
|
||||
func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Board, client *trello.Client, currentMember *trello.Member) (fullVikunjaHierachie []*models.ProjectWithTasksAndBuckets, err error) {
|
||||
|
||||
log.Debugf("[Trello Migration] ")
|
||||
|
||||
@@ -243,6 +243,8 @@ func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Bo
|
||||
|
||||
log.Debugf("[Trello Migration] Converting %d boards to vikunja projects", len(trelloData))
|
||||
|
||||
actionMemberCache := make(map[string]*trello.Member)
|
||||
|
||||
for index, board := range trelloData {
|
||||
project := &models.ProjectWithTasksAndBuckets{
|
||||
Project: models.Project{
|
||||
@@ -341,7 +343,7 @@ func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Bo
|
||||
log.Debugf("[Trello Migration] Downloading card attachment %s", attachment.ID)
|
||||
|
||||
buf, err := migration.DownloadFileWithHeaders(attachment.URL, map[string][]string{
|
||||
"Authorization": {`OAuth oauth_consumer_key="` + config.MigrationTrelloKey.GetString() + `", oauth_token="` + token + `"`},
|
||||
"Authorization": {`OAuth oauth_consumer_key="` + config.MigrationTrelloKey.GetString() + `", oauth_token="` + client.Token + `"`},
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -408,7 +410,26 @@ func convertTrelloDataToVikunja(organizationName string, trelloData []*trello.Bo
|
||||
}
|
||||
|
||||
if currentMember == nil || action.IDMemberCreator != currentMember.ID {
|
||||
comment.Comment = "*" + action.MemberCreator.FullName + "*:\n\n" + comment.Comment
|
||||
if action.MemberCreator == nil {
|
||||
if cached, ok := actionMemberCache[action.IDMemberCreator]; ok {
|
||||
action.MemberCreator = cached
|
||||
} else {
|
||||
member, err := client.GetMember(action.IDMemberCreator)
|
||||
if err != nil {
|
||||
log.Errorf("[Trello Migration] Could not get member %s for card %s", action.IDMemberCreator, card.ID)
|
||||
}
|
||||
actionMemberCache[action.IDMemberCreator] = member
|
||||
action.MemberCreator = member
|
||||
}
|
||||
}
|
||||
|
||||
fullName := action.IDMemberCreator
|
||||
if action.MemberCreator != nil {
|
||||
fullName = action.MemberCreator.FullName
|
||||
}
|
||||
|
||||
//action.MemberCreator.FullName
|
||||
comment.Comment = "*" + fullName + "*:\n\n" + comment.Comment
|
||||
}
|
||||
|
||||
comment.Comment, err = convertMarkdownToHTML(comment.Comment)
|
||||
@@ -489,7 +510,7 @@ func (m *Migration) Migrate(u *user.User) (err error) {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hierarchy, err := convertTrelloDataToVikunja(orgName, boards, client.Token, currentMember)
|
||||
hierarchy, err := convertTrelloDataToVikunja(orgName, boards, client, currentMember)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -462,7 +462,7 @@ func TestConvertTrelloToVikunja(t *testing.T) {
|
||||
|
||||
organizationMap := getTrelloOrganizationsWithBoards(trelloData)
|
||||
for organizationID, boards := range organizationMap {
|
||||
hierarchy, err := convertTrelloDataToVikunja(organizationID, boards, "", nil)
|
||||
hierarchy, err := convertTrelloDataToVikunja(organizationID, boards, &trello.Client{}, nil)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.NotNil(t, hierarchy)
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -72,6 +73,9 @@ func UploadTaskAttachment(c echo.Context) error {
|
||||
form, err := c.MultipartForm()
|
||||
if err != nil {
|
||||
_ = s.Rollback()
|
||||
if errors.Is(err, http.ErrNotMultipart) {
|
||||
return echo.NewHTTPError(http.StatusBadRequest, "No multipart form provided")
|
||||
}
|
||||
return handler.HandleHTTPError(err)
|
||||
}
|
||||
|
||||
|
||||
@@ -172,7 +172,7 @@ func EntryHandler(c echo.Context) error {
|
||||
func getProjectFromParam(c echo.Context) (project *models.ProjectWithTasksAndBuckets, err error) {
|
||||
param := c.Param("project")
|
||||
if param == "" {
|
||||
return nil, echo.ErrBadRequest
|
||||
return &models.ProjectWithTasksAndBuckets{}, nil
|
||||
}
|
||||
|
||||
s := db.NewSession()
|
||||
@@ -183,6 +183,21 @@ func getProjectFromParam(c echo.Context) (project *models.ProjectWithTasksAndBuc
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if intParam == models.FavoritesPseudoProjectID {
|
||||
return &models.ProjectWithTasksAndBuckets{Project: models.FavoritesPseudoProject}, nil
|
||||
}
|
||||
|
||||
if intParam < models.FavoritesPseudoProjectID {
|
||||
var sf *models.SavedFilter
|
||||
sf, err = models.GetSavedFilterSimpleByID(s, models.GetSavedFilterIDFromProjectID(intParam))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
project = &models.ProjectWithTasksAndBuckets{Project: *sf.ToProject()}
|
||||
return
|
||||
}
|
||||
|
||||
p, err := models.GetProjectSimpleByID(s, intParam)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+7
-10
@@ -141,22 +141,19 @@ func setupSentry(e *echo.Echo) {
|
||||
// Only capture errors not already handled by echo
|
||||
var herr *echo.HTTPError
|
||||
if errors.As(err, &herr) && herr.Code > 499 {
|
||||
var errToReport = err
|
||||
if herr.Internal == nil {
|
||||
errToReport = herr.Internal
|
||||
}
|
||||
|
||||
hub := sentryecho.GetHubFromContext(c)
|
||||
if hub != nil {
|
||||
hub.WithScope(func(scope *sentry.Scope) {
|
||||
scope.SetExtra("url", c.Request().URL)
|
||||
if herr.Internal == nil {
|
||||
hub.CaptureException(err)
|
||||
} else {
|
||||
hub.CaptureException(herr.Internal)
|
||||
}
|
||||
hub.CaptureException(errToReport)
|
||||
})
|
||||
} else {
|
||||
if herr.Internal == nil {
|
||||
sentry.CaptureException(err)
|
||||
} else {
|
||||
sentry.CaptureException(herr.Internal)
|
||||
}
|
||||
sentry.CaptureException(errToReport)
|
||||
log.Debugf("Could not add context for sending error '%s' to sentry", err.Error())
|
||||
}
|
||||
log.Debugf("Error '%s' sent to sentry", err.Error())
|
||||
|
||||
+9
-2
@@ -8395,7 +8395,11 @@ const docTemplate = `{
|
||||
},
|
||||
"filter": {
|
||||
"description": "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation.",
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.TaskCollection"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique numeric id of this view",
|
||||
@@ -8431,7 +8435,7 @@ const docTemplate = `{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/models.TaskCollection"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
@@ -8865,6 +8869,9 @@ const docTemplate = `{
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"s": {
|
||||
"type": "string"
|
||||
},
|
||||
"sort_by": {
|
||||
"description": "The query parameter to sort by. This is for ex. done, priority, etc.",
|
||||
"type": "array",
|
||||
|
||||
@@ -8387,7 +8387,11 @@
|
||||
},
|
||||
"filter": {
|
||||
"description": "The filter query to match tasks by. Check out https://vikunja.io/docs/filters for a full explanation.",
|
||||
"type": "string"
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/models.TaskCollection"
|
||||
}
|
||||
]
|
||||
},
|
||||
"id": {
|
||||
"description": "The unique numeric id of this view",
|
||||
@@ -8423,7 +8427,7 @@
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"filter": {
|
||||
"type": "string"
|
||||
"$ref": "#/definitions/models.TaskCollection"
|
||||
},
|
||||
"title": {
|
||||
"type": "string"
|
||||
@@ -8857,6 +8861,9 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"s": {
|
||||
"type": "string"
|
||||
},
|
||||
"sort_by": {
|
||||
"description": "The query parameter to sort by. This is for ex. done, priority, etc.",
|
||||
"type": "array",
|
||||
|
||||
@@ -538,9 +538,10 @@ definitions:
|
||||
If they are marked as done individually, they are moved into the done bucket.
|
||||
type: integer
|
||||
filter:
|
||||
allOf:
|
||||
- $ref: '#/definitions/models.TaskCollection'
|
||||
description: The filter query to match tasks by. Check out https://vikunja.io/docs/filters
|
||||
for a full explanation.
|
||||
type: string
|
||||
id:
|
||||
description: The unique numeric id of this view
|
||||
type: integer
|
||||
@@ -566,7 +567,7 @@ definitions:
|
||||
models.ProjectViewBucketConfiguration:
|
||||
properties:
|
||||
filter:
|
||||
type: string
|
||||
$ref: '#/definitions/models.TaskCollection'
|
||||
title:
|
||||
type: string
|
||||
type: object
|
||||
@@ -900,6 +901,8 @@ definitions:
|
||||
items:
|
||||
type: string
|
||||
type: array
|
||||
s:
|
||||
type: string
|
||||
sort_by:
|
||||
description: The query parameter to sort by. This is for ex. done, priority,
|
||||
etc.
|
||||
|
||||
+15
-2
@@ -372,10 +372,23 @@ func handleFailedPassword(user *User) {
|
||||
|
||||
a, _, err := keyvalue.Get(key)
|
||||
if err != nil {
|
||||
log.Errorf("Could get failed password attempts for user %d: %s", user.ID, err)
|
||||
log.Errorf("Could not get failed password attempts for user %d: %s", user.ID, err)
|
||||
return
|
||||
}
|
||||
attempts := a.(int64)
|
||||
attempts, ok := a.(int64)
|
||||
if !ok {
|
||||
attemptsStr, ok := a.(string)
|
||||
if !ok {
|
||||
log.Errorf("Unexpected type for failed password attempts: %v", a)
|
||||
return
|
||||
}
|
||||
var err error
|
||||
attempts, err = strconv.ParseInt(attemptsStr, 10, 64)
|
||||
if err != nil {
|
||||
log.Errorf("Could not convert failed password attempts to int64: %v, value: %s", err, attemptsStr)
|
||||
return
|
||||
}
|
||||
}
|
||||
if attempts != 3 {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -17,19 +17,26 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
)
|
||||
|
||||
func init() {
|
||||
govalidator.TagMap["username"] = func(i string) bool {
|
||||
// To avoid making this overly complicated, we only two things:
|
||||
// To avoid making this overly complicated, we only check three things:
|
||||
// 1. No Spaces
|
||||
// 2. Should not look like an url
|
||||
// 3. Should not contain , (because then it will be impossible to search for)
|
||||
if govalidator.HasWhitespace(i) {
|
||||
return false
|
||||
}
|
||||
|
||||
if govalidator.IsURL(i) {
|
||||
return false
|
||||
}
|
||||
|
||||
if govalidator.HasWhitespace(i) {
|
||||
if strings.Contains(i, ",") {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user