Compare commits
377
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a974bf09b5 | ||
|
|
9e39ef9397 | ||
|
|
c54181eeda | ||
|
|
b3bf92b7ce | ||
|
|
05dd05216d | ||
|
|
cd0ab54d57 | ||
|
|
a7bd9dad24 | ||
|
|
10e245f6d0 | ||
|
|
010dd1ea22 | ||
|
|
65a2814b2a | ||
|
|
4dd9d5de67 | ||
|
|
ec902b051b | ||
|
|
4b2b8e3b83 | ||
|
|
bc52da4029 | ||
|
|
2123da49a3 | ||
|
|
c53a761396 | ||
|
|
c84b50b3ee | ||
|
|
c5b82fc591 | ||
|
|
1f00180af6 | ||
|
|
33e851f543 | ||
|
|
16d7386975 | ||
|
|
6d9bda8cc1 | ||
|
|
abf92e29fa | ||
|
|
f4d628550f | ||
|
|
1d352fcbf3 | ||
|
|
a1105434bf | ||
|
|
acc7c9f8f5 | ||
|
|
cc64ca6406 | ||
|
|
89f78cd369 | ||
|
|
14c3b13823 | ||
|
|
c2a7438814 | ||
|
|
8fe37fd900 | ||
|
|
88b4ab1768 | ||
|
|
c252c8f0cd | ||
|
|
410ff28b71 | ||
|
|
d98dd7ecbb | ||
|
|
fc73085876 | ||
|
|
c71f7334b4 | ||
|
|
6a5342bd49 | ||
|
|
b4ea1bb86a | ||
|
|
378d0ae9bb | ||
|
|
6f27e1401a | ||
|
|
bf5cafc03f | ||
|
|
f4a7326b68 | ||
|
|
1451f6e46f | ||
|
|
427eb2a618 | ||
|
|
e4b541e653 | ||
|
|
45ff5907e6 | ||
|
|
5a7c3927f3 | ||
|
|
f425d98b4d | ||
|
|
4c55016c1a | ||
|
|
1a8f12ac13 | ||
|
|
7b873ec31c | ||
|
|
8b77832af2 | ||
|
|
14710d0bba | ||
|
|
945f25b818 | ||
|
|
f53b93d98a | ||
|
|
97e030a1fc | ||
|
|
b708db7025 | ||
|
|
6bfe71b30e | ||
|
|
db196bdffe | ||
|
|
9428acd5e1 | ||
|
|
58a60fb4ae | ||
|
|
f4ca22a6ad | ||
|
|
7e38a9f442 | ||
|
|
a0a77818b1 | ||
|
|
bdccd633fe | ||
|
|
1f3eb8f2a3 | ||
|
|
8891ef347e | ||
|
|
3b8ccbad4f | ||
|
|
cc1b4bbd1b | ||
|
|
9dd95101f9 | ||
|
|
20724f6fb5 | ||
|
|
e1ea84fcd4 | ||
|
|
8d730543c3 | ||
|
|
a3d133bfee | ||
|
|
7b37ad0594 | ||
|
|
4aeb228c3a | ||
|
|
b52d6dbf59 | ||
|
|
76f7797e56 | ||
|
|
55dd7d2981 | ||
|
|
44a43b9f86 | ||
|
|
eb95caf757 | ||
|
|
1085a6583b | ||
|
|
e698ac5a34 | ||
|
|
020fea686d | ||
|
|
addf8523f4 | ||
|
|
044aa1c742 | ||
|
|
552e1ce796 | ||
|
|
c1246d73d4 | ||
|
|
85fa397a46 | ||
|
|
af47e43c34 | ||
|
|
2539f15dfb | ||
|
|
5c1b2846a1 | ||
|
|
af8100d262 | ||
|
|
63f85c97ae | ||
|
|
2b1e3ffd2a | ||
|
|
5f470afd7c | ||
|
|
37c6f94ba2 | ||
|
|
b42cd5786e | ||
|
|
5072ab3d72 | ||
|
|
4b5ec018f5 | ||
|
|
efe5c7b3cb | ||
|
|
ba0c32aacf | ||
|
|
b6e142e8d0 | ||
|
|
750d565477 | ||
|
|
2b8515b91d | ||
|
|
68636f27da | ||
|
|
cf94cc8cab | ||
|
|
669b7452cd | ||
|
|
1601c9098d | ||
|
|
f132b0e718 | ||
|
|
5d12986e3f | ||
|
|
74e0197dc6 | ||
|
|
2dd133fb4c | ||
|
|
02c1de55c4 | ||
|
|
cbc63c853d | ||
|
|
b5c4a3a80c | ||
|
|
a8ea5e11ab | ||
|
|
812430b9cf | ||
|
|
9d7b6d3d9a | ||
|
|
c2b116de70 | ||
|
|
22e594e253 | ||
|
|
0d9c03e0f2 | ||
|
|
72bb6609a1 | ||
|
|
b259072e6a | ||
|
|
ffcc48ec87 | ||
|
|
5aa0b6a0cf | ||
|
|
b60efbd259 | ||
|
|
34ac29fcce | ||
|
|
75f3e930cd | ||
|
|
4ff8815fe1 | ||
|
|
850ec7efb0 | ||
|
|
115d1c3618 | ||
|
|
6d26497bbb | ||
|
|
06305eb6b3 | ||
|
|
7bd84a845c | ||
|
|
165ee9e7f3 | ||
|
|
2c9becec10 | ||
|
|
1463bce24c | ||
|
|
5c56d07215 | ||
|
|
6fb314b326 | ||
|
|
9b01666ec6 | ||
|
|
14c862d284 | ||
|
|
bcfd72c645 | ||
|
|
499f66b7ae | ||
|
|
8e37d5cb76 | ||
|
|
57ba073874 | ||
|
|
329de3aab3 | ||
|
|
74a74b7ec7 | ||
|
|
2063da9eec | ||
|
|
7a7e97770c | ||
|
|
bea131cfd9 | ||
|
|
5a0d1f1dc6 | ||
|
|
b5035f9d2b | ||
|
|
07bd9de0c6 | ||
|
|
efde364224 | ||
|
|
fde1763eef | ||
|
|
429c7ca2c1 | ||
|
|
bf56311faa | ||
|
|
c38aaeb224 | ||
|
|
1b9139b9b0 | ||
|
|
13763e3540 | ||
|
|
56bab7485f | ||
|
|
8bfd0493b2 | ||
|
|
435cb2e7f7 | ||
|
|
9ed33f5c08 | ||
|
|
83669fd9e0 | ||
|
|
35578e0021 | ||
|
|
e281de30b5 | ||
|
|
ec2c2e74f0 | ||
|
|
b031c9772f | ||
|
|
f22420d502 | ||
|
|
ebfd5f54d2 | ||
|
|
28b4eaee31 | ||
|
|
b83448b7a6 | ||
|
|
eed783e42f | ||
|
|
1d624612ee | ||
|
|
9576f72d7c | ||
|
|
7efc4d1bc8 | ||
|
|
670e605275 | ||
|
|
871b3ce071 | ||
|
|
ad3c5fcee5 | ||
|
|
e2a87036e0 | ||
|
|
40bb86bee5 | ||
|
|
4a4ed2cf89 | ||
|
|
7751e124a7 | ||
|
|
6137d07138 | ||
|
|
66d316f0b5 | ||
|
|
bf2d56c9d4 | ||
|
|
413798b321 | ||
|
|
bf1c8c6119 | ||
|
|
49fa0a8468 | ||
|
|
784cd42a7a | ||
|
|
4f74be0247 | ||
|
|
7ef1e0a3e5 | ||
|
|
bc5fd380e5 | ||
|
|
bcd306b84d | ||
|
|
ad2870335d | ||
|
|
917748777f | ||
|
|
324c966dda | ||
|
|
0984821b41 | ||
|
|
e8be657d97 | ||
|
|
75ce261f74 | ||
|
|
5f9d0fe763 | ||
|
|
bbfd5270db | ||
|
|
1e632397d2 | ||
|
|
d35454c099 | ||
|
|
2a6ba7e7f0 | ||
|
|
1e523a1a39 | ||
|
|
fb91e73a3c | ||
|
|
ebd6139c1f | ||
|
|
6990be705c | ||
|
|
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 |
+13
-13
@@ -139,7 +139,7 @@ steps:
|
||||
event: [ push, tag, pull_request ]
|
||||
|
||||
- name: api-lint
|
||||
image: golangci/golangci-lint:v1.59.1
|
||||
image: golangci/golangci-lint:v1.61.0
|
||||
pull: always
|
||||
environment:
|
||||
GOPROXY: 'https://goproxy.kolaente.de'
|
||||
@@ -364,7 +364,7 @@ steps:
|
||||
- api-build
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -378,7 +378,7 @@ steps:
|
||||
# - restore-cache
|
||||
|
||||
- name: frontend-lint
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -390,7 +390,7 @@ steps:
|
||||
- frontend-dependencies
|
||||
|
||||
- name: frontend-build-prod
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -402,7 +402,7 @@ steps:
|
||||
- frontend-dependencies
|
||||
|
||||
- name: frontend-test-unit
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
commands:
|
||||
- cd frontend
|
||||
@@ -413,7 +413,7 @@ steps:
|
||||
|
||||
- name: frontend-typecheck
|
||||
failure: ignore
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -544,7 +544,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: frontend-dependencies
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -556,7 +556,7 @@ steps:
|
||||
- pnpm install --fetch-timeout 100000
|
||||
|
||||
- name: frontend-build
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -716,7 +716,7 @@ steps:
|
||||
|
||||
# Build os packages and push it to our bucket
|
||||
- name: build-os-packages-unstable
|
||||
image: goreleaser/nfpm:v2.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
|
||||
@@ -901,7 +901,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -962,7 +962,7 @@ steps:
|
||||
- git fetch --tags
|
||||
|
||||
- name: build
|
||||
image: node:20.14.0-alpine
|
||||
image: node:20.16.0-alpine
|
||||
pull: always
|
||||
environment:
|
||||
PNPM_CACHE_FOLDER: .cache/pnpm
|
||||
@@ -1352,6 +1352,6 @@ steps:
|
||||
- failure
|
||||
---
|
||||
kind: signature
|
||||
hmac: 3cc4b45fcbbab5ccee14e5532b2d35f5db54cde29a8f6e854b563462e3bbb509
|
||||
hmac: eff6e40dc763fa1395078daaabcbef2aaa74fe0a36818a87e89015dbc4e9dae4
|
||||
|
||||
...
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1153
-1
File diff suppressed because it is too large
Load Diff
+13
-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/ ./
|
||||
|
||||
@@ -24,7 +24,7 @@ COPY --from=frontendbuilder /build/dist ./frontend/dist
|
||||
|
||||
ARG TARGETOS TARGETARCH TARGETVARIANT
|
||||
|
||||
ENV GOPROXY https://goproxy.kolaente.de
|
||||
ENV GOPROXY=https://goproxy.kolaente.de
|
||||
RUN export PATH=$PATH:$GOPATH/bin && \
|
||||
mage build:clean && \
|
||||
mage release:xgo "${TARGETOS}/${TARGETARCH}/${TARGETVARIANT}"
|
||||
@@ -35,7 +35,14 @@ RUN export PATH=$PATH:$GOPATH/bin && \
|
||||
|
||||
# The actual image
|
||||
FROM scratch
|
||||
LABEL maintainer="maintainers@vikunja.io"
|
||||
|
||||
LABEL org.opencontainers.image.authors='maintainers@vikunja.io'
|
||||
LABEL org.opencontainers.image.url='https://vikunja.io'
|
||||
LABEL org.opencontainers.image.documentation='https://vikunja.io/docs'
|
||||
LABEL org.opencontainers.image.source='https://code.vikunja.io/vikunja'
|
||||
LABEL org.opencontainers.image.licenses='AGPLv3'
|
||||
LABEL org.opencontainers.image.title='Vikunja'
|
||||
|
||||
WORKDIR /app/vikunja
|
||||
ENTRYPOINT [ "/app/vikunja/vikunja" ]
|
||||
EXPOSE 3456
|
||||
|
||||
@@ -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
+39
-34
@@ -9,13 +9,13 @@ dependencies:
|
||||
specifier: 2.0.0
|
||||
version: 2.0.0
|
||||
express:
|
||||
specifier: 4.19.2
|
||||
version: 4.19.2
|
||||
specifier: 4.21.0
|
||||
version: 4.21.0
|
||||
|
||||
devDependencies:
|
||||
electron:
|
||||
specifier: 29.4.3
|
||||
version: 29.4.3
|
||||
specifier: 29.4.6
|
||||
version: 29.4.6
|
||||
electron-builder:
|
||||
specifier: 24.13.3
|
||||
version: 24.13.3(electron-builder-squirrel-windows@24.13.3)
|
||||
@@ -445,8 +445,8 @@ packages:
|
||||
resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==}
|
||||
dev: true
|
||||
|
||||
/body-parser@1.20.2:
|
||||
resolution: {integrity: sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==}
|
||||
/body-parser@1.20.3:
|
||||
resolution: {integrity: sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==}
|
||||
engines: {node: '>= 0.8', npm: 1.2.8000 || >= 1.4.16}
|
||||
dependencies:
|
||||
bytes: 3.1.2
|
||||
@@ -457,7 +457,7 @@ packages:
|
||||
http-errors: 2.0.0
|
||||
iconv-lite: 0.4.24
|
||||
on-finished: 2.4.1
|
||||
qs: 6.11.0
|
||||
qs: 6.13.0
|
||||
raw-body: 2.5.2
|
||||
type-is: 1.6.18
|
||||
unpipe: 1.0.0
|
||||
@@ -917,8 +917,8 @@ packages:
|
||||
- supports-color
|
||||
dev: true
|
||||
|
||||
/electron@29.4.3:
|
||||
resolution: {integrity: sha512-Fr/D7Iru7zGFK5DtNf3Ck/DBxLHtP/o/32pRXsSoGTVqyHKu2genIiSUiOhbxdu+gQZw4Do+BDDR6inkyr4CGQ==}
|
||||
/electron@29.4.6:
|
||||
resolution: {integrity: sha512-fz8ndj8cmmf441t4Yh2FDP3Rn0JhLkVGvtUf2YVMbJ5SdJPlc0JWll9jYkhh60jDKVVCr/tBAmfxqRnXMWJpzg==}
|
||||
engines: {node: '>= 12.20.55'}
|
||||
hasBin: true
|
||||
requiresBuild: true
|
||||
@@ -943,6 +943,11 @@ packages:
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/encodeurl@2.0.0:
|
||||
resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dev: false
|
||||
|
||||
/end-of-stream@1.4.4:
|
||||
resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==}
|
||||
dependencies:
|
||||
@@ -995,36 +1000,36 @@ packages:
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/express@4.19.2:
|
||||
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
|
||||
/express@4.21.0:
|
||||
resolution: {integrity: sha512-VqcNGcj/Id5ZT1LZ/cfihi3ttTn+NJmkli2eZADigjq29qTlWi/hAQ43t/VLPq8+UX06FCEx3ByOYet6ZFblng==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
dependencies:
|
||||
accepts: 1.3.8
|
||||
array-flatten: 1.1.1
|
||||
body-parser: 1.20.2
|
||||
body-parser: 1.20.3
|
||||
content-disposition: 0.5.4
|
||||
content-type: 1.0.5
|
||||
cookie: 0.6.0
|
||||
cookie-signature: 1.0.6
|
||||
debug: 2.6.9
|
||||
depd: 2.0.0
|
||||
encodeurl: 1.0.2
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
etag: 1.8.1
|
||||
finalhandler: 1.2.0
|
||||
finalhandler: 1.3.1
|
||||
fresh: 0.5.2
|
||||
http-errors: 2.0.0
|
||||
merge-descriptors: 1.0.1
|
||||
merge-descriptors: 1.0.3
|
||||
methods: 1.1.2
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
path-to-regexp: 0.1.7
|
||||
path-to-regexp: 0.1.10
|
||||
proxy-addr: 2.0.7
|
||||
qs: 6.11.0
|
||||
qs: 6.13.0
|
||||
range-parser: 1.2.1
|
||||
safe-buffer: 5.2.1
|
||||
send: 0.18.0
|
||||
serve-static: 1.15.0
|
||||
send: 0.19.0
|
||||
serve-static: 1.16.2
|
||||
setprototypeof: 1.2.0
|
||||
statuses: 2.0.1
|
||||
type-is: 1.6.18
|
||||
@@ -1077,12 +1082,12 @@ packages:
|
||||
minimatch: 5.1.6
|
||||
dev: true
|
||||
|
||||
/finalhandler@1.2.0:
|
||||
resolution: {integrity: sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==}
|
||||
/finalhandler@1.3.1:
|
||||
resolution: {integrity: sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
encodeurl: 1.0.2
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
on-finished: 2.4.1
|
||||
parseurl: 1.3.3
|
||||
@@ -1543,8 +1548,8 @@ packages:
|
||||
engines: {node: '>= 0.6'}
|
||||
dev: false
|
||||
|
||||
/merge-descriptors@1.0.1:
|
||||
resolution: {integrity: sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==}
|
||||
/merge-descriptors@1.0.3:
|
||||
resolution: {integrity: sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==}
|
||||
dev: false
|
||||
|
||||
/methods@1.1.2:
|
||||
@@ -1724,8 +1729,8 @@ packages:
|
||||
minipass: 7.0.4
|
||||
dev: true
|
||||
|
||||
/path-to-regexp@0.1.7:
|
||||
resolution: {integrity: sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==}
|
||||
/path-to-regexp@0.1.10:
|
||||
resolution: {integrity: sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==}
|
||||
dev: false
|
||||
|
||||
/pend@1.2.0:
|
||||
@@ -1780,8 +1785,8 @@ packages:
|
||||
requiresBuild: true
|
||||
dev: true
|
||||
|
||||
/qs@6.11.0:
|
||||
resolution: {integrity: sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==}
|
||||
/qs@6.13.0:
|
||||
resolution: {integrity: sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==}
|
||||
engines: {node: '>=0.6'}
|
||||
dependencies:
|
||||
side-channel: 1.0.6
|
||||
@@ -1919,8 +1924,8 @@ packages:
|
||||
lru-cache: 6.0.0
|
||||
dev: true
|
||||
|
||||
/send@0.18.0:
|
||||
resolution: {integrity: sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==}
|
||||
/send@0.19.0:
|
||||
resolution: {integrity: sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
debug: 2.6.9
|
||||
@@ -1949,14 +1954,14 @@ packages:
|
||||
dev: true
|
||||
optional: true
|
||||
|
||||
/serve-static@1.15.0:
|
||||
resolution: {integrity: sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==}
|
||||
/serve-static@1.16.2:
|
||||
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
dependencies:
|
||||
encodeurl: 1.0.2
|
||||
encodeurl: 2.0.0
|
||||
escape-html: 1.0.3
|
||||
parseurl: 1.3.3
|
||||
send: 0.18.0
|
||||
send: 0.19.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
dev: false
|
||||
|
||||
+139
@@ -0,0 +1,139 @@
|
||||
{
|
||||
"nodes": {
|
||||
"devenv": {
|
||||
"locked": {
|
||||
"dir": "src/modules",
|
||||
"lastModified": 1726063457,
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"rev": "39bf6ce569103c9390d37322daa59468c31b3ce7",
|
||||
"treeHash": "839747a1cb35ba6d5b36cce9a739ab2ba5e4a5d4",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"dir": "src/modules",
|
||||
"owner": "cachix",
|
||||
"repo": "devenv",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1696426674,
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"rev": "0f9255e01c2351cc7d116c072cb317785dd33b33",
|
||||
"treeHash": "2addb7b71a20a25ea74feeaf5c2f6a6b30898ecb",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "edolstra",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"pre-commit-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"treeHash": "ca14199cabdfe1a06a7b1654c76ed49100a689f9",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1716977621,
|
||||
"owner": "cachix",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"rev": "4267e705586473d3e5c8d50299e71503f16a6fb6",
|
||||
"treeHash": "6d9f1f7ca0faf1bc2eeb397c78a49623260d3412",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"ref": "rolling",
|
||||
"repo": "devenv-nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-stable": {
|
||||
"locked": {
|
||||
"lastModified": 1725930920,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "44a71ff39c182edaf25a7ace5c9454e7cba2c658",
|
||||
"treeHash": "56e93544112b7bb7aa0c3093d537295683ef9148",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-24.05",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs-unstable": {
|
||||
"locked": {
|
||||
"lastModified": 1725983898,
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "1355a0cbfeac61d785b7183c0caaec1f97361b43",
|
||||
"treeHash": "d8ce0c1af7690da6161f0991484661331f5bc999",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"pre-commit-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"nixpkgs-stable": "nixpkgs-stable"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1725513492,
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"rev": "7570de7b9b504cfe92025dd1be797bf546f66528",
|
||||
"treeHash": "4b46d77870afecd8f642541cb4f4927326343b59",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "pre-commit-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"devenv": "devenv",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-unstable": "nixpkgs-unstable",
|
||||
"pre-commit-hooks": "pre-commit-hooks"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
"version": 7
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
{ pkgs, lib, config, inputs, ... }:
|
||||
|
||||
let
|
||||
pkgs-unstable = import inputs.nixpkgs-unstable { system = pkgs.stdenv.system; };
|
||||
in {
|
||||
packages = with pkgs-unstable; [
|
||||
# General tools
|
||||
git-cliff
|
||||
# API tools
|
||||
golangci-lint mage
|
||||
# Desktop
|
||||
electron
|
||||
] ++ lib.optionals (!pkgs.stdenv.isDarwin) [
|
||||
# Frontend tools (exclude on Darwin)
|
||||
cypress
|
||||
];
|
||||
|
||||
languages = {
|
||||
javascript = {
|
||||
enable = true;
|
||||
package = pkgs-unstable.nodejs-slim;
|
||||
pnpm = {
|
||||
enable = true;
|
||||
package = pkgs-unstable.pnpm;
|
||||
};
|
||||
};
|
||||
|
||||
go = {
|
||||
enable = true;
|
||||
};
|
||||
};
|
||||
|
||||
services.mailpit = {
|
||||
enable = true;
|
||||
package = pkgs-unstable.mailpit;
|
||||
};
|
||||
}
|
||||
+17
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
+82
-82
@@ -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.0.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.30.0",
|
||||
"@tiptap/core": "2.7.2",
|
||||
"@tiptap/extension-blockquote": "2.7.2",
|
||||
"@tiptap/extension-bold": "2.7.2",
|
||||
"@tiptap/extension-bullet-list": "2.7.2",
|
||||
"@tiptap/extension-code": "2.7.2",
|
||||
"@tiptap/extension-code-block": "2.7.2",
|
||||
"@tiptap/extension-code-block-lowlight": "2.7.2",
|
||||
"@tiptap/extension-document": "2.7.2",
|
||||
"@tiptap/extension-dropcursor": "2.7.2",
|
||||
"@tiptap/extension-gapcursor": "2.7.2",
|
||||
"@tiptap/extension-hard-break": "2.7.2",
|
||||
"@tiptap/extension-heading": "2.7.2",
|
||||
"@tiptap/extension-history": "2.7.2",
|
||||
"@tiptap/extension-horizontal-rule": "2.7.2",
|
||||
"@tiptap/extension-image": "2.7.2",
|
||||
"@tiptap/extension-italic": "2.7.2",
|
||||
"@tiptap/extension-link": "2.7.2",
|
||||
"@tiptap/extension-list-item": "2.7.2",
|
||||
"@tiptap/extension-ordered-list": "2.7.2",
|
||||
"@tiptap/extension-paragraph": "2.7.2",
|
||||
"@tiptap/extension-placeholder": "2.7.2",
|
||||
"@tiptap/extension-strike": "2.7.2",
|
||||
"@tiptap/extension-table": "2.7.2",
|
||||
"@tiptap/extension-table-cell": "2.7.2",
|
||||
"@tiptap/extension-table-header": "2.7.2",
|
||||
"@tiptap/extension-table-row": "2.7.2",
|
||||
"@tiptap/extension-task-item": "2.7.2",
|
||||
"@tiptap/extension-task-list": "2.7.2",
|
||||
"@tiptap/extension-text": "2.7.2",
|
||||
"@tiptap/extension-typography": "2.7.2",
|
||||
"@tiptap/extension-underline": "2.7.2",
|
||||
"@tiptap/pm": "2.7.2",
|
||||
"@tiptap/suggestion": "2.7.2",
|
||||
"@tiptap/vue-3": "2.7.2",
|
||||
"@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.6",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"flatpickr": "4.6.13",
|
||||
"flexsearch": "0.7.31",
|
||||
"flexsearch": "0.7.43",
|
||||
"floating-vue": "5.2.2",
|
||||
"is-touch-device": "1.0.1",
|
||||
"klona": "2.0.6",
|
||||
"lodash.debounce": "4.0.8",
|
||||
"lowlight": "2.9.0",
|
||||
"pinia": "2.1.7",
|
||||
"pinia": "2.2.2",
|
||||
"register-service-worker": "1.7.2",
|
||||
"sortablejs": "1.15.2",
|
||||
"sortablejs": "1.15.3",
|
||||
"tailwindcss": "3.4.12",
|
||||
"tippy.js": "6.3.7",
|
||||
"ufo": "1.5.3",
|
||||
"vue": "3.4.31",
|
||||
"ufo": "1.5.4",
|
||||
"vue": "3.5.6",
|
||||
"vue-advanced-cropper": "2.8.9",
|
||||
"vue-flatpickr-component": "11.0.5",
|
||||
"vue-i18n": "9.13.1",
|
||||
"vue-router": "4.4.0",
|
||||
"vue-i18n": "10.0.1",
|
||||
"vue-router": "4.4.5",
|
||||
"vuemoji-picker": "0.2.1",
|
||||
"workbox-precaching": "7.1.0",
|
||||
"zhyswan-vuedraggable": "4.1.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@4tw/cypress-drag-drop": "2.2.5",
|
||||
"@cypress/vite-dev-server": "5.1.1",
|
||||
"@cypress/vite-dev-server": "5.2.0",
|
||||
"@cypress/vue": "6.0.1",
|
||||
"@faker-js/faker": "8.4.1",
|
||||
"@histoire/plugin-screenshot": "0.17.17",
|
||||
"@histoire/plugin-vue": "0.17.17",
|
||||
"@rushstack/eslint-patch": "1.10.3",
|
||||
"@rushstack/eslint-patch": "1.10.4",
|
||||
"@tsconfig/node20": "20.1.4",
|
||||
"@types/codemirror": "5.60.15",
|
||||
"@types/dompurify": "3.0.5",
|
||||
"@types/flexsearch": "0.7.6",
|
||||
"@types/is-touch-device": "1.0.2",
|
||||
"@types/lodash.clonedeep": "4.5.9",
|
||||
"@types/lodash.debounce": "4.0.9",
|
||||
"@types/node": "20.14.9",
|
||||
"@types/node": "20.16.5",
|
||||
"@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.23.3",
|
||||
"caniuse-lite": "1.0.30001660",
|
||||
"csstype": "3.1.3",
|
||||
"cypress": "13.13.0",
|
||||
"esbuild": "0.23.0",
|
||||
"eslint": "8.57.0",
|
||||
"eslint-plugin-vue": "9.26.0",
|
||||
"cypress": "13.14.2",
|
||||
"esbuild": "0.23.1",
|
||||
"eslint": "8.57.1",
|
||||
"eslint-plugin-vue": "9.28.0",
|
||||
"happy-dom": "14.12.3",
|
||||
"histoire": "0.17.17",
|
||||
"postcss": "8.4.39",
|
||||
"postcss": "8.4.47",
|
||||
"postcss-easing-gradients": "3.0.1",
|
||||
"postcss-easings": "4.0.0",
|
||||
"postcss-preset-env": "9.5.15",
|
||||
"rollup": "4.18.0",
|
||||
"postcss-preset-env": "9.6.0",
|
||||
"rollup": "4.21.3",
|
||||
"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.1",
|
||||
"start-server-and-test": "2.0.8",
|
||||
"typescript": "5.6.2",
|
||||
"unplugin-inject-preload": "2.0.5",
|
||||
"vite": "5.4.6",
|
||||
"vite-plugin-pwa": "0.20.5",
|
||||
"vite-plugin-sentry": "1.4.0",
|
||||
"vite-svg-loader": "5.1.0",
|
||||
"vitest": "1.6.0",
|
||||
"vue-tsc": "2.0.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
+2902
-2572
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]
|
||||
}
|
||||
|
||||
|
||||
@@ -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,24 +21,20 @@
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
@@ -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,13 +33,6 @@
|
||||
'is-wide': wide
|
||||
}"
|
||||
>
|
||||
<BaseButton
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon icon="times" />
|
||||
</BaseButton>
|
||||
|
||||
<slot>
|
||||
<div class="header">
|
||||
<slot name="header" />
|
||||
@@ -137,8 +136,8 @@ $modal-width: 1024px;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin: 0;
|
||||
top: 25%;
|
||||
transform: translate(-50%, -25%);
|
||||
position: static;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
@@ -154,12 +153,15 @@ $modal-width: 1024px;
|
||||
// scrolling-content
|
||||
// used e.g. for <TaskDetailViewModal>
|
||||
.scrolling .modal-content {
|
||||
max-width: $modal-width;
|
||||
width: 100%;
|
||||
margin: $modal-margin auto;
|
||||
|
||||
max-height: none; // reset bulma
|
||||
overflow: visible; // reset bulma
|
||||
|
||||
@media not print {
|
||||
max-width: $modal-width;
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) {
|
||||
max-height: none; // reset bulma
|
||||
@@ -167,7 +169,7 @@ $modal-width: 1024px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media screen and (max-width: $desktop) {
|
||||
@media screen and (max-width: $desktop), print {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
@@ -194,7 +196,7 @@ $modal-width: 1024px;
|
||||
position: fixed;
|
||||
top: .5rem;
|
||||
right: $close-button-padding;
|
||||
color: var(--grey-900);
|
||||
color: var(--white);
|
||||
font-size: 2rem;
|
||||
|
||||
@media screen and (min-width: $desktop) and (max-width: calc(#{$desktop } + #{$close-button-min-space})) {
|
||||
@@ -203,15 +205,42 @@ $modal-width: 1024px;
|
||||
// we align the close button to the modal until there is enough space outside for it
|
||||
transform: translateX(calc((#{$modal-width} / 2) - #{$close-button-padding}));
|
||||
}
|
||||
// we can only use light color when there is enough space for the close button next to the modal
|
||||
@media screen and (min-width: calc(#{$desktop } + #{$close-button-min-space})) {
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) and (max-width: #{$desktop + $close-button-min-space}) {
|
||||
top: .75rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media print, screen and (max-width: $tablet) {
|
||||
.modal-mask {
|
||||
position: static;
|
||||
overflow: visible !important;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
height: auto;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
position: static;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: none;
|
||||
}
|
||||
|
||||
:deep(.card) {
|
||||
border: none !important;
|
||||
border-radius: 0 !important;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@@ -219,4 +248,10 @@ $modal-width: 1024px;
|
||||
.dark .close {
|
||||
color: var(--grey-900);
|
||||
}
|
||||
</style>
|
||||
|
||||
@media print, screen and (max-width: $tablet) {
|
||||
body:has(.modal-mask) #app {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -45,15 +45,11 @@
|
||||
<script lang="ts" setup>
|
||||
import {computed} from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
totalPages: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentPage: {
|
||||
type: Number,
|
||||
default: 0,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
totalPages: number,
|
||||
currentPage?: number
|
||||
}>(), {
|
||||
currentPage: 0,
|
||||
})
|
||||
|
||||
function createPagination(totalPages: number, currentPage: number) {
|
||||
|
||||
@@ -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,17 +566,18 @@ async function addTaskToBucket(bucketId: IBucket['id']) {
|
||||
bucketId,
|
||||
projectId: project.value.id,
|
||||
})
|
||||
toggleShowNewTaskInput(bucketId)
|
||||
newTaskText.value = ''
|
||||
kanbanStore.addTaskToBucket(task)
|
||||
scrollTaskContainerToBottom(bucketId)
|
||||
scrollTaskContainerToTop(bucketId)
|
||||
}
|
||||
|
||||
function scrollTaskContainerToBottom(bucketId: IBucket['id']) {
|
||||
function scrollTaskContainerToTop(bucketId: IBucket['id']) {
|
||||
const bucketEl = taskContainerRefs.value[bucketId]
|
||||
if (!bucketEl) {
|
||||
return
|
||||
}
|
||||
bucketEl.scrollTop = bucketEl.scrollHeight
|
||||
bucketEl.scrollTop = 0
|
||||
}
|
||||
|
||||
async function createNewBucket() {
|
||||
@@ -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
|
||||
|
||||
@@ -180,7 +180,7 @@ import ProgressBar from '@/components/misc/ProgressBar.vue'
|
||||
import BaseButton from '@/components/base/BaseButton.vue'
|
||||
|
||||
import AttachmentService from '@/services/attachment'
|
||||
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
|
||||
import {canPreview} from '@/models/attachment'
|
||||
import type {IAttachment} from '@/modelTypes/IAttachment'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
|
||||
@@ -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)
|
||||
@@ -272,18 +274,14 @@ async function viewOrDownload(attachment: IAttachment) {
|
||||
}
|
||||
}
|
||||
|
||||
function canPreview(attachment: IAttachment): boolean {
|
||||
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
|
||||
}
|
||||
|
||||
const copy = useCopyToClipboard()
|
||||
|
||||
function copyUrl(attachment: IAttachment) {
|
||||
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')})
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
</span>
|
||||
<div
|
||||
v-for="c in comments"
|
||||
:id="`comment-${c.id}`"
|
||||
:key="c.id"
|
||||
class="media comment"
|
||||
>
|
||||
@@ -56,6 +57,13 @@
|
||||
>
|
||||
· {{ $t('task.comment.edited', {date: formatDateSince(c.updated)}) }}
|
||||
</span>
|
||||
<a
|
||||
v-tooltip="$t('task.comment.permalink')"
|
||||
:href="`#comment-${c.id}`"
|
||||
class="comment-permalink"
|
||||
:title="$t('task.comment.permalink')"
|
||||
@click.prevent.stop="copy(getCommentUrl(`${c.id}`))"
|
||||
><Icon icon="link" /></a>
|
||||
<CustomTransition name="fade">
|
||||
<span
|
||||
v-if="
|
||||
@@ -199,18 +207,17 @@ import {getAvatarUrl, getDisplayName} from '@/models/user'
|
||||
import {useConfigStore} from '@/stores/config'
|
||||
import {useAuthStore} from '@/stores/auth'
|
||||
import Reactions from '@/components/input/Reactions.vue'
|
||||
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
|
||||
|
||||
const props = defineProps({
|
||||
taskId: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
canWrite: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
const props = withDefaults(defineProps<{
|
||||
taskId: number,
|
||||
canWrite?: boolean
|
||||
}>(), {
|
||||
canWrite: true,
|
||||
})
|
||||
|
||||
const copy = useCopyToClipboard()
|
||||
|
||||
const {t} = useI18n({useScope: 'global'})
|
||||
const configStore = useConfigStore()
|
||||
const authStore = useAuthStore()
|
||||
@@ -246,6 +253,8 @@ const actions = computed(() => {
|
||||
])))
|
||||
})
|
||||
|
||||
const frontendUrl = computed(() => configStore.frontendUrl)
|
||||
|
||||
async function attachmentUpload(files: File[] | FileList): (Promise<string[]>) {
|
||||
|
||||
const uploadPromises: Promise<string>[] = []
|
||||
@@ -368,6 +377,10 @@ async function deleteComment(commentToDelete: ITaskComment) {
|
||||
showDeleteModal.value = false
|
||||
}
|
||||
}
|
||||
|
||||
function getCommentUrl(commentId: string) {
|
||||
return `${frontendUrl.value}${location.pathname}${location.search}#comment-${commentId}`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -395,10 +408,23 @@ async function deleteComment(commentToDelete: ITaskComment) {
|
||||
}
|
||||
|
||||
|
||||
span {
|
||||
span,
|
||||
.comment-permalink {
|
||||
font-size: .75rem;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.comment-permalink {
|
||||
font-size: 1rem;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.25rem;
|
||||
border-radius: 1rem;
|
||||
color: var(--grey, hsl(0, 0%, 48%));
|
||||
}
|
||||
.comment-permalink:hover {
|
||||
color: var(--grey-dark, hsl(0, 0%, 29%));
|
||||
border-color: var(--grey-dark, hsl(0, 0%, 29%));
|
||||
}
|
||||
}
|
||||
|
||||
.image.is-avatar {
|
||||
|
||||
@@ -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,30 +19,23 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import {type PropType, ref, shallowReactive, watchEffect} from 'vue'
|
||||
import AttachmentService from '@/services/attachment'
|
||||
import {ref, shallowReactive, watchEffect} from 'vue'
|
||||
import AttachmentService, {PREVIEW_SIZE} from '@/services/attachment'
|
||||
import type {IAttachment} from '@/modelTypes/IAttachment'
|
||||
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
|
||||
import {canPreview} from '@/models/attachment'
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Object as PropType<IAttachment>,
|
||||
default: undefined,
|
||||
},
|
||||
})
|
||||
const props = defineProps<{
|
||||
modelValue?: IAttachment
|
||||
}>()
|
||||
|
||||
const attachmentService = shallowReactive(new AttachmentService())
|
||||
const blobUrl = ref<string | undefined>(undefined)
|
||||
|
||||
watchEffect(async () => {
|
||||
if (props.modelValue && canPreview(props.modelValue)) {
|
||||
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue)
|
||||
blobUrl.value = await attachmentService.getBlobUrl(props.modelValue, PREVIEW_SIZE.MD)
|
||||
}
|
||||
})
|
||||
|
||||
function canPreview(attachment: IAttachment): boolean {
|
||||
return SUPPORTED_IMAGE_SUFFIX.some((suffix) => attachment.file.name.toLowerCase().endsWith(suffix))
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@@ -1,20 +1,26 @@
|
||||
<template>
|
||||
<div class="heading">
|
||||
<div class="flex is-align-items-center">
|
||||
<BaseButton @click="copyUrl">
|
||||
<h1 class="title task-id">
|
||||
{{ textIdentifier }}
|
||||
</h1>
|
||||
</BaseButton>
|
||||
<div class="tw-flex tw-items-center md:tw-items-stretch tw-flex-col tw-gap-1 task-properties">
|
||||
<div class="tw-flex tw-items-center tw-gap-2">
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
/>
|
||||
<BaseButton @click="copyUrl">
|
||||
<h1 class="title task-id">
|
||||
{{ textIdentifier }}
|
||||
</h1>
|
||||
</BaseButton>
|
||||
</div>
|
||||
<Done
|
||||
class="heading__done"
|
||||
:is-done="task.done"
|
||||
/>
|
||||
<ColorBubble
|
||||
v-if="task.hexColor !== ''"
|
||||
:color="getHexColor(task.hexColor)"
|
||||
class="ml-2"
|
||||
/>
|
||||
<BaseButton
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon icon="times" />
|
||||
</BaseButton>
|
||||
</div>
|
||||
<h1
|
||||
class="title input"
|
||||
@@ -26,6 +32,12 @@
|
||||
>
|
||||
{{ task.title.trim() }}
|
||||
</h1>
|
||||
<BaseButton
|
||||
class="close"
|
||||
@click="$emit('close')"
|
||||
>
|
||||
<Icon icon="times" />
|
||||
</BaseButton>
|
||||
<CustomTransition name="fade">
|
||||
<span
|
||||
v-if="loading && saving"
|
||||
@@ -49,7 +61,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 +75,17 @@ 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,
|
||||
}>(), {
|
||||
canWrite: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update:task'])
|
||||
const emit = defineEmits<{
|
||||
'update:task': [task: ITask],
|
||||
'close': [],
|
||||
}>()
|
||||
|
||||
const router = useRouter()
|
||||
const copy = useCopyToClipboard()
|
||||
@@ -140,14 +151,11 @@ async function save(title: string) {
|
||||
.title.input {
|
||||
// 1.8rem is the font-size, 1.125 is the line-height, .3rem padding everywhere, 1px border around the whole thing.
|
||||
min-height: calc(1.8rem * 1.125 + .6rem + 2px);
|
||||
margin-right: 0;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
margin: 0 -.3rem .5rem -.3rem; // the title has 0.3rem padding - this make the text inside of it align with the rest
|
||||
}
|
||||
|
||||
@media screen and (min-width: $tablet) and (max-width: #{$desktop + $close-button-min-space}) {
|
||||
width: calc(100% - 6.5rem);
|
||||
}
|
||||
}
|
||||
|
||||
.title.task-id {
|
||||
@@ -155,12 +163,39 @@ async function save(title: string) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.heading__done {
|
||||
margin-left: .5rem;
|
||||
}
|
||||
|
||||
.color-bubble {
|
||||
height: .75rem;
|
||||
width: .75rem;
|
||||
}
|
||||
|
||||
.close {
|
||||
font-size: 2rem;
|
||||
margin-left: 0.5rem;
|
||||
line-height: 1;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media screen and (min-width: #{$desktop + 1px}) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.task-properties .close {
|
||||
display: none;
|
||||
position: absolute;
|
||||
right: 1.25rem;
|
||||
top: 1.1rem;
|
||||
|
||||
@media screen and (max-width: $tablet) {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
.task-properties {
|
||||
@media screen and (max-width: $tablet) {
|
||||
flex-direction: row;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -16,7 +16,7 @@
|
||||
v-if="coverImageBlobUrl"
|
||||
:src="coverImageBlobUrl"
|
||||
alt=""
|
||||
class="cover-image"
|
||||
class="tw-w-full"
|
||||
>
|
||||
<div class="p-2">
|
||||
<span class="task-id">
|
||||
@@ -93,7 +93,7 @@
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import {ref, computed, watch} from 'vue'
|
||||
import {computed, ref, watch} from 'vue'
|
||||
import {useRouter} from 'vue-router'
|
||||
|
||||
import PriorityLabel from '@/components/tasks/partials/PriorityLabel.vue'
|
||||
@@ -105,29 +105,27 @@ import ChecklistSummary from './ChecklistSummary.vue'
|
||||
import {getHexColor} from '@/models/task'
|
||||
import type {ITask} from '@/modelTypes/ITask'
|
||||
import {SUPPORTED_IMAGE_SUFFIX} from '@/models/attachment'
|
||||
import AttachmentService from '@/services/attachment'
|
||||
import AttachmentService, {PREVIEW_SIZE} from '@/services/attachment'
|
||||
|
||||
import {formatDateLong, formatISO, formatDateSince} from '@/helpers/time/formatDate'
|
||||
import {formatDateLong, formatDateSince, formatISO} from '@/helpers/time/formatDate'
|
||||
import {colorIsDark} from '@/helpers/color/colorIsDark'
|
||||
import {useTaskStore} from '@/stores/tasks'
|
||||
import AssigneeList from '@/components/tasks/partials/AssigneeList.vue'
|
||||
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,22 +154,22 @@ 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
|
||||
}
|
||||
|
||||
const attachmentService = new AttachmentService()
|
||||
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment)
|
||||
coverImageBlobUrl.value = await attachmentService.getBlobUrl(attachment, PREVIEW_SIZE.LG)
|
||||
}
|
||||
|
||||
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,8 @@ export const SUPPORTED_LOCALES = {
|
||||
'sl-SI': 'Slovenščina',
|
||||
'pt-BR': 'Português Brasileiro',
|
||||
'hr-HR': 'Hrvatski',
|
||||
'uk-UA': 'українська',
|
||||
'uk-UA': 'Українська',
|
||||
'lt-LT': 'Lietuvių Kalba',
|
||||
// IMPORTANT: Also add new languages to useDayjsLanguageSync
|
||||
} as const
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"forgotPassword": "نسيت كلمة المرور؟",
|
||||
"resetPassword": "إعادة تعيين كلمة المرور",
|
||||
"resetPasswordAction": "أرسل لي رابط إعادة تعيين كلمة المرور",
|
||||
"resetPasswordSuccess": "تحقق من صندوق الوارد الخاص بك! يجب أن تصلك رسالة مع إرشادات حول كيفية إعادة تعيين كلمة المرور الخاصة بك.",
|
||||
"resetPasswordSuccess": "Check your inbox! You should have an email with instructions on how to reset your password.",
|
||||
"passwordsDontMatch": "كلمتي المرور غير متطابقتين",
|
||||
"confirmEmailSuccess": "تم تأكيد بريدك الإلكتروني بنجاح! يمكنك تسجيل الدخول الآن.",
|
||||
"totpTitle": "رمز المصادقة الثنائية 2FA",
|
||||
@@ -61,7 +61,7 @@
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "الرجاء إدخال كلمة المرور.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"passwordNotMax": "Password must have at most 72 characters.",
|
||||
"showPassword": "إظهار كلمة المرور",
|
||||
"hidePassword": "إخفاء كلمة المرور",
|
||||
"noAccountYet": "ليس لديك حساب بعد؟",
|
||||
@@ -72,21 +72,21 @@
|
||||
"settings": {
|
||||
"title": "الإعدادات",
|
||||
"newPasswordTitle": "تحديث كلمة مرورك",
|
||||
"newPassword": "كلمة مرور جديدة",
|
||||
"newPasswordConfirm": "تأكيد كلمة المرور الجديدة",
|
||||
"currentPassword": "كلمة المرور الحالية",
|
||||
"newPassword": "New password",
|
||||
"newPasswordConfirm": "New password confirmation",
|
||||
"currentPassword": "Current password",
|
||||
"currentPasswordPlaceholder": "كلمة المرور الحالية الخاصة بك",
|
||||
"passwordsDontMatch": "كلمة المرور الجديدة وتأكيدها غير متطابقة.",
|
||||
"passwordUpdateSuccess": "تم تحديث كلمة المرور بنجاح.",
|
||||
"updateEmailTitle": "تحديث عنوان بريدك الإلكتروني",
|
||||
"updateEmailNew": "عنوان البريد الإلكتروني الجديد",
|
||||
"updateEmailNew": "New email address",
|
||||
"updateEmailSuccess": "تم تحديث عنوان بريدك الإلكتروني بنجاح. لقد أرسلنا لك رابط لتأكيده.",
|
||||
"general": {
|
||||
"title": "الإعدادات العامة",
|
||||
"name": "الاسم",
|
||||
"newName": "الاسم الجديد",
|
||||
"name": "My Name",
|
||||
"newName": "The new name",
|
||||
"savedSuccess": "تم تحديث الإعدادات بنجاح.",
|
||||
"emailReminders": "أرسل لي تذكيرات للمهام عبر البريد الإلكتروني",
|
||||
"emailReminders": "Send me reminders for tasks via email",
|
||||
"overdueReminders": "أرسل لي ملخصاً للمهام المتأخرة ولم تنجز كل يوم",
|
||||
"discoverableByName": "السماح للمستخدمين الآخرين بإضافتي كعضو إلى الفرق أو المشاريع عند بحثهم عن اسمي",
|
||||
"discoverableByEmail": "السماح للمستخدمين الآخرين بإضافتي كعضو إلى الفرق أو المشاريع عند بحثهم عن بريدي الإلكتروني الكامل",
|
||||
@@ -95,24 +95,24 @@
|
||||
"weekStartSunday": "الأحد",
|
||||
"weekStartMonday": "الإثنين",
|
||||
"language": "اللّغة",
|
||||
"defaultProject": "المشروع الافتراضي",
|
||||
"defaultView": "Default View",
|
||||
"timezone": "منطقة التوقيت",
|
||||
"defaultProject": "Default project",
|
||||
"defaultView": "Default view",
|
||||
"timezone": "Time zone",
|
||||
"overdueTasksRemindersTime": "وقت تذكير للمهام المتأخرة بالبريد الإلكتروني",
|
||||
"filterUsedOnOverview": "الفلتر المحفوظ والمستخدم في صفحة العرض العام"
|
||||
},
|
||||
"totp": {
|
||||
"title": "رمز المصادقة الثنائية 2FA",
|
||||
"enroll": "تسجيل",
|
||||
"finishSetupPart1": "لإنهاء التثبيت، استخدم هذا الرمز السرّي في تطبيق Totp الخاص بك (Google Authenticator أو ما شابه):",
|
||||
"finishSetupPart1": "To finish your setup, use this secret in your TOTP app (Google Authenticator or similar):",
|
||||
"finishSetupPart2": "بعد ذلك، أدخل الرمز من التطبيق الخاص بك أدناه.",
|
||||
"scanQR": "أو يمكنك مسح رمز الاستجابة السريعة QR هذا:",
|
||||
"passcode": "الرمز السّري",
|
||||
"passcodePlaceholder": "الرمز السّري تم إنشاؤه بواسطة تطبيق Totp الخاص بك",
|
||||
"passcodePlaceholder": "A code generated by your TOTP application",
|
||||
"setupSuccess": "لقد قمت بنجاح بإعداد المصادقة الثنائية 2FA!",
|
||||
"enterPassword": "الرجاء إدخال كلمة المرور",
|
||||
"disable": "تعطيل المصادقة الثنائية",
|
||||
"confirmSuccess": "لقد قمت بتأكيد إعداد Totp الخاص بك بنجاح ويمكنك استخدامه من الآن فصاعداً!",
|
||||
"confirmSuccess": "You've successfully confirmed your TOTP setup and can use it from now on!",
|
||||
"disableSuccess": "تم تعطيل المصادقة الثنائية بنجاح."
|
||||
},
|
||||
"caldav": {
|
||||
@@ -163,6 +163,7 @@
|
||||
"90d": "90 يوماً",
|
||||
"permissionExplanation": "نطاق الصلاحيات المسموح به هو ما يسمح لك القيام به رمز Api.",
|
||||
"titleRequired": "العنوان إجباري",
|
||||
"permissionRequired": "Please select at least one permission from the list.",
|
||||
"expired": "انتهت صلاحية هذا الرمز {ago}.",
|
||||
"tokenCreatedSuccess": "إليك رمز Api الجديد: {token}",
|
||||
"tokenCreatedNotSeeAgain": "قم بتخزينه في مكان آمن، لن تتمكن من عرضه مرة أخرى!",
|
||||
@@ -847,7 +848,8 @@
|
||||
"delete": "حذف هذا التعليق",
|
||||
"deleteText1": "هل أنت متأكد من رغبتك بحذف هذا التعليق؟",
|
||||
"deleteSuccess": "تم حذف التعليق بنجاح.",
|
||||
"addedSuccess": "تمت إضافة التعليق بنجاح."
|
||||
"addedSuccess": "تمت إضافة التعليق بنجاح.",
|
||||
"permalink": "Copy permalink to this comment"
|
||||
},
|
||||
"deferDueDate": {
|
||||
"title": "تأجيل تاريخ الاستحقاق",
|
||||
@@ -1141,9 +1143,9 @@
|
||||
"1012": "لم يتم تأكيد عنوان البريد الإلكتروني للمستخدم.",
|
||||
"1013": "كلمة المرور الجديدة فارغة.",
|
||||
"1014": "كلمة المرور القديمة فارغة.",
|
||||
"1015": "تم تفعيل Totp مسبقاً لهذا المستخدم.",
|
||||
"1016": "لم يتم تفعيل Totp لهذا المستخدم.",
|
||||
"1017": "رمز المرور الخاص بـ Totp غير صالح.",
|
||||
"1015": "TOTP is already enabled for this user.",
|
||||
"1016": "TOTP is not enabled for this user.",
|
||||
"1017": "The TOTP passcode is invalid.",
|
||||
"1018": "إعدادات نوع الصورة الرمزية للمستخدم غير صالحة.",
|
||||
"2001": "لا يمكن أن يكون الرقم التعريفي ID فارغاً أو 0.",
|
||||
"2002": "بعض بيانات الطلب غير صالحة.",
|
||||
@@ -1153,8 +1155,8 @@
|
||||
"3006": "المشاركة في المشروع غير موجودة.",
|
||||
"3007": "يوجد مشروع بهذا المعرف مسبقاً.",
|
||||
"3008": "المشروع مؤرشف وبالتالي يمكن الوصول إليه للقراءة فقط، وهذا ينطبق أيضا على جميع المهام المرتبطة بهذا المشروع.",
|
||||
"4001": "لا يمكن ترك نص مهمة المشروع فارغاً.",
|
||||
"4002": "مهمة المشروع غير موجودة.",
|
||||
"4001": "The task title cannot be empty.",
|
||||
"4002": "The task does not exist.",
|
||||
"4003": "يجب أن تنتمي جميع إجراءات التعديل المجمعة لنفس المشروع.",
|
||||
"4004": "تحتاج إلى مهمة واحدة على الأقل عند القيام بإجراءات التعديل بالجملة.",
|
||||
"4005": "ليس لديك الصلاحية لعرض المهمة.",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"forgotPassword": "Forgot your password?",
|
||||
"resetPassword": "Reset your password",
|
||||
"resetPasswordAction": "Send me a password reset link",
|
||||
"resetPasswordSuccess": "Check your inbox! You should have an e-mail with instructions on how to reset your password.",
|
||||
"resetPasswordSuccess": "Check your inbox! You should have an email with instructions on how to reset your password.",
|
||||
"passwordsDontMatch": "Passwords don't match",
|
||||
"confirmEmailSuccess": "You successfully confirmed your email! You can log in now.",
|
||||
"totpTitle": "Two Factor Authentication Code",
|
||||
@@ -61,7 +61,7 @@
|
||||
"usernameMustNotLookLikeUrl": "The username must not look like a URL.",
|
||||
"passwordRequired": "Please provide a password.",
|
||||
"passwordNotMin": "Password must have at least 8 characters.",
|
||||
"passwordNotMax": "Password must have at most 250 characters.",
|
||||
"passwordNotMax": "Password must have at most 72 characters.",
|
||||
"showPassword": "Show the password",
|
||||
"hidePassword": "Hide the password",
|
||||
"noAccountYet": "Don't have an account yet?",
|
||||
@@ -72,21 +72,21 @@
|
||||
"settings": {
|
||||
"title": "Settings",
|
||||
"newPasswordTitle": "Update Your Password",
|
||||
"newPassword": "New Password",
|
||||
"newPasswordConfirm": "New Password Confirmation",
|
||||
"currentPassword": "Current Password",
|
||||
"newPassword": "New password",
|
||||
"newPasswordConfirm": "New password confirmation",
|
||||
"currentPassword": "Current password",
|
||||
"currentPasswordPlaceholder": "Your current password",
|
||||
"passwordsDontMatch": "The new password and its confirmation don't match.",
|
||||
"passwordUpdateSuccess": "The password was successfully updated.",
|
||||
"updateEmailTitle": "Update Your Email Address",
|
||||
"updateEmailNew": "New Email Address",
|
||||
"updateEmailNew": "New email address",
|
||||
"updateEmailSuccess": "Your email address was successfully updated. We've sent you a link to confirm it.",
|
||||
"general": {
|
||||
"title": "General Settings",
|
||||
"name": "Name",
|
||||
"newName": "The new Name",
|
||||
"name": "My Name",
|
||||
"newName": "The new name",
|
||||
"savedSuccess": "The settings were successfully updated.",
|
||||
"emailReminders": "Send me reminders for tasks via Email",
|
||||
"emailReminders": "Send me reminders for tasks via email",
|
||||
"overdueReminders": "Send me a summary of my undone overdue tasks every day",
|
||||
"discoverableByName": "Allow other users to add me as a member to teams or projects when they search for my name",
|
||||
"discoverableByEmail": "Allow other users to add me as a member to teams or projects when they search for my full email",
|
||||
@@ -95,24 +95,24 @@
|
||||
"weekStartSunday": "Sunday",
|
||||
"weekStartMonday": "Monday",
|
||||
"language": "Language",
|
||||
"defaultProject": "Default Project",
|
||||
"defaultView": "Default View",
|
||||
"timezone": "Time Zone",
|
||||
"defaultProject": "Default project",
|
||||
"defaultView": "Default view",
|
||||
"timezone": "Time zone",
|
||||
"overdueTasksRemindersTime": "Overdue tasks reminder email time",
|
||||
"filterUsedOnOverview": "Saved filter used on the overview page"
|
||||
},
|
||||
"totp": {
|
||||
"title": "Two Factor Authentication",
|
||||
"enroll": "Enroll",
|
||||
"finishSetupPart1": "To finish your setup, use this secret in your totp app (Google Authenticator or similar):",
|
||||
"finishSetupPart1": "To finish your setup, use this secret in your TOTP app (Google Authenticator or similar):",
|
||||
"finishSetupPart2": "After that, enter a code from your app below.",
|
||||
"scanQR": "Alternatively you can scan this QR code:",
|
||||
"passcode": "Passcode",
|
||||
"passcodePlaceholder": "A code generated by your totp application",
|
||||
"passcodePlaceholder": "A code generated by your TOTP application",
|
||||
"setupSuccess": "You've successfully set up two factor authentication!",
|
||||
"enterPassword": "Please Enter Your Password",
|
||||
"disable": "Disable two factor authentication",
|
||||
"confirmSuccess": "You've successfully confirmed your totp setup and can use it from now on!",
|
||||
"confirmSuccess": "You've successfully confirmed your TOTP setup and can use it from now on!",
|
||||
"disableSuccess": "Two factor authentication was successfully disabled."
|
||||
},
|
||||
"caldav": {
|
||||
@@ -163,6 +163,7 @@
|
||||
"90d": "90 Days",
|
||||
"permissionExplanation": "Permissions allow you to scope what an api token is allowed to do.",
|
||||
"titleRequired": "The title is required",
|
||||
"permissionRequired": "Please select at least one permission from the list.",
|
||||
"expired": "This token has expired {ago}.",
|
||||
"tokenCreatedSuccess": "Here is your new api token: {token}",
|
||||
"tokenCreatedNotSeeAgain": "Store it in a secure location, you won't see it again!",
|
||||
@@ -847,7 +848,8 @@
|
||||
"delete": "Delete this comment",
|
||||
"deleteText1": "Are you sure you want to delete this comment?",
|
||||
"deleteSuccess": "The comment was deleted successfully.",
|
||||
"addedSuccess": "The comment was added successfully."
|
||||
"addedSuccess": "The comment was added successfully.",
|
||||
"permalink": "Copy permalink to this comment"
|
||||
},
|
||||
"deferDueDate": {
|
||||
"title": "Defer due date",
|
||||
@@ -1141,9 +1143,9 @@
|
||||
"1012": "Email address of the user not confirmed.",
|
||||
"1013": "New password is empty.",
|
||||
"1014": "Old password is empty.",
|
||||
"1015": "Totp is already enabled for this user.",
|
||||
"1016": "Totp is not enabled for this user.",
|
||||
"1017": "The totp passcode is invalid.",
|
||||
"1015": "TOTP is already enabled for this user.",
|
||||
"1016": "TOTP is not enabled for this user.",
|
||||
"1017": "The TOTP passcode is invalid.",
|
||||
"1018": "The user avatar type setting is invalid.",
|
||||
"2001": "ID cannot be empty or 0.",
|
||||
"2002": "Some of the request data was invalid.",
|
||||
@@ -1153,8 +1155,8 @@
|
||||
"3006": "The project share does not exist.",
|
||||
"3007": "A project with this identifier already exists.",
|
||||
"3008": "The project is archived and can therefore only be accessed read only. This is also true for all tasks associated with this project.",
|
||||
"4001": "The project task text cannot be empty.",
|
||||
"4002": "The project task does not exist.",
|
||||
"4001": "The task title cannot be empty.",
|
||||
"4002": "The task does not exist.",
|
||||
"4003": "All bulk editing tasks must belong to the same project.",
|
||||
"4004": "Need at least one task when bulk editing tasks.",
|
||||
"4005": "You do not have the right to see the task.",
|
||||
|
||||
@@ -43,7 +43,7 @@
|
||||
"forgotPassword": "Zapomenuté heslo?",
|
||||
"resetPassword": "Obnovit heslo",
|
||||
"resetPasswordAction": "Poslat odkaz na obnovení hesla",
|
||||
"resetPasswordSuccess": "Zkontrolujte doručenou poštu! Měli byste mít e-mail s pokyny, jak obnovit své heslo.",
|
||||
"resetPasswordSuccess": "Check your inbox! You should have an email with instructions on how to reset your password.",
|
||||
"passwordsDontMatch": "Hesla se neshodují",
|
||||
"confirmEmailSuccess": "Úspěšně jste potvrdili svůj e-mail! Nyní se můžete přihlásit.",
|
||||
"totpTitle": "Kód dvoufaktorového ověření",
|
||||
@@ -61,7 +61,7 @@
|
||||
"usernameMustNotLookLikeUrl": "Uživatelské jméno nesmí vypadat jako adresa URL.",
|
||||
"passwordRequired": "Zadejte prosím heslo.",
|
||||
"passwordNotMin": "Heslo musí mít nejméně 8 znaků.",
|
||||
"passwordNotMax": "Heslo může mít maximálně 250 znaků.",
|
||||
"passwordNotMax": "Password must have at most 72 characters.",
|
||||
"showPassword": "Ukázat heslo",
|
||||
"hidePassword": "Skrýt heslo",
|
||||
"noAccountYet": "Ještě nemáte účet?",
|
||||
@@ -72,21 +72,21 @@
|
||||
"settings": {
|
||||
"title": "Nastavení",
|
||||
"newPasswordTitle": "Aktualizujte své heslo",
|
||||
"newPassword": "Nové heslo",
|
||||
"newPasswordConfirm": "Potvrzení nového hesla",
|
||||
"currentPassword": "Stávající heslo",
|
||||
"newPassword": "New password",
|
||||
"newPasswordConfirm": "New password confirmation",
|
||||
"currentPassword": "Current password",
|
||||
"currentPasswordPlaceholder": "Vaše současné heslo",
|
||||
"passwordsDontMatch": "Nové heslo se neshoduje s potvrzením hesla.",
|
||||
"passwordUpdateSuccess": "Heslo bylo úspěšně změněno.",
|
||||
"updateEmailTitle": "Aktualizujte svou e-mailovou adresu",
|
||||
"updateEmailNew": "Nová e-mailová adresa",
|
||||
"updateEmailNew": "New email address",
|
||||
"updateEmailSuccess": "Vaše e-mailová adresa byla úspěšně aktualizována. Poslali jsme vám odkaz pro její potvrzení.",
|
||||
"general": {
|
||||
"title": "Obecná nastavení",
|
||||
"name": "Jméno",
|
||||
"newName": "Nové jméno",
|
||||
"name": "My Name",
|
||||
"newName": "The new name",
|
||||
"savedSuccess": "Nastavení bylo úspěšně aktualizováno.",
|
||||
"emailReminders": "Posílat mi připomenutí pro úkoly e-mailem",
|
||||
"emailReminders": "Send me reminders for tasks via email",
|
||||
"overdueReminders": "Pošlete mi každý den shrnutí mých zpožděných úkolů",
|
||||
"discoverableByName": "Umožnit ostatním uživatelům přidat mě jako člena do týmů nebo projektů, když hledají mé jméno",
|
||||
"discoverableByEmail": "Umožnit ostatním uživatelům, aby mě přidali jako člena do týmů nebo projektů, když hledají můj úplný e-mail",
|
||||
@@ -95,24 +95,24 @@
|
||||
"weekStartSunday": "Neděle",
|
||||
"weekStartMonday": "Pondělí",
|
||||
"language": "Jazyk",
|
||||
"defaultProject": "Výchozí projekt",
|
||||
"defaultView": "Default View",
|
||||
"timezone": "Časové pásmo",
|
||||
"defaultProject": "Default project",
|
||||
"defaultView": "Default view",
|
||||
"timezone": "Time zone",
|
||||
"overdueTasksRemindersTime": "Čas odeslání emailu o zpožděných úkolech",
|
||||
"filterUsedOnOverview": "Uložený filtr použitý na stránce přehledu"
|
||||
},
|
||||
"totp": {
|
||||
"title": "Dvoufaktorové ověření",
|
||||
"enroll": "Zápis",
|
||||
"finishSetupPart1": "Chcete-li dokončit nastavení, použijte tento tajný klíč ve vaší aplikaci (Google Authenticator nebo podobné):",
|
||||
"finishSetupPart1": "To finish your setup, use this secret in your TOTP app (Google Authenticator or similar):",
|
||||
"finishSetupPart2": "Poté zadejte kód z vaší aplikace.",
|
||||
"scanQR": "Případně můžete naskenovat tento QR kód:",
|
||||
"passcode": "Přístupový kód",
|
||||
"passcodePlaceholder": "Kód vygenerovaný vaší aplikací",
|
||||
"passcodePlaceholder": "A code generated by your TOTP application",
|
||||
"setupSuccess": "Úspěšně jste nastavili dvoufaktorové ověření!",
|
||||
"enterPassword": "Zadejte prosím heslo",
|
||||
"disable": "Zakázat dvoufaktorové ověření",
|
||||
"confirmSuccess": "Úspěšně jste potvrdili nastavení a od nynějška jej můžete používat!",
|
||||
"confirmSuccess": "You've successfully confirmed your TOTP setup and can use it from now on!",
|
||||
"disableSuccess": "Dvoufázové ověření bylo úspěšně vypnuto."
|
||||
},
|
||||
"caldav": {
|
||||
@@ -163,6 +163,7 @@
|
||||
"90d": "90 dní",
|
||||
"permissionExplanation": "Oprávnění vám umožní nastavit, k čemu lze api token použít.",
|
||||
"titleRequired": "Je vyžadován název",
|
||||
"permissionRequired": "Please select at least one permission from the list.",
|
||||
"expired": "Platnost tohoto tokenu vypršela {ago}.",
|
||||
"tokenCreatedSuccess": "Zde je tvůj nový api token: {token}",
|
||||
"tokenCreatedNotSeeAgain": "Ulož jej na zabezpečeném místě, už ho znovu neuvidíš!",
|
||||
@@ -847,7 +848,8 @@
|
||||
"delete": "Smazat tento komentář",
|
||||
"deleteText1": "Opravdu chcete smazat tento komentář?",
|
||||
"deleteSuccess": "Komentář byl úspěšně odstraněn.",
|
||||
"addedSuccess": "Komentář byl úspěšně přidán."
|
||||
"addedSuccess": "Komentář byl úspěšně přidán.",
|
||||
"permalink": "Copy permalink to this comment"
|
||||
},
|
||||
"deferDueDate": {
|
||||
"title": "Odložit datum dokončení",
|
||||
@@ -1141,9 +1143,9 @@
|
||||
"1012": "E-mailová adresa uživatele nebyla potvrzena.",
|
||||
"1013": "Nové heslo je prázdné.",
|
||||
"1014": "Staré heslo je prázdné.",
|
||||
"1015": "Totp je již pro tohoto uživatele povolen.",
|
||||
"1016": "Totp není pro tohoto uživatele povolen.",
|
||||
"1017": "Kód totp je neplatný.",
|
||||
"1015": "TOTP is already enabled for this user.",
|
||||
"1016": "TOTP is not enabled for this user.",
|
||||
"1017": "The TOTP passcode is invalid.",
|
||||
"1018": "Nastavení typu avatara uživatele je neplatné.",
|
||||
"2001": "ID nemůže být prázdné nebo 0.",
|
||||
"2002": "Některé údaje požadavku byly neplatné.",
|
||||
@@ -1153,8 +1155,8 @@
|
||||
"3006": "Sdílení projektu neexistuje.",
|
||||
"3007": "Projekt s tímto identifikátorem již existuje.",
|
||||
"3008": "Projekt je archivován, a proto je přístupný pouze pro čtení. To platí i pro všechny úkoly spojené s tímto projektem.",
|
||||
"4001": "Text úkolu projektu nemůže být prázdný.",
|
||||
"4002": "Úkol projektu neexistuje.",
|
||||
"4001": "The task title cannot be empty.",
|
||||
"4002": "The task does not exist.",
|
||||
"4003": "Všechny úkoly pro hromadnou úpravu musí patřit do stejného projektu.",
|
||||
"4004": "Při hromadných úpravách úkolů je potřeba alespoň jeden úkol.",
|
||||
"4005": "Nemáte právo vidět tento úkol.",
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user