Compare commits
428
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c934124b88 | ||
|
|
b7bba814e3 | ||
|
|
8c285968af | ||
|
|
b91a5d9adf | ||
|
|
17b281072f | ||
|
|
d47555e3c2 | ||
|
|
3f98f47256 | ||
|
|
00a4d63344 | ||
|
|
82f37df16d | ||
|
|
dfd3e276ab | ||
|
|
3a0a3b49af | ||
|
|
6d7f81fa7e | ||
|
|
154e3e4587 | ||
|
|
372363d254 | ||
|
|
61e6990ab3 | ||
|
|
714298a94e | ||
|
|
a0d05211ff | ||
|
|
4d454de228 | ||
|
|
fcb70b884c | ||
|
|
33c9ea802a | ||
|
|
ab29ac7a5c | ||
|
|
7a1519da43 | ||
|
|
7a839925ea | ||
|
|
60fa211334 | ||
|
|
e318e604b5 | ||
|
|
6ea482a10b | ||
|
|
a9b37db504 | ||
|
|
7f3c074958 | ||
|
|
2e4de2919f | ||
|
|
addcb9c1f6 | ||
|
|
6b3e7f0d25 | ||
|
|
6dbfbc43da | ||
|
|
3e3efa85ea | ||
|
|
5dc94bfa64 | ||
|
|
04c6b6ba36 | ||
|
|
85d0c8b1b9 | ||
|
|
f32472eb24 | ||
|
|
99bed5c616 | ||
|
|
166d9cf860 | ||
|
|
16994c5d36 | ||
|
|
8d849b3d30 | ||
|
|
884eef5407 | ||
|
|
96d8de06da | ||
|
|
d6bab95376 | ||
|
|
a983142ad6 | ||
|
|
98c878b201 | ||
|
|
b2e983fde6 | ||
|
|
724ec5751f | ||
|
|
17618301bc | ||
|
|
83b27d813a | ||
|
|
a0ad6e7d28 | ||
|
|
940d149e2a | ||
|
|
802f661f0d | ||
|
|
f9829a6338 | ||
|
|
a87419fa07 | ||
|
|
b321ad5fc0 | ||
|
|
edfe41ef87 | ||
|
|
4cbed7c2ed | ||
|
|
25c8476883 | ||
|
|
15cde08db6 | ||
|
|
96835f6a28 | ||
|
|
b1c4f0c216 | ||
|
|
d2fbd468e6 | ||
|
|
a751178c8b | ||
|
|
2b9b9216dc | ||
|
|
f732082708 | ||
|
|
f56df8aa34 | ||
|
|
e290d79c15 | ||
|
|
51f26d14b9 | ||
|
|
320fc75c27 | ||
|
|
4857bfbbdb | ||
|
|
62c238e4bc | ||
|
|
d27b62db6e | ||
|
|
a21036340e | ||
|
|
3398dee481 | ||
|
|
063aa7afec | ||
|
|
df3f5e73ed | ||
|
|
c57636182a | ||
|
|
4d2a567c46 | ||
|
|
21cc3722a1 | ||
|
|
6a68736ca5 | ||
|
|
9e5d6bad8d | ||
|
|
2b0fbe2575 | ||
|
|
1e7d9c982d | ||
|
|
20c52a8325 | ||
|
|
a46009dfc9 | ||
|
|
6ab12b9dd1 | ||
|
|
aeb404f313 | ||
|
|
9c5df7d389 | ||
|
|
ffd36261cb | ||
|
|
8812af2d77 | ||
|
|
93f7dd611a | ||
|
|
0e683ae7bf | ||
|
|
deef8da370 | ||
|
|
a0988daeaf | ||
|
|
6225b24da8 | ||
|
|
983c02964c | ||
|
|
7bc93757c7 | ||
|
|
b4c5df1ef8 | ||
|
|
fd7d83bdaf | ||
|
|
53d62d35f4 | ||
|
|
a266fbf2b9 | ||
|
|
971f328256 | ||
|
|
89d643b9cf | ||
|
|
a1c4fbf936 | ||
|
|
1049b27d37 | ||
|
|
6b850c56f7 | ||
|
|
614f70db36 | ||
|
|
b297cb5398 | ||
|
|
8e32d099c4 | ||
|
|
19a1dc9daf | ||
|
|
7f28a514ea | ||
|
|
44f5e42c0b | ||
|
|
2fc9504285 | ||
|
|
2b15bb5154 | ||
|
|
f1451abebe | ||
|
|
c9f47797cf | ||
|
|
42b33d6553 | ||
|
|
b4b2136869 | ||
|
|
3d9a719424 | ||
|
|
5c5f42d4ca | ||
|
|
fc1208029e | ||
|
|
4a40bdfe89 | ||
|
|
b01e531fb7 | ||
|
|
02ab944020 | ||
|
|
1df4a4ea2e | ||
|
|
61cb072ba3 | ||
|
|
02b8ea40af | ||
|
|
b5eaa51560 | ||
|
|
eb6663e1f5 | ||
|
|
b94802169c | ||
|
|
dfda2e5500 | ||
|
|
d23484c8f3 | ||
|
|
78b8ea6211 | ||
|
|
93cc2e1cb0 | ||
|
|
52fb43a6f7 | ||
|
|
8f5273600a | ||
|
|
40105ee4ce | ||
|
|
5bfd99dd77 | ||
|
|
ac87035742 | ||
|
|
10edeafa46 | ||
|
|
01a7a62541 | ||
|
|
c5f043c346 | ||
|
|
1b55be6a15 | ||
|
|
99e401cc67 | ||
|
|
9deda69baf | ||
|
|
34fe37bfb7 | ||
|
|
a88939ff9a | ||
|
|
89e4c3722a | ||
|
|
4e27d7da70 | ||
|
|
be5822712e | ||
|
|
a5a54a40f6 | ||
|
|
ac977f02df | ||
|
|
3d43603b23 | ||
|
|
9d3f8635d6 | ||
|
|
dab0c39b70 | ||
|
|
f71cc9e77b | ||
|
|
5aa6b230cf | ||
|
|
785ee36921 | ||
|
|
476cadbbed | ||
|
|
69c609478f | ||
|
|
77ac70c47a | ||
|
|
1371692bb6 | ||
|
|
bd1b1ca23c | ||
|
|
85445ea032 | ||
|
|
0b9f3070fd | ||
|
|
cca02a3f2e | ||
|
|
5c82333977 | ||
|
|
239cabd34a | ||
|
|
4701c91c1a | ||
|
|
5775c51be5 | ||
|
|
1cdf8f4271 | ||
|
|
442009a47b | ||
|
|
e70f5bcce3 | ||
|
|
a355d9798e | ||
|
|
7fa171c3d3 | ||
|
|
672fc5d9c7 | ||
|
|
586132ce8c | ||
|
|
bf08dc2585 | ||
|
|
761d278b9a | ||
|
|
6da9bc964e | ||
|
|
d508fe3fb8 | ||
|
|
e7075762ab | ||
|
|
eb89f68f73 | ||
|
|
08b4bcaff9 | ||
|
|
0a29a88a26 | ||
|
|
313b99e296 | ||
|
|
95ef4e1045 | ||
|
|
8b8ec19bb3 | ||
|
|
7646c7f0c9 | ||
|
|
c8f7a57566 | ||
|
|
1c64b75f86 | ||
|
|
fc8252e751 | ||
|
|
1c9590075a | ||
|
|
800f4545c1 | ||
|
|
a462697b30 | ||
|
|
5049cbf236 | ||
|
|
a2ef74cade | ||
|
|
fe44b7d473 | ||
|
|
01c4f1fc0e | ||
|
|
5fba4ed6ef | ||
|
|
d885b43328 | ||
|
|
459c8daed6 | ||
|
|
5768648760 | ||
|
|
198b2e3b70 | ||
|
|
be8ecb6d36 | ||
|
|
4c73c74587 | ||
|
|
cfa58ae599 | ||
|
|
39d0409f57 | ||
|
|
7b804efbe2 | ||
|
|
f812a67269 | ||
|
|
1a131d79f9 | ||
|
|
a253f76060 | ||
|
|
c85da01294 | ||
|
|
499f3f000c | ||
|
|
1059b00298 | ||
|
|
37324aeaf2 | ||
|
|
7b275794dc | ||
|
|
72db97203a | ||
|
|
27c14b6903 | ||
|
|
ec94cf6813 | ||
|
|
6a9b5cb7d0 | ||
|
|
8248049530 | ||
|
|
d1678fe420 | ||
|
|
08821ea8a8 | ||
|
|
e9a466fa31 | ||
|
|
de9f686480 | ||
|
|
d08e9650ba | ||
|
|
0434a96c3a | ||
|
|
2b9b77bef2 | ||
|
|
f5040ad2f4 | ||
|
|
7bc77ae8c0 | ||
|
|
715269a5d0 | ||
|
|
53605c24f0 | ||
|
|
da0b741b69 | ||
|
|
73f923bc47 | ||
|
|
25a8c7ea80 | ||
|
|
950de7c954 | ||
|
|
42ba9240b0 | ||
|
|
3da6299021 | ||
|
|
1ac0f412b9 | ||
|
|
e7fbdc2727 | ||
|
|
7117303d57 | ||
|
|
0ca43dc147 | ||
|
|
c47ea2716c | ||
|
|
cbdcc61414 | ||
|
|
194a7d4cf8 | ||
|
|
9695785a0c | ||
|
|
d9f555554e | ||
|
|
6e38bcf349 | ||
|
|
fc780a90ae | ||
|
|
d586f691b7 | ||
|
|
9e8e43bd12 | ||
|
|
c554a96b4b | ||
|
|
3ecc81094f | ||
|
|
811ccc1baa | ||
|
|
d7c5451729 | ||
|
|
96884372b4 | ||
|
|
dddba4d64a | ||
|
|
a0e3efe2d1 | ||
|
|
d707e1576a | ||
|
|
3aaf363413 | ||
|
|
28fff10fec | ||
|
|
8729c24e1d | ||
|
|
eee895105a | ||
|
|
ef0bf3156e | ||
|
|
a25834b089 | ||
|
|
526bd1f170 | ||
|
|
bea81f83f7 | ||
|
|
03381f47bf | ||
|
|
ec8f962615 | ||
|
|
dd8be14fdb | ||
|
|
12474e5be6 | ||
|
|
34691b8edf | ||
|
|
f98a336920 | ||
|
|
918a05cfda | ||
|
|
84e8478e94 | ||
|
|
fd6692ea1a | ||
|
|
ddc8b416c0 | ||
|
|
a4e98a33c5 | ||
|
|
ee99ee4d02 | ||
|
|
acf9f836a7 | ||
|
|
7c54d1accf | ||
|
|
891db0216b | ||
|
|
f1d9e4f25c | ||
|
|
82ee9f18eb | ||
|
|
ed5feee33a | ||
|
|
3566b889be | ||
|
|
db81701d38 | ||
|
|
cb706416c6 | ||
|
|
f8fb9d7407 | ||
|
|
5cc420b289 | ||
|
|
327905bb90 | ||
|
|
f1481d702c | ||
|
|
c97dbd3cff | ||
|
|
d2602a7629 | ||
|
|
744b40e7f7 | ||
|
|
1e42e54db1 | ||
|
|
8a6689f4fa | ||
|
|
092a65b025 | ||
|
|
5e04abaa06 | ||
|
|
fe8e0ef78b | ||
|
|
ecdb03aca8 | ||
|
|
58e492872a | ||
|
|
ee20e6175a | ||
|
|
24ae38713b | ||
|
|
c8585d1a69 | ||
|
|
3be685ad08 | ||
|
|
aaa2abced4 | ||
|
|
ddc18aa177 | ||
|
|
07130bdc3a | ||
|
|
2f92e407cc | ||
|
|
6e72606d74 | ||
|
|
577f5ae69a | ||
|
|
8ad7e7c905 | ||
|
|
b4e9d9437e | ||
|
|
0c70aedeb1 | ||
|
|
99e90c0b02 | ||
|
|
38744dfd5d | ||
|
|
eb07be1a62 | ||
|
|
825d1add49 | ||
|
|
a15831eb33 | ||
|
|
871e0acd8a | ||
|
|
5af908b2e5 | ||
|
|
449e9a911c | ||
|
|
098c99fd2e | ||
|
|
c9591fe464 | ||
|
|
df6a9b67fd | ||
|
|
d02c349231 | ||
|
|
fd3f1de861 | ||
|
|
f38a5c9220 | ||
|
|
9d2e79f725 | ||
|
|
85889fff56 | ||
|
|
8ac0eb4aa4 | ||
|
|
1966cc3c0e | ||
|
|
20bdb01187 | ||
|
|
3ff56d7987 | ||
|
|
46aa2fff0f | ||
|
|
a10f9ca225 | ||
|
|
0bc1832050 | ||
|
|
78811d916a | ||
|
|
f19f19bb75 | ||
|
|
f2fdbad7d4 | ||
|
|
4b9b9da122 | ||
|
|
10832165c3 | ||
|
|
d6c3b5a9a1 | ||
|
|
2f6338484b | ||
|
|
f586e51aad | ||
|
|
fd12c8705e | ||
|
|
5545b0e447 | ||
|
|
043a6dd049 | ||
|
|
30769fb6ea | ||
|
|
553a97f03d | ||
|
|
8c9623dd0e | ||
|
|
9b232a326a | ||
|
|
68b9d391b0 | ||
|
|
b718aa1fbd | ||
|
|
a114b9c8d3 | ||
|
|
48e045b4a3 | ||
|
|
8439c9cc1e | ||
|
|
4bd02a1181 | ||
|
|
f2d90f31bc | ||
|
|
dcf88560a7 | ||
|
|
2dc541571c | ||
|
|
08c2cd6b63 | ||
|
|
bb25a3397b | ||
|
|
918c2224d8 | ||
|
|
323ecdaab6 | ||
|
|
51040cf1af | ||
|
|
e5e0db1f6a | ||
|
|
62412ce29b | ||
|
|
7c42fb5d75 | ||
|
|
42c2d0884b | ||
|
|
ec0c05fd47 | ||
|
|
8711f7a935 | ||
|
|
7f27cee6a3 | ||
|
|
5644130f01 | ||
|
|
38c30d1eee | ||
|
|
471d0fcd4a | ||
|
|
0b424fe95e | ||
|
|
70615d6843 | ||
|
|
2982acc0ec | ||
|
|
2c6bc7742f | ||
|
|
02a7958865 | ||
|
|
af639a180c | ||
|
|
faa797f461 | ||
|
|
e2eb711ac9 | ||
|
|
7dcb5fec9a | ||
|
|
f507a46324 | ||
|
|
a863b5d375 | ||
|
|
0a81855bc1 | ||
|
|
6064d72aa7 | ||
|
|
35dea13b22 | ||
|
|
7ef6ddf8f7 | ||
|
|
c37fe49890 | ||
|
|
413d1f9ad7 | ||
|
|
3317280062 | ||
|
|
136ef58820 | ||
|
|
1eb1aa2575 | ||
|
|
43e38fae17 | ||
|
|
af54ee6c5c | ||
|
|
161fd1f1b6 | ||
|
|
479b786761 | ||
|
|
daeefeb487 | ||
|
|
a56331d39d | ||
|
|
42c458a736 | ||
|
|
5cf57a520c | ||
|
|
b500981434 | ||
|
|
b0c41ca436 | ||
|
|
df1f805294 | ||
|
|
e640149a23 | ||
|
|
5e36bf797e | ||
|
|
038e4ba386 | ||
|
|
0231d79b5a | ||
|
|
fe0e22a5cd | ||
|
|
20efacfa59 | ||
|
|
5e84a237fb | ||
|
|
f9e0b43707 | ||
|
|
ed0ef385e9 | ||
|
|
f2a0ae108c | ||
|
|
a721d9286b | ||
|
|
bc2baa3bbd | ||
|
|
51660f7677 | ||
|
|
f08039b23c | ||
|
|
9d0dcda60f | ||
|
|
c42fbd6e0e | ||
|
|
0b14c311b4 | ||
|
|
31d427546b |
+92
-94
@@ -139,7 +139,7 @@ steps:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-lint
|
||||
image: golangci/golangci-lint:v1.59.1
|
||||
image: golangci/golangci-lint:v1.61.0
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
@@ -364,7 +364,7 @@ steps:
|
||||
- api-build
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -378,7 +378,7 @@ steps:
|
||||
# - restore-cache
|
||||
|
||||
- name: frontend-lint
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -390,7 +390,7 @@ steps:
|
||||
- frontend-dependencies
|
||||
|
||||
- name: frontend-build-prod
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -402,7 +402,7 @@ steps:
|
||||
- frontend-dependencies
|
||||
|
||||
- name: frontend-test-unit
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
commands:
|
||||
- cd frontend
|
||||
@@ -413,7 +413,7 @@ steps:
|
||||
|
||||
- name: frontend-typecheck
|
||||
failure: ignore
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -544,7 +544,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -556,7 +556,7 @@ steps:
|
||||
- pnpm install --fetch-timeout 100000
|
||||
|
||||
- name: frontend-build
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -580,7 +580,7 @@ steps:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: before-static-build
|
||||
image: techknowlogick/xgo:latest
|
||||
image: ghcr.io/techknowlogick/xgo:go-1.23.x
|
||||
pull: always
|
||||
commands:
|
||||
- export PATH=$PATH:$GOPATH/bin
|
||||
@@ -589,7 +589,7 @@ steps:
|
||||
depends_on: [ fetch-tags, mage ]
|
||||
|
||||
- name: static-build-windows
|
||||
image: techknowlogick/xgo:latest
|
||||
image: ghcr.io/techknowlogick/xgo:go-1.23.x
|
||||
pull: always
|
||||
environment:
|
||||
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
|
||||
@@ -605,7 +605,7 @@ steps:
|
||||
- frontend-build
|
||||
|
||||
- name: static-build-linux
|
||||
image: techknowlogick/xgo:latest
|
||||
image: ghcr.io/techknowlogick/xgo:go-1.23.x
|
||||
pull: always
|
||||
environment:
|
||||
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
|
||||
@@ -621,7 +621,7 @@ steps:
|
||||
- frontend-build
|
||||
|
||||
- name: static-build-darwin
|
||||
image: techknowlogick/xgo:latest
|
||||
image: ghcr.io/techknowlogick/xgo:go-1.23.x
|
||||
pull: always
|
||||
environment:
|
||||
# This path does not exist. However, when we set the gopath to /go, the build fails. Not sure why.
|
||||
@@ -647,7 +647,7 @@ steps:
|
||||
- ./mage-static release:compress
|
||||
|
||||
- name: after-build-static
|
||||
image: techknowlogick/xgo:latest
|
||||
image: ghcr.io/techknowlogick/xgo:go-1.23.x
|
||||
pull: always
|
||||
depends_on:
|
||||
- after-build-compress
|
||||
@@ -676,13 +676,13 @@ steps:
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
bucket: vikunja
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
from_secret: hetzner_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
from_secret: hetzner_secret_access_key
|
||||
endpoint: https://fsn1.your-objectstorage.com
|
||||
region: fsn1
|
||||
path_style: true
|
||||
strip_prefix: dist/zip/
|
||||
source: dist/zip/*
|
||||
@@ -698,13 +698,13 @@ steps:
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
bucket: vikunja
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
from_secret: hetzner_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
from_secret: hetzner_secret_access_key
|
||||
endpoint: https://fsn1.your-objectstorage.com
|
||||
region: fsn1
|
||||
path_style: true
|
||||
strip_prefix: dist/zip/
|
||||
source: dist/zip/*
|
||||
@@ -716,7 +716,7 @@ steps:
|
||||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm:v2.37.1
|
||||
image: goreleaser/nfpm:v2.40.0
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
@@ -732,7 +732,7 @@ steps:
|
||||
depends_on: [ after-build-compress ]
|
||||
|
||||
- name: build-os-packages-version
|
||||
image: goreleaser/nfpm:v2.37.1
|
||||
image: goreleaser/nfpm:v2.40.0
|
||||
pull: always
|
||||
commands:
|
||||
- apk add git go
|
||||
@@ -750,13 +750,13 @@ steps:
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
bucket: vikunja
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
from_secret: hetzner_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
from_secret: hetzner_secret_access_key
|
||||
endpoint: https://fsn1.your-objectstorage.com
|
||||
region: fsn1
|
||||
path_style: true
|
||||
strip_prefix: dist/os-packages/
|
||||
source: dist/os-packages/*
|
||||
@@ -772,13 +772,13 @@ steps:
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
bucket: vikunja
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
from_secret: hetzner_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
from_secret: hetzner_secret_access_key
|
||||
endpoint: https://fsn1.your-objectstorage.com
|
||||
region: fsn1
|
||||
path_style: true
|
||||
strip_prefix: dist/os-packages/
|
||||
source: dist/os-packages/*
|
||||
@@ -837,7 +837,6 @@ steps:
|
||||
repo: vikunja/vikunja
|
||||
tags: unstable
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
@@ -869,7 +868,6 @@ steps:
|
||||
from_secret: docker_password
|
||||
repo: vikunja/vikunja
|
||||
platforms:
|
||||
- linux/386
|
||||
- linux/amd64
|
||||
- linux/arm/v6
|
||||
- linux/arm/v7
|
||||
@@ -901,7 +899,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -931,13 +929,13 @@ steps:
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
bucket: vikunja
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
from_secret: hetzner_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
from_secret: hetzner_secret_access_key
|
||||
endpoint: https://fsn1.your-objectstorage.com
|
||||
region: fsn1
|
||||
path_style: true
|
||||
source: frontend/vikunja-frontend-unstable.zip
|
||||
target: /
|
||||
@@ -962,7 +960,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -990,13 +988,13 @@ steps:
|
||||
image: plugins/s3
|
||||
pull: always
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
bucket: vikunja
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
from_secret: hetzner_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
from_secret: hetzner_secret_access_key
|
||||
endpoint: https://fsn1.your-objectstorage.com
|
||||
region: fsn1
|
||||
path_style: true
|
||||
source: frontend/vikunja-frontend-${DRONE_TAG##v}.zip
|
||||
target: /
|
||||
@@ -1095,8 +1093,8 @@ steps:
|
||||
# settings:
|
||||
# restore: true
|
||||
# bucket: kolaente.dev-drone-dependency-cache
|
||||
# endpoint: https://s3.fr-par.scw.cloud
|
||||
# region: fr-par
|
||||
# endpoint: https://fsn1.your-objectstorage.com
|
||||
# region: fsn1
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
@@ -1133,8 +1131,8 @@ steps:
|
||||
# settings:
|
||||
# rebuild: true
|
||||
# bucket: kolaente.dev-drone-dependency-cache
|
||||
# endpoint: https://s3.fr-par.scw.cloud
|
||||
# region: fr-par
|
||||
# endpoint: https://fsn1.your-objectstorage.com
|
||||
# region: fsn1
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
@@ -1176,8 +1174,8 @@ steps:
|
||||
# settings:
|
||||
# restore: true
|
||||
# bucket: kolaente.dev-drone-dependency-cache
|
||||
# endpoint: https://s3.fr-par.scw.cloud
|
||||
# region: fr-par
|
||||
# endpoint: https://fsn1.your-objectstorage.com
|
||||
# region: fsn1
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
@@ -1200,30 +1198,30 @@ steps:
|
||||
- unzip vikunja-frontend-$$VERSION.zip -d frontend
|
||||
- sed -i 's/\\/api\\/v1//g' frontend/index.html
|
||||
- ./bumpp.sh
|
||||
- yarn install
|
||||
- cat package.json
|
||||
- yarn dist --linux --windows
|
||||
|
||||
# - name: rebuild-cache
|
||||
# image: meltwater/drone-cache:dev
|
||||
# pull: true
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: cache_aws_access_key_id
|
||||
# AWS_SECRET_ACCESS_KEY:
|
||||
# from_secret: cache_aws_secret_access_key
|
||||
# settings:
|
||||
# rebuild: true
|
||||
# bucket: kolaente.dev-drone-dependency-cache
|
||||
# endpoint: https://s3.fr-par.scw.cloud
|
||||
# region: fr-par
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
# - '.cache'
|
||||
# depends_on:
|
||||
# - build
|
||||
- corepack enable && pnpm config set store-dir .cache/pnpm
|
||||
- pnpm install --fetch-timeout 100000
|
||||
- pnpm dist --linux --windows
|
||||
|
||||
# - name: rebuild-cache
|
||||
# image: meltwater/drone-cache:dev
|
||||
# pull: true
|
||||
# environment:
|
||||
# AWS_ACCESS_KEY_ID:
|
||||
# from_secret: cache_aws_access_key_id
|
||||
# AWS_SECRET_ACCESS_KEY:
|
||||
# from_secret: cache_aws_secret_access_key
|
||||
# settings:
|
||||
# rebuild: true
|
||||
# bucket: kolaente.dev-drone-dependency-cache
|
||||
# endpoint: https://fsn1.your-objectstorage.com
|
||||
# region: fsn1
|
||||
# path_style: true
|
||||
# cache_key: '{{ .Repo.Name }}_{{ checksum "desktop/yarn.lock" }}_{{ arch }}_{{ os }}'
|
||||
# mount:
|
||||
# - '.cache'
|
||||
# depends_on:
|
||||
# - build
|
||||
|
||||
- name: rename-unstable
|
||||
image: bash
|
||||
pull: true
|
||||
@@ -1241,13 +1239,13 @@ steps:
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
bucket: vikunja
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
from_secret: hetzner_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
from_secret: hetzner_secret_access_key
|
||||
endpoint: https://fsn1.your-objectstorage.com
|
||||
region: fsn1
|
||||
path_style: true
|
||||
strip_prefix: desktop/dist/
|
||||
source: desktop/dist/Vikunja-Desktop*
|
||||
@@ -1263,13 +1261,13 @@ steps:
|
||||
image: plugins/s3
|
||||
pull: true
|
||||
settings:
|
||||
bucket: vikunja-releases
|
||||
bucket: vikunja
|
||||
access_key:
|
||||
from_secret: aws_access_key_id
|
||||
from_secret: hetzner_access_key_id
|
||||
secret_key:
|
||||
from_secret: aws_secret_access_key
|
||||
endpoint: https://s3.fr-par.scw.cloud
|
||||
region: fr-par
|
||||
from_secret: hetzner_secret_access_key
|
||||
endpoint: https://fsn1.your-objectstorage.com
|
||||
region: fsn1
|
||||
path_style: true
|
||||
strip_prefix: desktop/dist/
|
||||
source: desktop/dist/*
|
||||
@@ -1298,9 +1296,9 @@ steps:
|
||||
# - name: build
|
||||
# environment:
|
||||
# ACCESS_KEY:
|
||||
# from_secret: aws_access_key_id
|
||||
# from_secret: hetzner_access_key_id
|
||||
# SECRET_KEY:
|
||||
# from_secret: aws_secret_access_key
|
||||
# from_secret: hetzner_secret_access_key
|
||||
# commands:
|
||||
# - git fetch --tags
|
||||
# - export VERSION=${DRONE_TAG##v}
|
||||
@@ -1313,9 +1311,9 @@ steps:
|
||||
# - sed -i '' "s/\$${version}/$$VERSION/g" package.json
|
||||
# - yarn install
|
||||
# - yarn dist --mac
|
||||
# - mc config host add scw-fr-par https://s3.fr-par.scw.cloud $ACCESS_KEY $SECRET_KEY --api S3v4
|
||||
# - mc cp ./dist/*.dmg scw-fr-par/vikunja-releases/desktop/$VERSION/
|
||||
# - mc cp ./dist/*.dmg.blockmap scw-fr-par/vikunja-releases/desktop/$VERSION/
|
||||
# - mc config host add scw-fsn1 https://fsn1.your-objectstorage.com $ACCESS_KEY $SECRET_KEY --api S3v4
|
||||
# - mc cp ./dist/*.dmg scw-fsn1/vikunja/desktop/$VERSION/
|
||||
# - mc cp ./dist/*.dmg.blockmap scw-fsn1/vikunja/desktop/$VERSION/
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
@@ -1352,6 +1350,6 @@ steps:
|
||||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 3cc4b45fcbbab5ccee14e5532b2d35f5db54cde29a8f6e854b563462e3bbb509
|
||||
hmac: 02fb3097f3e50facb2579de5c3b94e513b97e535a049a28ddb67f85e494d65b1
|
||||
|
||||
...
|
||||
|
||||
@@ -1 +1,3 @@
|
||||
use flake
|
||||
source_url "https://raw.githubusercontent.com/cachix/devenv/95f329d49a8a5289d31e0982652f7058a189bfca/direnvrc" "sha256-d+8cBpDfDBj41inrADaJt+bDWhOktwslgoP5YiGJ1v0="
|
||||
|
||||
use devenv
|
||||
+11
-1
@@ -28,4 +28,14 @@ vendor/
|
||||
os-packages/
|
||||
mage_output_file.go
|
||||
mage-static
|
||||
.direnv/
|
||||
.DS_Store
|
||||
|
||||
# Devenv
|
||||
.devenv*
|
||||
devenv.local.nix
|
||||
|
||||
# direnv
|
||||
.direnv
|
||||
|
||||
# pre-commit
|
||||
.pre-commit-config.yaml
|
||||
|
||||
+13
-2
@@ -4,7 +4,9 @@ run:
|
||||
|
||||
linters:
|
||||
enable:
|
||||
- megacheck
|
||||
- gosimple
|
||||
- staticcheck
|
||||
- unused
|
||||
- govet
|
||||
- gocritic
|
||||
- gocyclo
|
||||
@@ -15,7 +17,6 @@ linters:
|
||||
- revive
|
||||
- misspell
|
||||
disable:
|
||||
- scopelint # Obsolete, using exportloopref instead
|
||||
- durationcheck
|
||||
- goconst
|
||||
- musttag
|
||||
@@ -108,3 +109,13 @@ issues:
|
||||
text: 'structtag: struct field Position repeats json tag "position" also at'
|
||||
linters:
|
||||
- govet
|
||||
- path: pkg/cmd/user.go
|
||||
text: 'G115: integer overflow conversion uintptr -> int'
|
||||
linters:
|
||||
- gosec
|
||||
- text: 'G115: integer overflow conversion int64 -> uint64'
|
||||
linters:
|
||||
- gosec
|
||||
- text: 'G115: integer overflow conversion int -> uint64'
|
||||
linters:
|
||||
- gosec
|
||||
|
||||
Vendored
+13
-11
@@ -1,13 +1,15 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"codezombiech.gitignore",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"vue.volar",
|
||||
"lokalise.i18n-ally",
|
||||
"mgmcdermott.vscode-language-babel",
|
||||
"mikestead.dotenv",
|
||||
"Syler.sass-indented",
|
||||
"vitest.explorer"
|
||||
]
|
||||
"recommendations": [
|
||||
"codezombiech.gitignore",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"editorconfig.editorconfig",
|
||||
"vue.volar",
|
||||
"lokalise.i18n-ally",
|
||||
"mgmcdermott.vscode-language-babel",
|
||||
"mikestead.dotenv",
|
||||
"Syler.sass-indented",
|
||||
"vitest.explorer",
|
||||
"mkhl.direnv",
|
||||
"golang.Go"
|
||||
]
|
||||
}
|
||||
Vendored
+1
-1
@@ -26,7 +26,7 @@
|
||||
|
||||
// i18n ally
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/i18n/lang"
|
||||
"frontend/src/i18n/lang"
|
||||
],
|
||||
"i18n-ally.sortKeys": true,
|
||||
"i18n-ally.keepFulfilled": true,
|
||||
|
||||
+474
-1
@@ -5,7 +5,480 @@ All notable changes to this project will be documented in this file.
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres
|
||||
to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
All releases can be found on https://code.vikunja.io/api/releases.
|
||||
All releases can be found on https://code.vikunja.io/vikunja/releases.
|
||||
|
||||
## [0.24.6] - 2024-12-22
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(export)* Update only current user export file id
|
||||
|
||||
### Features
|
||||
|
||||
* Use hetzner object storage for releases ([3f98f47](3f98f47256628ac559a947f6c9541c0bf03001b6))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* Release preparation ([17b2810](17b281072f095730a0fbb28458d713fa883f43ec))
|
||||
* Sign drone config ([b91a5d9](b91a5d9adfb7144126f5824224970ad4015dcd6a))
|
||||
* Do not generate config yaml ([8c28596](8c285968afff7ebd087ce824b52b0a9f28a8ec20))
|
||||
|
||||
## [0.24.5] - 2024-11-21
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(attachments)* Check permissions when accessing all attachments
|
||||
* *(saved filters)* Check permissions when accessing tasks of a filter
|
||||
* Pin xgo to 1.22.x ([87b2aac](87b2aaccb8cdcbe1ecb6092951a0bfe224ad7006))
|
||||
* Upgrade xgo ([19b63c8](19b63c86c51f67614b867c75a58cda1774685edd))
|
||||
* Upgrade xgo docker image everywhere ([04b40f8](04b40f8a7dcd01a86ddb8b27596073d1e50f9e97))
|
||||
* *(ci)* Do not build linux 368 docker images
|
||||
* Disable 368 releases ([73db10f](73db10fb02268e07d29842493df55f4d645ac503))
|
||||
- **BREAKING**: disable 368 releases
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* Sign drone config ([17c4878](17c487875b5771c0971ee8bf030807171de2dddc))
|
||||
* Go mod tidy ([9639025](96390257e0911089ae33a9565e8be7fa954c772c))
|
||||
|
||||
## [0.24.4] - 2024-09-29
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(attachment)* Do not use image previews
|
||||
* *(checkbox)* Use sibling css selector instead of has
|
||||
* *(files)* Only use service rootpath for files when the files path is not absolute
|
||||
* *(filters)* Explicitly search in json when using postgres
|
||||
* *(task)* Paginate task comments
|
||||
* *(task)* Do not show close button when the task was not opened via modal
|
||||
* *(task)* Improve task delete modal on mobile
|
||||
* *(test)* Use correct selector for modal header
|
||||
* Partial fix to allow list tasks in ios reminders app (#2717)
|
||||
* *(attachments)* Revert "chore(attachments): refactor building image preview"
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update desktop lockfile
|
||||
* *(deps)* Update dependency vue to v3.5.7
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency vue-i18n to v10.0.2
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency vue to v3.5.8
|
||||
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v5.1.0
|
||||
* *(deps)* Update dependency vue-i18n to v10.0.3
|
||||
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v5.2.0
|
||||
* *(deps)* Update dependency @sentry/vue to v8.31.0
|
||||
* *(deps)* Update dependency tailwindcss to v3.4.13
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency @sentry/vue to v8.32.0
|
||||
* *(deps)* Update tiptap to v2.7.3
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dependency vue to v3.5.9
|
||||
* *(deps)* Update dependency dompurify to v3.1.7
|
||||
* *(deps)* Update tiptap to v2.7.4
|
||||
* *(deps)* Update dependency vue to v3.5.10
|
||||
* *(deps)* Update dev-dependencies
|
||||
|
||||
## [0.24.3] - 2024-09-20
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(a11y)* Hide unfocusable buttons
|
||||
* *(api)* Return 404 response when using a token and the route does not exist
|
||||
* *(auth)* Restrict max password length to 72 bytes
|
||||
* *(caldav)* Make sure colors are correctly saved and returned
|
||||
* *(caldav)* Reject invalid project id with error 400
|
||||
* *(editor)* Restore the current value, not the one from a previous task
|
||||
* *(files)* Use absolute path everywhere
|
||||
* *(filter)* Do not replace labels keyword when the value is 'label'
|
||||
* *(filter)* Make sure tasks are in a correct bucket and position when they are part of a date filter
|
||||
* *(filters)* Immediately propagate changes
|
||||
* *(filters)* Do not replace filter or project values when the id value resolves to undefined
|
||||
* *(filters)* Correctly transform and populate saved filter when creating and editing
|
||||
* *(home)* Explicitly use filter for tasks on home page when one is set
|
||||
* *(kanban)* Save updated position to store
|
||||
* *(kanban)* Make task creation loading spinner actually visible
|
||||
* *(kanban)* Make kanban full width on mobile
|
||||
* *(kanban)* Do not mark first bucked as done bucket in filter bucket mode
|
||||
* *(kanban)* Correctly paginate filtered kanban buckets
|
||||
* *(label)* Ignore existing ID during creation
|
||||
* *(labels)* Trigger task.updated event when removing a label from a task
|
||||
* *(labels)* Test error assertion
|
||||
* *(labels)* Remove input interactivity when label edit is disabled
|
||||
* *(labels)* Trigger task updated for bulk label task update
|
||||
* *(modal)* Make sure modal and its content scrolls properly on mobile
|
||||
* *(modal)* Do not prevent scrolling on mobile
|
||||
* *(modal)* Make scrolling on iOS Safari work
|
||||
* *(multiselect)* Make selectPlaceholder optional
|
||||
* *(notifications)* Only add project subscription as task subscription when the user is not already subscribed to the task
|
||||
* *(password)* Validate password before sending request to api
|
||||
* *(project)* Show description in title attribute without html
|
||||
* *(project)* Reset id before creating
|
||||
* *(projects)* Do not hide 6th project on project overview
|
||||
* *(projects)* Description not visible on mobile
|
||||
* *(reminders)* Notify subscribed users as well
|
||||
* *(service worker)* Use correct workbox version
|
||||
* *(subscription)* Always return task subscription when subscribed to task and project
|
||||
* *(subscriptions)* Ignore task subscription when the user is subscribed to the project
|
||||
* *(subscriptions)* Correctly inherit subscriptions
|
||||
* *(subscriptions)* Cleanup and simplify fetching subscribers for tasks and projects logic
|
||||
* *(subscriptions)* Do not panic when a task does not have a subscription
|
||||
* *(table)* Make sorting for two-word properties work
|
||||
* *(task)* Set done at date when moving a task to the done bucket
|
||||
* *(task)* Specify task index when creating multiple tasks at once
|
||||
* *(task)* Cyclomatic complexity
|
||||
* *(task)* Make print styles work when printing task detail view from kanban
|
||||
* *(task)* Multiple overlapping defer due date popups
|
||||
* *(task)* Align task title on mobile popup
|
||||
* *(task)* Dragging and dropping on mobile
|
||||
* *(task)* Add task to filter view after it was updated
|
||||
* *(task)* Cleanup old task positions and task buckets when adding an updated or created task to filter
|
||||
* *(task)* Mark related task as done from the task detail view
|
||||
* *(task)* Open focused task when pressing enter
|
||||
* *(test)* Cypress test selector
|
||||
* *(typesense)* Only fail silently when a project was not found during indexing
|
||||
* *(typesense)* Add new tasks to typesense properly
|
||||
* *(typesense)* Make sure task positions are recreated properly when updating them
|
||||
* *(typesense)* Use emplace instead of upsert to update documents
|
||||
* *(typesense)* Index tasks one by one
|
||||
* *(typesense)* Force position to always be float instead of auto-inferring
|
||||
* *(typesense)* Use typesense bulk insert, log all errors
|
||||
* *(user)* Do not create user with existing id
|
||||
* *(view)* Do not crash when saving a view
|
||||
* *(view)* Correctly resolve label for filtered views or buckets
|
||||
* *(view)* Correctly resolve bucket filter when paginating
|
||||
* *(view)* Correctly get paginated task results
|
||||
* *(views)* Add migration for filtered kanban buckets* Lint ([53d62d3](53d62d35f4488940a96d755de93ded64b8ac34a3))
|
||||
* Reset id before creating ([93f7dd6](93f7dd611ad288a149f5da5463867d224334815f))
|
||||
* Test selector ([063aa7a](063aa7afec717c3ed05be9d2ca73bde3d0bd8d35))
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update dependency @intlify/unplugin-vue-i18n to v5
|
||||
* *(deps)* Update dependency @kyvg/vue3-notification to v3.3.0
|
||||
* *(deps)* Update dependency @sentry/vue to v8.28.0
|
||||
* *(deps)* Update dependency @sentry/vue to v8.29.0
|
||||
* *(deps)* Update dependency @sentry/vue to v8.30.0
|
||||
* *(deps)* Update dependency axios to v1.7.7
|
||||
* *(deps)* Update dependency date-fns to v4
|
||||
* *(deps)* Update dependency dayjs to v1.11.13
|
||||
* *(deps)* Update dependency express to v4.20.0
|
||||
* *(deps)* Update dependency express to v4.21.0
|
||||
* *(deps)* Update dependency go to v1.23.1
|
||||
* *(deps)* Update dependency pinia to v2.2.2
|
||||
* *(deps)* Update dependency sortablejs to v1.15.3
|
||||
* *(deps)* Update dependency tailwindcss to v3.4.10
|
||||
* *(deps)* Update dependency tailwindcss to v3.4.11
|
||||
* *(deps)* Update dependency tailwindcss to v3.4.12
|
||||
* *(deps)* Update dependency vue to v3.5.3
|
||||
* *(deps)* Update dependency vue to v3.5.4
|
||||
* *(deps)* Update dependency vue to v3.5.5
|
||||
* *(deps)* Update dependency vue to v3.5.6
|
||||
* *(deps)* Update dependency vue-i18n to v10
|
||||
* *(deps)* Update dependency vue-i18n to v10.0.1
|
||||
* *(deps)* Update dependency vue-i18n to v9.14.0
|
||||
* *(deps)* Update dependency vue-router to v4.4.3
|
||||
* *(deps)* Update dependency vue-router to v4.4.4
|
||||
* *(deps)* Update dependency vue-router to v4.4.5
|
||||
* *(deps)* Update dependency vuemoji-picker to v0.3.1* Chore(deps): update goreleaser/nfpm docker tag to v2.40.0 (#2647)
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update github.com/wneessen/go-mail to v0.4.4
|
||||
* *(deps)* Update golangci
|
||||
* *(deps)* Update module dario.cat/mergo to v1.0.1
|
||||
* *(deps)* Update module github.com/gabriel-vasile/mimetype to v1.4.5
|
||||
* *(deps)* Update module github.com/getsentry/sentry-go to v0.29.0
|
||||
* *(deps)* Update module github.com/mattn/go-sqlite3 to v1.14.23
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.20.3
|
||||
* *(deps)* Update module github.com/prometheus/client_golang to v1.20.4
|
||||
* *(deps)* Update module github.com/redis/go-redis/v9 to v9.6.1
|
||||
* *(deps)* Update module github.com/threedotslabs/watermill to v1.3.7
|
||||
* *(deps)* Update module github.com/typesense/typesense-go to v2
|
||||
* *(deps)* Update module github.com/typesense/typesense-go to v2
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.27.0
|
||||
* *(deps)* Update module golang.org/x/image to v0.20.0
|
||||
* *(deps)* Update module golang.org/x/oauth2 to v0.23.0
|
||||
* *(deps)* Update module golang.org/x/term to v0.24.0
|
||||
* *(deps)* Update module golang.org/x/text to v0.18.0
|
||||
* *(deps)* Update pnpm to v9.10.0
|
||||
* *(deps)* Update tiptap to 2.6.6
|
||||
* *(deps)* Update tiptap to v2.7.0
|
||||
* *(deps)* Update tiptap to v2.7.1
|
||||
* *(deps)* Update tiptap to v2.7.2
|
||||
* *(deps)* Update vueuse to v11
|
||||
* *(deps)* Update vueuse to v11.1.0
|
||||
* *(deps)*: update dependency flexsearch to v0.7.43 (#2095)
|
||||
* *(deps)*: update golangci/golangci-lint docker tag to v1.61.0 (#2678)
|
||||
|
||||
### Documentation
|
||||
|
||||
* *(api)* Use correct return type for the /user endpoint
|
||||
|
||||
### Features
|
||||
|
||||
* *(event)* Simplify dispatching task updated event from only a task id
|
||||
* *(navigation)* Use focus-visible for nav items
|
||||
* *(task)* Use focus-visible for task focus styles
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(attachments)* Refactor building image preview
|
||||
* *(devenv)* Do not install cypress on darwin
|
||||
* *(docker)* Use new env format
|
||||
* *(docs)* Clarify usage of related model creation
|
||||
* *(errors)* Always add internal error to echo error
|
||||
* *(files)* Use absolute file path to retrieve and save files
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(logging)* Simplify log template string
|
||||
* *(magefile)* Use tx.Sync instead of Sync2
|
||||
* *(subscription)* Return subscription entity type using json Marshaler
|
||||
* *(tasks)* Move drag options to direct attributes instead of v-bind
|
||||
* *(typesense)* Add more debug logging
|
||||
* *(web)* Move web handler package to Vikunja
|
||||
* *(web)* Remove unused echo context
|
||||
* *(web)* Use errors.As instead of type assertion
|
||||
* *(web)* Remove redundant use of fmt.Sprintf
|
||||
* *(web)* Directly use new db session
|
||||
* *(web)* Use config directly
|
||||
* *(web)* Use web auth factory directly
|
||||
* *(web)* Use logger directly
|
||||
* *(web)* Always set internal error* Remove console.log ([40105ee](40105ee4ced980f52565baec4c3219b0ddd4f6ec))
|
||||
* Fix comment ([1df4a4e](1df4a4ea2e2ca4332347468e8973a2dcbab06ed7))
|
||||
* Add go and direnv to recommended vscode extensions ([6ab12b9](6ab12b9dd133b52ed7267b6e9334081c2f9719ca))
|
||||
* Remove console.log ([1e7d9c9](1e7d9c982d3d472e9b4082991b41e6567556f2b2))
|
||||
* Rearrange cron registers ([4857bfb](4857bfbbdb8401b6ef02b1dc8de93f2a09e8bc3a))
|
||||
|
||||
### Other
|
||||
|
||||
* *(other)* [skip ci] Updated swagger docs
|
||||
|
||||
## [0.24.2] - 2024-08-12
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(i18n)* Change casing of Ukrainian language in selector
|
||||
* *(kanban)* Always make cover image full width
|
||||
* *(mail)* Do not fail testmail command when the connection could not be closed.
|
||||
* *(migration)* Make sure tasks are associated to the correct view and bucket for data imported from Vikunja dump
|
||||
* *(migration)* Ensure project background gets exported and imported
|
||||
* *(projects)* Trigger only single mutation
|
||||
* *(task)* Do not allow moving a task to the project the task already belongs to
|
||||
* *(task)* Set current project after moving a task
|
||||
* *(task)* Move task into new kanban bucket when moving between projects
|
||||
* *(views)* Do not create task bucket and task position entries when duplicating a project* Emit for DatepickerWithValues ([3aaf363](3aaf3634134a6989337bad02ac99a9329d33b17f))
|
||||
* Textarea autosize for LanguageTool ([d9f5555](d9f555554e5ecfa9d1243c565e2f42c77f7a7597))
|
||||
* Remove console log ([0ca43dc](0ca43dc147acd04d9f9b566325ccde0a5782680f))
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update module github.com/coreos/go-oidc/v3 to v3.11.0
|
||||
* *(deps)* Update flake
|
||||
* *(deps)* Update go toolchain to 1.22.5
|
||||
* *(deps)* Update dependency node to v20.16.0
|
||||
|
||||
### Documentation
|
||||
|
||||
* Clarify Todoist redirect url ([7117303](7117303d5705199bb39fb6661b20deebea96959f))
|
||||
|
||||
### Features
|
||||
|
||||
* *(editor)* Support custom protocol for links* Use withDefaults for Reminders ([8729c24](8729c24e1d2bdc783e750dc1166a8ab31a716107))
|
||||
* Improve projects store ([d707e15](d707e1576a006baa73c529b432e694dee237db76))
|
||||
* Improve label store ([a0e3efe](a0e3efe2d12521e773334a19ac985a2011575c3c))
|
||||
* Improve priority visibility ([dddba4d](dddba4d64a9ddddd6f442c8180661d2d498de994))
|
||||
* Add tailwind with prefix (#2513) ([d7c5451](d7c54517297e750d92618376f52045b11da6e82c))
|
||||
* Improve ProjectSettingsViews ([811ccc1](811ccc1baa2fe1fb9fcc8b5515c8c3f3b591e96a))
|
||||
* Add missing peer dependency ([d586f69](d586f691b7a245bbffd5df4e7a4937f0527047e0))
|
||||
* Switch from nix flakes to devenv ([73f923b](73f923bc47d32a6a9a689e461901295b07646ebf))
|
||||
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* Remove lodash.debounce ([fc780a9](fc780a90ae856038db73d3ad63fcdd5627211c36))
|
||||
* Improve error message ([6e38bcf](6e38bcf3498a15a1dc1f6fbef254c08513b408b3))
|
||||
* Use nixpkgs unstable for more recent packages ([f5040ad](f5040ad2f4996fe1ccf03172b1a1ee6007068ee7))
|
||||
|
||||
### Other
|
||||
|
||||
* *(other)* [skip ci] Updated swagger docs
|
||||
|
||||
## [0.24.1] - 2024-07-18
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* *(api tokens)* Show error message when the user tries to create an api token without at least one permission selected
|
||||
* *(filter)* Make sure filter values are properly escaped before executing them
|
||||
* *(filters)* Add task to buckets of saved filters when creating the task
|
||||
* *(filters)* Add tasks to filter buckets when updating the filter
|
||||
* *(filters)* Do not create a default filter for list view when creating a saved filter
|
||||
* *(filters)* Immediately emit filter query when editing saved filter
|
||||
* *(filters)* Make sure filters are replaced case-insensitively before filtering on the server
|
||||
* *(filters)* Only insert task buckets and positions when there are any
|
||||
* *(filters)* Reload tasks silently when marking one done in the list
|
||||
* *(filters)* Show actual error message from api when the filter query is invalid
|
||||
* *(filters)* Trim spaces when parsing filter values
|
||||
* *(kanban)* Dispatch task updated event when task is moved between buckets
|
||||
* *(kanban)* Dispatch task updated event when task position is updated
|
||||
* *(kanban)* Do not allow to create new tasks in saved filter
|
||||
* *(kanban)* Do not move repeating task into a different bucket
|
||||
* *(kanban)* Make sure tasks which changed their done status are moved around in buckets
|
||||
* *(kanban)* Move repeating task back to old bucket when moved to the done bucket
|
||||
* *(kanban)* Move task to done bucket in all views when moved to done bucket in one view
|
||||
* *(kanban)* Move task to done bucket when it was marked done from the task detail view
|
||||
* *(kanban)* Put task into correct bucket when creating via kanban board
|
||||
* *(kanban)* Update task done status after moving it into done bucket
|
||||
* *(kanban)* Use correct assertion in the test
|
||||
* *(kanban)* Use correct text color for deletion button
|
||||
* *(migration)* Correctly set bucket for related tasks
|
||||
* *(migration)* Failed migration typo
|
||||
* *(migration)* Revert to old path for migration routing
|
||||
* *(project)* Do not use project id of nil project in error
|
||||
* *(projects)* Do not create backlog bucket when duplicating views
|
||||
* *(projects)* Do not create buckets in the original project when duplicating a project
|
||||
* *(quick add magic)* Create the task even when it only contains quick add magic keywords
|
||||
* *(settings)* Overflow of select on mobile
|
||||
* *(task)* Use backdropView prop
|
||||
* *(tasks)* Do not use typesense modified options to search with database
|
||||
* *(tasks)* Explicitly add task position to select statement when looking up tasks with Typesense
|
||||
* *(tasks)* Limit to max 250 entries when using typesense
|
||||
* *(translation)* TOTP casing
|
||||
* *(typesense)* Do not crash after creating a project when tasks are not yet indexed
|
||||
* *(typesense)* Do not use modified opts for db fallback search
|
||||
* *(typesense)* Reindex tasks when their position changed
|
||||
* *(vscode)* I18n-ally locales path* ProjectSearch default value ([f08039b](f08039b23c1ff8ca208f5c5911f788780ecc80ee))
|
||||
* Add info log message when starting to run migrations ([5e36bf7](5e36bf797e99d99f83cb084f8c1707d994d0559f))
|
||||
* Add missing disabled prop ([ed0ef38](ed0ef385e9ba34524d3309bec87eeb44d9dba471))
|
||||
* App bottom padding ([51660f7](51660f76779e189bca2a24f1e4fca34b8a1a2898))
|
||||
* Disable button if loading ([a721d92](a721d9286bbb26e0a182992af6b2128e03d5670f))
|
||||
* Dropdown item disabled prop ([3317280](3317280062c8506d092d5121b92eb4992177f9c2))
|
||||
* Gitignore dist path ([7ef6ddf](7ef6ddf8f7630e6370bed762779b4ebabbf8962b))
|
||||
* Lint ([7c42fb5](7c42fb5d75fd9a5aa01abaa852452875f07a1f61))
|
||||
* Missing error handling ([744b40e](744b40e7f780851d5ce8a288271f78acc462ed14))
|
||||
* Muliselect optional props ([0a81855](0a81855bc1b403233dec18f036c80f3cad70edee))
|
||||
* Reorder mail options (#2533) ([136ef58](136ef58820b8e2d27ad0ca50bdd9adabd8e4a95d))
|
||||
* Scss deprecation warning ([db81701](db81701d3841e12c39b751e919d70fdafc1869d6))
|
||||
* Spelling mail ([2dc5415](2dc541571cbecc02a178182217d1e4596e4a62aa))
|
||||
* Wrapped button ([af639a1](af639a180cf56519e9a3a31710da6fee5b305735))
|
||||
|
||||
### Dependencies
|
||||
|
||||
* *(deps)* Update dependency @github/hotkey to v3.1.1 (#2329)
|
||||
* *(deps)* Update dependency @sentry/vue to v8.14.0
|
||||
* *(deps)* Update dependency @sentry/vue to v8.15.0
|
||||
* *(deps)* Update dependency @sentry/vue to v8.17.0
|
||||
* *(deps)* Update dependency @sentry/vue to v8.18.0
|
||||
* *(deps)* Update dependency dayjs to v1.11.12
|
||||
* *(deps)* Update dependency dompurify to v3.1.6
|
||||
* *(deps)* Update dependency ufo to v1.5.4
|
||||
* *(deps)* Update dependency vue to v3.4.32
|
||||
* *(deps)* Update dependency vue-tsc to v2.0.26
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update dev-dependencies
|
||||
* *(deps)* Update flake
|
||||
* *(deps)* Update font awesome to v6.6.0
|
||||
* *(deps)* Update goreleaser/nfpm docker tag to v2.38.0
|
||||
* *(deps)* Update module github.com/arran4/golang-ical to v0.3.1 (#2606)
|
||||
* *(deps)* Update module github.com/microcosm-cc/bluemonday to v1.0.27
|
||||
* *(deps)* Update module golang.org/x/crypto to v0.25.0
|
||||
* *(deps)* Update module golang.org/x/term to v0.22.0
|
||||
* *(deps)* Update pnpm to v9.5.0
|
||||
* *(deps)* Update tiptap to v2.5.4
|
||||
|
||||
### Features
|
||||
|
||||
* *(Multiselect)* Hide multiselect during loading
|
||||
* *(project)* Add " - duplicate" suffix to duplicated projects title* Move powered by to bottom ([f9e0b43](f9e0b4370700b4cc9c8e99923737bb5aa022a1d0))
|
||||
* Add withDefaults and emit types to PrioritySelect ([aaa2abc](aaa2abced4258645a9a47b228cece21741bfea25))
|
||||
* Add withDefaults, defineEmits and defineSlots types for Dropdown ([5545b0e](5545b0e447d406837324186c765cf2e8c8ed47c4))
|
||||
* Define prop and emit types DatepickerWithRange ([daeefeb](daeefeb487966bfc6535b3d5c8e1dd07ec8a0951))
|
||||
* Define prop and emit types for FilterPopup ([9d2e79f](9d2e79f7253006d504bd916ab8d820187d50700b))
|
||||
* DefineModel and withDefaults for PercentDoneSelect ([8ac0eb4](8ac0eb4aa4fbe5a3b147e7f31a29aa2c2f84a1ca))
|
||||
* Improve BaseButtonEmits type ([c37fe49](c37fe4989015ee18b38bd89b9470c14484759cc8))
|
||||
* Improve main nav spacing and open speed ([faa797f](faa797f461a57e8a9e115b760af92ab0024f6eed))
|
||||
* Improve settings label casing ([20efacf](20efacfa59a7f7ff81fd17c2c3b3c9a1d5528e5d))
|
||||
* Remove PropTypes helper from FilePreview ([0bc1832](0bc18320509e6db7ae484f7336282b7e8e5e841e))
|
||||
* Remove PropTypes helper from ProjectInfo ([8ad7e7c](8ad7e7c9055e50768ea5b0ad5798d395bb5d3514))
|
||||
* Remove PropTypes helper from ProjectSettingsEdit ([85889ff](85889fff5603f0b7c7a0f833cae7c32610eeede4))
|
||||
* Remove eslint rule vue/no-required-prop-with-default ([df1f805](df1f805294e34f8bfaaf60ea65f18cabbd932d9c))
|
||||
* Remove props destructuring ProjectSettingsViews ([20bdb01](20bdb011877fe17fe5c266eafe77b96dce1646d9))
|
||||
* Remove props destructuring for DeferTask ([46aa2ff](46aa2fff0f01bf233e4edd39e6675c358c488794))
|
||||
* Remove props destructuring for Filters ([3ff56d7](3ff56d79874d013ad97c8635fd580edf95290b39))
|
||||
* Remove props destructuring for ProjectView ([553a97f](553a97f03de3e16593c006ca4a9043fadf5d6a6f))
|
||||
* Remove props destructuring for ProjectWrapper ([38744df](38744dfd5db0acb53ae03db5e2021265876df2f7))
|
||||
* Remove props destructuring from Attachments ([07130bd](07130bdc3a65bfb4ed1775e61312b68c62521a4a))
|
||||
* Remove props destructuring from ColorPicker ([eb07be1](eb07be1a62a9af377b044a7e0268d9d7a8bab77c))
|
||||
* Remove props destructuring from FilterDelete ([825d1ad](825d1add49595184593ff6fb3ab228cf3fbde9cb))
|
||||
* Remove props destructuring from FilterEdit ([449e9a9](449e9a911c2fcca7712479cd78dbfe80d5a4bde2))
|
||||
* Remove props destructuring from FilterInput ([fd6692e](fd6692ea1afce3950c2295a4e8fb551af80e52aa))
|
||||
* Remove props destructuring from KanbanCard ([ddc18aa](ddc18aa17739963afa5d156241aa07b7100374f4))
|
||||
* Remove props destructuring from NoAuthWrapper ([c9591fe](c9591fe464e24f38126d5562ef7d0e6060f14b39))
|
||||
* Remove props destructuring from ProjectKanban ([5af908b](5af908b2e5508712004ee1c702915e4def0ca007))
|
||||
* Remove props destructuring from ProjectList ([0c70aed](0c70aedeb1a5ca05db3cc2b87138d28427eac959))
|
||||
* Remove props destructuring from ProjectTable ([99e90c0](99e90c0b02561f1c7b8f49b93ece11b878137e48))
|
||||
* Remove props destructuring from ShowTasks ([d02c349](d02c349231f29326511755dcae5003d3b3c01ca0))
|
||||
* Remove props destructuring from SingleTaskInProject ([a15831e](a15831eb33b5104e19f0b85b882dd8ec5336ba62))
|
||||
* Remove props destructuring from TaskDetailView ([a10f9ca](a10f9ca22580f6fcc435f152776867b380d5be6c))
|
||||
* Remove props destructuring from ViewEditForm ([2f92e40](2f92e407ccf42892675dc1583dfa057474cb2489))
|
||||
* Remove unnecessary prop from Card ([1eb1aa2](1eb1aa257574733894f865913a4bb173e22774ea))
|
||||
* Rename TheNavigation to AppHeader ([43e38fa](43e38fae17d987d96820dcdfd936fcf8d11899ff))
|
||||
* Simplify playPopSound setting check ([42c458a](42c458a736d88bf4b0f18fa23c08273ddf9c7ca5))
|
||||
* Type defineEmits for ApiConfig ([1966cc3](1966cc3c0e7a590651db2a5bd0acdcd83e948aa7))
|
||||
* Type defineEmits for Card ([043a6dd](043a6dd049ba22fa1c87ed88aa0b2f8dba4f2593))
|
||||
* Type defineEmits for CreateEdit ([098c99f](098c99fd2ede44ddf231e1b504b9cf6d8c0ed17b))
|
||||
* Type defineEmits for Heading ([fd3f1de](fd3f1de861ac593c0f780c3f341a4b0c5d7b2f1b))
|
||||
* Type defineEmits for ProjectSearch ([f38a5c9](f38a5c9220c6404152e261a78e4829e52cef5d1b))
|
||||
* Use withDefaults for Comments ([f19f19b](f19f19bb75a457dc2073d8a06f064eae04a3b772))
|
||||
* Use withDefaults for DateTableCell ([f586e51](f586e51aad5194043fec37e3b0af94a30c95f3a3))
|
||||
* Use withDefaults for Datepicker ([78811d9](78811d916aeaa105560c5b7c091290f3b2e38683))
|
||||
* Use withDefaults for DatepickerInline ([df6a9b6](df6a9b67fd2a462e5027538e521da174ecd24bc6))
|
||||
* Use withDefaults for DatepickerWithValues ([cb70641](cb706416c61ba31d1fd54d8b9bd8549cc9a33528))
|
||||
* Use withDefaults for EditLabels ([6e72606](6e72606d7454fbb3b17302d3ef1d2cdf186873ae))
|
||||
* Use withDefaults for FancyCheckbox ([b4e9d94](b4e9d9437e4fb4b8e5e56c633fc4c74ee45a6225))
|
||||
* Use withDefaults for Heading ([5cf57a5](5cf57a520cd7048aba0656ce00333cc7cfcc14f9))
|
||||
* Use withDefaults for Pagination ([1083216](10832165c3798bef63590c292426feb67e178f15))
|
||||
* Use withDefaults for Password ([577f5ae](577f5ae69a31e14cdff6e0aed5f3091d07484faa))
|
||||
* Use withDefaults for PriorityLabel ([871e0ac](871e0acd8aa8ba6a9ad7fb08bda8badf20104f94))
|
||||
* Use withDefaults for ProgressBar ([2f63384](2f6338484bda4670133f3db6c122dbaa6a231c15))
|
||||
* Use withDefaults for ProjectCardGrid ([479b786](479b786761948b0a0427676c42e082e75579029c))
|
||||
* Use withDefaults for Reactions ([d6c3b5a](d6c3b5a9a10a04c8fac47ad998ebf434e93dfa71))
|
||||
* Use withDefaults for ReminderPeriod ([4b9b9da](4b9b9da122a2d085dcff8fbb8dd2693062689fd4))
|
||||
* Use withDefaults for RepeatAfter ([c8585d1](c8585d1a691403e6331cd9aff852873c0f7771d3))
|
||||
* Use withDefaults for SelectProject ([fd12c87](fd12c8705ea9b28db6947fd98f2d34ce46fc15b2))
|
||||
* Use withDefaults for SelectUser ([b500981](b50098143474be8d86e0ae0543bab1787585dfe7))
|
||||
* Use withDefaults for Subscription ([30769fb](30769fb6eadb21d45629dcf2c67cc1174d26e828))
|
||||
* Use withDefaults for UserTeam ([f2fdbad](f2fdbad7d4bde5880694a611a4f9d68fe2017367))
|
||||
* Use withDefaults in ReminderDetail ([a56331d](a56331d39d12c65838b13987fe2096c4ca2a703f))
|
||||
* WithDefaults for EditAssignees ([f1481d7](f1481d702cc426cddfeda0edae6a6cd06d9f3380))
|
||||
* WithDefaults for Multiselect ([413d1f9](413d1f9ad7e9ba0234d81a1564745cd03d9f1044))
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
* *(i18n)* Update translations via Crowdin
|
||||
* *(popup)* Trigger close function directly
|
||||
* *(project)* Rename receiver
|
||||
* 0.24.0 release preparations ([0b14c31](0b14c311b4d344aecf974489eee19ab939c676ad))
|
||||
* Go mod tidy ([e640149](e640149a23c931838a93b737804287a9ed570268))
|
||||
* Update golangci lint config ([d2602a7](d2602a7629ee3d8cd67beb8ef1a44ca3cd2dd7f9))
|
||||
* *(other)* [skip ci] Updated swagger docs
|
||||
|
||||
## [0.24.0] - 2024-07-02
|
||||
|
||||
|
||||
+6
-6
@@ -1,11 +1,11 @@
|
||||
# syntax=docker/dockerfile:1
|
||||
FROM --platform=$BUILDPLATFORM node:20.14.0-alpine AS frontendbuilder
|
||||
FROM --platform=$BUILDPLATFORM node:20.16.0-alpine AS frontendbuilder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
ENV PNPM_CACHE_FOLDER .cache/pnpm/
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD true
|
||||
ENV CYPRESS_INSTALL_BINARY 0
|
||||
ENV PNPM_CACHE_FOLDER=.cache/pnpm/
|
||||
ENV PUPPETEER_SKIP_DOWNLOAD=true
|
||||
ENV CYPRESS_INSTALL_BINARY=0
|
||||
|
||||
COPY frontend/ ./
|
||||
|
||||
@@ -13,7 +13,7 @@ RUN corepack enable && \
|
||||
pnpm install && \
|
||||
pnpm run build
|
||||
|
||||
FROM --platform=$BUILDPLATFORM techknowlogick/xgo:go-1.21.x AS apibuilder
|
||||
FROM --platform=$BUILDPLATFORM ghcr.io/techknowlogick/xgo:go-1.23.x AS apibuilder
|
||||
|
||||
RUN go install github.com/magefile/mage@latest && \
|
||||
mv /go/bin/mage /usr/local/go/bin
|
||||
@@ -24,7 +24,7 @@ COPY --from=frontendbuilder /build/dist ./frontend/dist
|
||||
|
||||
ARG TARGETOS TARGETARCH TARGETVARIANT
|
||||
|
||||
ENV GOPROXY https://goproxy.kolaente.de
|
||||
ENV GOPROXY=https://goproxy.kolaente.de
|
||||
RUN export PATH=$PATH:$GOPATH/bin && \
|
||||
mage build:clean && \
|
||||
mage release:xgo "${TARGETOS}/${TARGETARCH}/${TARGETVARIANT}"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
[](https://drone.kolaente.de/vikunja/vikunja)
|
||||
[](LICENSE)
|
||||
[](https://dl.vikunja.io)
|
||||
[](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)
|
||||
|
||||
+6
-5
@@ -227,12 +227,13 @@ migration:
|
||||
clientid:
|
||||
# The client secret, also required for making requests to the todoist api
|
||||
clientsecret:
|
||||
# The url where clients are redirected after they authorized Vikunja to access their todoist items.
|
||||
# This needs to match the url you entered when registering your Vikunja instance at todoist.
|
||||
# This is usually the frontend url where the frontend then makes a request to /migration/todoist/migrate
|
||||
# The url where clients are redirected after they authorized Vikunja to access their Todoist items.
|
||||
# In Todoist, this is called `OAuth redirect URL` and it needs to match the url you entered when registering
|
||||
# your Vikunja instance at the Todoist developer console.
|
||||
# When using the official Vikunja frontend, set this to <service.publicurl>/migrate/todoist.
|
||||
# Otherwise, set this to an url which then makes a request to /api/v1/migration/todoist/migrate
|
||||
# with the code obtained from the todoist api.
|
||||
# Note that the Vikunja frontend expects this to be /migrate/todoist
|
||||
redirecturl: <frontend url>/migrate/todoist
|
||||
redirecturl: <service.publicurl>/migrate/todoist
|
||||
trello:
|
||||
# Whether to enable the trello migrator or not
|
||||
enable: false
|
||||
|
||||
@@ -51,11 +51,11 @@
|
||||
}
|
||||
},
|
||||
"devDependencies": {
|
||||
"electron": "29.4.3",
|
||||
"electron": "29.4.6",
|
||||
"electron-builder": "24.13.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"connect-history-api-fallback": "2.0.0",
|
||||
"express": "4.19.2"
|
||||
"express": "4.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+1554
-1250
File diff suppressed because it is too large
Load Diff
+139
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1726063457,
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "39bf6ce569103c9390d37322daa59468c31b3ce7",
|
||||
"treeHash": "839747a1cb35ba6d5b36cce9a739ab2ba5e4a5d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"treeHash": "2addb7b71a20a25ea74feeaf5c2f6a6b30898ecb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"treeHash": "ca14199cabdfe1a06a7b1654c76ed49100a689f9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1716977621,
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
|
||||
"treeHash": "6d9f1f7ca0faf1bc2eeb397c78a49623260d3412",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1725930920,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "44a71ff39c182edaf25a7ace5c9454e7cba2c658",
|
||||
"treeHash": "56e93544112b7bb7aa0c3093d537295683ef9148",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1725983898,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1355a0cbfeac61d785b7183c0caaec1f97361b43",
|
||||
"treeHash": "d8ce0c1af7690da6161f0991484661331f5bc999",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1725513492,
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "7570de7b9b504cfe92025dd1be797bf546f66528",
|
||||
"treeHash": "4b46d77870afecd8f642541cb4f4927326343b59",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
{ pkgs, lib, config, inputs, ... }:
|
||||
|
||||
let
|
||||
pkgs-unstable = import inputs.nixpkgs-unstable { system = pkgs.stdenv.system; };
|
||||
in {
|
||||
packages = with pkgs-unstable; [
|
||||
# General tools
|
||||
git-cliff
|
||||
# API tools
|
||||
golangci-lint mage
|
||||
# Desktop
|
||||
electron
|
||||
] ++ lib.optionals (!pkgs.stdenv.isDarwin) [
|
||||
# Frontend tools (exclude on Darwin)
|
||||
cypress
|
||||
];
|
||||
|
||||
languages = {
|
||||
javascript = {
|
||||
enable = true;
|
||||
package = pkgs-unstable.nodejs-slim;
|
||||
pnpm = {
|
||||
enable = true;
|
||||
package = pkgs-unstable.pnpm;
|
||||
};
|
||||
};
|
||||
|
||||
go = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
# yaml-language-server: $schema=https://devenv.sh/devenv.schema.json
|
||||
inputs:
|
||||
nixpkgs:
|
||||
url: github:cachix/devenv-nixpkgs/rolling
|
||||
nixpkgs-unstable:
|
||||
url: github:NixOS/nixpkgs/nixos-unstable
|
||||
|
||||
# If you're using non-OSS software, you can set allowUnfree to true.
|
||||
allowUnfree: true
|
||||
|
||||
# If you're willing to use a package that's vulnerable
|
||||
# permittedInsecurePackages:
|
||||
# - "openssl-1.1.1w"
|
||||
|
||||
# If you have more than one devenv you can merge them
|
||||
#imports:
|
||||
# - ./backend
|
||||
Generated
-25
@@ -1,25 +0,0 @@
|
||||
{
|
||||
"nodes": {
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1718149104,
|
||||
"narHash": "sha256-Ds1QpobBX2yoUDx9ZruqVGJ/uQPgcXoYuobBguyKEh8=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "e913ae340076bbb73d9f4d3d065c2bca7caafb16",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"id": "nixpkgs",
|
||||
"type": "indirect"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
description = "Vikunja dev environment";
|
||||
|
||||
outputs = { self, nixpkgs }:
|
||||
let pkgs = nixpkgs.legacyPackages.x86_64-linux;
|
||||
in {
|
||||
defaultPackage.x86_64-linux =
|
||||
pkgs.mkShell { buildInputs = with pkgs; [
|
||||
# General tools
|
||||
git-cliff
|
||||
# Frontend tools
|
||||
pnpm cypress
|
||||
# API tools
|
||||
go golangci-lint mage
|
||||
# Desktop
|
||||
electron
|
||||
];
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -42,7 +42,6 @@ module.exports = {
|
||||
'error',
|
||||
{ 'script': { 'lang': 'ts' } },
|
||||
],
|
||||
'vue/no-required-prop-with-default': ['error', { 'autofix': true }],
|
||||
'vue/no-duplicate-attr-inheritance': 'error',
|
||||
'vue/no-empty-component-block': 'error',
|
||||
'vue/html-indent': ['error', 'tab'],
|
||||
|
||||
+2
-1
@@ -10,9 +10,10 @@ stats.html
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
/dist*
|
||||
dist
|
||||
coverage
|
||||
*.zip
|
||||
.vite/
|
||||
|
||||
# Test files
|
||||
cypress/screenshots
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
20.15.0
|
||||
20.16.0
|
||||
@@ -116,9 +116,9 @@ describe('Project View Kanban', () => {
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||
.contains('Limit: Not Set')
|
||||
.click()
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item .field input.input')
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .field input.input')
|
||||
.first()
|
||||
.type(3)
|
||||
.type('3')
|
||||
cy.get('[data-cy="setBucketLimit"]')
|
||||
.first()
|
||||
.click()
|
||||
@@ -148,7 +148,7 @@ describe('Project View Kanban', () => {
|
||||
cy.get('.kanban .bucket .bucket-header .dropdown.options .dropdown-menu .dropdown-item')
|
||||
.contains('Delete')
|
||||
.click()
|
||||
cy.get('.modal-mask .modal-container .modal-content .header')
|
||||
cy.get('.modal-mask .modal-container .modal-content .modal-header')
|
||||
.should('contain', 'Delete the bucket')
|
||||
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
||||
.contains('Do it!')
|
||||
@@ -251,7 +251,7 @@ describe('Project View Kanban', () => {
|
||||
.should('be.visible')
|
||||
.contains('Delete')
|
||||
.click()
|
||||
cy.get('.modal-mask .modal-container .modal-content .header')
|
||||
cy.get('.modal-mask .modal-container .modal-content .modal-header')
|
||||
.should('contain', 'Delete this task')
|
||||
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
||||
.contains('Do it!')
|
||||
|
||||
@@ -356,7 +356,7 @@ describe('Task', () => {
|
||||
.should('be.visible')
|
||||
.contains('Delete')
|
||||
.click()
|
||||
cy.get('.modal-mask .modal-container .modal-content .header')
|
||||
cy.get('.modal-mask .modal-container .modal-content .modal-header')
|
||||
.should('contain', 'Delete this task')
|
||||
cy.get('.modal-mask .modal-container .modal-content .actions .button')
|
||||
.contains('Do it!')
|
||||
@@ -484,7 +484,7 @@ describe('Task', () => {
|
||||
|
||||
addLabelToTaskAndVerify(labels[0].title)
|
||||
|
||||
cy.get('.modal-content .close')
|
||||
cy.get('.modal-container > .close')
|
||||
.click()
|
||||
|
||||
cy.get('.bucket .task')
|
||||
@@ -850,7 +850,7 @@ describe('Task', () => {
|
||||
|
||||
uploadAttachmentAndVerify(tasks[0].id)
|
||||
|
||||
cy.get('.modal-content .close')
|
||||
cy.get('.modal-container > .close')
|
||||
.click()
|
||||
|
||||
cy.get('.bucket .task .footer .icon svg.fa-paperclip')
|
||||
|
||||
+84
-84
@@ -13,7 +13,7 @@
|
||||
},
|
||||
"homepage": "https://vikunja.io/",
|
||||
"funding": "https://opencollective.com/vikunja",
|
||||
"packageManager": "pnpm@9.4.0",
|
||||
"packageManager": "pnpm@9.10.0",
|
||||
"keywords": [
|
||||
"todo",
|
||||
"productivity",
|
||||
@@ -50,137 +50,137 @@
|
||||
"story:preview": "histoire preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.5.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.5.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.5.2",
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/vue-fontawesome": "3.0.8",
|
||||
"@github/hotkey": "3.1.0",
|
||||
"@github/hotkey": "3.1.1",
|
||||
"@infectoone/vue-ganttastic": "2.3.2",
|
||||
"@intlify/unplugin-vue-i18n": "4.0.0",
|
||||
"@kyvg/vue3-notification": "3.2.1",
|
||||
"@intlify/unplugin-vue-i18n": "5.2.0",
|
||||
"@kyvg/vue3-notification": "3.3.0",
|
||||
"@sentry/tracing": "7.114.0",
|
||||
"@sentry/vue": "8.13.0",
|
||||
"@tiptap/core": "2.4.0",
|
||||
"@tiptap/extension-blockquote": "2.4.0",
|
||||
"@tiptap/extension-bold": "2.4.0",
|
||||
"@tiptap/extension-bullet-list": "2.4.0",
|
||||
"@tiptap/extension-code": "2.4.0",
|
||||
"@tiptap/extension-code-block-lowlight": "2.4.0",
|
||||
"@tiptap/extension-document": "2.4.0",
|
||||
"@tiptap/extension-dropcursor": "2.4.0",
|
||||
"@tiptap/extension-gapcursor": "2.4.0",
|
||||
"@tiptap/extension-hard-break": "2.4.0",
|
||||
"@tiptap/extension-heading": "2.4.0",
|
||||
"@tiptap/extension-history": "2.4.0",
|
||||
"@tiptap/extension-horizontal-rule": "2.4.0",
|
||||
"@tiptap/extension-image": "2.4.0",
|
||||
"@tiptap/extension-italic": "2.4.0",
|
||||
"@tiptap/extension-link": "2.4.0",
|
||||
"@tiptap/extension-list-item": "2.4.0",
|
||||
"@tiptap/extension-ordered-list": "2.4.0",
|
||||
"@tiptap/extension-paragraph": "2.4.0",
|
||||
"@tiptap/extension-placeholder": "2.4.0",
|
||||
"@tiptap/extension-strike": "2.4.0",
|
||||
"@tiptap/extension-table": "2.4.0",
|
||||
"@tiptap/extension-table-cell": "2.4.0",
|
||||
"@tiptap/extension-table-header": "2.4.0",
|
||||
"@tiptap/extension-table-row": "2.4.0",
|
||||
"@tiptap/extension-task-item": "2.4.0",
|
||||
"@tiptap/extension-task-list": "2.4.0",
|
||||
"@tiptap/extension-text": "2.4.0",
|
||||
"@tiptap/extension-typography": "2.4.0",
|
||||
"@tiptap/extension-underline": "2.4.0",
|
||||
"@tiptap/pm": "2.4.0",
|
||||
"@tiptap/suggestion": "2.4.0",
|
||||
"@tiptap/vue-3": "2.4.0",
|
||||
"@vueuse/core": "10.11.0",
|
||||
"@vueuse/router": "10.11.0",
|
||||
"axios": "1.7.2",
|
||||
"@sentry/vue": "8.32.0",
|
||||
"@tiptap/core": "2.7.4",
|
||||
"@tiptap/extension-blockquote": "2.7.4",
|
||||
"@tiptap/extension-bold": "2.7.4",
|
||||
"@tiptap/extension-bullet-list": "2.7.4",
|
||||
"@tiptap/extension-code": "2.7.4",
|
||||
"@tiptap/extension-code-block": "2.7.4",
|
||||
"@tiptap/extension-code-block-lowlight": "2.7.4",
|
||||
"@tiptap/extension-document": "2.7.4",
|
||||
"@tiptap/extension-dropcursor": "2.7.4",
|
||||
"@tiptap/extension-gapcursor": "2.7.4",
|
||||
"@tiptap/extension-hard-break": "2.7.4",
|
||||
"@tiptap/extension-heading": "2.7.4",
|
||||
"@tiptap/extension-history": "2.7.4",
|
||||
"@tiptap/extension-horizontal-rule": "2.7.4",
|
||||
"@tiptap/extension-image": "2.7.4",
|
||||
"@tiptap/extension-italic": "2.7.4",
|
||||
"@tiptap/extension-link": "2.7.4",
|
||||
"@tiptap/extension-list-item": "2.7.4",
|
||||
"@tiptap/extension-ordered-list": "2.7.4",
|
||||
"@tiptap/extension-paragraph": "2.7.4",
|
||||
"@tiptap/extension-placeholder": "2.7.4",
|
||||
"@tiptap/extension-strike": "2.7.4",
|
||||
"@tiptap/extension-table": "2.7.4",
|
||||
"@tiptap/extension-table-cell": "2.7.4",
|
||||
"@tiptap/extension-table-header": "2.7.4",
|
||||
"@tiptap/extension-table-row": "2.7.4",
|
||||
"@tiptap/extension-task-item": "2.7.4",
|
||||
"@tiptap/extension-task-list": "2.7.4",
|
||||
"@tiptap/extension-text": "2.7.4",
|
||||
"@tiptap/extension-typography": "2.7.4",
|
||||
"@tiptap/extension-underline": "2.7.4",
|
||||
"@tiptap/pm": "2.7.4",
|
||||
"@tiptap/suggestion": "2.7.4",
|
||||
"@tiptap/vue-3": "2.7.4",
|
||||
"@vueuse/core": "11.1.0",
|
||||
"@vueuse/router": "11.1.0",
|
||||
"axios": "1.7.7",
|
||||
"blurhash": "2.0.5",
|
||||
"bulma-css-variables": "0.9.33",
|
||||
"change-case": "5.4.4",
|
||||
"date-fns": "3.6.0",
|
||||
"dayjs": "1.11.11",
|
||||
"dompurify": "3.1.5",
|
||||
"date-fns": "4.1.0",
|
||||
"dayjs": "1.11.13",
|
||||
"dompurify": "3.1.7",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
"flexsearch": "0.7.43",
|
||||
"floating-vue": "5.2.2",
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lowlight": "2.9.0",
|
||||
"pinia": "2.1.7",
|
||||
"pinia": "2.2.2",
|
||||
"register-service-worker": "1.7.2",
|
||||
"sortablejs": "1.15.2",
|
||||
"sortablejs": "1.15.3",
|
||||
"tailwindcss": "3.4.13",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.5.3",
|
||||
"vue": "3.4.31",
|
||||
"ufo": "1.5.4",
|
||||
"vue": "3.5.10",
|
||||
"vue-advanced-cropper": "2.8.9",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "9.13.1",
|
||||
"vue-router": "4.4.0",
|
||||
"vuemoji-picker": "0.2.1",
|
||||
"vue-i18n": "10.0.3",
|
||||
"vue-router": "4.4.5",
|
||||
"vuemoji-picker": "0.3.1",
|
||||
"workbox-precaching": "7.1.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.1.1",
|
||||
"@cypress/vite-dev-server": "5.2.0",
|
||||
"@cypress/vue": "6.0.1",
|
||||
"@faker-js/faker": "8.4.1",
|
||||
"@histoire/plugin-screenshot": "0.17.17",
|
||||
"@histoire/plugin-vue": "0.17.17",
|
||||
"@rushstack/eslint-patch": "1.10.3",
|
||||
"@rushstack/eslint-patch": "1.10.4",
|
||||
"@tsconfig/node20": "20.1.4",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/flexsearch": "0.7.6",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/is-touch-device": "1.0.3",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/node": "20.16.10",
|
||||
"@types/sortablejs": "1.15.8",
|
||||
"@typescript-eslint/eslint-plugin": "7.15.0",
|
||||
"@typescript-eslint/parser": "7.15.0",
|
||||
"@vitejs/plugin-legacy": "5.4.1",
|
||||
"@vitejs/plugin-vue": "5.0.5",
|
||||
"@typescript-eslint/eslint-plugin": "7.18.0",
|
||||
"@typescript-eslint/parser": "7.18.0",
|
||||
"@vitejs/plugin-legacy": "5.4.2",
|
||||
"@vitejs/plugin-vue": "5.1.4",
|
||||
"@vue/eslint-config-typescript": "13.0.0",
|
||||
"@vue/test-utils": "2.4.6",
|
||||
"@vue/tsconfig": "0.5.1",
|
||||
"autoprefixer": "10.4.19",
|
||||
"browserslist": "4.23.1",
|
||||
"caniuse-lite": "1.0.30001639",
|
||||
"autoprefixer": "10.4.20",
|
||||
"browserslist": "4.24.0",
|
||||
"caniuse-lite": "1.0.30001664",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.13.0",
|
||||
"esbuild": "0.23.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-vue": "9.26.0",
|
||||
"cypress": "13.15.0",
|
||||
"esbuild": "0.24.0",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-vue": "9.28.0",
|
||||
"happy-dom": "14.12.3",
|
||||
"histoire": "0.17.17",
|
||||
"postcss": "8.4.39",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-preset-env": "9.5.15",
|
||||
"rollup": "4.18.0",
|
||||
"postcss-preset-env": "9.6.0",
|
||||
"rollup": "4.22.5",
|
||||
"rollup-plugin-visualizer": "5.12.0",
|
||||
"sass": "1.77.6",
|
||||
"start-server-and-test": "2.0.4",
|
||||
"typescript": "5.5.3",
|
||||
"unplugin-inject-preload": "2.0.4",
|
||||
"vite": "5.3.2",
|
||||
"vite-plugin-pwa": "0.20.0",
|
||||
"sass": "1.79.4",
|
||||
"start-server-and-test": "2.0.8",
|
||||
"typescript": "5.6.2",
|
||||
"unplugin-inject-preload": "2.0.5",
|
||||
"vite": "5.4.8",
|
||||
"vite-plugin-pwa": "0.20.5",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.6.0",
|
||||
"vue-tsc": "2.0.24",
|
||||
"vue-tsc": "2.1.6",
|
||||
"wait-on": "7.2.0",
|
||||
"workbox-cli": "7.1.0"
|
||||
},
|
||||
"pnpm": {
|
||||
"patchedDependencies": {
|
||||
"flexsearch@0.7.31": "patches/flexsearch@0.7.31.patch",
|
||||
"@github/hotkey@3.1.0": "patches/@github__hotkey@3.1.0.patch"
|
||||
"@github/hotkey@3.1.1": "patches/@github__hotkey@3.1.1.patch",
|
||||
"flexsearch@0.7.43": "patches/flexsearch@0.7.43.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
diff --git a/index.d.ts b/index.d.ts
|
||||
deleted file mode 100644
|
||||
index 9f39f41073864b83968bdaa242ac4e3c3149685a..0000000000000000000000000000000000000000
|
||||
diff --git a/package.json b/package.json
|
||||
index 8968f5bf8010ff194240591c8b83299f7328e79d..6d84b6f590a841b129ed8b3860cb786df5a185c0 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -22,8 +22,6 @@
|
||||
},
|
||||
"main": "dist/flexsearch.bundle.js",
|
||||
"browser": "dist/flexsearch.bundle.js",
|
||||
- "module": "dist/module/index.js",
|
||||
- "types": "./index.d.ts",
|
||||
"preferGlobal": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -0,0 +1,18 @@
|
||||
diff --git a/package.json b/package.json
|
||||
index c154e54029c94be444916fb2249941e7182d80ed..54a65c42a42c4627506e016132becc43b47a517c 100644
|
||||
--- a/package.json
|
||||
+++ b/package.json
|
||||
@@ -28,13 +28,11 @@
|
||||
"email": "info@nextapps.de"
|
||||
},
|
||||
"main": "dist/flexsearch.bundle.min.js",
|
||||
- "module": "dist/flexsearch.bundle.module.min.js",
|
||||
"browser": {
|
||||
"dist/flexsearch.bundle.min.js": "./dist/flexsearch.bundle.min.js",
|
||||
"dist/flexsearch.bundle.module.min.js": "./dist/flexsearch.bundle.module.min.js",
|
||||
"worker_threads": false
|
||||
},
|
||||
- "types": "./index.d.ts",
|
||||
"scripts": {
|
||||
"build": "npm run copy && npm run build:bundle",
|
||||
"build:bundle": "node task/build RELEASE=bundle DEBUG=false SUPPORT_WORKER=true SUPPORT_ENCODER=true SUPPORT_CACHE=true SUPPORT_ASYNC=true SUPPORT_STORE=true SUPPORT_TAGS=true SUPPORT_SUGGESTION=true SUPPORT_SERIALIZE=true SUPPORT_DOCUMENT=true POLYFILL=false",
|
||||
Generated
+2930
-2612
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<Ready>
|
||||
<template v-if="authUser">
|
||||
<TheNavigation />
|
||||
<AppHeader />
|
||||
<ContentAuth />
|
||||
</template>
|
||||
<ContentLinkShare v-else-if="authLinkShare" />
|
||||
@@ -30,7 +30,7 @@ import Notification from '@/components/misc/Notification.vue'
|
||||
import UpdateNotification from '@/components/home/UpdateNotification.vue'
|
||||
import KeyboardShortcuts from '@/components/misc/keyboard-shortcuts/index.vue'
|
||||
|
||||
import TheNavigation from '@/components/home/TheNavigation.vue'
|
||||
import AppHeader from '@/components/home/AppHeader.vue'
|
||||
import ContentAuth from '@/components/home/ContentAuth.vue'
|
||||
import ContentLinkShare from '@/components/home/ContentLinkShare.vue'
|
||||
import NoAuthWrapper from '@/components/misc/NoAuthWrapper.vue'
|
||||
|
||||
@@ -72,7 +72,7 @@ export interface BaseButtonProps extends /* @vue-ignore */ HTMLAttributes {
|
||||
openExternalInNewTab?: boolean
|
||||
}
|
||||
|
||||
export interface BaseButtonEmits {
|
||||
export type BaseButtonEmits = {
|
||||
click: [payload: MouseEvent]
|
||||
}
|
||||
|
||||
|
||||
@@ -3,33 +3,22 @@
|
||||
v-cy="'checkbox'"
|
||||
class="base-checkbox"
|
||||
>
|
||||
<input
|
||||
:id="checkboxId"
|
||||
type="checkbox"
|
||||
class="is-sr-only"
|
||||
:checked="modelValue"
|
||||
:disabled="disabled || undefined"
|
||||
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
|
||||
<label
|
||||
class="base-checkbox__label"
|
||||
>
|
||||
|
||||
<slot
|
||||
name="label"
|
||||
:checkbox-id="checkboxId"
|
||||
>
|
||||
<label
|
||||
:for="checkboxId"
|
||||
class="base-checkbox__label"
|
||||
<input
|
||||
type="checkbox"
|
||||
class="is-sr-only"
|
||||
:checked="modelValue"
|
||||
:disabled="disabled || undefined"
|
||||
@change="(event) => emit('update:modelValue', (event.target as HTMLInputElement).checked)"
|
||||
>
|
||||
<slot />
|
||||
</label>
|
||||
</slot>
|
||||
<slot />
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref} from 'vue'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
|
||||
withDefaults(defineProps<{
|
||||
modelValue?: boolean,
|
||||
disabled: boolean,
|
||||
@@ -38,10 +27,8 @@ withDefaults(defineProps<{
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
}>()
|
||||
|
||||
const checkboxId = ref(`checkbox_${createRandomID()}`)
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -53,7 +40,7 @@ const checkboxId = ref(`checkbox_${createRandomID()}`)
|
||||
}
|
||||
|
||||
.base-checkbox:has(input:disabled) .base-checkbox__label {
|
||||
cursor:not-allowed;
|
||||
cursor: not-allowed;
|
||||
pointer-events: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,97 @@
|
||||
<template>
|
||||
<nav
|
||||
v-if="totalPages > 1"
|
||||
aria-label="pagination"
|
||||
class="pagination is-centered p-4"
|
||||
role="navigation"
|
||||
>
|
||||
<slot
|
||||
name="previous"
|
||||
:disabled="currentPage === 1"
|
||||
>
|
||||
{{ $t('misc.previous') }}
|
||||
</slot>
|
||||
<slot
|
||||
name="next"
|
||||
:disabled="currentPage === totalPages"
|
||||
>
|
||||
{{ $t('misc.next') }}
|
||||
</slot>
|
||||
<ul class="pagination-list">
|
||||
<li
|
||||
v-for="(p, i) in pages"
|
||||
:key="`page-${i}`"
|
||||
>
|
||||
<span
|
||||
v-if="p.isEllipsis"
|
||||
class="pagination-ellipsis"
|
||||
>…</span>
|
||||
<slot
|
||||
v-else
|
||||
name="page-link"
|
||||
:page="p"
|
||||
:is-current="p.number === currentPage"
|
||||
>
|
||||
{{ p.number }}
|
||||
</slot>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed} from 'vue'
|
||||
|
||||
const props = defineProps<{
|
||||
totalPages: number,
|
||||
currentPage: number
|
||||
}>()
|
||||
|
||||
function createPagination(totalPages: number, currentPage: number) {
|
||||
const pages = []
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
if (
|
||||
i > 0 &&
|
||||
(i + 1) < totalPages &&
|
||||
((i + 1) > currentPage + 1 || (i + 1) < currentPage - 1)
|
||||
) {
|
||||
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
|
||||
pages.push({
|
||||
number: 0,
|
||||
isEllipsis: true,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pages.push({
|
||||
number: i + 1,
|
||||
isEllipsis: false,
|
||||
})
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pagination {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pagination-previous,
|
||||
.pagination-next {
|
||||
&:not(:disabled):hover {
|
||||
background: $scheme-main;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.pagination-list {
|
||||
&, & li {
|
||||
margin-top: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -114,13 +114,19 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/DatemathHelp.vue'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
const props = defineProps<{
|
||||
modelValue: {
|
||||
required: false,
|
||||
dateFrom: Date | string,
|
||||
dateTo: Date | string,
|
||||
},
|
||||
})
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: {
|
||||
dateFrom: Date | string,
|
||||
dateTo: Date | string
|
||||
}]
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<div class="datepicker-with-range-container">
|
||||
<Popup
|
||||
:open="open"
|
||||
@close="() => emit('close')"
|
||||
@update:open="(open) => !open && $emit('update:open', false)"
|
||||
>
|
||||
<template #content="{isOpen}">
|
||||
<div
|
||||
@@ -91,18 +91,18 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import DatemathHelp from '@/components/date/DatemathHelp.vue'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string | Date | null,
|
||||
open: boolean
|
||||
}>(), {
|
||||
modelValue: null,
|
||||
open: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string | Date | null],
|
||||
'update:open': [open: boolean],
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
|
||||
@@ -69,6 +69,10 @@ const shouldShowMessage = computed(() => {
|
||||
@media screen and (min-width: $tablet) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media print {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.has-update-available {
|
||||
bottom: 5rem;
|
||||
|
||||
+14
-7
@@ -70,7 +70,7 @@
|
||||
>
|
||||
<span class="username">{{ authStore.userDisplayName }}</span>
|
||||
<span
|
||||
class="icon is-small"
|
||||
class="ml-1 dropdown-icon icon is-small"
|
||||
:style="{
|
||||
transform: open ? 'rotate(180deg)' : 'rotate(0)',
|
||||
}"
|
||||
@@ -161,13 +161,8 @@ $user-dropdown-width-mobile: 5rem;
|
||||
|
||||
background: var(--site-background);
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
padding-right: .5rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
padding-left: 2rem;
|
||||
padding-right: 1rem;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
@@ -263,11 +258,19 @@ $user-dropdown-width-mobile: 5rem;
|
||||
}
|
||||
|
||||
.username-dropdown-trigger {
|
||||
padding-left: 1rem;
|
||||
padding-left: .75rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
font-size: .85rem;
|
||||
font-weight: 700;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
padding-right: .5rem;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
padding-right: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
@@ -278,6 +281,10 @@ $user-dropdown-width-mobile: 5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-icon {
|
||||
transition: transform $transition;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
border-radius: 100%;
|
||||
vertical-align: middle;
|
||||
@@ -48,7 +48,10 @@
|
||||
class="task-detail-view-modal"
|
||||
@close="closeModal()"
|
||||
>
|
||||
<component :is="currentModal" />
|
||||
<component
|
||||
:is="currentModal"
|
||||
@close="closeModal()"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
<BaseButton
|
||||
@@ -160,6 +163,7 @@ projectStore.loadAllProjects()
|
||||
}
|
||||
|
||||
.app-content {
|
||||
display: flow-root;
|
||||
z-index: 10;
|
||||
position: relative;
|
||||
padding: 1.5rem 0.5rem 0;
|
||||
|
||||
@@ -109,7 +109,7 @@
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<PoweredByLink />
|
||||
<PoweredByLink class="mt-auto" />
|
||||
</aside>
|
||||
</template>
|
||||
|
||||
@@ -146,6 +146,8 @@ const savedFilterProjects = computed(() => projectStore.savedFilterProjects)
|
||||
}
|
||||
|
||||
.menu-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--site-background);
|
||||
color: $vikunja-nav-color;
|
||||
padding: 1rem 0;
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="list-menu loader-container is-loading-small"
|
||||
:class="{'is-loading': isLoading}"
|
||||
>
|
||||
<div>
|
||||
<div class="navigation-item">
|
||||
<BaseButton
|
||||
v-if="canCollapse && childProjects?.length > 0"
|
||||
class="collapse-project-button"
|
||||
@@ -193,4 +193,18 @@ const childProjects = computed(() => {
|
||||
.is-touch .handle.has-color-bubble {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.navigation-item:has(*:focus-visible) {
|
||||
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
|
||||
background-color: var(--white);
|
||||
|
||||
.favorite, .menu-list-dropdown {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.navigation-item a:focus-visible {
|
||||
// The focus ring is already added to the navigation-item, so we don't need to add it again.
|
||||
box-shadow: none;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
'has-no-shadow': !shadow || variant === 'tertiary',
|
||||
}
|
||||
]"
|
||||
:disabled="disabled || loading"
|
||||
:style="{
|
||||
'--button-white-space': wrap ? 'break-spaces' : 'nowrap',
|
||||
}"
|
||||
@@ -44,6 +45,7 @@ export interface ButtonProps extends /* @vue-ignore */ BaseButtonProps {
|
||||
icon?: IconProp
|
||||
iconColor?: string
|
||||
loading?: boolean
|
||||
disabled?: boolean
|
||||
shadow?: boolean
|
||||
wrap?: boolean
|
||||
}
|
||||
@@ -53,6 +55,7 @@ const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
icon: undefined,
|
||||
iconColor: undefined,
|
||||
loading: false,
|
||||
disabled: false,
|
||||
shadow: true,
|
||||
wrap: true,
|
||||
})
|
||||
|
||||
@@ -70,13 +70,7 @@ import {computed, ref, watch} from 'vue'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
import XButton from '@/components/input/Button.vue'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
modelValue: string,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const model = defineModel<string>({ required: true })
|
||||
|
||||
const DEFAULT_COLORS = [
|
||||
'#1973ff',
|
||||
@@ -93,7 +87,7 @@ const defaultColors = ref(DEFAULT_COLORS)
|
||||
const colorListID = ref(createRandomID())
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
model,
|
||||
(newValue) => {
|
||||
if (newValue === '' || newValue.startsWith('var(')) {
|
||||
color.value = ''
|
||||
@@ -123,7 +117,7 @@ function update(force = false) {
|
||||
}
|
||||
|
||||
lastChangeTimeout.value = setTimeout(() => {
|
||||
emit('update:modelValue', color.value)
|
||||
model.value = color.value
|
||||
}, 500)
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, onMounted, onBeforeUnmount, toRef, watch, type PropType} from 'vue'
|
||||
import {ref, onMounted, onBeforeUnmount, toRef, watch} from 'vue'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import DatepickerInline from '@/components/input/DatepickerInline.vue'
|
||||
@@ -44,26 +44,24 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Date, null, String] as PropType<Date | null | string>,
|
||||
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
|
||||
default: null,
|
||||
},
|
||||
chooseDateLabel: {
|
||||
type: String,
|
||||
default() {
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
return t('input.datepicker.chooseDate')
|
||||
},
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: Date | null | string,
|
||||
chooseDateLabel: string,
|
||||
disabled?: boolean,
|
||||
}>(), {
|
||||
modelValue: null,
|
||||
chooseDateLabel: () => {
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
return t('input.datepicker.chooseDate')
|
||||
},
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close', 'closeOnChange'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: Date | null],
|
||||
'close': [value: boolean],
|
||||
'closeOnChange': [value: boolean],
|
||||
}>()
|
||||
|
||||
const date = ref<Date | null>()
|
||||
const show = ref(false)
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, onBeforeUnmount, onMounted, type PropType, ref, toRef, watch} from 'vue'
|
||||
import {computed, onBeforeUnmount, onMounted, ref, toRef, watch} from 'vue'
|
||||
import flatPickr from 'vue-flatpickr-component'
|
||||
import 'flatpickr/dist/flatpickr.css'
|
||||
|
||||
@@ -84,15 +84,15 @@ import {createDateFromString} from '@/helpers/time/createDateFromString'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {getFlatpickrLanguage} from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: [Date, null, String] as PropType<Date | null | string>,
|
||||
validator: prop => prop instanceof Date || prop === null || typeof prop === 'string',
|
||||
default: null,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: Date | null | string
|
||||
}>(), {
|
||||
modelValue: null,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'close-on-change'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [Date | null],
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
|
||||
@@ -21,28 +21,23 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import CheckboxIcon from '@/assets/checkbox.svg?component'
|
||||
|
||||
import BaseCheckbox from '@/components/base/BaseCheckbox.vue'
|
||||
|
||||
defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
},
|
||||
isBlock: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withDefaults(defineProps<{
|
||||
modelValue: boolean,
|
||||
disabled?: boolean,
|
||||
isBlock?: boolean
|
||||
}>(), {
|
||||
modelValue: false,
|
||||
disabled: false,
|
||||
isBlock: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: 'update:modelValue', value: boolean): void
|
||||
'update:modelValue': [value: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.fancy-checkbox {
|
||||
display: inline-block;
|
||||
@@ -74,8 +69,8 @@ const emit = defineEmits<{
|
||||
}
|
||||
}
|
||||
|
||||
.fancy-checkbox:not(:has(input:disabled)):hover .fancy-checkbox__icon,
|
||||
.fancy-checkbox:has(input:checked) .fancy-checkbox__icon {
|
||||
.fancy-checkbox:hover input:not(:disabled) + .fancy-checkbox__icon,
|
||||
.fancy-checkbox input:checked + .fancy-checkbox__icon {
|
||||
--stroke-color: var(--primary);
|
||||
}
|
||||
</style>
|
||||
@@ -84,13 +79,13 @@ const emit = defineEmits<{
|
||||
// Since css-has-pseudo doesn't work with deep classes,
|
||||
// the following rules can't be scoped
|
||||
|
||||
.fancy-checkbox:has(:not(input:checked)) .fancy-checkbox__icon {
|
||||
.fancy-checkbox :not(input:checked) + .fancy-checkbox__icon {
|
||||
path {
|
||||
transition-delay: 0.05s;
|
||||
}
|
||||
}
|
||||
|
||||
.fancy-checkbox:has(input:checked) .fancy-checkbox__icon {
|
||||
.fancy-checkbox input:checked + .fancy-checkbox__icon {
|
||||
path {
|
||||
stroke-dashoffset: 60;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
<div
|
||||
ref="multiselectRoot"
|
||||
class="multiselect"
|
||||
:class="{'has-search-results': searchResultsVisible}"
|
||||
:class="{'has-search-results': searchResultsVisible, 'is-disabled': disabled}"
|
||||
tabindex="-1"
|
||||
:aria-disabled="disabled"
|
||||
@focus="focus"
|
||||
>
|
||||
<div
|
||||
@@ -12,7 +13,7 @@
|
||||
>
|
||||
<div
|
||||
class="input-wrapper input"
|
||||
:class="{'has-multiple': hasMultiple, 'has-removal-button': removalAvailable}"
|
||||
:class="{'has-multiple': hasMultiple, 'has-removal-button': removalAvailable && !disabled}"
|
||||
>
|
||||
<slot
|
||||
v-if="Array.isArray(internalValue)"
|
||||
@@ -31,6 +32,7 @@
|
||||
>
|
||||
{{ label !== '' ? item[label] : item }}
|
||||
<BaseButton
|
||||
v-if="!disabled"
|
||||
class="delete is-small"
|
||||
@click="() => remove(item)"
|
||||
/>
|
||||
@@ -40,10 +42,13 @@
|
||||
</slot>
|
||||
|
||||
<input
|
||||
v-if="!disabled"
|
||||
:id="id"
|
||||
ref="searchInput"
|
||||
v-model="query"
|
||||
type="text"
|
||||
class="input"
|
||||
:name="name"
|
||||
:placeholder="placeholder"
|
||||
:autocomplete="autocompleteEnabled ? undefined : 'off'"
|
||||
:spellcheck="autocompleteEnabled ? undefined : 'false'"
|
||||
@@ -53,7 +58,7 @@
|
||||
@focus="handleFocus"
|
||||
>
|
||||
<BaseButton
|
||||
v-if="removalAvailable"
|
||||
v-if="removalAvailable && !disabled"
|
||||
class="removal-button"
|
||||
@click="resetSelectedValue"
|
||||
>
|
||||
@@ -119,9 +124,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance, type PropType,
|
||||
} from 'vue'
|
||||
import {computed, onBeforeUnmount, onMounted, ref, toRefs, watch, type ComponentPublicInstance} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
@@ -129,132 +132,82 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
|
||||
const props = defineProps({
|
||||
/**
|
||||
* When true, shows a loading spinner
|
||||
*/
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* The placeholder of the search input
|
||||
*/
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* The search results where the @search listener needs to put the results into
|
||||
*/
|
||||
searchResults: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: Array as PropType<{ [id: string]: any }>,
|
||||
default: () => [],
|
||||
},
|
||||
/**
|
||||
* The name of the property of the searched object to show the user.
|
||||
* If empty the component will show all raw data of an entry.
|
||||
*/
|
||||
label: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
/**
|
||||
* The object with the value, updated every time an entry is selected.
|
||||
*/
|
||||
modelValue: {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
type: [Object] as PropType<{ [key: string]: any }>,
|
||||
default: null,
|
||||
},
|
||||
/**
|
||||
* If true, will provide an "add this as a new value" entry which fires an @create event when clicking on it.
|
||||
*/
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* The text shown next to the new value option.
|
||||
*/
|
||||
createPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
return t('input.multiselect.createPlaceholder')
|
||||
},
|
||||
},
|
||||
/**
|
||||
* The text shown next to an option.
|
||||
*/
|
||||
selectPlaceholder: {
|
||||
type: String,
|
||||
default() {
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
return t('input.multiselect.selectPlaceholder')
|
||||
},
|
||||
},
|
||||
/**
|
||||
* If true, allows for selecting multiple items. v-model will be an array with all selected values in that case.
|
||||
*/
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* If true, displays the search results inline instead of using a dropdown.
|
||||
*/
|
||||
inline: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
/**
|
||||
* If true, shows search results when no query is specified.
|
||||
*/
|
||||
showEmpty: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke.
|
||||
*/
|
||||
searchDelay: {
|
||||
type: Number,
|
||||
default: 200,
|
||||
},
|
||||
closeAfterSelect: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
/**
|
||||
* If false, the search input will get the autocomplete="off" attributes attached to it.
|
||||
*/
|
||||
autocompleteEnabled: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
/** The object with the value, updated every time an entry is selected */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
modelValue: { [id: string]: any } | null
|
||||
/** When true, shows a loading spinner */
|
||||
loading?: boolean
|
||||
/** The placeholder of the search input */
|
||||
placeholder?: string
|
||||
/** The search results where the @search listener needs to put the results into */
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
searchResults?: { [id: string]: any }[]
|
||||
/** The name of the property of the searched object to show the user. If empty the component will show all raw data of an entry */
|
||||
label?: string
|
||||
/** The id attribute of the input element */
|
||||
id?: string
|
||||
/** The name attribute of the input element */
|
||||
name?: string
|
||||
/** If true, will provide an 'add this as a new value' entry which fires an @create event when clicking on it. */
|
||||
creatable?: boolean
|
||||
/** The text shown next to the new value option. */
|
||||
createPlaceholder?: string
|
||||
/** The text shown next to an option. */
|
||||
selectPlaceholder?: string
|
||||
/** If true, allows for selecting multiple items. v-model will be an array with all selected values in that case. */
|
||||
multiple?: boolean
|
||||
/** If true, displays the search results inline instead of using a dropdown. */
|
||||
inline?: boolean
|
||||
/** If true, shows search results when no query is specified. */
|
||||
showEmpty?: boolean
|
||||
/** The delay in ms after which the search event will be fired. Used to avoid hitting the network on every keystroke. */
|
||||
searchDelay?: number
|
||||
/** If true, closes the dropdown after an entry is selected */
|
||||
closeAfterSelect?: boolean
|
||||
/** If false, the search input will get the autocomplete="off" attributes attached to it. */
|
||||
autocompleteEnabled?: boolean
|
||||
/** If true, disables the multiselect input */
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
modelValue: null,
|
||||
loading: false,
|
||||
placeholder: '',
|
||||
searchResults: () => [],
|
||||
label: '',
|
||||
creatable: false,
|
||||
createPlaceholder: () => useI18n().t('input.multiselect.createPlaceholder'),
|
||||
selectPlaceholder: () => useI18n().t('input.multiselect.selectPlaceholder'),
|
||||
multiple: false,
|
||||
inline: false,
|
||||
showEmpty: false,
|
||||
searchDelay: 200,
|
||||
closeAfterSelect: true,
|
||||
autocompleteEnabled: true,
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: null): void
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
'update:modelValue': [value: { [key: string]: any }[] | null],
|
||||
/**
|
||||
* Triggered every time the search query input changes
|
||||
*/
|
||||
(e: 'search', query: string): void
|
||||
'search': [query: string],
|
||||
/**
|
||||
* Triggered every time an option from the search results is selected. Also triggers a change in v-model.
|
||||
*/
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(e: 'select', value: { [key: string]: any }): void
|
||||
'select': [value: { [key: string]: any }],
|
||||
/**
|
||||
* If nothing or no exact match was found and `creatable` is true, this event is triggered with the current value of the search query.
|
||||
*/
|
||||
(e: 'create', query: string): void
|
||||
'create': [query: string],
|
||||
/**
|
||||
* If `multiple` is enabled, this will be fired every time an item is removed from the array of selected items.
|
||||
*/
|
||||
(e: 'remove', value: null): void
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
'remove': [value: { [key: string]: any }],
|
||||
}>()
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -324,7 +277,7 @@ const hasMultiple = computed(() => {
|
||||
return props.multiple && Array.isArray(internalValue.value) && internalValue.value.length > 0
|
||||
})
|
||||
|
||||
const removalAvailable = computed(() => !props.multiple && internalValue.value !== null && query.value !== '')
|
||||
const removalAvailable = computed(() => !props.multiple && internalValue.value !== null && query.value !== '' && !(props.loading || localLoading.value))
|
||||
function resetSelectedValue() {
|
||||
select(null)
|
||||
}
|
||||
@@ -501,6 +454,18 @@ function focus() {
|
||||
.control.is-loading::after {
|
||||
top: .75rem;
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
.input-wrapper, .input, & {
|
||||
cursor: default !important;
|
||||
|
||||
&:focus-within,&:focus-visible, &:hover, &:focus {
|
||||
border-color: transparent !important;
|
||||
background: transparent !important;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
|
||||
@@ -32,51 +32,37 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, watch} from 'vue'
|
||||
import {ref, watchEffect} from 'vue'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import {validatePassword} from '@/helpers/validatePasswort'
|
||||
|
||||
const props = defineProps({
|
||||
tabindex: String,
|
||||
modelValue: String,
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: string,
|
||||
tabindex?: string,
|
||||
// This prop is a workaround to trigger validation from the outside when the user never had focus in the input.
|
||||
validateInitially: Boolean,
|
||||
validateMinLength: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
validateInitially?: boolean,
|
||||
validateMinLength?: boolean,
|
||||
}>(), {
|
||||
validateMinLength: true,
|
||||
})
|
||||
const emit = defineEmits(['submit', 'update:modelValue'])
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string],
|
||||
'submit': [event: Event],
|
||||
}>()
|
||||
const {t} = useI18n()
|
||||
const passwordFieldType = ref('password')
|
||||
const password = ref('')
|
||||
const isValid = ref<true | string>(props.validateInitially === true ? true : '')
|
||||
const validateAfterFirst = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.validateInitially,
|
||||
() => props.validateInitially && validate(),
|
||||
{immediate: true},
|
||||
)
|
||||
watchEffect(() => props.validateInitially && validate())
|
||||
|
||||
const validate = useDebounceFn(() => {
|
||||
if (password.value === '') {
|
||||
isValid.value = t('user.auth.passwordRequired')
|
||||
return
|
||||
}
|
||||
|
||||
if (props.validateMinLength && password.value.length < 8) {
|
||||
isValid.value = t('user.auth.passwordNotMin')
|
||||
return
|
||||
}
|
||||
|
||||
if (props.validateMinLength && password.value.length > 250) {
|
||||
isValid.value = t('user.auth.passwordNotMax')
|
||||
return
|
||||
}
|
||||
|
||||
isValid.value = true
|
||||
const valid = validatePassword(password.value, props.validateMinLength)
|
||||
isValid.value = valid === true ? true : t(valid)
|
||||
}, 100)
|
||||
|
||||
function togglePasswordFieldType() {
|
||||
|
||||
@@ -13,27 +13,25 @@ import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {useColorScheme} from '@/composables/useColorScheme'
|
||||
|
||||
const {
|
||||
entityKind,
|
||||
entityId,
|
||||
disabled = false,
|
||||
} = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
entityKind: ReactionKind,
|
||||
entityId: number,
|
||||
disabled?: boolean,
|
||||
}>()
|
||||
}>(), {
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const model = defineModel<IReactionPerEntity>()
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const {t} = useI18n()
|
||||
const reactionService = new ReactionService()
|
||||
const {isDark} = useColorScheme()
|
||||
|
||||
const model = defineModel<IReactionPerEntity>()
|
||||
|
||||
async function addReaction(value: string) {
|
||||
const reaction = new ReactionModel({
|
||||
id: entityId,
|
||||
kind: entityKind,
|
||||
id: props.entityId,
|
||||
kind: props.entityKind,
|
||||
value,
|
||||
})
|
||||
await reactionService.create(reaction)
|
||||
@@ -52,8 +50,8 @@ async function addReaction(value: string) {
|
||||
|
||||
async function removeReaction(value: string) {
|
||||
const reaction = new ReactionModel({
|
||||
id: entityId,
|
||||
kind: entityKind,
|
||||
id: props.entityId,
|
||||
kind: props.entityKind,
|
||||
value,
|
||||
})
|
||||
await reactionService.delete(reaction)
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
||||
import {computed, ref, shallowReactive, watchEffect} from 'vue'
|
||||
|
||||
import Multiselect from '@/components/input/Multiselect.vue'
|
||||
|
||||
@@ -20,23 +20,16 @@ import type {IProject} from '@/modelTypes/IProject'
|
||||
import ProjectService from '@/services/project'
|
||||
import {includesById} from '@/helpers/utils'
|
||||
|
||||
type ProjectFilterFunc = (p: IProject) => boolean
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<IProject[]>,
|
||||
default: () => [],
|
||||
},
|
||||
projectFilter: {
|
||||
type: Function as PropType<ProjectFilterFunc>,
|
||||
default: () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
return (_: IProject) => true
|
||||
},
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: IProject[],
|
||||
projectFilter: (p: IProject) => boolean
|
||||
}>(), {
|
||||
modelValue: () => [],
|
||||
projectFilter: () => () => true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: IProject[]): void
|
||||
'update:modelValue': [value: IProject[]]
|
||||
}>()
|
||||
|
||||
const projects = ref<IProject[]>([])
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed, ref, shallowReactive, watchEffect, type PropType} from 'vue'
|
||||
import {computed, ref, shallowReactive, watchEffect} from 'vue'
|
||||
|
||||
import Multiselect from '@/components/input/Multiselect.vue'
|
||||
|
||||
@@ -20,14 +20,14 @@ import type {IUser} from '@/modelTypes/IUser'
|
||||
import UserService from '@/services/user'
|
||||
import {includesById} from '@/helpers/utils'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<IUser[]>,
|
||||
default: () => [],
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: IUser[] | undefined
|
||||
}>(), {
|
||||
modelValue: () => [],
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:modelValue', value: IUser[]): void
|
||||
'update:modelValue': [value: IUser[]]
|
||||
}>()
|
||||
|
||||
const users = ref<IUser[]>([])
|
||||
|
||||
@@ -314,7 +314,17 @@ const internalMode = ref<Mode>('preview')
|
||||
const isEditing = computed(() => internalMode.value === 'edit' && isEditEnabled)
|
||||
const contentHasChanged = ref<boolean>(false)
|
||||
|
||||
let lastSavedState = modelValue
|
||||
let lastSavedState = ''
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
(newValue) => {
|
||||
if (!contentHasChanged.value) {
|
||||
lastSavedState = newValue
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
watch(
|
||||
() => internalMode.value,
|
||||
@@ -325,6 +335,13 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
const additionalLinkProtocols = [
|
||||
'ftp',
|
||||
'git',
|
||||
'obsidian',
|
||||
'notion',
|
||||
]
|
||||
|
||||
const extensions : Extensions = [
|
||||
// Starterkit:
|
||||
Blockquote,
|
||||
@@ -379,7 +396,11 @@ const extensions : Extensions = [
|
||||
Underline,
|
||||
Link.configure({
|
||||
openOnClick: false,
|
||||
validate: (href: string) => /^https?:\/\//.test(href),
|
||||
validate: (href: string) => (new RegExp(
|
||||
`^(https?|${additionalLinkProtocols.join('|')}):\\/\\/`,
|
||||
'i',
|
||||
)).test(href),
|
||||
protocols: additionalLinkProtocols,
|
||||
}),
|
||||
Table.configure({
|
||||
resizable: true,
|
||||
@@ -696,6 +717,9 @@ watch(
|
||||
|
||||
<style lang="scss">
|
||||
.tiptap__editor {
|
||||
transition: box-shadow $transition;
|
||||
border-radius: $radius;
|
||||
|
||||
&.tiptap__editor-is-edit-enabled {
|
||||
min-height: 10rem;
|
||||
|
||||
@@ -711,9 +735,6 @@ watch(
|
||||
cursor: text;
|
||||
}
|
||||
}
|
||||
|
||||
transition: box-shadow $transition;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
.tiptap p::before {
|
||||
|
||||
@@ -71,14 +71,14 @@ import {success} from '@/message'
|
||||
import Message from '@/components/misc/Message.vue'
|
||||
import ButtonLink from '@/components/misc/ButtonLink.vue'
|
||||
|
||||
const props = defineProps({
|
||||
configureOpen: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
configureOpen?: boolean
|
||||
}>(), {
|
||||
configureOpen: false,
|
||||
})
|
||||
const emit = defineEmits(['foundApi'])
|
||||
const emit = defineEmits<{
|
||||
'foundApi': [url: string],
|
||||
}>()
|
||||
|
||||
const apiUrl = ref(window.API_URL)
|
||||
const configureApi = ref(window.API_URL === '')
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
{{ title }}
|
||||
</p>
|
||||
<BaseButton
|
||||
v-if="hasClose"
|
||||
v-if="$attrs.onClose"
|
||||
v-tooltip="$t('misc.close')"
|
||||
class="card-header-icon"
|
||||
:aria-label="$t('misc.close')"
|
||||
@@ -51,7 +51,6 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||
withDefaults(defineProps<{
|
||||
title?: string
|
||||
padding?: boolean
|
||||
hasClose?: boolean
|
||||
closeIcon?: IconProp
|
||||
shadow?: boolean
|
||||
hasContent?: boolean
|
||||
@@ -59,14 +58,15 @@ withDefaults(defineProps<{
|
||||
}>(), {
|
||||
title: '',
|
||||
padding: true,
|
||||
hasClose: false,
|
||||
closeIcon: 'times',
|
||||
shadow: true,
|
||||
hasContent: true,
|
||||
loading: false,
|
||||
})
|
||||
|
||||
defineEmits(['close'])
|
||||
defineEmits<{
|
||||
'close': []
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -9,7 +9,6 @@
|
||||
:shadow="false"
|
||||
:padding="false"
|
||||
class="has-text-left"
|
||||
:has-close="true"
|
||||
:loading="loading"
|
||||
@close="$router.back()"
|
||||
>
|
||||
@@ -23,7 +22,7 @@
|
||||
v-if="tertiary !== ''"
|
||||
:shadow="false"
|
||||
variant="tertiary"
|
||||
@click.prevent.stop="$emit('tertiary')"
|
||||
@click.prevent.stop="$emit('tertiary', $event)"
|
||||
>
|
||||
{{ tertiary }}
|
||||
</x-button>
|
||||
@@ -39,7 +38,7 @@
|
||||
:icon="primaryIcon"
|
||||
:disabled="primaryDisabled || loading"
|
||||
class="ml-2"
|
||||
@click.prevent.stop="primary()"
|
||||
@click.prevent.stop="primary"
|
||||
>
|
||||
{{ primaryLabel || $t('misc.create') }}
|
||||
</x-button>
|
||||
@@ -71,10 +70,14 @@ withDefaults(defineProps<{
|
||||
loading: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['create', 'primary', 'tertiary'])
|
||||
const emit = defineEmits<{
|
||||
'create': [event: MouseEvent],
|
||||
'primary': [event: MouseEvent],
|
||||
'tertiary': [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
function primary() {
|
||||
emit('create')
|
||||
emit('primary')
|
||||
function primary(event: MouseEvent) {
|
||||
emit('create', event)
|
||||
emit('primary', event)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<div
|
||||
ref="dropdown"
|
||||
class="dropdown"
|
||||
@pointerenter="initialMount = true"
|
||||
>
|
||||
<slot
|
||||
name="trigger"
|
||||
@@ -22,7 +23,8 @@
|
||||
|
||||
<CustomTransition name="fade">
|
||||
<div
|
||||
v-if="open"
|
||||
v-if="initialMount || open"
|
||||
v-show="open"
|
||||
class="dropdown-menu"
|
||||
>
|
||||
<div class="dropdown-content">
|
||||
@@ -34,21 +36,34 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, type PropType} from 'vue'
|
||||
import {ref} from 'vue'
|
||||
import {onClickOutside} from '@vueuse/core'
|
||||
import type {IconProp} from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
defineProps({
|
||||
triggerIcon: {
|
||||
type: String as PropType<IconProp>,
|
||||
default: 'ellipsis-h',
|
||||
},
|
||||
withDefaults(defineProps<{
|
||||
triggerIcon?: IconProp
|
||||
}>(), {
|
||||
triggerIcon: 'ellipsis-h',
|
||||
})
|
||||
const emit = defineEmits(['close'])
|
||||
|
||||
const emit = defineEmits<{
|
||||
'close': [event: PointerEvent]
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
'trigger': (props: {
|
||||
close: () => void,
|
||||
toggleOpen: () => void,
|
||||
open: boolean
|
||||
}) => void,
|
||||
'default': () => void
|
||||
}>()
|
||||
|
||||
|
||||
const initialMount = ref(false)
|
||||
const open = ref(false)
|
||||
|
||||
function close() {
|
||||
@@ -60,7 +75,7 @@ function toggleOpen() {
|
||||
}
|
||||
|
||||
const dropdown = ref()
|
||||
onClickOutside(dropdown, (e: Event) => {
|
||||
onClickOutside(dropdown, (e) => {
|
||||
if (!open.value) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
<template>
|
||||
<BaseButton
|
||||
class="dropdown-item"
|
||||
:disabled="disabled"
|
||||
:class="{'is-disabled': disabled}"
|
||||
>
|
||||
<span
|
||||
|
||||
@@ -28,7 +28,7 @@ import {
|
||||
faDownload,
|
||||
faEllipsisH,
|
||||
faEllipsisV,
|
||||
faExclamation,
|
||||
faExclamationCircle,
|
||||
faEye,
|
||||
faEyeSlash,
|
||||
faFile,
|
||||
@@ -138,7 +138,7 @@ library.add(faCopy)
|
||||
library.add(faDownload)
|
||||
library.add(faEllipsisH)
|
||||
library.add(faEllipsisV)
|
||||
library.add(faExclamation)
|
||||
library.add(faExclamationCircle)
|
||||
library.add(faEye)
|
||||
library.add(faEyeSlash)
|
||||
library.add(faFillDrip)
|
||||
|
||||
@@ -20,6 +20,12 @@
|
||||
class="modal-container"
|
||||
@mousedown.self.prevent.stop="$emit('close')"
|
||||
>
|
||||
<BaseButton
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon icon="times" />
|
||||
</BaseButton>
|
||||
<div
|
||||
class="modal-content"
|
||||
:class="{
|
||||
@@ -27,15 +33,8 @@
|
||||
'is-wide': wide
|
||||
}"
|
||||
>
|
||||
<BaseButton
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon icon="times" />
|
||||
</BaseButton>
|
||||
|
||||
<slot>
|
||||
<div class="header">
|
||||
<div class="modal-header">
|
||||
<slot name="header" />
|
||||
</div>
|
||||
<div class="content">
|
||||
@@ -137,11 +136,11 @@ $modal-width: 1024px;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin: 0;
|
||||
top: 25%;
|
||||
transform: translate(-50%, -25%);
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
.modal-header {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -154,12 +153,15 @@ $modal-width: 1024px;
|
||||
// scrolling-content
|
||||
// used e.g. for <TaskDetailViewModal>
|
||||
.scrolling .modal-content {
|
||||
max-width: $modal-width;
|
||||
width: 100%;
|
||||
margin: $modal-margin auto;
|
||||
|
||||
max-height: none; // reset bulma
|
||||
overflow: visible; // reset bulma
|
||||
|
||||
@media not print {
|
||||
max-width: $modal-width;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
max-height: none; // reset bulma
|
||||
@@ -167,7 +169,7 @@ $modal-width: 1024px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $desktop) {
|
||||
@media screen and (max-width: $desktop), print {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -194,7 +196,7 @@ $modal-width: 1024px;
|
||||
position: fixed;
|
||||
top: .5rem;
|
||||
right: $close-button-padding;
|
||||
color: var(--grey-900);
|
||||
color: var(--white);
|
||||
font-size: 2rem;
|
||||
|
||||
@media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
|
||||
@@ -203,15 +205,50 @@ $modal-width: 1024px;
|
||||
// we align the close button to the modal until there is enough space outside for it
|
||||
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
|
||||
}
|
||||
// we can only use light color when there is enough space for the close button next to the modal
|
||||
@media screen and (min-width: calc(#{$desktop } + #{$close-button-min-space})) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) and (max-width: #{$desktop + $close-button-min-space}) {
|
||||
top: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: $tablet) {
|
||||
.modal-mask {
|
||||
position: static;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.card) {
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content:has(.modal-header) {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 0 1rem;
|
||||
min-height: 100vh
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -219,4 +256,10 @@ $modal-width: 1024px;
|
||||
.dark .close {
|
||||
color: var(--grey-900);
|
||||
}
|
||||
</style>
|
||||
|
||||
@media print, screen and (max-width: $tablet) {
|
||||
body:has(.modal-mask) #app {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -53,11 +53,11 @@ import ApiConfig from '@/components/misc/ApiConfig.vue'
|
||||
import {useTitle} from '@/composables/useTitle'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
|
||||
const {
|
||||
showApiConfig = true,
|
||||
} = defineProps<{
|
||||
withDefaults(defineProps<{
|
||||
showApiConfig?: boolean
|
||||
}>()
|
||||
}>(), {
|
||||
showApiConfig: true,
|
||||
})
|
||||
const configStore = useConfigStore()
|
||||
const motd = computed(() => configStore.motd)
|
||||
|
||||
|
||||
@@ -1,93 +1,49 @@
|
||||
<template>
|
||||
<nav
|
||||
v-if="totalPages > 1"
|
||||
aria-label="pagination"
|
||||
class="pagination is-centered p-4"
|
||||
role="navigation"
|
||||
<BasePagination
|
||||
:total-pages="totalPages"
|
||||
:current-page="currentPage"
|
||||
>
|
||||
<RouterLink
|
||||
:disabled="currentPage === 1 || undefined"
|
||||
:to="getRouteForPagination(currentPage - 1)"
|
||||
class="pagination-previous"
|
||||
>
|
||||
{{ $t('misc.previous') }}
|
||||
</RouterLink>
|
||||
<RouterLink
|
||||
:disabled="currentPage === totalPages || undefined"
|
||||
:to="getRouteForPagination(currentPage + 1)"
|
||||
class="pagination-next"
|
||||
>
|
||||
{{ $t('misc.next') }}
|
||||
</RouterLink>
|
||||
<ul class="pagination-list">
|
||||
<li
|
||||
v-for="(p, i) in pages"
|
||||
:key="`page-${i}`"
|
||||
<template #previous="{ disabled }">
|
||||
<RouterLink
|
||||
:disabled="disabled || undefined"
|
||||
:to="getRouteForPagination(currentPage - 1)"
|
||||
class="pagination-previous"
|
||||
>
|
||||
<span
|
||||
v-if="p.isEllipsis"
|
||||
class="pagination-ellipsis"
|
||||
>…</span>
|
||||
<RouterLink
|
||||
v-else
|
||||
class="pagination-link"
|
||||
:aria-label="'Goto page ' + p.number"
|
||||
:class="{ 'is-current': p.number === currentPage }"
|
||||
:to="getRouteForPagination(p.number)"
|
||||
>
|
||||
{{ p.number }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
{{ $t('misc.previous') }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
<template #next="{ disabled }">
|
||||
<RouterLink
|
||||
:disabled="disabled || undefined"
|
||||
:to="getRouteForPagination(currentPage + 1)"
|
||||
class="pagination-next"
|
||||
>
|
||||
{{ $t('misc.next') }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
<template #page-link="{ page, isCurrent }">
|
||||
<RouterLink
|
||||
class="pagination-link"
|
||||
:aria-label="'Goto page ' + page.number"
|
||||
:class="{ 'is-current': isCurrent }"
|
||||
:to="getRouteForPagination(page.number)"
|
||||
>
|
||||
{{ page.number }}
|
||||
</RouterLink>
|
||||
</template>
|
||||
</BasePagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed} from 'vue'
|
||||
import BasePagination from '@/components/base/BasePagination.vue'
|
||||
|
||||
const props = defineProps({
|
||||
totalPages: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
withDefaults(defineProps<{
|
||||
totalPages: number,
|
||||
currentPage?: number
|
||||
}>(), {
|
||||
currentPage: 0,
|
||||
})
|
||||
|
||||
function createPagination(totalPages: number, currentPage: number) {
|
||||
const pages = []
|
||||
for (let i = 0; i < totalPages; i++) {
|
||||
|
||||
// Show ellipsis instead of all pages
|
||||
if (
|
||||
i > 0 && // Always at least the first page
|
||||
(i + 1) < totalPages && // And the last page
|
||||
(
|
||||
// And the current with current + 1 and current - 1
|
||||
(i + 1) > currentPage + 1 ||
|
||||
(i + 1) < currentPage - 1
|
||||
)
|
||||
) {
|
||||
// Only add an ellipsis if the last page isn't already one
|
||||
if (pages[i - 1] && !pages[i - 1].isEllipsis) {
|
||||
pages.push({
|
||||
number: 0,
|
||||
isEllipsis: true,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
pages.push({
|
||||
number: i + 1,
|
||||
isEllipsis: false,
|
||||
})
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
function getRouteForPagination(page = 1, type = null) {
|
||||
return {
|
||||
name: type,
|
||||
@@ -99,20 +55,4 @@ function getRouteForPagination(page = 1, type = null) {
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const pages = computed(() => createPagination(props.totalPages, props.currentPage))
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.pagination {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
|
||||
.pagination-previous,
|
||||
.pagination-next {
|
||||
&:not(:disabled):hover {
|
||||
background: $scheme-main;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</script>
|
||||
@@ -0,0 +1,55 @@
|
||||
<template>
|
||||
<BasePagination
|
||||
:total-pages="totalPages"
|
||||
:current-page="currentPage"
|
||||
>
|
||||
<template #previous="{ disabled }">
|
||||
<BaseButton
|
||||
:disabled="disabled"
|
||||
class="pagination-previous"
|
||||
@click="changePage(currentPage - 1)"
|
||||
>
|
||||
{{ $t('misc.previous') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
<template #next="{ disabled }">
|
||||
<BaseButton
|
||||
:disabled="disabled"
|
||||
class="pagination-next"
|
||||
@click="changePage(currentPage + 1)"
|
||||
>
|
||||
{{ $t('misc.next') }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
<template #page-link="{ page, isCurrent }">
|
||||
<BaseButton
|
||||
class="pagination-link"
|
||||
:aria-label="'Goto page ' + page.number"
|
||||
:class="{ 'is-current': isCurrent }"
|
||||
@click="changePage(page.number)"
|
||||
>
|
||||
{{ page.number }}
|
||||
</BaseButton>
|
||||
</template>
|
||||
</BasePagination>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import BasePagination from '@/components/base/BasePagination.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
totalPages: number,
|
||||
currentPage: number
|
||||
}>(), {
|
||||
currentPage: 1,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['pageChanged'])
|
||||
|
||||
function changePage(page: number) {
|
||||
if (page >= 1 && page <= props.totalPages) {
|
||||
emit('pageChanged', page)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@@ -34,7 +34,22 @@ const props = withDefaults(defineProps<{
|
||||
open: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['close'])
|
||||
const emit = defineEmits<{
|
||||
'update:open': [open: boolean]
|
||||
}>()
|
||||
|
||||
defineSlots<{
|
||||
trigger(props: {
|
||||
isOpen: boolean,
|
||||
toggle: () => boolean,
|
||||
close: () => void,
|
||||
}) : void
|
||||
content(props: {
|
||||
isOpen: boolean,
|
||||
toggle: () => boolean,
|
||||
close: () => void
|
||||
}): void
|
||||
}>()
|
||||
|
||||
const openValue = ref(props.open)
|
||||
watchEffect(() => {
|
||||
@@ -42,22 +57,22 @@ watchEffect(() => {
|
||||
})
|
||||
|
||||
function close() {
|
||||
if (!openValue.value) {
|
||||
return
|
||||
}
|
||||
openValue.value = false
|
||||
emit('close')
|
||||
emit('update:open', false)
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
openValue.value = !openValue.value
|
||||
emit('update:open', openValue.value)
|
||||
return openValue.value
|
||||
}
|
||||
|
||||
const popup = ref<HTMLElement | null>(null)
|
||||
|
||||
onClickOutside(popup, () => {
|
||||
if (!openValue.value) {
|
||||
return
|
||||
}
|
||||
close()
|
||||
})
|
||||
onClickOutside(popup, () => close())
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -13,19 +13,13 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
defineProps({
|
||||
value: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
isSmall: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isPrimary: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
withDefaults(defineProps<{
|
||||
value: number
|
||||
isSmall?: boolean
|
||||
isPrimary?: boolean
|
||||
}>(), {
|
||||
isSmall: false,
|
||||
isPrimary: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
|
||||
@@ -47,18 +47,20 @@ import {success} from '@/message'
|
||||
import type { IconProp } from '@fortawesome/fontawesome-svg-core'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: ISubscription | null,
|
||||
entity: ISubscription['entity'],
|
||||
entityId: number,
|
||||
isButton?: boolean,
|
||||
modelValue?: ISubscription,
|
||||
type?: 'button' | 'dropdown',
|
||||
}>(), {
|
||||
isButton: true,
|
||||
modelValue: null,
|
||||
isButton: true,
|
||||
type: 'button',
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [subscription: ISubscription | null]
|
||||
}>()
|
||||
|
||||
const subscriptionEntity = computed<string | null>(() => props.modelValue?.entity ?? null)
|
||||
|
||||
@@ -91,7 +93,7 @@ const tooltipText = computed(() => {
|
||||
|
||||
const buttonText = computed(() => props.modelValue ? t('task.subscription.unsubscribe') : t('task.subscription.subscribe'))
|
||||
const iconName = computed<IconProp>(() => props.modelValue ? ['far', 'bell-slash'] : 'bell')
|
||||
const disabled = computed(() => props.modelValue && subscriptionEntity.value !== props.entity)
|
||||
const disabled = computed(() => props.modelValue && subscriptionEntity.value !== props.entity || false)
|
||||
|
||||
function changeSubscription() {
|
||||
if (disabled.value) {
|
||||
|
||||
@@ -16,13 +16,13 @@
|
||||
class="switch-view"
|
||||
>
|
||||
<BaseButton
|
||||
v-for="v in views"
|
||||
:key="v.id"
|
||||
v-for="view in views"
|
||||
:key="view.id"
|
||||
class="switch-view-button"
|
||||
:class="{'is-active': v.id === viewId}"
|
||||
:to="{ name: 'project.view', params: { projectId, viewId: v.id } }"
|
||||
:class="{'is-active': view.id === viewId}"
|
||||
:to="{ name: 'project.view', params: { projectId, viewId: view.id } }"
|
||||
>
|
||||
{{ getViewTitle(v) }}
|
||||
{{ getViewTitle(view) }}
|
||||
</BaseButton>
|
||||
</div>
|
||||
<slot name="header" />
|
||||
@@ -62,10 +62,7 @@ import type {IProject} from '@/modelTypes/IProject'
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
const {
|
||||
projectId,
|
||||
viewId,
|
||||
} = defineProps<{
|
||||
const props = defineProps<{
|
||||
projectId: IProject['id'],
|
||||
viewId: IProjectView['id'],
|
||||
}>()
|
||||
@@ -88,7 +85,7 @@ const currentProject = computed<IProject>(() => {
|
||||
})
|
||||
useTitle(() => currentProject.value?.id ? getProjectTitle(currentProject.value) : '')
|
||||
|
||||
const views = computed(() => projectStore.projects[projectId]?.views)
|
||||
const views = computed(() => projectStore.projects[props.projectId]?.views)
|
||||
|
||||
// watchEffect would be called every time the prop would get a value assigned, even if that value was the same as before.
|
||||
// This resulted in loading and setting the project multiple times, even when navigating away from it.
|
||||
@@ -96,7 +93,7 @@ const views = computed(() => projectStore.projects[projectId]?.views)
|
||||
// project background and then navigating to home. It also highlighted the project in the menu and didn't allow changing any
|
||||
// of it, most likely due to the rights not being properly populated.
|
||||
watch(
|
||||
() => projectId,
|
||||
() => props.projectId,
|
||||
// loadProject
|
||||
async (projectIdToLoad: number) => {
|
||||
const projectData = {id: projectIdToLoad}
|
||||
@@ -112,7 +109,7 @@ watch(
|
||||
)
|
||||
&& typeof currentProject.value !== 'undefined' && currentProject.value.maxRight !== null
|
||||
) {
|
||||
loadedProjectId.value = projectId
|
||||
loadedProjectId.value = projectIdToLoad
|
||||
return
|
||||
}
|
||||
|
||||
@@ -122,21 +119,29 @@ watch(
|
||||
loadedProjectId.value = 0
|
||||
const projectFromStore = projectStore.projects[projectData.id]
|
||||
if (projectFromStore) {
|
||||
baseStore.handleSetCurrentProject({project: projectFromStore})
|
||||
baseStore.handleSetCurrentProject({project: projectFromStore, currentProjectViewId: props.viewId})
|
||||
}
|
||||
|
||||
// We create an extra project object instead of creating it in project.value because that would trigger a ui update which would result in bad ux.
|
||||
const project = new ProjectModel(projectData)
|
||||
try {
|
||||
const loadedProject = await projectService.value.get(project)
|
||||
baseStore.handleSetCurrentProject({project: loadedProject})
|
||||
baseStore.handleSetCurrentProject({project: loadedProject, currentProjectViewId: props.viewId})
|
||||
} finally {
|
||||
loadedProjectId.value = projectId
|
||||
loadedProjectId.value = projectIdToLoad
|
||||
}
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
watch(
|
||||
() => props.viewId,
|
||||
() => {
|
||||
baseStore.setCurrentProjectViewId(props.viewId)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function getViewTitle(view: IProjectView) {
|
||||
switch (view.title) {
|
||||
case 'List':
|
||||
|
||||
@@ -23,17 +23,16 @@ import {
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
import {createRandomID} from '@/helpers/randomId'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
projectId,
|
||||
inputLabel = undefined,
|
||||
} = defineProps<{
|
||||
const props = defineProps<{
|
||||
modelValue: string,
|
||||
projectId?: number,
|
||||
inputLabel?: string,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'blur'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string],
|
||||
'blur': [],
|
||||
}>()
|
||||
|
||||
const userService = new UserService()
|
||||
const projectUserService = new ProjectUserService()
|
||||
@@ -49,9 +48,9 @@ const {
|
||||
const id = ref(createRandomID())
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
() => props.modelValue,
|
||||
() => {
|
||||
filterQuery.value = modelValue
|
||||
filterQuery.value = props.modelValue
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
@@ -59,7 +58,7 @@ watch(
|
||||
watch(
|
||||
() => filterQuery.value,
|
||||
() => {
|
||||
if (filterQuery.value !== modelValue) {
|
||||
if (filterQuery.value !== props.modelValue) {
|
||||
emit('update:modelValue', filterQuery.value)
|
||||
}
|
||||
},
|
||||
@@ -204,6 +203,7 @@ const autocompleteResultType = ref<'labels' | 'assignees' | 'projects' | null>(n
|
||||
const autocompleteResults = ref<any[]>([])
|
||||
|
||||
function handleFieldInput() {
|
||||
if (!filterInput.value) return
|
||||
const cursorPosition = filterInput.value.selectionStart
|
||||
const textUpToCursor = filterQuery.value.substring(0, cursorPosition)
|
||||
autocompleteResults.value = []
|
||||
@@ -218,33 +218,35 @@ function handleFieldInput() {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
let search = keyword
|
||||
if (operator === 'in' || operator === '?=') {
|
||||
const keywords = keyword.split(',')
|
||||
search = keywords[keywords.length - 1].trim()
|
||||
}
|
||||
if (matched.startsWith('label')) {
|
||||
autocompleteResultType.value = 'labels'
|
||||
autocompleteResults.value = labelStore.filterLabelsByQuery([], search)
|
||||
}
|
||||
if (matched.startsWith('assignee')) {
|
||||
autocompleteResultType.value = 'assignees'
|
||||
if (projectId) {
|
||||
projectUserService.getAll({projectId}, {s: search})
|
||||
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
|
||||
} else {
|
||||
userService.getAll({}, {s: search})
|
||||
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
|
||||
}
|
||||
}
|
||||
if (!projectId && matched.startsWith('project')) {
|
||||
autocompleteResultType.value = 'projects'
|
||||
autocompleteResults.value = projectStore.searchProject(search)
|
||||
}
|
||||
autocompleteMatchText.value = keyword
|
||||
autocompleteMatchPosition.value = match.index + prefix.length - 1 + keyword.replace(search, '').length
|
||||
if(!keyword) {
|
||||
return
|
||||
}
|
||||
|
||||
let search = keyword
|
||||
if (operator === 'in' || operator === '?=') {
|
||||
const keywords = keyword.split(',')
|
||||
search = keywords[keywords.length - 1].trim()
|
||||
}
|
||||
if (matched.startsWith('label')) {
|
||||
autocompleteResultType.value = 'labels'
|
||||
autocompleteResults.value = labelStore.filterLabelsByQuery([], search)
|
||||
}
|
||||
if (matched.startsWith('assignee')) {
|
||||
autocompleteResultType.value = 'assignees'
|
||||
if (props.projectId) {
|
||||
projectUserService.getAll({projectId: props.projectId}, {s: search})
|
||||
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
|
||||
} else {
|
||||
userService.getAll({}, {s: search})
|
||||
.then(users => autocompleteResults.value = users.length > 1 ? users : [])
|
||||
}
|
||||
}
|
||||
if (!props.projectId && matched.startsWith('project')) {
|
||||
autocompleteResultType.value = 'projects'
|
||||
autocompleteResults.value = projectStore.searchProject(search)
|
||||
}
|
||||
autocompleteMatchText.value = keyword
|
||||
autocompleteMatchPosition.value = match.index + prefix.length - 1 + keyword.replace(search, '').length
|
||||
})
|
||||
}
|
||||
|
||||
@@ -304,8 +306,7 @@ const blurDebounced = useDebounceFn(() => emit('blur'), 500)
|
||||
/>
|
||||
<DatepickerWithValues
|
||||
v-model="currentDatepickerValue"
|
||||
:open="datePickerPopupOpen"
|
||||
@close="() => datePickerPopupOpen = false"
|
||||
v-model:open="datePickerPopupOpen"
|
||||
@update:modelValue="updateDateInQuery"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -19,8 +19,8 @@
|
||||
v-model="value"
|
||||
:has-title="true"
|
||||
class="filter-popup"
|
||||
@update:modelValue="emitChanges"
|
||||
@showResultsButtonClicked="() => modalOpen = false"
|
||||
:change-immediately="false"
|
||||
@showResults="showResults"
|
||||
/>
|
||||
</Modal>
|
||||
</template>
|
||||
@@ -32,8 +32,12 @@ import Filters from '@/components/project/partials/Filters.vue'
|
||||
|
||||
import {type TaskFilterParams} from '@/services/taskCollection'
|
||||
|
||||
const props = defineProps(['modelValue'])
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const props = defineProps<{
|
||||
modelValue: TaskFilterParams,
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: TaskFilterParams]
|
||||
}>()
|
||||
|
||||
const value = ref<TaskFilterParams>({})
|
||||
|
||||
@@ -42,23 +46,27 @@ watch(
|
||||
(modelValue: TaskFilterParams) => {
|
||||
value.value = modelValue
|
||||
},
|
||||
{immediate: true},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
function emitChanges(newValue: TaskFilterParams) {
|
||||
emit('update:modelValue', {
|
||||
...value.value,
|
||||
filter: newValue.filter,
|
||||
s: newValue.s,
|
||||
})
|
||||
}
|
||||
|
||||
const hasFilters = computed(() => {
|
||||
return value.value.filter !== '' ||
|
||||
value.value.s !== ''
|
||||
})
|
||||
|
||||
const modalOpen = ref(false)
|
||||
|
||||
function showResults() {
|
||||
emit('update:modelValue', {
|
||||
...value.value,
|
||||
filter: value.value.filter,
|
||||
s: value.value.s,
|
||||
})
|
||||
modalOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -7,13 +7,14 @@
|
||||
<FilterInput
|
||||
v-model="filterQuery"
|
||||
:project-id="projectId"
|
||||
@blur="change()"
|
||||
@update:modelValue="() => change('modelValue')"
|
||||
@blur="() => change('blur')"
|
||||
/>
|
||||
|
||||
<div class="field is-flex is-flex-direction-column">
|
||||
<FancyCheckbox
|
||||
v-model="params.filter_include_nulls"
|
||||
@blur="change()"
|
||||
@change="() => change('always')"
|
||||
>
|
||||
{{ $t('filters.attributes.includeNulls') }}
|
||||
</FancyCheckbox>
|
||||
@@ -58,17 +59,21 @@ import {useProjectStore} from '@/stores/projects'
|
||||
import {FILTER_OPERATORS, transformFilterStringForApi, transformFilterStringFromApi} from '@/helpers/filters'
|
||||
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
|
||||
|
||||
const {
|
||||
hasTitle = false,
|
||||
hasFooter = true,
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: TaskFilterParams,
|
||||
hasTitle?: boolean,
|
||||
hasFooter?: boolean,
|
||||
modelValue: TaskFilterParams,
|
||||
}>()
|
||||
changeImmediately?: boolean,
|
||||
}>(), {
|
||||
hasTitle: false,
|
||||
hasFooter: true,
|
||||
changeImmediately: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'showResultsButtonClicked'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: TaskFilterParams],
|
||||
'showResults': [],
|
||||
}>()
|
||||
|
||||
const route = useRoute()
|
||||
const projectId = computed(() => {
|
||||
@@ -102,20 +107,38 @@ const projectStore = useProjectStore()
|
||||
|
||||
// Using watchDebounced to prevent the filter re-triggering itself.
|
||||
watch(
|
||||
() => modelValue,
|
||||
() => props.modelValue,
|
||||
(value: TaskFilterParams) => {
|
||||
const val = {...value}
|
||||
val.filter = transformFilterStringFromApi(
|
||||
val?.filter || '',
|
||||
labelId => labelStore.getLabelById(labelId)?.title,
|
||||
labelId => labelStore.getLabelById(labelId)?.title || null,
|
||||
projectId => projectStore.projects[projectId]?.title || null,
|
||||
)
|
||||
params.value = val
|
||||
},
|
||||
{immediate: true},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
function change() {
|
||||
function change(event: 'blur' | 'modelValue' | 'always') {
|
||||
if (event !== 'always') {
|
||||
// The filter edit setting needs to save immediately, but the filter query edit in project views should
|
||||
// only change on blur, or it will show the filter replaced for api when the query is not yet complete.
|
||||
// This is highly confusing UX, hence we want to avoid that.
|
||||
// The approach taken here allows us to either toggle on blur or immediately, depending on the prop
|
||||
// value provided. This probably is a hacky way to do this, but it is also the most effective.
|
||||
if (props.changeImmediately && event === 'blur') {
|
||||
return
|
||||
}
|
||||
|
||||
if (!props.changeImmediately && event === 'modelValue') {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const filter = transformFilterStringForApi(
|
||||
filterQuery.value,
|
||||
labelTitle => labelStore.getLabelByExactTitle(labelTitle)?.id || null,
|
||||
@@ -139,7 +162,7 @@ function change() {
|
||||
s,
|
||||
}
|
||||
|
||||
if (JSON.stringify(modelValue) === JSON.stringify(newParams)) {
|
||||
if (JSON.stringify(props.modelValue) === JSON.stringify(newParams)) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -148,7 +171,7 @@ function change() {
|
||||
|
||||
function changeAndEmitButton() {
|
||||
change()
|
||||
emit('showResultsButtonClicked')
|
||||
emit('showResults')
|
||||
}
|
||||
|
||||
function clearFiltersAndEmit() {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
<BaseButton
|
||||
class="project-button"
|
||||
:aria-label="project.title"
|
||||
:title="project.description"
|
||||
:title="textOnlyDescription"
|
||||
:to="{
|
||||
name: 'project.index',
|
||||
params: { projectId: project.id}
|
||||
@@ -53,6 +53,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed} from 'vue'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
@@ -68,6 +69,10 @@ const props = defineProps<{
|
||||
const {background, blurHashUrl} = useProjectBackground(() => props.project)
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const textOnlyDescription = computed(() => {
|
||||
return props.project.description ? props.project.description.replace(/<[^>]*>/g, '') : ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
<template>
|
||||
<ul class="project-grid">
|
||||
<ul
|
||||
class="project-grid"
|
||||
:class="{ 'show-even-number-of-projects': showEvenNumberOfProjects }"
|
||||
>
|
||||
<li
|
||||
v-for="(item, index) in filteredProjects"
|
||||
:key="`project_${item.id}_${index}`"
|
||||
@@ -11,24 +14,21 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {computed, type PropType} from 'vue'
|
||||
import {computed} from 'vue'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
|
||||
import ProjectCard from './ProjectCard.vue'
|
||||
|
||||
const props = defineProps({
|
||||
projects: {
|
||||
type: Array as PropType<IProject[]>,
|
||||
default: () => [],
|
||||
},
|
||||
showArchived: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
itemLimit: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
projects: IProject[],
|
||||
showArchived?: boolean,
|
||||
itemLimit?: boolean,
|
||||
showEvenNumberOfProjects?: boolean,
|
||||
}>(), {
|
||||
projects: () => [],
|
||||
showArchived: false,
|
||||
itemLimit: false,
|
||||
showEvenNumberOfProjects: false,
|
||||
})
|
||||
|
||||
const filteredProjects = computed(() => {
|
||||
@@ -63,9 +63,13 @@ const filteredProjects = computed(() => {
|
||||
|
||||
@media screen and (min-width: $widescreen) {
|
||||
--project-grid-columns: 5;
|
||||
}
|
||||
|
||||
.project-grid-item:nth-child(6) {
|
||||
display: none;
|
||||
&.show-even-number-of-projects {
|
||||
@media screen and (min-width: $widescreen) {
|
||||
.project-grid-item:nth-child(5) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@
|
||||
@click="() => unCollapseBucket(bucket)"
|
||||
>
|
||||
<span
|
||||
v-if="view?.doneBucketId === bucket.id"
|
||||
v-if="bucket.id !== 0 && view?.doneBucketId === bucket.id"
|
||||
v-tooltip="$t('project.kanban.doneBucketHint')"
|
||||
class="icon is-small has-text-success mr-2"
|
||||
>
|
||||
@@ -71,41 +71,40 @@
|
||||
trigger-icon="ellipsis-v"
|
||||
@close="() => showSetLimitInput = false"
|
||||
>
|
||||
<div
|
||||
v-if="showSetLimitInput"
|
||||
class="field has-addons"
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
ref="bucketLimitInputRef"
|
||||
v-focus.always
|
||||
:value="bucket.limit"
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
@keyup.esc="() => showSetLimitInput = false"
|
||||
@keyup.enter="() => {setBucketLimit(bucket.id, true); showSetLimitInput = false}"
|
||||
@input="setBucketLimit(bucket.id)"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button
|
||||
v-cy="'setBucketLimit'"
|
||||
:disabled="bucket.limit < 0"
|
||||
:icon="['far', 'save']"
|
||||
:shadow="false"
|
||||
@click="() => {setBucketLimit(bucket.id, true); showSetLimitInput = false}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DropdownItem
|
||||
v-else
|
||||
@click.stop="showSetLimitInput = true"
|
||||
>
|
||||
<div
|
||||
v-if="showSetLimitInput"
|
||||
class="field has-addons"
|
||||
>
|
||||
<div class="control">
|
||||
<input
|
||||
ref="bucketLimitInputRef"
|
||||
v-focus.always
|
||||
:value="bucket.limit"
|
||||
class="input"
|
||||
type="number"
|
||||
min="0"
|
||||
@keyup.esc="() => showSetLimitInput = false"
|
||||
@keyup.enter="() => {setBucketLimit(bucket.id, true); showSetLimitInput = false}"
|
||||
@input="setBucketLimit(bucket.id)"
|
||||
>
|
||||
</div>
|
||||
<div class="control">
|
||||
<x-button
|
||||
v-cy="'setBucketLimit'"
|
||||
:disabled="bucket.limit < 0"
|
||||
:icon="['far', 'save']"
|
||||
:shadow="false"
|
||||
@click="setBucketLimit(bucket.id, true)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template v-else>
|
||||
{{
|
||||
$t('project.kanban.limit', {limit: bucket.limit > 0 ? bucket.limit : $t('project.kanban.noLimit')})
|
||||
}}
|
||||
</template>
|
||||
{{
|
||||
$t('project.kanban.limit', {limit: bucket.limit > 0 ? bucket.limit : $t('project.kanban.noLimit')})
|
||||
}}
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="$t('project.kanban.doneBucketHintExtended')"
|
||||
@@ -131,6 +130,7 @@
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-tooltip="buckets.length <= 1 ? $t('project.kanban.deleteLast') : ''"
|
||||
class="has-text-danger"
|
||||
:class="{'is-disabled': buckets.length <= 1}"
|
||||
icon-class="has-text-danger"
|
||||
icon="trash-alt"
|
||||
@@ -156,7 +156,7 @@
|
||||
>
|
||||
<template #footer>
|
||||
<div
|
||||
v-if="canWrite"
|
||||
v-if="canCreateTasks"
|
||||
class="bucket-footer"
|
||||
>
|
||||
<div
|
||||
@@ -309,10 +309,7 @@ import ProjectViewModel from '@/models/projectView'
|
||||
import TaskBucketService from '@/services/taskBucket'
|
||||
import TaskBucketModel from '@/models/taskBucket'
|
||||
|
||||
const {
|
||||
projectId,
|
||||
viewId,
|
||||
} = defineProps<{
|
||||
const props = defineProps<{
|
||||
projectId: number,
|
||||
viewId: IProjectView['id'],
|
||||
}>()
|
||||
@@ -392,9 +389,10 @@ const bucketDraggableComponentData = computed(() => ({
|
||||
{'dragging-disabled': !canWrite.value},
|
||||
],
|
||||
}))
|
||||
const project = computed(() => projectId ? projectStore.projects[projectId] : null)
|
||||
const view = computed<IProjectView | null>(() => project.value?.views.find(v => v.id === viewId) || null)
|
||||
const project = computed(() => props.projectId ? projectStore.projects[props.projectId] : null)
|
||||
const view = computed(() => project.value?.views.find(v => v.id === props.viewId) as IProjectView || null)
|
||||
const canWrite = computed(() => baseStore.currentProject?.maxRight > Rights.READ && view.value.bucketConfigurationMode === 'manual')
|
||||
const canCreateTasks = computed(() => canWrite.value && props.projectId > 0)
|
||||
const buckets = computed(() => kanbanStore.buckets)
|
||||
const loading = computed(() => kanbanStore.isLoading)
|
||||
|
||||
@@ -403,10 +401,10 @@ const taskLoading = computed(() => taskStore.isLoading || taskPositionService.va
|
||||
watch(
|
||||
() => ({
|
||||
params: params.value,
|
||||
projectId,
|
||||
viewId,
|
||||
projectId: props.projectId,
|
||||
viewId: props.viewId,
|
||||
}),
|
||||
({params}) => {
|
||||
({params, projectId, viewId}) => {
|
||||
if (projectId === undefined || Number(projectId) === 0) {
|
||||
return
|
||||
}
|
||||
@@ -435,8 +433,8 @@ function handleTaskContainerScroll(id: IBucket['id'], el: HTMLElement) {
|
||||
}
|
||||
|
||||
kanbanStore.loadNextTasksForBucket(
|
||||
projectId,
|
||||
viewId,
|
||||
props.projectId,
|
||||
props.viewId,
|
||||
params.value,
|
||||
id,
|
||||
)
|
||||
@@ -490,12 +488,6 @@ async function updateTaskPosition(e) {
|
||||
taskBefore !== null ? taskBefore.position : null,
|
||||
taskAfter !== null ? taskAfter.position : null,
|
||||
)
|
||||
if (
|
||||
oldBucket !== undefined && // This shouldn't actually be `undefined`, but let's play it safe.
|
||||
newBucket.id !== oldBucket.id
|
||||
) {
|
||||
newTask.done = project.value?.doneBucketId === newBucket.id
|
||||
}
|
||||
|
||||
let bucketHasChanged = false
|
||||
if (
|
||||
@@ -516,19 +508,25 @@ async function updateTaskPosition(e) {
|
||||
try {
|
||||
const newPosition = new TaskPositionModel({
|
||||
position,
|
||||
projectViewId: viewId,
|
||||
projectViewId: props.viewId,
|
||||
taskId: newTask.id,
|
||||
})
|
||||
await taskPositionService.value.update(newPosition)
|
||||
newTask.position = position
|
||||
|
||||
if(bucketHasChanged) {
|
||||
await taskBucketService.value.update(new TaskBucketModel({
|
||||
const updatedTaskBucket = await taskBucketService.value.update(new TaskBucketModel({
|
||||
taskId: newTask.id,
|
||||
bucketId: newTask.bucketId,
|
||||
projectViewId: viewId,
|
||||
projectViewId: props.viewId,
|
||||
projectId: project.value.id,
|
||||
}))
|
||||
newTask.done = updatedTaskBucket.taskDone
|
||||
if (updatedTaskBucket.bucketId !== newTask.bucketId) {
|
||||
kanbanStore.moveTaskToBucket(newTask, updatedTaskBucket.bucketId)
|
||||
}
|
||||
}
|
||||
kanbanStore.setTaskInBucket(newTask)
|
||||
|
||||
// Make sure the first and second task don't both get position 0 assigned
|
||||
if (newTaskIndex === 0 && taskAfter !== null && taskAfter.position === 0) {
|
||||
@@ -549,6 +547,9 @@ async function updateTaskPosition(e) {
|
||||
}
|
||||
|
||||
function toggleShowNewTaskInput(bucketId: IBucket['id']) {
|
||||
if (loading.value || taskLoading.value) {
|
||||
return
|
||||
}
|
||||
showNewTaskInput.value[bucketId] = !showNewTaskInput.value[bucketId]
|
||||
newTaskInputFocused.value = false
|
||||
}
|
||||
@@ -565,6 +566,7 @@ async function addTaskToBucket(bucketId: IBucket['id']) {
|
||||
bucketId,
|
||||
projectId: project.value.id,
|
||||
})
|
||||
toggleShowNewTaskInput(bucketId)
|
||||
newTaskText.value = ''
|
||||
kanbanStore.addTaskToBucket(task)
|
||||
scrollTaskContainerToBottom(bucketId)
|
||||
@@ -586,7 +588,7 @@ async function createNewBucket() {
|
||||
await kanbanStore.createBucket(new BucketModel({
|
||||
title: newBucketTitle.value,
|
||||
projectId: project.value.id,
|
||||
projectViewId: viewId,
|
||||
projectViewId: props.viewId,
|
||||
}))
|
||||
newBucketTitle.value = ''
|
||||
}
|
||||
@@ -606,7 +608,7 @@ async function deleteBucket() {
|
||||
bucket: new BucketModel({
|
||||
id: bucketToDelete.value,
|
||||
projectId: project.value.id,
|
||||
projectViewId: viewId,
|
||||
projectViewId: props.viewId,
|
||||
}),
|
||||
params: params.value,
|
||||
})
|
||||
@@ -635,7 +637,7 @@ async function saveBucketTitle(bucketId: IBucket['id'], bucketTitle: string) {
|
||||
await kanbanStore.updateBucket({
|
||||
id: bucketId,
|
||||
title: bucketTitle,
|
||||
projectId,
|
||||
projectId: props.projectId,
|
||||
})
|
||||
success({message: i18n.global.t('project.kanban.bucketTitleSavedSuccess')})
|
||||
bucketTitleEditable.value = false
|
||||
@@ -657,7 +659,7 @@ function updateBucketPosition(e: { newIndex: number }) {
|
||||
|
||||
kanbanStore.updateBucket({
|
||||
id: bucket.id,
|
||||
projectId,
|
||||
projectId: props.projectId,
|
||||
position: calculateItemPosition(
|
||||
bucketBefore !== null ? bucketBefore.position : null,
|
||||
bucketAfter !== null ? bucketAfter.position : null,
|
||||
@@ -672,7 +674,7 @@ async function saveBucketLimit(bucketId: IBucket['id'], limit: number) {
|
||||
|
||||
await kanbanStore.updateBucket({
|
||||
...kanbanStore.getBucketById(bucketId),
|
||||
projectId,
|
||||
projectId: props.projectId,
|
||||
limit,
|
||||
})
|
||||
success({message: t('project.kanban.bucketLimitSavedSuccess')})
|
||||
@@ -769,6 +771,18 @@ function unCollapseBucket(bucket: IBucket) {
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.control.is-loading {
|
||||
&::after {
|
||||
top: 30%;
|
||||
right: 50%;
|
||||
transform: translate(-50%, 0);
|
||||
--loader-border-color: var(--grey-500);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<style lang="scss">
|
||||
$ease-out: all .3s cubic-bezier(0.23, 1, 0.32, 1);
|
||||
$bucket-width: 300px;
|
||||
@@ -791,8 +805,9 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
||||
}
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
height: calc(#{$crazy-height-calculation} - #{$filter-container-height});
|
||||
height: calc(#{$crazy-height-calculation} - #{$filter-container-height} + 9px);
|
||||
scroll-snap-type: x mandatory;
|
||||
margin: 0 -0.5rem;
|
||||
}
|
||||
|
||||
&-bucket-container {
|
||||
@@ -870,7 +885,6 @@ $filter-container-height: '1rem - #{$switch-view-height}';
|
||||
// to hide the fact we just made the button smaller.
|
||||
min-width: calc(#{$bucket-width} + 1rem);
|
||||
background: transparent;
|
||||
padding-right: 1rem;
|
||||
|
||||
.button {
|
||||
background: var(--grey-100);
|
||||
|
||||
@@ -45,7 +45,6 @@
|
||||
|
||||
<draggable
|
||||
v-if="tasks && tasks.length > 0"
|
||||
v-bind="DRAG_OPTIONS"
|
||||
v-model="tasks"
|
||||
group="tasks"
|
||||
handle=".handle"
|
||||
@@ -59,6 +58,8 @@
|
||||
},
|
||||
type: 'transition-group'
|
||||
}"
|
||||
:animation="100"
|
||||
ghost-class="task-ghost"
|
||||
@start="() => drag = true"
|
||||
@end="saveTaskPosition"
|
||||
>
|
||||
@@ -120,10 +121,7 @@ import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import TaskPositionService from '@/services/taskPosition'
|
||||
import TaskPositionModel from '@/models/taskPosition'
|
||||
|
||||
const {
|
||||
projectId,
|
||||
viewId,
|
||||
} = defineProps<{
|
||||
const props = defineProps<{
|
||||
projectId: IProject['id'],
|
||||
viewId: IProjectView['id'],
|
||||
}>()
|
||||
@@ -131,10 +129,6 @@ const {
|
||||
const ctaVisible = ref(false)
|
||||
|
||||
const drag = ref(false)
|
||||
const DRAG_OPTIONS = {
|
||||
animation: 100,
|
||||
ghostClass: 'task-ghost',
|
||||
} as const
|
||||
|
||||
const {
|
||||
tasks: allTasks,
|
||||
@@ -144,7 +138,7 @@ const {
|
||||
loadTasks,
|
||||
params,
|
||||
sortByParam,
|
||||
} = useTaskList(() => projectId, () => viewId, {position: 'asc'})
|
||||
} = useTaskList(() => props.projectId, () => props.viewId, {position: 'asc'})
|
||||
|
||||
const taskPositionService = ref(new TaskPositionService())
|
||||
|
||||
@@ -153,7 +147,7 @@ watch(
|
||||
allTasks,
|
||||
() => {
|
||||
tasks.value = [...allTasks.value]
|
||||
if (projectId < 0) {
|
||||
if (props.projectId < 0) {
|
||||
return
|
||||
}
|
||||
const tasksById = {}
|
||||
@@ -223,6 +217,13 @@ function updateTaskList(task: ITask) {
|
||||
}
|
||||
|
||||
function updateTasks(updatedTask: ITask) {
|
||||
if (props.projectId < 0) {
|
||||
// In the case of a filter, we'll reload the filter in the background to avoid tasks which do
|
||||
// not match the filter show up here
|
||||
loadTasks(false)
|
||||
return
|
||||
}
|
||||
|
||||
for (const t in tasks.value) {
|
||||
if (tasks.value[t].id === updatedTask.id) {
|
||||
tasks.value[t] = updatedTask
|
||||
@@ -242,7 +243,7 @@ async function saveTaskPosition(e) {
|
||||
|
||||
await taskPositionService.value.update(new TaskPositionModel({
|
||||
position,
|
||||
projectViewId: viewId,
|
||||
projectViewId: props.viewId,
|
||||
taskId: task.id,
|
||||
}))
|
||||
tasks.value[e.newIndex] = {
|
||||
@@ -311,7 +312,7 @@ function prepareFiltersAndLoadTasks() {
|
||||
|
||||
.list-view {
|
||||
padding-bottom: 1rem;
|
||||
|
||||
|
||||
:deep(.card) {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -289,11 +289,9 @@ import type {ITask} from '@/modelTypes/ITask'
|
||||
import type {IProject} from '@/modelTypes/IProject'
|
||||
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
|
||||
import type {IProjectView} from '@/modelTypes/IProjectView'
|
||||
import { camelCase } from 'change-case'
|
||||
|
||||
const {
|
||||
projectId,
|
||||
viewId,
|
||||
} = defineProps<{
|
||||
const props = defineProps<{
|
||||
projectId: IProject['id'],
|
||||
viewId: IProjectView['id'],
|
||||
}>()
|
||||
@@ -322,7 +320,7 @@ const SORT_BY_DEFAULT: SortBy = {
|
||||
const activeColumns = useStorage('tableViewColumns', {...ACTIVE_COLUMNS_DEFAULT})
|
||||
const sortBy = useStorage<SortBy>('tableViewSortBy', {...SORT_BY_DEFAULT})
|
||||
|
||||
const taskList = useTaskList(() => projectId, () => viewId, sortBy.value)
|
||||
const taskList = useTaskList(() => props.projectId, () => props.viewId, sortBy.value)
|
||||
|
||||
const {
|
||||
loading,
|
||||
@@ -358,7 +356,7 @@ function sort(property: keyof SortBy) {
|
||||
|
||||
function setActiveColumnsSortParam() {
|
||||
sortByParam.value = Object.keys(sortBy.value)
|
||||
.filter(prop => activeColumns.value[prop])
|
||||
.filter(prop => activeColumns.value[camelCase(prop)])
|
||||
.reduce((obj, key) => {
|
||||
obj[key] = sortBy.value[key]
|
||||
return obj
|
||||
|
||||
@@ -8,17 +8,19 @@ import {useLabelStore} from '@/stores/labels'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import FilterInputDocs from '@/components/project/partials/FilterInputDocs.vue'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
loading = false,
|
||||
showSaveButtons = false,
|
||||
} = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: IProjectView,
|
||||
loading?: bool,
|
||||
showSaveButtons?: bool,
|
||||
}>()
|
||||
loading?: boolean,
|
||||
showSaveButtons?: boolean,
|
||||
}>(), {
|
||||
loading: false,
|
||||
showSaveButtons: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue', 'cancel'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: IProjectView],
|
||||
'cancel': [],
|
||||
}>()
|
||||
|
||||
const view = ref<IProjectView>()
|
||||
|
||||
@@ -26,16 +28,16 @@ const labelStore = useLabelStore()
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
onBeforeMount(() => {
|
||||
const transform = filterString => transformFilterStringFromApi(
|
||||
const transform = (filterString: string) => transformFilterStringFromApi(
|
||||
filterString,
|
||||
labelId => labelStore.getLabelById(labelId)?.title,
|
||||
labelId => labelStore.getLabelById(labelId)?.title || null,
|
||||
projectId => projectStore.projects[projectId]?.title || null,
|
||||
)
|
||||
|
||||
const transformed = {
|
||||
...modelValue,
|
||||
filter: transform(modelValue.filter),
|
||||
bucketConfiguration: modelValue.bucketConfiguration.map(bc => ({
|
||||
...props.modelValue,
|
||||
filter: transform(props.modelValue.filter),
|
||||
bucketConfiguration: props.modelValue.bucketConfiguration.map(bc => ({
|
||||
title: bc.title,
|
||||
filter: transform(bc.filter),
|
||||
})),
|
||||
@@ -47,9 +49,9 @@ onBeforeMount(() => {
|
||||
})
|
||||
|
||||
function save() {
|
||||
const transformFilter = filterQuery => transformFilterStringForApi(
|
||||
const transformFilter = (filterQuery: string) => transformFilterStringForApi(
|
||||
filterQuery,
|
||||
labelTitle => labelStore.filterLabelsByQuery([], labelTitle)[0]?.id || null,
|
||||
labelTitle => labelStore.getLabelByExactTitle(labelTitle)?.id || null,
|
||||
projectTitle => {
|
||||
const found = projectStore.findProjectByExactname(projectTitle)
|
||||
return found?.id || null
|
||||
@@ -73,7 +75,7 @@ function validateTitle() {
|
||||
}
|
||||
|
||||
function handleBubbleSave() {
|
||||
if (showSaveButtons) {
|
||||
if (props.showSaveButtons) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -162,7 +162,6 @@ export default {name: 'UserTeamShare'}
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, computed, shallowReactive, type Ref} from 'vue'
|
||||
import type {PropType} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import UserProjectService from '@/services/userProject'
|
||||
@@ -192,23 +191,16 @@ import User from '@/components/misc/User.vue'
|
||||
|
||||
// FIXME: I think this whole thing can now only manage user/team sharing for projects? Maybe remove a little generalization?
|
||||
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<'project'>,
|
||||
default: '',
|
||||
},
|
||||
shareType: {
|
||||
type: String as PropType<'user' | 'team'>,
|
||||
default: '',
|
||||
},
|
||||
id: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
userIsAdmin: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
type: 'project',
|
||||
shareType: 'user' | 'team',
|
||||
id: number,
|
||||
userIsAdmin?: boolean
|
||||
}>(), {
|
||||
type: '',
|
||||
shareType: '',
|
||||
id: 0,
|
||||
userIsAdmin: false,
|
||||
})
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
@@ -51,6 +51,7 @@
|
||||
import {computed, ref} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import {useElementHover} from '@vueuse/core'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
import {RELATION_KIND} from '@/types/IRelationKind'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
@@ -66,6 +67,8 @@ import {useAuthStore} from '@/stores/auth'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
|
||||
import {useAutoHeightTextarea} from '@/composables/useAutoHeightTextarea'
|
||||
import TaskService from '@/services/task'
|
||||
import TaskModel from '@/models/task'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
defaultPosition?: number,
|
||||
@@ -81,6 +84,7 @@ const {textarea: newTaskInput} = useAutoHeightTextarea(newTaskTitle)
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const authStore = useAuthStore()
|
||||
const taskStore = useTaskStore()
|
||||
const router = useRouter()
|
||||
|
||||
// enable only if we don't have a modal
|
||||
// onStartTyping(() => {
|
||||
@@ -129,21 +133,55 @@ async function addTask() {
|
||||
const allLabels = tasksToCreate.map(({title}) => getLabelsFromPrefix(title, authStore.settings.frontendSettings.quickAddMagicMode) ?? [])
|
||||
await taskStore.ensureLabelsExist(allLabels.flat())
|
||||
|
||||
const newTasks = tasksToCreate.map(async ({title, project}) => {
|
||||
const taskCollectionService = new TaskService()
|
||||
const projectIndices = new Map<number, number>()
|
||||
|
||||
let currentProjectId = authStore.settings.defaultProjectId
|
||||
if (typeof router.currentRoute.value.params.projectId !== 'undefined') {
|
||||
currentProjectId = Number(router.currentRoute.value.params.projectId)
|
||||
}
|
||||
|
||||
// Create a map of project indices before creating tasks
|
||||
if (tasksToCreate.length > 1) {
|
||||
for (const {project} of tasksToCreate) {
|
||||
const projectId = project !== null
|
||||
? await taskStore.findProjectId({project, projectId: 0})
|
||||
: currentProjectId
|
||||
|
||||
if (!projectIndices.has(projectId)) {
|
||||
const newestTask = await taskCollectionService.getAll(new TaskModel({}), {
|
||||
sort_by: ['id'],
|
||||
order_by: ['desc'],
|
||||
per_page: 1,
|
||||
filter: `project_id = ${projectId}`,
|
||||
})
|
||||
projectIndices.set(projectId, newestTask[0]?.index || 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const newTasks = tasksToCreate.map(async ({title, project}, index) => {
|
||||
if (title === '') {
|
||||
return
|
||||
}
|
||||
|
||||
// If the task has a project specified, make sure to use it
|
||||
let projectId = null
|
||||
if (project !== null) {
|
||||
projectId = await taskStore.findProjectId({project, projectId: 0})
|
||||
const projectId = project !== null
|
||||
? await taskStore.findProjectId({project, projectId: 0})
|
||||
: currentProjectId
|
||||
|
||||
// Calculate new index for this task per project
|
||||
let taskIndex: number | undefined
|
||||
if (tasksToCreate.length > 1) {
|
||||
const lastIndex = projectIndices.get(projectId)
|
||||
taskIndex = lastIndex + index + 1
|
||||
}
|
||||
|
||||
const task = await taskStore.createNewTask({
|
||||
title,
|
||||
projectId: projectId || authStore.settings.defaultProjectId,
|
||||
position: props.defaultPosition,
|
||||
index: taskIndex,
|
||||
})
|
||||
createdTasks[title] = task
|
||||
return task
|
||||
|
||||
@@ -194,15 +194,17 @@ import {useTaskStore} from '@/stores/tasks'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
import FilePreview from '@/components/tasks/partials/FilePreview.vue'
|
||||
|
||||
const {
|
||||
task,
|
||||
editEnabled = true,
|
||||
} = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
task: ITask,
|
||||
editEnabled: boolean,
|
||||
}>()
|
||||
}>(), {
|
||||
editEnabled: true,
|
||||
})
|
||||
|
||||
// FIXME: this should go through the store
|
||||
const emit = defineEmits(['taskChanged'])
|
||||
const emit = defineEmits<{
|
||||
'taskChanged': [ITask],
|
||||
}>()
|
||||
const taskStore = useTaskStore()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
@@ -238,7 +240,7 @@ function uploadNewAttachment() {
|
||||
}
|
||||
|
||||
function uploadFilesToTask(files: File[] | FileList) {
|
||||
uploadFiles(attachmentService, task.id, files)
|
||||
uploadFiles(attachmentService, props.task.id, files)
|
||||
}
|
||||
|
||||
const attachmentToDelete = ref<IAttachment | null>(null)
|
||||
@@ -279,11 +281,11 @@ function canPreview(attachment: IAttachment): boolean {
|
||||
const copy = useCopyToClipboard()
|
||||
|
||||
function copyUrl(attachment: IAttachment) {
|
||||
copy(generateAttachmentUrl(task.id, attachment.id))
|
||||
copy(generateAttachmentUrl(props.task.id, attachment.id))
|
||||
}
|
||||
|
||||
async function setCoverImage(attachment: IAttachment | null) {
|
||||
const updatedTask = await taskStore.setCoverImage(task, attachment)
|
||||
const updatedTask = await taskStore.setCoverImage(props.task, attachment)
|
||||
emit('taskChanged', updatedTask)
|
||||
success({message: t('task.attachment.successfullyChangedCoverImage')})
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="enabled"
|
||||
class="content details"
|
||||
ref="commentsRef"
|
||||
class="content details comments-container"
|
||||
>
|
||||
<h3
|
||||
v-if="canWrite || comments.length > 0"
|
||||
@@ -15,7 +16,7 @@
|
||||
<div class="comments">
|
||||
<span
|
||||
v-if="taskCommentService.loading && saving === null && !creating"
|
||||
class="is-inline-flex is-align-items-center"
|
||||
class="is-flex is-align-items-center my-4 ml-2"
|
||||
>
|
||||
<span class="loader is-inline-block mr-2" />
|
||||
{{ $t('task.comment.loading') }}
|
||||
@@ -107,6 +108,14 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<PaginationEmit
|
||||
v-if="taskCommentService.totalPages > 1"
|
||||
:total-pages="taskCommentService.totalPages"
|
||||
:current-page="currentPage"
|
||||
@pageChanged="changePage"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="canWrite"
|
||||
class="media comment d-print-none"
|
||||
@@ -160,6 +169,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<Modal
|
||||
:enabled="showDeleteModal"
|
||||
@close="showDeleteModal = false"
|
||||
@@ -185,6 +195,7 @@ import {useI18n} from 'vue-i18n'
|
||||
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import Editor from '@/components/input/AsyncEditor'
|
||||
import PaginationEmit from '@/components/misc/PaginationEmit.vue'
|
||||
|
||||
import TaskCommentService from '@/services/taskComment'
|
||||
import TaskCommentModel from '@/models/taskComment'
|
||||
@@ -200,15 +211,11 @@ import {useConfigStore} from '@/stores/config'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import Reactions from '@/components/input/Reactions.vue'
|
||||
|
||||
const props = defineProps({
|
||||
taskId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
canWrite: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
taskId: number,
|
||||
canWrite?: boolean
|
||||
}>(), {
|
||||
canWrite: true,
|
||||
})
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
@@ -246,6 +253,12 @@ const actions = computed(() => {
|
||||
])))
|
||||
})
|
||||
|
||||
const frontendUrl = computed(() => configStore.frontendUrl)
|
||||
|
||||
const currentPage = ref(1)
|
||||
|
||||
const commentsRef = ref<HTMLElement | null>(null)
|
||||
|
||||
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
|
||||
|
||||
const uploadPromises: Promise<string>[] = []
|
||||
@@ -271,12 +284,21 @@ async function loadComments(taskId: ITask['id']) {
|
||||
newComment.taskId = taskId
|
||||
commentEdit.taskId = taskId
|
||||
commentToDelete.taskId = taskId
|
||||
comments.value = await taskCommentService.getAll({taskId})
|
||||
comments.value = await taskCommentService.getAll({taskId}, {}, currentPage.value)
|
||||
}
|
||||
|
||||
async function changePage(page: number) {
|
||||
commentsRef.value?.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' })
|
||||
currentPage.value = page
|
||||
await loadComments(props.taskId)
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.taskId,
|
||||
loadComments,
|
||||
() => {
|
||||
currentPage.value = 1 // Reset to first page when task changes
|
||||
loadComments(props.taskId)
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
@@ -408,4 +430,8 @@ async function deleteComment(commentToDelete: ITaskComment) {
|
||||
.media-content {
|
||||
width: calc(100% - 48px - 2rem);
|
||||
}
|
||||
|
||||
.comments-container {
|
||||
scroll-margin-top: 4rem;
|
||||
}
|
||||
</style>
|
||||
@@ -9,10 +9,9 @@
|
||||
<script setup lang="ts">
|
||||
import {formatISO, formatDateLong, formatDateSince} from '@/helpers/time/formatDate'
|
||||
|
||||
defineProps({
|
||||
date: {
|
||||
type: Date,
|
||||
default: 0,
|
||||
},
|
||||
withDefaults(defineProps<{
|
||||
date?: Date
|
||||
}>(), {
|
||||
date: 0,
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -46,13 +46,13 @@ import TaskService from '@/services/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import { getFlatpickrLanguage } from '@/helpers/flatpickrLanguage'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
const props = defineProps<{
|
||||
modelValue: ITask,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ITask]
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
@@ -65,7 +65,7 @@ const lastValue = ref<Date | null>()
|
||||
const changeInterval = ref<ReturnType<typeof setInterval>>()
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
task.value = { ...value }
|
||||
dueDate.value = value.dueDate
|
||||
@@ -135,18 +135,8 @@ async function updateDueDate() {
|
||||
$defer-task-max-width: 350px + 100px;
|
||||
|
||||
.defer-task {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
max-width: $defer-task-max-width;
|
||||
border-radius: $radius;
|
||||
border: 1px solid var(--grey-200);
|
||||
padding: 1rem;
|
||||
margin: 1rem;
|
||||
background: var(--white);
|
||||
color: var(--text);
|
||||
cursor: default;
|
||||
z-index: 10;
|
||||
box-shadow: var(--shadow-lg);
|
||||
|
||||
@media screen and (max-width: ($defer-task-max-width)) {
|
||||
left: .5rem;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, shallowReactive, watch, nextTick, type PropType} from 'vue'
|
||||
import {ref, shallowReactive, watch, nextTick} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import User from '@/components/misc/User.vue'
|
||||
@@ -46,25 +46,19 @@ import type {IUser} from '@/modelTypes/IUser'
|
||||
import {getDisplayName} from '@/models/user'
|
||||
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
|
||||
|
||||
const props = defineProps({
|
||||
taskId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
projectId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
modelValue: {
|
||||
type: Array as PropType<IUser[]>,
|
||||
default: () => [],
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: IUser[] | undefined,
|
||||
taskId: number,
|
||||
projectId: number,
|
||||
disabled?: boolean,
|
||||
}>(), {
|
||||
modelValue: () => [],
|
||||
disabled: false,
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: IUser[] | undefined],
|
||||
}>()
|
||||
|
||||
const taskStore = useTaskStore()
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
:create-placeholder="$t('task.label.createPlaceholder')"
|
||||
:search-delay="10"
|
||||
:close-after-select="false"
|
||||
:disabled="disabled"
|
||||
@search="findLabel"
|
||||
@select="addLabel"
|
||||
@create="createAndAddLabel"
|
||||
@@ -21,6 +22,7 @@
|
||||
>
|
||||
<span>{{ label.title }}</span>
|
||||
<BaseButton
|
||||
v-if="!disabled"
|
||||
v-cy="'taskDetail.removeLabel'"
|
||||
class="delete is-small"
|
||||
@click="removeLabel(label)"
|
||||
@@ -46,7 +48,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {type PropType, ref, computed, shallowReactive, watch} from 'vue'
|
||||
import {ref, computed, shallowReactive, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import LabelModel from '@/models/label'
|
||||
@@ -60,27 +62,21 @@ import {useLabelStore} from '@/stores/labels'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {getRandomColorHex} from '@/helpers/color/randomColor'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Array as PropType<ILabel[]>,
|
||||
default: () => [],
|
||||
},
|
||||
taskId: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
creatable: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: ILabel[] | undefined
|
||||
taskId?: number
|
||||
disabled?: boolean
|
||||
creatable?: boolean
|
||||
}>(), {
|
||||
modelValue: () => [],
|
||||
taskId: 0,
|
||||
disabled: false,
|
||||
creatable: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [labels: ILabel[]],
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
|
||||
@@ -19,17 +19,14 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {type PropType, ref, shallowReactive, watchEffect} from 'vue'
|
||||
import {ref, shallowReactive, watchEffect} from 'vue'
|
||||
import AttachmentService from '@/services/attachment'
|
||||
import type {IAttachment} from '@/modelTypes/IAttachment'
|
||||
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<IAttachment>,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
const props = defineProps<{
|
||||
modelValue?: IAttachment
|
||||
}>()
|
||||
|
||||
const attachmentService = shallowReactive(new AttachmentService())
|
||||
const blobUrl = ref<string | undefined>(undefined)
|
||||
|
||||
@@ -1,20 +1,27 @@
|
||||
<template>
|
||||
<div class="heading">
|
||||
<div class="flex is-align-items-center">
|
||||
<BaseButton @click="copyUrl">
|
||||
<h1 class="title task-id">
|
||||
{{ textIdentifier }}
|
||||
</h1>
|
||||
</BaseButton>
|
||||
<div class="tw-flex tw-items-center md:tw-items-stretch tw-flex-col tw-gap-1 task-properties">
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
/>
|
||||
<BaseButton @click="copyUrl">
|
||||
<h1 class="title task-id">
|
||||
{{ textIdentifier }}
|
||||
</h1>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<Done
|
||||
class="heading__done"
|
||||
:is-done="task.done"
|
||||
/>
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
class="ml-2"
|
||||
/>
|
||||
<BaseButton
|
||||
v-if="hasClose"
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon icon="times" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
<h1
|
||||
class="title input"
|
||||
@@ -26,6 +33,13 @@
|
||||
>
|
||||
{{ task.title.trim() }}
|
||||
</h1>
|
||||
<BaseButton
|
||||
v-if="hasClose"
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon icon="times" />
|
||||
</BaseButton>
|
||||
<CustomTransition name="fade">
|
||||
<span
|
||||
v-if="loading && saving"
|
||||
@@ -49,7 +63,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, computed, type PropType} from 'vue'
|
||||
import {ref, computed} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
@@ -63,18 +77,19 @@ import {useTaskStore} from '@/stores/tasks'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {getHexColor, getTaskIdentifier} from '@/models/task'
|
||||
|
||||
const props = defineProps({
|
||||
task: {
|
||||
type: Object as PropType<ITask>,
|
||||
required: true,
|
||||
},
|
||||
canWrite: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
task: ITask,
|
||||
canWrite: boolean | undefined,
|
||||
hasClose: boolean | undefined,
|
||||
}>(), {
|
||||
canWrite: false,
|
||||
hasClose: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:task'])
|
||||
const emit = defineEmits<{
|
||||
'update:task': [task: ITask],
|
||||
'close': [],
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const copy = useCopyToClipboard()
|
||||
@@ -140,14 +155,11 @@ async function save(title: string) {
|
||||
.title.input {
|
||||
// 1.8rem is the font-size, 1.125 is the line-height, .3rem padding everywhere, 1px border around the whole thing.
|
||||
min-height: calc(1.8rem * 1.125 + .6rem + 2px);
|
||||
margin-right: 0;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin: 0 -.3rem .5rem -.3rem; // the title has 0.3rem padding - this make the text inside of it align with the rest
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) and (max-width: #{$desktop + $close-button-min-space}) {
|
||||
width: calc(100% - 6.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
.title.task-id {
|
||||
@@ -155,12 +167,39 @@ async function save(title: string) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.heading__done {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.color-bubble {
|
||||
height: .75rem;
|
||||
width: .75rem;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 2rem;
|
||||
margin-left: 0.5rem;
|
||||
line-height: 1;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: #{$desktop + 1px}) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-properties .close {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
top: 1.1rem;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.task-properties {
|
||||
@media screen and (max-width: $tablet) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@
|
||||
v-if="coverImageBlobUrl"
|
||||
:src="coverImageBlobUrl"
|
||||
alt=""
|
||||
class="cover-image"
|
||||
class="tw-w-full"
|
||||
>
|
||||
<div class="p-2">
|
||||
<span class="task-id">
|
||||
@@ -111,23 +111,21 @@ import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatD
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {playPopSound} from '@/helpers/playPop'
|
||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
|
||||
const {
|
||||
task,
|
||||
loading = false,
|
||||
} = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
task: ITask,
|
||||
loading: boolean,
|
||||
}>()
|
||||
}>(), {
|
||||
loading: false,
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
const loadingInternal = ref(false)
|
||||
|
||||
const color = computed(() => getHexColor(task.hexColor))
|
||||
const color = computed(() => getHexColor(props.task.hexColor))
|
||||
|
||||
async function toggleTaskDone(task: ITask) {
|
||||
loadingInternal.value = true
|
||||
@@ -137,7 +135,7 @@ async function toggleTaskDone(task: ITask) {
|
||||
done: !task.done,
|
||||
})
|
||||
|
||||
if (updatedTask.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
|
||||
if (updatedTask.done) {
|
||||
playPopSound()
|
||||
}
|
||||
} finally {
|
||||
@@ -148,7 +146,7 @@ async function toggleTaskDone(task: ITask) {
|
||||
function openTaskDetail() {
|
||||
router.push({
|
||||
name: 'task.detail',
|
||||
params: {id: task.id},
|
||||
params: {id: props.task.id},
|
||||
state: {backdropView: router.currentRoute.value.fullPath},
|
||||
})
|
||||
}
|
||||
@@ -156,12 +154,12 @@ function openTaskDetail() {
|
||||
const coverImageBlobUrl = ref<string | null>(null)
|
||||
|
||||
async function maybeDownloadCoverImage() {
|
||||
if (!task.coverImageAttachmentId) {
|
||||
if (!props.task.coverImageAttachmentId) {
|
||||
coverImageBlobUrl.value = null
|
||||
return
|
||||
}
|
||||
|
||||
const attachment = task.attachments.find(a => a.id === task.coverImageAttachmentId)
|
||||
const attachment = props.task.attachments.find(a => a.id === props.task.coverImageAttachmentId)
|
||||
if (!attachment || !SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))) {
|
||||
return
|
||||
}
|
||||
@@ -171,7 +169,7 @@ async function maybeDownloadCoverImage() {
|
||||
}
|
||||
|
||||
watch(
|
||||
() => task.coverImageAttachmentId,
|
||||
() => props.task.coverImageAttachmentId,
|
||||
maybeDownloadCoverImage,
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
@@ -4,63 +4,25 @@
|
||||
v-model.number="percentDone"
|
||||
:disabled="disabled || undefined"
|
||||
>
|
||||
<option value="0">
|
||||
0%
|
||||
</option>
|
||||
<option value="0.1">
|
||||
10%
|
||||
</option>
|
||||
<option value="0.2">
|
||||
20%
|
||||
</option>
|
||||
<option value="0.3">
|
||||
30%
|
||||
</option>
|
||||
<option value="0.4">
|
||||
40%
|
||||
</option>
|
||||
<option value="0.5">
|
||||
50%
|
||||
</option>
|
||||
<option value="0.6">
|
||||
60%
|
||||
</option>
|
||||
<option value="0.7">
|
||||
70%
|
||||
</option>
|
||||
<option value="0.8">
|
||||
80%
|
||||
</option>
|
||||
<option value="0.9">
|
||||
90%
|
||||
</option>
|
||||
<option value="1">
|
||||
100%
|
||||
<option
|
||||
v-for="option in PERCENT_OPTIONS"
|
||||
:key="option"
|
||||
:value="option"
|
||||
>
|
||||
{{ option * 100 }}%
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {computed} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withDefaults(defineProps<{
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const percentDone = defineModel<number>({ required: true })
|
||||
|
||||
const percentDone = computed({
|
||||
get: () => props.modelValue,
|
||||
set(percentDone) {
|
||||
emit('update:modelValue', percentDone)
|
||||
},
|
||||
})
|
||||
const PERCENT_OPTIONS = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1] as const
|
||||
</script>
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<template>
|
||||
<span
|
||||
v-if="!done && (showAll || priority >= priorities.HIGH)"
|
||||
:class="{'not-so-high': priority === priorities.HIGH, 'high-priority': priority >= priorities.HIGH}"
|
||||
:class="{
|
||||
'not-so-high': priority === priorities.HIGH,
|
||||
'high-priority': priority >= priorities.HIGH
|
||||
}"
|
||||
class="priority-label"
|
||||
>
|
||||
<span
|
||||
v-if="priority >= priorities.HIGH"
|
||||
class="icon"
|
||||
>
|
||||
<Icon icon="exclamation" />
|
||||
<Icon icon="exclamation-circle" />
|
||||
</span>
|
||||
<span>
|
||||
<template v-if="priority === priorities.UNSET">{{ $t('task.priority.unset') }}</template>
|
||||
@@ -18,47 +21,36 @@
|
||||
<template v-if="priority === priorities.URGENT">{{ $t('task.priority.urgent') }}</template>
|
||||
<template v-if="priority === priorities.DO_NOW">{{ $t('task.priority.doNow') }}</template>
|
||||
</span>
|
||||
<span
|
||||
v-if="priority === priorities.DO_NOW"
|
||||
class="icon pr-0"
|
||||
>
|
||||
<Icon icon="exclamation" />
|
||||
</span>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {PRIORITIES as priorities} from '@/constants/priorities'
|
||||
|
||||
defineProps({
|
||||
priority: {
|
||||
default: 0,
|
||||
type: Number,
|
||||
},
|
||||
showAll: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
done: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withDefaults(defineProps<{
|
||||
priority: number,
|
||||
showAll?: boolean,
|
||||
done?: boolean
|
||||
}>(), {
|
||||
priority: priorities.UNSET,
|
||||
showAll: false,
|
||||
done: false,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
span.high-priority {
|
||||
.high-priority {
|
||||
color: var(--danger);
|
||||
width: auto !important; // To override the width set in tasks
|
||||
}
|
||||
|
||||
.icon {
|
||||
vertical-align: top;
|
||||
width: auto !important;
|
||||
padding: 0 .5rem;
|
||||
}
|
||||
.not-so-high {
|
||||
color: var(--warning);
|
||||
}
|
||||
|
||||
&.not-so-high {
|
||||
color: var(--warning);
|
||||
}
|
||||
.icon {
|
||||
vertical-align: top;
|
||||
width: auto !important;
|
||||
padding-right: .5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -3,7 +3,6 @@
|
||||
<select
|
||||
v-model="priority"
|
||||
:disabled="disabled || undefined"
|
||||
@change="updateData"
|
||||
>
|
||||
<option :value="PRIORITIES.UNSET">
|
||||
{{ $t('task.priority.unset') }}
|
||||
@@ -28,32 +27,17 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch} from 'vue'
|
||||
import {PRIORITIES} from '@/constants/priorities'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
withDefaults(defineProps<{
|
||||
disabled?: boolean
|
||||
}>(), {
|
||||
disabled: false,
|
||||
})
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
|
||||
const priority = ref(0)
|
||||
const priority = defineModel<number>({
|
||||
required: true,
|
||||
default: 0,
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
priority.value = value
|
||||
},
|
||||
{immediate: true},
|
||||
)
|
||||
|
||||
function updateData() {
|
||||
emit('update:modelValue', priority.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -31,19 +31,23 @@ import {useProjectStore} from '@/stores/projects'
|
||||
import {getProjectTitle} from '@/helpers/getProjectTitle'
|
||||
|
||||
import ProjectModel from '@/models/project'
|
||||
|
||||
import Multiselect from '@/components/input/Multiselect.vue'
|
||||
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: IProject | null
|
||||
modelValue?: IProject
|
||||
savedFiltersOnly?: boolean
|
||||
filter?: (project: IProject) => boolean,
|
||||
}>(), {
|
||||
modelValue: () => new ProjectModel(),
|
||||
savedFiltersOnly: false,
|
||||
filter: () => true,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: IProject]
|
||||
}>()
|
||||
|
||||
const project: IProject = reactive(new ProjectModel())
|
||||
const project = reactive<IProject>(new ProjectModel())
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
@@ -55,6 +59,7 @@ watch(
|
||||
)
|
||||
|
||||
const projectStore = useProjectStore()
|
||||
|
||||
const foundProjects = ref<IProject[]>([])
|
||||
function findProjects(query: string) {
|
||||
if (query === '') {
|
||||
@@ -62,11 +67,13 @@ function findProjects(query: string) {
|
||||
}
|
||||
|
||||
if (props.savedFiltersOnly) {
|
||||
foundProjects.value = projectStore.searchSavedFilter(query)
|
||||
const found = projectStore.searchSavedFilter(query)
|
||||
foundProjects.value = found.filter(props.filter)
|
||||
return
|
||||
}
|
||||
|
||||
foundProjects.value = projectStore.searchProject(query)
|
||||
const found = projectStore.searchProject(query)
|
||||
foundProjects.value = found.filter(props.filter)
|
||||
}
|
||||
|
||||
function select(p: IProject | null) {
|
||||
|
||||
@@ -123,7 +123,7 @@
|
||||
<FancyCheckbox
|
||||
v-model="task.done"
|
||||
class="task-done-checkbox"
|
||||
@update:modelValue="toggleTaskDone(t)"
|
||||
@update:modelValue="toggleTaskDone(task)"
|
||||
/>
|
||||
<RouterLink
|
||||
:to="{ name: route.name as string, params: { id: task.id } }"
|
||||
@@ -204,7 +204,6 @@ import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
|
||||
import {error, success} from '@/message'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import {useProjectStore} from '@/stores/projects'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {playPopSound} from '@/helpers/playPop'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -380,7 +379,7 @@ async function createAndRelateTask(title: string) {
|
||||
async function toggleTaskDone(task: ITask) {
|
||||
await taskStore.update(task)
|
||||
|
||||
if (task.done && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
|
||||
if (task.done) {
|
||||
playPopSound()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<Popup @close="showFormSwitch = null">
|
||||
<Popup @update:open="showFormSwitch = null">
|
||||
<template #trigger="{toggle}">
|
||||
<SimpleButton
|
||||
v-tooltip="reminder.reminder && reminder.relativeTo !== null ? formatDateShort(reminder.reminder) : null"
|
||||
@@ -50,7 +50,7 @@
|
||||
/>
|
||||
|
||||
<DatepickerInline
|
||||
v-if="activeForm === 'absolute'"
|
||||
v-else-if="activeForm === 'absolute'"
|
||||
v-model="reminderDate"
|
||||
@update:modelValue="setReminderDateAndClose(close)"
|
||||
/>
|
||||
@@ -88,38 +88,38 @@ import Card from '@/components/misc/Card.vue'
|
||||
import SimpleButton from '@/components/input/SimpleButton.vue'
|
||||
import {useDebounceFn} from '@vueuse/core'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
clearAfterUpdate = false,
|
||||
defaultRelativeTo = REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE,
|
||||
} = defineProps<{
|
||||
modelValue?: ITaskReminder,
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: ITaskReminder | undefined,
|
||||
clearAfterUpdate?: boolean,
|
||||
defaultRelativeTo?: null | IReminderPeriodRelativeTo,
|
||||
}>()
|
||||
defaultRelativeTo?: IReminderPeriodRelativeTo | null,
|
||||
}>(), {
|
||||
modelValue: new TaskReminderModel() as ITaskReminder,
|
||||
clearAfterUpdate: false,
|
||||
defaultRelativeTo: REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ITaskReminder | undefined],
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const reminder = ref<ITaskReminder>(new TaskReminderModel())
|
||||
|
||||
const presets = computed<TaskReminderModel[]>(() => [
|
||||
{reminder: null, relativePeriod: 0, relativeTo: defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -2 * SECONDS_A_HOUR, relativeTo: defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY, relativeTo: defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 3, relativeTo: defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 7, relativeTo: defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 30, relativeTo: defaultRelativeTo},
|
||||
])
|
||||
const presets = computed(() => [
|
||||
{reminder: null, relativePeriod: 0, relativeTo: props.defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -2 * SECONDS_A_HOUR, relativeTo: props.defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY, relativeTo: props.defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 3, relativeTo: props.defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 7, relativeTo: props.defaultRelativeTo},
|
||||
{reminder: null, relativePeriod: -1 * SECONDS_A_DAY * 30, relativeTo: props.defaultRelativeTo},
|
||||
] as ITaskReminder[])
|
||||
const reminderDate = ref<Date | null>(null)
|
||||
|
||||
type availableForms = null | 'relative' | 'absolute'
|
||||
const showFormSwitch = ref<null | 'relative' | 'absolute'>(null)
|
||||
|
||||
const showFormSwitch = ref<availableForms>(null)
|
||||
|
||||
const activeForm = computed<availableForms>(() => {
|
||||
if (defaultRelativeTo === null) {
|
||||
const activeForm = computed(() => {
|
||||
if (props.defaultRelativeTo === null) {
|
||||
return 'absolute'
|
||||
}
|
||||
|
||||
@@ -127,7 +127,6 @@ const activeForm = computed<availableForms>(() => {
|
||||
})
|
||||
|
||||
const reminderText = computed(() => {
|
||||
|
||||
if (reminder.value.relativeTo !== null) {
|
||||
return formatReminder(reminder.value)
|
||||
}
|
||||
@@ -140,7 +139,7 @@ const reminderText = computed(() => {
|
||||
})
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
() => props.modelValue,
|
||||
(newReminder) => {
|
||||
if (newReminder) {
|
||||
reminder.value = newReminder
|
||||
@@ -160,12 +159,12 @@ watch(
|
||||
function updateData() {
|
||||
emit('update:modelValue', reminder.value)
|
||||
|
||||
if (clearAfterUpdate) {
|
||||
if (props.clearAfterUpdate) {
|
||||
reminder.value = new TaskReminderModel()
|
||||
}
|
||||
}
|
||||
|
||||
function setReminderDateAndClose(close) {
|
||||
function setReminderDateAndClose(close: () => void) {
|
||||
reminder.value.reminder = reminderDate.value === null
|
||||
? null
|
||||
: new Date(reminderDate.value)
|
||||
@@ -175,7 +174,7 @@ function setReminderDateAndClose(close) {
|
||||
}
|
||||
|
||||
|
||||
function setReminderFromPreset(preset, close) {
|
||||
function setReminderFromPreset(preset: ITaskReminder, close: () => void) {
|
||||
reminder.value = preset
|
||||
updateData()
|
||||
close()
|
||||
@@ -183,14 +182,14 @@ function setReminderFromPreset(preset, close) {
|
||||
|
||||
const updateDataAndMaybeClose = useDebounceFn(updateDataAndMaybeCloseNow, 500)
|
||||
|
||||
function updateDataAndMaybeCloseNow(close) {
|
||||
function updateDataAndMaybeCloseNow(close: () => void) {
|
||||
updateData()
|
||||
if (clearAfterUpdate) {
|
||||
if (props.clearAfterUpdate) {
|
||||
close()
|
||||
}
|
||||
}
|
||||
|
||||
function formatReminder(reminder: TaskReminderModel) {
|
||||
function formatReminder(reminder: ITaskReminder) {
|
||||
const period = secondsToPeriod(reminder.relativePeriod)
|
||||
|
||||
if (period.amount === 0) {
|
||||
@@ -248,6 +247,8 @@ function translateUnit(amount: number, unit: PeriodUnit): string {
|
||||
return t('time.units.weeks', amount)
|
||||
case 'years':
|
||||
return t('time.units.years', amount)
|
||||
default:
|
||||
throw new Error(`Unknown unit: ${unit}`)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -73,13 +73,13 @@ import TaskReminderModel from '@/models/taskReminder'
|
||||
import type {ITaskReminder} from '@/modelTypes/ITaskReminder'
|
||||
import {type IReminderPeriodRelativeTo, REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
} = defineProps<{
|
||||
modelValue?: ITaskReminder,
|
||||
const props = defineProps<{
|
||||
modelValue: ITaskReminder,
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [ITaskReminder]
|
||||
}>()
|
||||
|
||||
const reminder = ref<ITaskReminder>(new TaskReminderModel())
|
||||
|
||||
@@ -98,14 +98,17 @@ const period = ref<PeriodInput>({
|
||||
})
|
||||
|
||||
watch(
|
||||
() => modelValue,
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
const p = secondsToPeriod(value?.relativePeriod)
|
||||
period.value.durationUnit = p.unit
|
||||
period.value.duration = Math.abs(p.amount)
|
||||
period.value.relativeTo = value?.relativeTo || REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE
|
||||
},
|
||||
{immediate: true},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
watch(
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<div
|
||||
v-for="(r, index) in reminders"
|
||||
:key="index"
|
||||
:class="{ 'overdue': r.reminder < new Date() }"
|
||||
:class="{ 'overdue': r.reminder < now }"
|
||||
class="reminder-input"
|
||||
>
|
||||
<ReminderDetail
|
||||
@@ -40,21 +40,25 @@ import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import ReminderDetail from '@/components/tasks/partials/ReminderDetail.vue'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {REMINDER_PERIOD_RELATIVE_TO_TYPES} from '@/types/IReminderPeriodRelativeTo'
|
||||
import { useNow } from '@vueuse/core'
|
||||
|
||||
const {
|
||||
modelValue,
|
||||
disabled = false,
|
||||
} = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: ITask,
|
||||
disabled?: boolean,
|
||||
}>()
|
||||
}>(), {
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [ITask]
|
||||
}>()
|
||||
|
||||
const reminders = ref<ITaskReminder[]>([])
|
||||
|
||||
const now = useNow({interval: 1000})
|
||||
|
||||
watch(
|
||||
() => modelValue.reminders,
|
||||
() => props.modelValue.reminders,
|
||||
(newVal) => {
|
||||
reminders.value = newVal
|
||||
},
|
||||
@@ -62,19 +66,19 @@ watch(
|
||||
)
|
||||
|
||||
const defaultRelativeTo = computed(() => {
|
||||
if (typeof modelValue === 'undefined') {
|
||||
if (typeof props.modelValue === 'undefined') {
|
||||
return null
|
||||
}
|
||||
|
||||
if (modelValue?.dueDate) {
|
||||
if (props.modelValue?.dueDate) {
|
||||
return REMINDER_PERIOD_RELATIVE_TO_TYPES.DUEDATE
|
||||
}
|
||||
|
||||
if (modelValue.dueDate === null && modelValue.startDate !== null) {
|
||||
if (props.modelValue.dueDate === null && props.modelValue.startDate !== null) {
|
||||
return REMINDER_PERIOD_RELATIVE_TO_TYPES.STARTDATE
|
||||
}
|
||||
|
||||
if (modelValue.dueDate === null && modelValue.startDate === null && modelValue.endDate !== null) {
|
||||
if (props.modelValue.dueDate === null && props.modelValue.startDate === null && props.modelValue.endDate !== null) {
|
||||
return REMINDER_PERIOD_RELATIVE_TO_TYPES.ENDDATE
|
||||
}
|
||||
|
||||
@@ -83,7 +87,7 @@ const defaultRelativeTo = computed(() => {
|
||||
|
||||
function updateData() {
|
||||
emit('update:modelValue', {
|
||||
...modelValue,
|
||||
...props.modelValue,
|
||||
reminders: reminders.value,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, reactive, watch, type PropType} from 'vue'
|
||||
import {ref, reactive, watch} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import {error} from '@/message'
|
||||
@@ -104,19 +104,17 @@ import type {IRepeatAfter} from '@/types/IRepeatAfter'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import TaskModel from '@/models/task'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<ITask>,
|
||||
default: () => ({}),
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
modelValue: ITask | undefined,
|
||||
disabled: boolean
|
||||
}>(), {
|
||||
modelValue: () => {},
|
||||
disabled: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:modelValue'])
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: ITask | undefined],
|
||||
}>()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
@@ -134,7 +132,10 @@ watch(
|
||||
Object.assign(repeatAfter, value.repeatAfter)
|
||||
}
|
||||
},
|
||||
{immediate: true},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
function updateData() {
|
||||
|
||||
@@ -1,17 +1,17 @@
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
ref="taskContainerRef"
|
||||
:class="{'is-loading': taskService.loading}"
|
||||
class="task loader-container single-task"
|
||||
tabindex="-1"
|
||||
@mouseup.stop.self="openTaskDetail"
|
||||
@mousedown.stop.self="focusTaskLink"
|
||||
@click="openTaskDetail"
|
||||
@keyup.enter="openTaskDetail"
|
||||
>
|
||||
<FancyCheckbox
|
||||
v-model="task.done"
|
||||
:disabled="(isArchived || disabled) && !canMarkAsDone"
|
||||
@update:modelValue="markAsDone"
|
||||
@click.stop
|
||||
/>
|
||||
|
||||
<ColorBubble
|
||||
@@ -23,8 +23,6 @@
|
||||
<div
|
||||
:class="{ 'done': task.done, 'show-project': showProject && project}"
|
||||
class="tasktext"
|
||||
@mouseup.stop.self="openTaskDetail"
|
||||
@mousedown.stop.self="focusTaskLink"
|
||||
>
|
||||
<span>
|
||||
<RouterLink
|
||||
@@ -33,6 +31,7 @@
|
||||
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
|
||||
class="task-project mr-1"
|
||||
:class="{'mr-2': task.hexColor !== ''}"
|
||||
@click.stop
|
||||
>
|
||||
{{ project.title }}
|
||||
</RouterLink>
|
||||
@@ -42,15 +41,15 @@
|
||||
:color="getHexColor(task.hexColor)"
|
||||
class="mr-1"
|
||||
/>
|
||||
|
||||
|
||||
<PriorityLabel
|
||||
:priority="task.priority"
|
||||
:done="task.done"
|
||||
class="pr-2"
|
||||
/>
|
||||
|
||||
|
||||
<RouterLink
|
||||
ref="taskLink"
|
||||
ref="taskLinkRef"
|
||||
:to="taskDetailRoute"
|
||||
class="task-link"
|
||||
tabindex="-1"
|
||||
@@ -73,29 +72,33 @@
|
||||
:inline="true"
|
||||
/>
|
||||
|
||||
<!-- FIXME: use popup -->
|
||||
<BaseButton
|
||||
<Popup
|
||||
v-if="+new Date(task.dueDate) > 0"
|
||||
v-tooltip="formatDateLong(task.dueDate)"
|
||||
class="dueDate"
|
||||
@click.prevent.stop="showDefer = !showDefer"
|
||||
>
|
||||
<time
|
||||
:datetime="formatISO(task.dueDate)"
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
class="is-italic"
|
||||
:aria-expanded="showDefer ? 'true' : 'false'"
|
||||
>
|
||||
– {{ $t('task.detail.due', {at: dueDateFormatted}) }}
|
||||
</time>
|
||||
</BaseButton>
|
||||
<CustomTransition name="fade">
|
||||
<DeferTask
|
||||
v-if="+new Date(task.dueDate) > 0 && showDefer"
|
||||
ref="deferDueDate"
|
||||
v-model="task"
|
||||
/>
|
||||
</CustomTransition>
|
||||
<template #trigger="{toggle, isOpen}">
|
||||
<BaseButton
|
||||
v-tooltip="formatDateLong(task.dueDate)"
|
||||
class="dueDate"
|
||||
@click.prevent.stop="toggle()"
|
||||
>
|
||||
<time
|
||||
:datetime="formatISO(task.dueDate)"
|
||||
:class="{'overdue': task.dueDate <= new Date() && !task.done}"
|
||||
class="is-italic"
|
||||
:aria-expanded="isOpen ? 'true' : 'false'"
|
||||
>
|
||||
– {{ $t('task.detail.due', {at: dueDateFormatted}) }}
|
||||
</time>
|
||||
</BaseButton>
|
||||
</template>
|
||||
<template #content="{isOpen}">
|
||||
<DeferTask
|
||||
v-if="isOpen"
|
||||
v-model="task"
|
||||
/>
|
||||
</template>
|
||||
</Popup>
|
||||
|
||||
|
||||
<span>
|
||||
<span
|
||||
@@ -138,6 +141,7 @@
|
||||
v-tooltip="$t('task.detail.belongsToProject', {project: project.title})"
|
||||
:to="{ name: 'project.index', params: { projectId: task.projectId } }"
|
||||
class="task-project"
|
||||
@click.stop
|
||||
>
|
||||
{{ project.title }}
|
||||
</RouterLink>
|
||||
@@ -145,7 +149,7 @@
|
||||
<BaseButton
|
||||
:class="{'is-favorite': task.isFavorite}"
|
||||
class="favorite"
|
||||
@click="toggleFavorite"
|
||||
@click.stop="toggleFavorite"
|
||||
>
|
||||
<Icon
|
||||
v-if="task.isFavorite"
|
||||
@@ -176,7 +180,7 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {ref, watch, shallowReactive, onMounted, onBeforeUnmount, computed} from 'vue'
|
||||
import {ref, watch, shallowReactive, onMounted, computed} from 'vue'
|
||||
import {useI18n} from 'vue-i18n'
|
||||
|
||||
import TaskModel, {getHexColor} from '@/models/task'
|
||||
@@ -191,11 +195,10 @@ import ProgressBar from '@/components/misc/ProgressBar.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
import FancyCheckbox from '@/components/input/FancyCheckbox.vue'
|
||||
import ColorBubble from '@/components/misc/ColorBubble.vue'
|
||||
import CustomTransition from '@/components/misc/CustomTransition.vue'
|
||||
import Popup from '@/components/misc/Popup.vue'
|
||||
|
||||
import TaskService from '@/services/task'
|
||||
|
||||
import {closeWhenClickedOutside} from '@/helpers/closeWhenClickedOutside'
|
||||
import {formatDateSince, formatISO, formatDateLong} from '@/helpers/time/formatDate'
|
||||
import {success} from '@/message'
|
||||
|
||||
@@ -205,60 +208,54 @@ import {useTaskStore} from '@/stores/tasks'
|
||||
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
|
||||
import {useIntervalFn} from '@vueuse/core'
|
||||
import {playPopSound} from '@/helpers/playPop'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import {isEditorContentEmpty} from '@/helpers/editorContentEmpty'
|
||||
import {TASK_REPEAT_MODES} from '@/types/IRepeatMode'
|
||||
|
||||
const {
|
||||
theTask,
|
||||
isArchived = false,
|
||||
showProject = false,
|
||||
disabled = false,
|
||||
canMarkAsDone = true,
|
||||
allTasks = [],
|
||||
} = defineProps<{
|
||||
const props = withDefaults(defineProps<{
|
||||
theTask: ITask,
|
||||
isArchived?: boolean,
|
||||
showProject?: boolean,
|
||||
disabled?: boolean,
|
||||
canMarkAsDone?: boolean,
|
||||
allTasks?: ITask[],
|
||||
}>(), {
|
||||
isArchived: false,
|
||||
showProject: false,
|
||||
disabled: false,
|
||||
canMarkAsDone: true,
|
||||
allTasks: () => [],
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
'taskUpdated': [task: ITask],
|
||||
}>()
|
||||
|
||||
const emit = defineEmits(['taskUpdated'])
|
||||
|
||||
function getTaskById(taskId: number): ITask | undefined {
|
||||
if (typeof allTasks === 'undefined' || allTasks.length === 0) {
|
||||
if (typeof props.allTasks === 'undefined' || props.allTasks.length === 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return allTasks.find(t => t.id === taskId)
|
||||
return props.allTasks.find(t => t.id === taskId)
|
||||
}
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
|
||||
const taskService = shallowReactive(new TaskService())
|
||||
const task = ref<ITask>(new TaskModel())
|
||||
const showDefer = ref(false)
|
||||
|
||||
const isRepeating = computed(() => task.value.repeatAfter.amount > 0 || (task.value.repeatAfter.amount === 0 && task.value.repeatMode === TASK_REPEAT_MODES.REPEAT_MODE_MONTH))
|
||||
|
||||
watch(
|
||||
() => theTask,
|
||||
() => props.theTask,
|
||||
newVal => {
|
||||
task.value = newVal
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
deep: true,
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(() => {
|
||||
task.value = theTask
|
||||
document.addEventListener('click', hideDeferDueDatePopup)
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
document.removeEventListener('click', hideDeferDueDatePopup)
|
||||
})
|
||||
|
||||
const baseStore = useBaseStore()
|
||||
const projectStore = useProjectStore()
|
||||
const taskStore = useTaskStore()
|
||||
@@ -266,7 +263,7 @@ const taskStore = useTaskStore()
|
||||
const project = computed(() => projectStore.projects[task.value.projectId])
|
||||
const projectColor = computed(() => project.value ? project.value?.hexColor : '')
|
||||
|
||||
const showProjectSeparately = computed(() => !showProject && currentProject.value?.id !== task.value.projectId && project.value)
|
||||
const showProjectSeparately = computed(() => !props.showProject && currentProject.value?.id !== task.value.projectId && project.value)
|
||||
|
||||
const currentProject = computed(() => {
|
||||
return typeof baseStore.currentProject === 'undefined' ? {
|
||||
@@ -311,7 +308,7 @@ async function markAsDone(checked: boolean, wasReverted: boolean = false) {
|
||||
return
|
||||
}
|
||||
|
||||
if (checked && useAuthStore().settings.frontendSettings.playSoundWhenDone) {
|
||||
if (checked) {
|
||||
playPopSound()
|
||||
}
|
||||
emit('taskUpdated', newTask)
|
||||
@@ -347,35 +344,22 @@ async function toggleFavorite() {
|
||||
emit('taskUpdated', task.value)
|
||||
}
|
||||
|
||||
const deferDueDate = ref<typeof DeferTask | null>(null)
|
||||
|
||||
function hideDeferDueDatePopup(e) {
|
||||
if (!showDefer.value) {
|
||||
return
|
||||
}
|
||||
closeWhenClickedOutside(e, deferDueDate.value.$el, () => {
|
||||
showDefer.value = false
|
||||
})
|
||||
}
|
||||
|
||||
const taskLink = ref<HTMLElement | null>(null)
|
||||
const taskContainerRef = ref<HTMLElement | null>(null)
|
||||
const taskLinkRef = ref<HTMLElement | null>(null)
|
||||
|
||||
function hasTextSelected() {
|
||||
const isTextSelected = window.getSelection().toString()
|
||||
return !(typeof isTextSelected === 'undefined' || isTextSelected === '' || isTextSelected === '\n')
|
||||
}
|
||||
|
||||
function openTaskDetail() {
|
||||
if (!hasTextSelected()) {
|
||||
taskLink.value.$el.click()
|
||||
function openTaskDetail(event: MouseEvent | KeyboardEvent) {
|
||||
if (event.target instanceof HTMLElement) {
|
||||
const isInteractiveElement = event.target.closest('a, button, .favorite, [role="button"]')
|
||||
if (isInteractiveElement || hasTextSelected()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function focusTaskLink() {
|
||||
if (!hasTextSelected()) {
|
||||
taskContainerRef.value.focus()
|
||||
}
|
||||
taskLinkRef.value?.$el.click()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -394,7 +378,7 @@ function focusTaskLink() {
|
||||
background-color: var(--grey-100);
|
||||
}
|
||||
|
||||
&:focus-within, &:focus {
|
||||
&:has(*:focus-visible) {
|
||||
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
|
||||
|
||||
a.task-link {
|
||||
@@ -402,6 +386,16 @@ function focusTaskLink() {
|
||||
}
|
||||
}
|
||||
|
||||
@supports not selector(:focus-within) {
|
||||
:focus {
|
||||
box-shadow: 0 0 0 2px hsla(var(--primary-hsl), 0.5);
|
||||
|
||||
a.task-link {
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tasktext,
|
||||
&.tasktext {
|
||||
text-overflow: ellipsis;
|
||||
@@ -418,6 +412,15 @@ function focusTaskLink() {
|
||||
.dueDate {
|
||||
display: inline-block;
|
||||
margin-left: 5px;
|
||||
|
||||
&:focus-visible {
|
||||
box-shadow: none;
|
||||
|
||||
time {
|
||||
box-shadow: 0 0 0 1px hsla(var(--primary-hsl), 0.5);
|
||||
border-radius: 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.overdue {
|
||||
@@ -463,6 +466,7 @@ function focusTaskLink() {
|
||||
text-align: center;
|
||||
width: 27px;
|
||||
transition: opacity $transition, color $transition;
|
||||
border-radius: $radius;
|
||||
|
||||
&:hover {
|
||||
color: var(--warning);
|
||||
@@ -539,4 +543,17 @@ function focusTaskLink() {
|
||||
.subtask-nested {
|
||||
margin-left: 1.75rem;
|
||||
}
|
||||
|
||||
:deep(.popup) {
|
||||
border-radius: $radius;
|
||||
background-color: var(--white);
|
||||
box-shadow: var(--shadow-lg);
|
||||
color: var(--text);
|
||||
top: unset;
|
||||
|
||||
&.is-open {
|
||||
padding: 1rem;
|
||||
border: 1px solid var(--grey-200);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -105,8 +105,10 @@ export function useTaskList(
|
||||
const totalPages = computed(() => taskCollectionService.totalPages)
|
||||
|
||||
const tasks = ref<ITask[]>([])
|
||||
async function loadTasks() {
|
||||
tasks.value = []
|
||||
async function loadTasks(resetBeforeLoad: boolean = true) {
|
||||
if(resetBeforeLoad) {
|
||||
tasks.value = []
|
||||
}
|
||||
try {
|
||||
tasks.value = await taskCollectionService.getAll(...getAllTasksParams.value)
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
export const calculateItemPosition = (positionBefore: number | null, positionAfter: number | null): number => {
|
||||
export const calculateItemPosition = (
|
||||
positionBefore: number | null = null,
|
||||
positionAfter: number | null = null,
|
||||
): number => {
|
||||
if (positionBefore === null) {
|
||||
if (positionAfter === null) {
|
||||
return 0
|
||||
@@ -13,6 +16,6 @@ export const calculateItemPosition = (positionBefore: number | null, positionAft
|
||||
return positionBefore + Math.pow(2, 16)
|
||||
}
|
||||
|
||||
// If we have both a task before and after it, we acually calculate the position
|
||||
// If we have both a task before and after it, we actually calculate the position
|
||||
return positionBefore + (positionAfter - positionBefore) / 2
|
||||
}
|
||||
@@ -137,6 +137,36 @@ describe('Filter Transformation', () => {
|
||||
|
||||
expect(transformed).toBe('labels in 2 || labels in l')
|
||||
})
|
||||
|
||||
it('should correctly transform the cases of fields', () => {
|
||||
const transformed = transformFilterStringForApi(
|
||||
'startdate > now',
|
||||
nullTitleToIdResolver,
|
||||
nullTitleToIdResolver,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('start_date > now')
|
||||
})
|
||||
|
||||
it('should correctly resolve label when the label is called label', () => {
|
||||
const transformed = transformFilterStringForApi(
|
||||
'labels = label',
|
||||
(title: string) => 1,
|
||||
nullTitleToIdResolver,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('labels = 1')
|
||||
})
|
||||
|
||||
it('should correctly resolve project when the project is called project', () => {
|
||||
const transformed = transformFilterStringForApi(
|
||||
'project = project',
|
||||
nullTitleToIdResolver,
|
||||
(title: string) => 1,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('project = 1')
|
||||
})
|
||||
})
|
||||
|
||||
describe('To API', () => {
|
||||
@@ -189,6 +219,26 @@ describe('Filter Transformation', () => {
|
||||
expect(transformed).toBe('labels in lorem, ipsum')
|
||||
})
|
||||
|
||||
it('should not touch the label value when it is undefined', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'labels = one',
|
||||
(id: number) => undefined,
|
||||
nullIdToTitleResolver,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('labels = one')
|
||||
})
|
||||
|
||||
it('should not touch the label value when it is null', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'labels = one',
|
||||
(id: number) => null,
|
||||
nullIdToTitleResolver,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('labels = one')
|
||||
})
|
||||
|
||||
it('should correctly resolve projects', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'project = 1',
|
||||
@@ -218,6 +268,26 @@ describe('Filter Transformation', () => {
|
||||
|
||||
expect(transformed).toBe('project in lorem, ipsum')
|
||||
})
|
||||
|
||||
it('should not touch the project value when it is undefined', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'project = one',
|
||||
nullIdToTitleResolver,
|
||||
(id: number) => undefined,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('project = one')
|
||||
})
|
||||
|
||||
it('should not touch the project value when it is null', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
'project = one',
|
||||
nullIdToTitleResolver,
|
||||
(id: number) => null,
|
||||
)
|
||||
|
||||
expect(transformed).toBe('project = one')
|
||||
})
|
||||
|
||||
it('should transform the same attribute multiple times', () => {
|
||||
const transformed = transformFilterStringFromApi(
|
||||
|
||||
@@ -66,54 +66,45 @@ export function transformFilterStringForApi(
|
||||
labelResolver: (title: string) => number | null,
|
||||
projectResolver: (title: string) => number | null,
|
||||
): string {
|
||||
|
||||
filter = filter.trim()
|
||||
|
||||
if (filter.trim() === '') {
|
||||
if (filter === '') {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Transform labels to ids
|
||||
LABEL_FIELDS.forEach(field => {
|
||||
const pattern = getFilterFieldRegexPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
let keywords = [keyword.trim()]
|
||||
if (operator === 'in' || operator === '?=') {
|
||||
keywords = keyword.trim().split(',').map(k => k.trim())
|
||||
}
|
||||
|
||||
keywords.forEach(k => {
|
||||
const labelId = labelResolver(k)
|
||||
if (labelId !== null) {
|
||||
filter = filter.replace(k, String(labelId))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
AVAILABLE_FILTER_FIELDS.forEach(f => {
|
||||
filter = filter.replace(new RegExp(f, 'ig'), f)
|
||||
})
|
||||
// Transform projects to ids
|
||||
PROJECT_FIELDS.forEach(field => {
|
||||
const pattern = getFilterFieldRegexPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
// Transform labels and projects to ids
|
||||
function transformFieldToIds(
|
||||
fields: string[],
|
||||
resolver: (title: string) => number | null,
|
||||
filter: string,
|
||||
): string {
|
||||
fields.forEach(field => {
|
||||
const pattern = getFilterFieldRegexPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (!keyword) {
|
||||
continue
|
||||
}
|
||||
|
||||
let keywords = [keyword.trim()]
|
||||
if (operator === 'in' || operator === '?=') {
|
||||
keywords = keyword.trim().split(',').map(k => k.trim())
|
||||
}
|
||||
|
||||
|
||||
let replaced = keyword
|
||||
|
||||
keywords.forEach(k => {
|
||||
const projectId = projectResolver(k)
|
||||
if (projectId !== null) {
|
||||
replaced = replaced.replace(k, String(projectId))
|
||||
const id = resolver(k)
|
||||
if (id !== null) {
|
||||
replaced = replaced.replace(k, String(id))
|
||||
}
|
||||
})
|
||||
|
||||
@@ -122,8 +113,15 @@ export function transformFilterStringForApi(
|
||||
replaced +
|
||||
filter.substring(actualKeywordStart + keyword.length)
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
return filter
|
||||
}
|
||||
|
||||
// Transform labels to ids
|
||||
filter = transformFieldToIds(LABEL_FIELDS, labelResolver, filter)
|
||||
|
||||
// Transform projects to ids
|
||||
filter = transformFieldToIds(PROJECT_FIELDS, projectResolver, filter)
|
||||
|
||||
// Transform all attributes to snake case
|
||||
AVAILABLE_FILTER_FIELDS.forEach(f => {
|
||||
@@ -135,8 +133,8 @@ export function transformFilterStringForApi(
|
||||
|
||||
export function transformFilterStringFromApi(
|
||||
filter: string,
|
||||
labelResolver: (id: number) => string | null,
|
||||
projectResolver: (id: number) => string | null,
|
||||
labelResolver: (id: number) => string | null | undefined,
|
||||
projectResolver: (id: number) => string | null | undefined,
|
||||
): string {
|
||||
|
||||
if (filter.trim() === '') {
|
||||
@@ -148,53 +146,40 @@ export function transformFilterStringFromApi(
|
||||
filter = filter.replaceAll(snakeCase(f), f)
|
||||
})
|
||||
|
||||
// Function to transform fields to their titles
|
||||
function transformFieldsToTitles(
|
||||
fields: string[],
|
||||
resolver: (id: number) => string | null | undefined,
|
||||
) {
|
||||
fields.forEach(field => {
|
||||
const pattern = getFilterFieldRegexPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
let keywords = [keyword.trim()]
|
||||
if (operator === 'in' || operator === '?=') {
|
||||
keywords = keyword.trim().split(',').map(k => k.trim())
|
||||
}
|
||||
|
||||
keywords.forEach(k => {
|
||||
const title = resolver(parseInt(k))
|
||||
if (title) {
|
||||
filter = filter.replace(k, title)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Transform labels to their titles
|
||||
LABEL_FIELDS.forEach(field => {
|
||||
const pattern = getFilterFieldRegexPattern(field)
|
||||
transformFieldsToTitles(LABEL_FIELDS, labelResolver)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
let keywords = [keyword.trim()]
|
||||
if (operator === 'in' || operator === '?=') {
|
||||
keywords = keyword.trim().split(',').map(k => k.trim())
|
||||
}
|
||||
|
||||
keywords.forEach(k => {
|
||||
const labelTitle = labelResolver(parseInt(k))
|
||||
if (labelTitle !== null) {
|
||||
filter = filter.replace(k, labelTitle)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// Transform projects to ids
|
||||
PROJECT_FIELDS.forEach(field => {
|
||||
const pattern = getFilterFieldRegexPattern(field)
|
||||
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = pattern.exec(filter)) !== null) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const [matched, prefix, operator, space, keyword] = match
|
||||
if (keyword) {
|
||||
let keywords = [keyword.trim()]
|
||||
if (operator === 'in' || operator === '?=') {
|
||||
keywords = keyword.trim().split(',').map(k => k.trim())
|
||||
}
|
||||
|
||||
keywords.forEach(k => {
|
||||
const project = projectResolver(parseInt(k))
|
||||
if (project !== null) {
|
||||
filter = filter.replace(k, project)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
// Transform projects to their titles
|
||||
transformFieldsToTitles(PROJECT_FIELDS, projectResolver)
|
||||
|
||||
return filter
|
||||
}
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
|
||||
import popSoundFile from '@/assets/audio/pop.mp3'
|
||||
|
||||
export const playSoundWhenDoneKey = 'playSoundWhenTaskDone'
|
||||
|
||||
export function playPopSound() {
|
||||
const playSoundWhenDone = useAuthStore().settings.frontendSettings.playSoundWhenDone
|
||||
|
||||
if (!playSoundWhenDone)
|
||||
return
|
||||
|
||||
try {
|
||||
const popSound = new Audio(popSoundFile)
|
||||
popSound.play()
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
export function validatePassword(password: string, validateMinLength: boolean = true): string | true {
|
||||
if (password === '') {
|
||||
return 'user.auth.passwordRequired'
|
||||
}
|
||||
|
||||
if (validateMinLength && password.length < 8) {
|
||||
return 'user.auth.passwordNotMin'
|
||||
}
|
||||
|
||||
if (validateMinLength && password.length > 72) {
|
||||
return 'user.auth.passwordNotMax'
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export const SUPPORTED_LOCALES = {
|
||||
'sl-SI': 'Slovenščina',
|
||||
'pt-BR': 'Português Brasileiro',
|
||||
'hr-HR': 'Hrvatski',
|
||||
'uk-UA': 'українська',
|
||||
'uk-UA': 'Українська',
|
||||
// IMPORTANT: Also add new languages to useDayjsLanguageSync
|
||||
} as const
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user