Compare commits
533 Commits
v1.7.2
...
fix-archiv
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e5959fa8da | ||
|
|
b47c7aca34 | ||
|
|
ca91e7a2ed | ||
|
|
b92fddaf65 | ||
|
|
b7de565bac | ||
|
|
9c8e1a9f6e | ||
|
|
7e479b0505 | ||
|
|
8f65ec2ede | ||
|
|
6912ae09cf | ||
|
|
eaa3315348 | ||
|
|
5669bd73cc | ||
|
|
57989384fa | ||
|
|
74c04b2d71 | ||
|
|
74565debbb | ||
|
|
a2744823c1 | ||
|
|
a918459105 | ||
|
|
f9acf7778f | ||
|
|
8537bd00c7 | ||
|
|
4b62c34cc3 | ||
|
|
94d84f2b16 | ||
|
|
14e7c33886 | ||
|
|
60ee9f77ca | ||
|
|
3eaba6fe1a | ||
|
|
cef14ed254 | ||
|
|
0828ae6017 | ||
|
|
3af54d7186 | ||
|
|
f01a4433ed | ||
|
|
b64b29cf6e | ||
|
|
d93b431554 | ||
|
|
d00bd159d2 | ||
|
|
816a8c08f0 | ||
|
|
2c3113334a | ||
|
|
857a0797b4 | ||
|
|
efc3511e15 | ||
|
|
aaccd2e7d2 | ||
|
|
1921143bfd | ||
|
|
07b7df8e68 | ||
|
|
c4804cfcb7 | ||
|
|
6bc5f1df47 | ||
|
|
f8a8cc691c | ||
|
|
a4b2a137e5 | ||
|
|
96b5c3da1d | ||
|
|
a716c0968a | ||
|
|
48c8333ed1 | ||
|
|
8224ff13c3 | ||
|
|
e150cd25e8 | ||
|
|
6206a0cdd1 | ||
|
|
d66b54dd13 | ||
|
|
5491c9444e | ||
|
|
0fc5708253 | ||
|
|
918342eeb7 | ||
|
|
ec66e0f291 | ||
|
|
d76a1f539b | ||
|
|
c81501c2ea | ||
|
|
d62bd4e99a | ||
|
|
863ce50a27 | ||
|
|
a9c4e626ac | ||
|
|
982df96c3c | ||
|
|
bc070cbeb9 | ||
|
|
dbac5f82d3 | ||
|
|
58c7786cf7 | ||
|
|
14db941950 | ||
|
|
36a531c204 | ||
|
|
0fa1f74699 | ||
|
|
9189723970 | ||
|
|
d7bcdeb5a7 | ||
|
|
4d9e81e22f | ||
|
|
466c39d12a | ||
|
|
86e7d04c80 | ||
|
|
a6b5ae3fde | ||
|
|
bb3f41dd69 | ||
|
|
b2bdf4a49d | ||
|
|
91268cdd4c | ||
|
|
6b621d2faa | ||
|
|
93db4fd2d8 | ||
|
|
f166552de0 | ||
|
|
aab3352624 | ||
|
|
26f33e5bc6 | ||
|
|
da4db42380 | ||
|
|
aa46b49ab7 | ||
|
|
b70e893364 | ||
|
|
a6bb454df5 | ||
|
|
099e76aeab | ||
|
|
309b00f2d9 | ||
|
|
acd181133c | ||
|
|
86f8999d30 | ||
|
|
661a64656e | ||
|
|
3380103a8f | ||
|
|
7ebf6e4150 | ||
|
|
05fdd9765c | ||
|
|
72a8c4dd83 | ||
|
|
423630bea6 | ||
|
|
2c9bcbae0b | ||
|
|
f70747ea15 | ||
|
|
b4c1525958 | ||
|
|
ed5df26704 | ||
|
|
d8253d41fd | ||
|
|
4eb7988e2c | ||
|
|
4f68aed812 | ||
|
|
322d01d3e1 | ||
|
|
aa34a0cce3 | ||
|
|
93a988f8f1 | ||
|
|
630fa5b07e | ||
|
|
77f623d261 | ||
|
|
30a4e88cff | ||
|
|
01b897bd8b | ||
|
|
be36e6de6a | ||
|
|
96b7a7ccb3 | ||
|
|
d8dd65d7ee | ||
|
|
aecdcb847d | ||
|
|
a383d389c4 | ||
|
|
565b2edfdd | ||
|
|
c1b4beeb64 | ||
|
|
9d489564d6 | ||
|
|
24a4260e55 | ||
|
|
af8e61ece6 | ||
|
|
f9836d4dfb | ||
|
|
982b867a04 | ||
|
|
a6c9bd5c09 | ||
|
|
8008b9d0cb | ||
|
|
7f01db17d0 | ||
|
|
9ec44bdadd | ||
|
|
660290121c | ||
|
|
7dab7fad81 | ||
|
|
7b0630143d | ||
|
|
17258783c2 | ||
|
|
9398d86fba | ||
|
|
88ef90fce7 | ||
|
|
4213ec0986 | ||
|
|
f4b2563629 | ||
|
|
baf0e0c1f5 | ||
|
|
b888a65b8b | ||
|
|
a6eda5a0b8 | ||
|
|
6449082349 | ||
|
|
44127d4bf6 | ||
|
|
0c95e7ca1e | ||
|
|
e29ac75f3d | ||
|
|
7dba89a03a | ||
|
|
61d23ddc6f | ||
|
|
fc921143d3 | ||
|
|
395c79b32a | ||
|
|
7c683efce6 | ||
|
|
f07eb17dff | ||
|
|
242906162f | ||
|
|
c423f6ecd8 | ||
|
|
3c83320c20 | ||
|
|
89068641ee | ||
|
|
71e5c0d743 | ||
|
|
3e46fe777d | ||
|
|
a73d790a95 | ||
|
|
40b8596275 | ||
|
|
6487ed966f | ||
|
|
c5cb8ed5a7 | ||
|
|
d7ee3b72a6 | ||
|
|
36e2443267 | ||
|
|
2c1b95a3cb | ||
|
|
0628624edf | ||
|
|
2bd854ff18 | ||
|
|
18b6ed080c | ||
|
|
5010f0e0e1 | ||
|
|
82c7145163 | ||
|
|
cb60e70ae9 | ||
|
|
f8a255a9f0 | ||
|
|
38040cc246 | ||
|
|
0961bf088f | ||
|
|
76afb87624 | ||
|
|
8dc8513263 | ||
|
|
4d3f2bf1e4 | ||
|
|
4c82154eb0 | ||
|
|
858334cc64 | ||
|
|
7bb02a9b63 | ||
|
|
1551cdf517 | ||
|
|
8a8a1ac060 | ||
|
|
123e43e626 | ||
|
|
03e54ffdff | ||
|
|
c897074cb3 | ||
|
|
03fa4dc816 | ||
|
|
3ea4c635d0 | ||
|
|
78ed0852ea | ||
|
|
89d46dcab4 | ||
|
|
411626c038 | ||
|
|
56e3215785 | ||
|
|
b81f55057a | ||
|
|
e2dc1c2684 | ||
|
|
e2c5367050 | ||
|
|
15c48b919d | ||
|
|
7ad36b07b1 | ||
|
|
85dbb18663 | ||
|
|
a712be416e | ||
|
|
2246e12a6a | ||
|
|
0975eb7d78 | ||
|
|
f4610dc6eb | ||
|
|
df5b2abf21 | ||
|
|
e865627158 | ||
|
|
fdd6b78fa8 | ||
|
|
58db7712ea | ||
|
|
325ec9ae55 | ||
|
|
935a2a240d | ||
|
|
a0ca4f0a33 | ||
|
|
a0c47f8115 | ||
|
|
957776871d | ||
|
|
fe7d318f3d | ||
|
|
b8155835b6 | ||
|
|
74d9e63888 | ||
|
|
7c5601eed6 | ||
|
|
49acc1a88f | ||
|
|
f072b06b81 | ||
|
|
4be99a93c8 | ||
|
|
e761c9aec9 | ||
|
|
1032e8fb06 | ||
|
|
36d9cd1c76 | ||
|
|
281dcf464e | ||
|
|
90bed2da26 | ||
|
|
ba1f1a99ed | ||
|
|
cdd838cffe | ||
|
|
d1997c0f65 | ||
|
|
1cfc20365e | ||
|
|
ed8877ca6b | ||
|
|
de0dc2782f | ||
|
|
e33dd1527f | ||
|
|
9171ffc88a | ||
|
|
eb65468382 | ||
|
|
7c40172c40 | ||
|
|
ab48cccefc | ||
|
|
1b38ebe89e | ||
|
|
c235f05340 | ||
|
|
3a7219a94f | ||
|
|
accff8c8b6 | ||
|
|
9a9ac07ab2 | ||
|
|
2d6433ab4d | ||
|
|
937d93894a | ||
|
|
fc122027cb | ||
|
|
1e790b7a20 | ||
|
|
33dcef981a | ||
|
|
1c59fd7ed3 | ||
|
|
4d559d4094 | ||
|
|
96b852a0e7 | ||
|
|
aacf6b5d52 | ||
|
|
2573b5728c | ||
|
|
9eb2c04a26 | ||
|
|
71b19be030 | ||
|
|
48d28dc317 | ||
|
|
73c5127088 | ||
|
|
a8831b2c9e | ||
|
|
d8a40611f8 | ||
|
|
762afcfc21 | ||
|
|
6b0e5ae392 | ||
|
|
660621bffb | ||
|
|
ba64441619 | ||
|
|
d63234f385 | ||
|
|
bd4223c721 | ||
|
|
5cbdbc7520 | ||
|
|
5c95b5ac98 | ||
|
|
ef1800c50a | ||
|
|
5c2a6d6f7c | ||
|
|
acdff604e4 | ||
|
|
0aff6c1561 | ||
|
|
6c556263c6 | ||
|
|
e639456a82 | ||
|
|
5dedf7bec3 | ||
|
|
cb8ef37c79 | ||
|
|
ad007bd51a | ||
|
|
ae499d513a | ||
|
|
507c80afc7 | ||
|
|
3d02cacc4d | ||
|
|
f9e96922eb | ||
|
|
791239eabb | ||
|
|
4c72f6d1fb | ||
|
|
34e3310669 | ||
|
|
4bd45d1f5b | ||
|
|
55d02d4955 | ||
|
|
f5fd8c9fe5 | ||
|
|
ff39027869 | ||
|
|
3b1bae3775 | ||
|
|
ef0dde23d0 | ||
|
|
512537afe5 | ||
|
|
e85782da4d | ||
|
|
d86a10af32 | ||
|
|
ceab78e2d8 | ||
|
|
11afab61d7 | ||
|
|
801e691011 | ||
|
|
b3f7c648db | ||
|
|
3842950309 | ||
|
|
57ef6f02bc | ||
|
|
ffb916a48a | ||
|
|
3f78ee72b4 | ||
|
|
2d41ce0ae7 | ||
|
|
484a47e0fd | ||
|
|
09d67f1c05 | ||
|
|
167ca9a595 | ||
|
|
6d9df29a4e | ||
|
|
5ef7ca7723 | ||
|
|
bac0313d7c | ||
|
|
af64824733 | ||
|
|
ad490ce006 | ||
|
|
ff4de2c77b | ||
|
|
bc542aad12 | ||
|
|
1211077dc8 | ||
|
|
9de6a61ea5 | ||
|
|
cc1f059950 | ||
|
|
ef3a754033 | ||
|
|
e24bd88971 | ||
|
|
caf2193a0f | ||
|
|
cb27f36f66 | ||
|
|
6ec8e7ac3b | ||
|
|
dd566ee112 | ||
|
|
7f230f2999 | ||
|
|
48d8e2f453 | ||
|
|
865842e5b8 | ||
|
|
fda26f521f | ||
|
|
70e35f77bf | ||
|
|
0af348fb02 | ||
|
|
b67ae13f3c | ||
|
|
d9252e37b3 | ||
|
|
3b6e3d636d | ||
|
|
0443ca3185 | ||
|
|
01e2708233 | ||
|
|
9d3f09fafa | ||
|
|
22a208e505 | ||
|
|
cbc26cd135 | ||
|
|
8c5c7d13b8 | ||
|
|
7e042c9dab | ||
|
|
4a744bac5c | ||
|
|
89c22acb95 | ||
|
|
007ed35a66 | ||
|
|
2bb0e268c8 | ||
|
|
d6c7601a14 | ||
|
|
a79ec6c5ed | ||
|
|
4eea383f74 | ||
|
|
45746fbf34 | ||
|
|
7ac40d2d99 | ||
|
|
bebd157586 | ||
|
|
02a6fa7418 | ||
|
|
bc16421b64 | ||
|
|
bcc4cf8b57 | ||
|
|
d7418c7ad1 | ||
|
|
c7fb25e3f8 | ||
|
|
9e26af66bf | ||
|
|
b741ea79e5 | ||
|
|
7d2f5065be | ||
|
|
a9e7f94409 | ||
|
|
d3be13182d | ||
|
|
a4658be5b6 | ||
|
|
b3f252ee46 | ||
|
|
f98b5764ea | ||
|
|
9f8fcec1ad | ||
|
|
deb4b71e7b | ||
|
|
90a2b07e5f | ||
|
|
e69f909b61 | ||
|
|
0686575484 | ||
|
|
c7cadedd21 | ||
|
|
78d41878e8 | ||
|
|
a527ccd8ed | ||
|
|
5f5f745892 | ||
|
|
6160d67032 | ||
|
|
641341d75d | ||
|
|
146af217a0 | ||
|
|
4a0410b609 | ||
|
|
e858a42455 | ||
|
|
b037cb0e82 | ||
|
|
5455b436f6 | ||
|
|
786e13b0b7 | ||
|
|
759fb1bcbc | ||
|
|
227c962ec4 | ||
|
|
f03ffd13a0 | ||
|
|
00120a6b37 | ||
|
|
884121bdae | ||
|
|
48b92ce0b1 | ||
|
|
f8e1326837 | ||
|
|
00cf15935b | ||
|
|
172ee5a228 | ||
|
|
2f2a43e9c6 | ||
|
|
a9e32dfc99 | ||
|
|
5450b0fd0b | ||
|
|
e09581aa78 | ||
|
|
e08a8ff132 | ||
|
|
8ee0a0fad0 | ||
|
|
4b3ed4f43d | ||
|
|
6ed2e11730 | ||
|
|
e784dafefe | ||
|
|
6cdc6f7bdb | ||
|
|
60a7a7cf94 | ||
|
|
765c0ea17c | ||
|
|
de7de4cd8a | ||
|
|
19473ddba9 | ||
|
|
14421b533d | ||
|
|
21fc5d590f | ||
|
|
24651db512 | ||
|
|
27644bd032 | ||
|
|
0c2e7fae9f | ||
|
|
9d4824bb23 | ||
|
|
3b19b4212b | ||
|
|
69c2b3ffbb | ||
|
|
92ca6261b8 | ||
|
|
9e5e160845 | ||
|
|
6a66e920fe | ||
|
|
cab9f6ab4e | ||
|
|
0395f01c33 | ||
|
|
de88f17a1a | ||
|
|
9db8f9c924 | ||
|
|
7328a27524 | ||
|
|
8ee41e9608 | ||
|
|
0ed6403141 | ||
|
|
6980661a8e | ||
|
|
4797313f9e | ||
|
|
b17774da6e | ||
|
|
a4bba21610 | ||
|
|
7e1d522601 | ||
|
|
cd2225bbc8 | ||
|
|
62af22c928 | ||
|
|
2b531c5302 | ||
|
|
41ed0cee1d | ||
|
|
ce7da62a88 | ||
|
|
f93772ebc9 | ||
|
|
f63b052864 | ||
|
|
43dbdd049f | ||
|
|
6a489eaaf3 | ||
|
|
3a783a722a | ||
|
|
44481e1c2a | ||
|
|
e3d1970369 | ||
|
|
2c7708dab1 | ||
|
|
e156ff77f8 | ||
|
|
5f5f22ee8c | ||
|
|
011b60b3bf | ||
|
|
9bd8669cf4 | ||
|
|
4b5a47f127 | ||
|
|
db45e7907c | ||
|
|
03f400620e | ||
|
|
d938a528c3 | ||
|
|
2309749d15 | ||
|
|
b37b8eaeaf | ||
|
|
9c57243d5a | ||
|
|
6518cd464c | ||
|
|
7e32a16969 | ||
|
|
349c94ee23 | ||
|
|
167774e3b0 | ||
|
|
f2ba1bd4ab | ||
|
|
3ce8041a12 | ||
|
|
a4bbf8f233 | ||
|
|
1b813c4ea2 | ||
|
|
e6c2d85197 | ||
|
|
23ae20efe7 | ||
|
|
37b3f03809 | ||
|
|
43540b008c | ||
|
|
169ccbb47f | ||
|
|
0af35817a6 | ||
|
|
c942542079 | ||
|
|
8a6d8e0549 | ||
|
|
0c4dbebaeb | ||
|
|
5995ec299c | ||
|
|
e2c82fe3dc | ||
|
|
39f6c12d33 | ||
|
|
ae24d9ef03 | ||
|
|
5a1f5757c1 | ||
|
|
b194b88c49 | ||
|
|
b86fbb3cd8 | ||
|
|
9fca104059 | ||
|
|
9369a697e3 | ||
|
|
ebbafbe55d | ||
|
|
6bf9ba397e | ||
|
|
7b7af75802 | ||
|
|
723ce6c893 | ||
|
|
ed3be361b5 | ||
|
|
0b6990f828 | ||
|
|
2af94410f5 | ||
|
|
b348565449 | ||
|
|
aa06255e26 | ||
|
|
a0d967cdc7 | ||
|
|
494a72709d | ||
|
|
8947cebf04 | ||
|
|
b51993db0c | ||
|
|
5c8f80a907 | ||
|
|
9fab40d12a | ||
|
|
420a5fa782 | ||
|
|
fe335ea865 | ||
|
|
ad04a483e3 | ||
|
|
b3228f1e27 | ||
|
|
249935fc66 | ||
|
|
53c0ccfba8 | ||
|
|
4706adb0d5 | ||
|
|
53b6bf0986 | ||
|
|
65d9a75421 | ||
|
|
c089192904 | ||
|
|
a8af3310b4 | ||
|
|
58e6989307 | ||
|
|
18f7ea2a7e | ||
|
|
4e8219c6fa | ||
|
|
24f7ef69c7 | ||
|
|
1578c13bf5 | ||
|
|
ca2aa5d7f7 | ||
|
|
4d43f83443 | ||
|
|
319869bc6d | ||
|
|
405fb52cb2 | ||
|
|
76a6fad4e2 | ||
|
|
47be49253b | ||
|
|
5177c793a7 | ||
|
|
bb06ac0c42 | ||
|
|
20668bef9e | ||
|
|
747f8d4567 | ||
|
|
501927c844 | ||
|
|
d03ae91d11 | ||
|
|
cc2f45f764 | ||
|
|
713dcd5d08 | ||
|
|
8b69c90bf1 | ||
|
|
5513122ca9 | ||
|
|
7507ac31f7 | ||
|
|
49e51675d3 | ||
|
|
056d54d313 | ||
|
|
00eac849fe | ||
|
|
75a17dae2c | ||
|
|
62c81ac785 | ||
|
|
15c5170195 | ||
|
|
4b302c0330 | ||
|
|
7977fa40e7 | ||
|
|
45c4c507bc | ||
|
|
33f2e03e13 | ||
|
|
56391021a4 | ||
|
|
ab831c2604 | ||
|
|
11fc4d88aa | ||
|
|
57dd1982a0 | ||
|
|
3f754dc662 | ||
|
|
0c5b1a88a6 | ||
|
|
1d5fdef4b4 | ||
|
|
815b8597d1 | ||
|
|
53e3a7ae7f | ||
|
|
04974d37d6 | ||
|
|
8c1e53a8df | ||
|
|
8399b00a10 | ||
|
|
9244adee30 | ||
|
|
3265a9de7b | ||
|
|
ec602f3e15 | ||
|
|
6ed7f672fc | ||
|
|
c9bb49e0f7 |
@@ -3,7 +3,10 @@ root = true
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = tab
|
||||
indent_style = tab
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{js,vue}]
|
||||
indent_style = tab
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
root: true,
|
||||
extends: [
|
||||
'@nextcloud',
|
||||
],
|
||||
@@ -7,6 +8,7 @@ module.exports = {
|
||||
'jsdoc/require-param-type': ['off'],
|
||||
'jsdoc/check-param-names': ['off'],
|
||||
'jsdoc/no-undefined-types': ['off'],
|
||||
'jsdoc/require-property-description' : ['off']
|
||||
'jsdoc/require-property-description': ['off'],
|
||||
'import/no-named-as-default-member': ['off']
|
||||
},
|
||||
}
|
||||
|
||||
8
.github/workflows/appbuild.yml
vendored
@@ -1,7 +1,11 @@
|
||||
name: Package build
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- stable*
|
||||
|
||||
jobs:
|
||||
build:
|
||||
@@ -20,7 +24,7 @@ jobs:
|
||||
- name: Set up npm7
|
||||
run: npm i -g npm@7
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@2.18.0
|
||||
uses: shivammathur/setup-php@2.21.2
|
||||
with:
|
||||
php-version: '7.4'
|
||||
tools: composer
|
||||
|
||||
2
.github/workflows/appstore-build-publish.yml
vendored
@@ -66,7 +66,7 @@ jobs:
|
||||
run: npm i -g npm@"${{ steps.versions.outputs.npmVersion }}"
|
||||
|
||||
- name: Set up php ${{ env.PHP_VERSION }}
|
||||
uses: shivammathur/setup-php@2.18.0
|
||||
uses: shivammathur/setup-php@2.21.2
|
||||
with:
|
||||
php-version: ${{ env.PHP_VERSION }}
|
||||
coverage: none
|
||||
|
||||
7
.github/workflows/command-rebase.yml
vendored
@@ -9,9 +9,14 @@ on:
|
||||
issue_comment:
|
||||
types: created
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
rebase:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: none
|
||||
|
||||
# On pull requests and if the comment starts with `/rebase`
|
||||
if: github.event.issue.pull_request != '' && startsWith(github.event.comment.body, '/rebase')
|
||||
@@ -32,7 +37,7 @@ jobs:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
- name: Automatic Rebase
|
||||
uses: cirrus-actions/rebase@1.5
|
||||
uses: cirrus-actions/rebase@1.7
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
|
||||
|
||||
112
.github/workflows/cypress.yml
vendored
Normal file
@@ -0,0 +1,112 @@
|
||||
name: Cypress
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable*
|
||||
|
||||
env:
|
||||
APP_NAME: deck
|
||||
CYPRESS_baseUrl: http://localhost:8081/index.php
|
||||
|
||||
jobs:
|
||||
cypress:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
node-version: [14.x]
|
||||
# containers: [1, 2, 3]
|
||||
php-versions: [ '7.4' ]
|
||||
databases: [ 'sqlite' ]
|
||||
server-versions: [ 'master' ]
|
||||
|
||||
steps:
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Set up npm7
|
||||
run: npm i -g npm@7
|
||||
|
||||
- name: Checkout server
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
repository: nextcloud/server
|
||||
ref: ${{ matrix.server-versions }}
|
||||
|
||||
- name: Checkout submodules
|
||||
shell: bash
|
||||
run: |
|
||||
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
|
||||
git submodule sync --recursive
|
||||
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
|
||||
|
||||
- name: Checkout ${{ env.APP_NAME }}
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
path: apps/${{ env.APP_NAME }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.21.2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: mbstring, iconv, fileinfo, intl, sqlite, pdo_sqlite, zip, gd, apcu
|
||||
ini-values:
|
||||
apc.enable_cli=on
|
||||
coverage: none
|
||||
|
||||
- name: Set up Nextcloud
|
||||
env:
|
||||
DB_PORT: 4444
|
||||
PHP_CLI_SERVER_WORKERS: 10
|
||||
run: |
|
||||
mkdir data
|
||||
php occ maintenance:install --verbose --database=${{ matrix.databases }} --database-name=nextcloud --database-host=127.0.0.1 --database-port=$DB_PORT --database-user=root --database-pass=rootpassword --admin-user admin --admin-pass admin
|
||||
php occ config:system:set memcache.local --value="\\OC\\Memcache\\APCu"
|
||||
php occ config:system:set debug --value=true --type=boolean
|
||||
php -f index.php
|
||||
php -S 0.0.0.0:8081 &
|
||||
export OC_PASS=1234561
|
||||
php occ user:add --password-from-env user1
|
||||
php occ user:add --password-from-env user2
|
||||
php occ app:enable deck
|
||||
php occ app:list
|
||||
cd apps/deck
|
||||
composer install --no-dev
|
||||
npm ci
|
||||
npm run build
|
||||
cd ../../
|
||||
curl -v http://localhost:8081/index.php/login
|
||||
|
||||
- name: Cypress run
|
||||
uses: cypress-io/github-action@v4
|
||||
with:
|
||||
record: true
|
||||
parallel: false
|
||||
wait-on: '${{ env.CYPRESS_baseUrl }}'
|
||||
working-directory: 'apps/${{ env.APP_NAME }}'
|
||||
config: defaultCommandTimeout=10000,video=false
|
||||
env:
|
||||
CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }}
|
||||
npm_package_name: ${{ env.APP_NAME }}
|
||||
|
||||
- name: Upload test failure screenshots
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: Upload screenshots
|
||||
path: apps/${{ env.APP_NAME }}/cypress/screenshots/
|
||||
retention-days: 5
|
||||
|
||||
- name: Upload nextcloud logs
|
||||
uses: actions/upload-artifact@v3
|
||||
if: failure()
|
||||
with:
|
||||
name: Upload nextcloud log
|
||||
path: data/nextcloud.log
|
||||
retention-days: 5
|
||||
@@ -8,13 +8,20 @@ name: Dependabot
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- main
|
||||
- master
|
||||
- stable*
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
auto-approve-merge:
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
# for hmarr/auto-approve-action to approve PRs
|
||||
pull-requests: write
|
||||
|
||||
steps:
|
||||
# Github actions bot approve
|
||||
|
||||
16
.github/workflows/integration.yml
vendored
@@ -2,6 +2,14 @@ name: Integration tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/integration.yml'
|
||||
- 'appinfo/**'
|
||||
- 'lib/**'
|
||||
- 'templates/**'
|
||||
- 'tests/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -19,7 +27,7 @@ jobs:
|
||||
matrix:
|
||||
php-versions: ['7.4']
|
||||
databases: ['sqlite', 'mysql', 'pgsql']
|
||||
server-versions: ['stable24']
|
||||
server-versions: ['master']
|
||||
|
||||
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
|
||||
|
||||
@@ -54,7 +62,7 @@ jobs:
|
||||
auth_header="$(git config --local --get http.https://github.com/.extraheader)"
|
||||
git submodule sync --recursive
|
||||
git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1
|
||||
cd build/integration && composer require --dev phpunit/phpunit:~8
|
||||
cd build/integration && composer require --dev phpunit/phpunit:~9
|
||||
|
||||
- name: Checkout app
|
||||
uses: actions/checkout@v3
|
||||
@@ -62,7 +70,7 @@ jobs:
|
||||
path: apps/${{ env.APP_NAME }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.18.0
|
||||
uses: shivammathur/setup-php@2.21.2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
tools: phpunit
|
||||
@@ -71,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Set up PHPUnit
|
||||
working-directory: apps/${{ env.APP_NAME }}
|
||||
run: composer i
|
||||
run: composer i --no-dev
|
||||
|
||||
- name: Set up Nextcloud
|
||||
run: |
|
||||
|
||||
4
.github/workflows/lint.yml
vendored
@@ -19,7 +19,7 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up php${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.18.0
|
||||
uses: shivammathur/setup-php@2.21.2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
coverage: none
|
||||
@@ -33,7 +33,7 @@ jobs:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up php
|
||||
uses: shivammathur/setup-php@2.18.0
|
||||
uses: shivammathur/setup-php@2.21.2
|
||||
with:
|
||||
php-version: 7.4
|
||||
coverage: none
|
||||
|
||||
2
.github/workflows/nightly.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
- name: Set up npm7
|
||||
run: npm i -g npm@7
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@2.18.0
|
||||
uses: shivammathur/setup-php@2.21.2
|
||||
with:
|
||||
php-version: '7.4'
|
||||
tools: composer
|
||||
|
||||
12
.github/workflows/phpunit.yml
vendored
@@ -2,6 +2,14 @@ name: PHPUnit
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- '.github/workflows/phpunit.yml'
|
||||
- 'appinfo/**'
|
||||
- 'lib/**'
|
||||
- 'templates/**'
|
||||
- 'tests/**'
|
||||
- 'composer.json'
|
||||
- 'composer.lock'
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
@@ -20,7 +28,7 @@ jobs:
|
||||
matrix:
|
||||
php-versions: ['7.4', '8.0', '8.1']
|
||||
databases: ['sqlite', 'mysql', 'pgsql']
|
||||
server-versions: ['stable24']
|
||||
server-versions: ['master']
|
||||
|
||||
name: php${{ matrix.php-versions }}-${{ matrix.databases }}-${{ matrix.server-versions }}
|
||||
|
||||
@@ -62,7 +70,7 @@ jobs:
|
||||
path: apps/${{ env.APP_NAME }}
|
||||
|
||||
- name: Set up php ${{ matrix.php-versions }}
|
||||
uses: shivammathur/setup-php@2.18.0
|
||||
uses: shivammathur/setup-php@2.21.2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
tools: phpunit
|
||||
|
||||
2
.github/workflows/update-nextcloud-ocp.yml
vendored
@@ -49,7 +49,7 @@ jobs:
|
||||
continue-on-error: true
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v3
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.COMMAND_BOT_PAT }}
|
||||
commit-message: Update psalm baseline
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
lang_map = bg_BG: bg, cs_CZ: cs, fi_FI: fi, hu_HU: hu, nb_NO: nb, sk_SK: sk, th_TH: th, ja_JP: ja
|
||||
lang_map = hu_HU: hu, nb_NO: nb, sk_SK: sk, th_TH: th, ja_JP: ja, bg_BG: bg, cs_CZ: cs, fi_FI: fi
|
||||
|
||||
[o:nextcloud:p:nextcloud:r:deck]
|
||||
file_filter = translationfiles/<lang>/deck.po
|
||||
|
||||
55
CHANGELOG.md
@@ -1,30 +1,45 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## 1.7.2
|
||||
## 1.8.0-beta.1
|
||||
### Enhancements
|
||||
|
||||
- Nextcloud 25 compatibility
|
||||
- Performance improvements
|
||||
- Use capped memory cache for board permissions @juliushaertl [#3980](https://github.com/nextcloud/deck/pull/3980)
|
||||
- Improve CalDAV integration performance @juliushaertl [#3982](https://github.com/nextcloud/deck/pull/3982)
|
||||
- Simpify query for getting shared files @juliushaertl [#3983](https://github.com/nextcloud/deck/pull/3983)
|
||||
- Accessibility improvements
|
||||
- Add a11y label for sidebar button @marcelklehr [#3986](https://github.com/nextcloud/deck/pull/3986)
|
||||
- Improve filter popover accessibility @juliushaertl [#3820](https://github.com/nextcloud/deck/pull/3820)
|
||||
- Set ids to skip to content/navigation @juliushaertl [#3924](https://github.com/nextcloud/deck/pull/3924)
|
||||
- Invert icons properly in dark mode @juliushaertl [#3939](https://github.com/nextcloud/deck/pull/3939)
|
||||
- Bump dependencies
|
||||
|
||||
### Fixed
|
||||
|
||||
- Cache user membership for circles [#4132](https://github.com/nextcloud/deck/pull/4132)
|
||||
- Set event link also for notifications that get emitted from activities [#4118](https://github.com/nextcloud/deck/pull/4118)
|
||||
- Fix Card menu not displaying when description is not set [#4103](https://github.com/nextcloud/deck/pull/4103)
|
||||
- disable Create card button while no stack is chosen [#4019](https://github.com/nextcloud/deck/pull/4019)
|
||||
- to nextcloud/OCP package in stable24 [#4093](https://github.com/nextcloud/deck/pull/4093)
|
||||
- Fix attachment creator name: show display name [#4037](https://github.com/nextcloud/deck/pull/4037)
|
||||
- Use capped memory cache for board permissions [#3997](https://github.com/nextcloud/deck/pull/3997)
|
||||
- Improve CalDAV integration performance [#3995](https://github.com/nextcloud/deck/pull/3995)
|
||||
- Fetch attachment folder for the correct user during cron job [#3959](https://github.com/nextcloud/deck/pull/3959)
|
||||
- Switch to 'markdown-it-task-checkbox' for rendering of task lists [#3925](https://github.com/nextcloud/deck/pull/3925)
|
||||
- Prevent opening card and applyLabelFilter on card drag end [#3917](https://github.com/nextcloud/deck/pull/3917)
|
||||
- Fix for issue #3637 [#3901](https://github.com/nextcloud/deck/pull/3901)
|
||||
- Fix z-index for deck sidebar [#3885](https://github.com/nextcloud/deck/pull/3885)
|
||||
- set last modified when the card was found. Fixes #3763 @ylebre [#3796](https://github.com/nextcloud/deck/pull/3796)
|
||||
- Increase file count after sharing @luka-nextcloud [#3682](https://github.com/nextcloud/deck/pull/3682)
|
||||
- Align Duedate-delete icon properly - fixes nextcloud/deck#3791 @Ben-Ro [#3811](https://github.com/nextcloud/deck/pull/3811)
|
||||
- Fix for issue #3637 @flummer [#3833](https://github.com/nextcloud/deck/pull/3833)
|
||||
- Switch to 'markdown-it-task-checkbox' for rendering of task lists @q-wertz [#3898](https://github.com/nextcloud/deck/pull/3898)
|
||||
- Make rename functions accessibly by keyboard navigation @juliushaertl [#3813](https://github.com/nextcloud/deck/pull/3813)
|
||||
- Prevent opening card and applyLabelFilter on card drag end @eneiluj [#3916](https://github.com/nextcloud/deck/pull/3916)
|
||||
- Inserted required property in the rename list field, to prevent the l… @mstolf [#3862](https://github.com/nextcloud/deck/pull/3862)
|
||||
- Fix share provider for master changes @nickvergessen [#3942](https://github.com/nextcloud/deck/pull/3942)
|
||||
- Fetch attachment folder for the correct user during cron job @juliushaertl [#3952](https://github.com/nextcloud/deck/pull/3952)
|
||||
- Fix z-index for deck sidebar @Raudius [#3884](https://github.com/nextcloud/deck/pull/3884)
|
||||
|
||||
## 1.7.1
|
||||
### Other
|
||||
|
||||
### Fixed
|
||||
- Align Duedate-delete icon properly - fixes nextcloud/deck#3791 [#3817](https://github.com/nextcloud/deck/pull/3817)
|
||||
- Increase file count after sharing [#3806](https://github.com/nextcloud/deck/pull/3806)
|
||||
- Fetch full board data after cloning [#3781](https://github.com/nextcloud/deck/pull/3781)
|
||||
- Switch from OC::$server->get to OCP\Server::get @CarlSchwan [#3801](https://github.com/nextcloud/deck/pull/3801)
|
||||
- Add performance section in README @eneiluj [#3830](https://github.com/nextcloud/deck/pull/3830)
|
||||
- Fix static analysis by stubbing more circle methods @juliushaertl [#3900](https://github.com/nextcloud/deck/pull/3900)
|
||||
- fix(docs): fix links to JSON schemas for Trello @wiktor2200 [#3872](https://github.com/nextcloud/deck/pull/3872)
|
||||
- Move to OCP\Collaboration\Resources\LoadAdditionalScriptsEvent @juliushaertl [#3818](https://github.com/nextcloud/deck/pull/3818)
|
||||
- Rename settings to deck settings @PVince81 [#3928](https://github.com/nextcloud/deck/pull/3928)
|
||||
- SCSS cleanup @juliushaertl [#3803](https://github.com/nextcloud/deck/pull/3803)
|
||||
- Hide deprecated projects in sidebar and card details by default @Pytal [#3984](https://github.com/nextcloud/deck/pull/3984)
|
||||
|
||||
## 1.7.0
|
||||
|
||||
@@ -76,7 +91,7 @@ All notable changes to this project will be documented in this file.
|
||||
- Adapt the card modal to upstream changes [#3764](https://github.com/nextcloud/deck/pull/3764)
|
||||
- Fix text selection in dark mode and modal view [#3765](https://github.com/nextcloud/deck/pull/3765)
|
||||
- Add missing indices [#3754](https://github.com/nextcloud/deck/pull/3754)
|
||||
- Handle qb mapper exception messages properly @juliushaertl [#3769](https://github.com/nextcloud/deck/pull/3769)
|
||||
|
||||
|
||||
## 1.6.0-beta1
|
||||
|
||||
|
||||
10
Makefile
@@ -30,6 +30,16 @@ build: clean-dist install-deps build-js
|
||||
|
||||
release: clean-dist install-deps-nodev build-js
|
||||
|
||||
lint: lint-js lint-php
|
||||
|
||||
lint-js:
|
||||
npm run lint
|
||||
npm run stylelint
|
||||
|
||||
lint-php:
|
||||
composer run lint 1>/dev/null
|
||||
composer run cs:check
|
||||
|
||||
build-js: install-deps-js
|
||||
npm run build
|
||||
|
||||
|
||||
22
README.md
@@ -24,9 +24,9 @@ Deck is a kanban style organization tool aimed at personal planning and project
|
||||
### 3rd-Party Integrations
|
||||
|
||||
- [trello-to-deck](https://github.com/maxammann/trello-to-deck) - Migrates cards from Trello
|
||||
- [mail2deck](https://github.com/newroco/mail2deck) - Provides an "email in" solution
|
||||
- [mail2deck](https://github.com/newroco/mail2deck) - Provides an "email in" solution
|
||||
- [A-deck](https://github.com/leoossa/A-deck) - Chrome Extension that allows to create new card in selected stack based on current tab
|
||||
|
||||
|
||||
## Installation/Update
|
||||
|
||||
This app is supposed to work on the two latest Nextcloud versions.
|
||||
@@ -52,14 +52,32 @@ Please make sure you have installed the following dependencies: `make, which, ta
|
||||
|
||||
Instead of setting everything up manually, you can just [download the nightly build](https://github.com/nextcloud/deck/releases/tag/nightly) instead. These builds are updated every 24 hours, and are pre-configured with all the needed dependencies.
|
||||
|
||||
## Performance limitations
|
||||
|
||||
Deck is not yet ready for intensive usage.
|
||||
A lot of database queries are generated when the number of boards, cards and attachments is high.
|
||||
For example, a user having access to 13 boards, with each board having on average 100 cards,
|
||||
and each card having on average 5 attachments,
|
||||
would generate 6500 database queries when doing the file related queries
|
||||
which would increase the page loading time significantly.
|
||||
|
||||
Improvements on Nextcloud server and Deck itself will improve the situation.
|
||||
|
||||
## Developing
|
||||
|
||||
### Nextcloud environment
|
||||
|
||||
You need to setup a [development environment](https://docs.nextcloud.com/server/latest/developer_manual//getting_started/devenv.html) of the current nextcloud version. You can also alternatively install & run the [nextcloud docker container](https://github.com/juliushaertl/nextcloud-docker-dev).
|
||||
After the finished installation, you can clone the deck project directly in the `/[nextcloud-docker-dev-dir]/workspace/server/apps/` folder.
|
||||
|
||||
### PHP
|
||||
|
||||
Nothing to prepare, just dig into the code.
|
||||
|
||||
### JavaScript
|
||||
|
||||
This requires at least Node 16 and npm 7 to be installed.
|
||||
|
||||
Deck requires running a `make build-js` to install npm dependencies and build the JavaScript code using webpack. While developing you can also use `make watch` to rebuild everytime the code changes.
|
||||
|
||||
#### Hot reloading
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
- 🚀 Get your project organized
|
||||
|
||||
</description>
|
||||
<version>1.7.2</version>
|
||||
<version>1.9.0-beta.1</version>
|
||||
<licence>agpl</licence>
|
||||
<author>Julius Härtl</author>
|
||||
<namespace>Deck</namespace>
|
||||
@@ -34,7 +34,7 @@
|
||||
<database min-version="9.4">pgsql</database>
|
||||
<database>sqlite</database>
|
||||
<database min-version="8.0">mysql</database>
|
||||
<nextcloud min-version="24" max-version="24"/>
|
||||
<nextcloud min-version="26" max-version="26"/>
|
||||
</dependencies>
|
||||
<background-jobs>
|
||||
<job>OCA\Deck\Cron\DeleteCron</job>
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"symfony/event-dispatcher": "^4.0",
|
||||
"vimeo/psalm": "^4.3",
|
||||
"php-parallel-lint/php-parallel-lint": "^1.2",
|
||||
"nextcloud/ocp": "dev-stable24"
|
||||
"nextcloud/ocp": "dev-master"
|
||||
},
|
||||
"config": {
|
||||
"optimize-autoloader": true,
|
||||
@@ -36,6 +36,7 @@
|
||||
"cs:check": "php-cs-fixer fix --dry-run --diff",
|
||||
"cs:fix": "php-cs-fixer fix",
|
||||
"psalm": "psalm",
|
||||
"psalm:update-baseline": "psalm --update-baseline",
|
||||
"psalm:fix": "psalm --alter --issues=InvalidReturnType,InvalidNullableReturnType,MismatchingDocblockParamType,MismatchingDocblockReturnType,MissingParamType,InvalidFalsableReturnType",
|
||||
"test": [
|
||||
"@test:unit",
|
||||
|
||||
421
composer.lock
generated
9
css/deck.css
Normal file
@@ -0,0 +1,9 @@
|
||||
.icon-deck {
|
||||
background-image: url(../img/deck-dark.svg);
|
||||
filter: var(--background-invert-if-dark);
|
||||
}
|
||||
|
||||
.icon-deck-white, .icon-deck.icon-white {
|
||||
background-image: url(../img/deck.svg);
|
||||
filter: var(--background-invert-if-dark);
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@include icon-black-white('deck', 'deck', 1);
|
||||
@@ -1,41 +0,0 @@
|
||||
/**
|
||||
* Custom icons
|
||||
*/
|
||||
@include icon-black-white('deck', 'deck', 1);
|
||||
@include icon-black-white('archive', 'deck', 1);
|
||||
@include icon-black-white('circles', 'deck', 1);
|
||||
@include icon-black-white('clone', 'deck', 1);
|
||||
@include icon-black-white('filter', 'deck', 1);
|
||||
@include icon-black-white('filter_set', 'deck', 1);
|
||||
@include icon-black-white('attach', 'deck', 1);
|
||||
@include icon-black-white('reply', 'deck', 1);
|
||||
@include icon-black-white('notifications-dark', 'deck', 1);
|
||||
@include icon-black-white('description', 'deck', 1);
|
||||
|
||||
.icon-toggle-compact-collapsed {
|
||||
@include icon-color('toggle-view-expand', 'deck', $color-black);
|
||||
}
|
||||
|
||||
.icon-toggle-compact-expanded {
|
||||
@include icon-color('toggle-view-collapse', 'deck', $color-black);
|
||||
}
|
||||
.icon-activity {
|
||||
@include icon-color('activity-dark', 'activity', $color-black);
|
||||
}
|
||||
.icon-comment--unread {
|
||||
@include icon-color('comment', 'actions', $color-primary, 1, true);
|
||||
}
|
||||
|
||||
.avatardiv.circles {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.icon-circles {
|
||||
opacity: 1;
|
||||
background-size: 20px;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.icon-colorpicker {
|
||||
background-image: url('../img/color_picker.svg');
|
||||
}
|
||||
17
cypress.config.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const { defineConfig } = require('cypress')
|
||||
|
||||
module.exports = defineConfig({
|
||||
projectId: '1s7wkc',
|
||||
viewportWidth: 1280,
|
||||
viewportHeight: 720,
|
||||
e2e: {
|
||||
// We've imported your old cypress plugins here.
|
||||
// You may want to clean this up later by importing these.
|
||||
setupNodeEvents(on, config) {
|
||||
return require('./cypress/plugins/index.js')(on, config)
|
||||
},
|
||||
baseUrl: 'http://nextcloud.local/index.php',
|
||||
experimentalSessionAndOrigin: true,
|
||||
specPattern: 'cypress/e2e/**/*.{js,jsx,ts,tsx}',
|
||||
},
|
||||
})
|
||||
41
cypress/e2e/boardFeatures.js
Normal file
@@ -0,0 +1,41 @@
|
||||
import { randHash } from '../utils'
|
||||
const randUser = randHash()
|
||||
|
||||
describe('Board', function() {
|
||||
const password = 'pass123'
|
||||
|
||||
before(function() {
|
||||
cy.nextcloudCreateUser(randUser, password)
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login(randUser, password)
|
||||
})
|
||||
|
||||
it('Can create a board', function() {
|
||||
const board = 'Test'
|
||||
|
||||
cy.intercept({
|
||||
method: 'POST',
|
||||
url: '/index.php/apps/deck/boards',
|
||||
}).as('createBoardRequest')
|
||||
|
||||
// Click "Add board"
|
||||
cy.openLeftSidebar()
|
||||
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry')
|
||||
.eq(3).find('a').first().click({ force: true })
|
||||
|
||||
// Type the board title
|
||||
cy.get('.board-create form input[type=text]')
|
||||
.type(board, { force: true })
|
||||
|
||||
// Submit
|
||||
cy.get('.board-create form input[type=submit]')
|
||||
.first().click({ force: true })
|
||||
|
||||
cy.wait('@createBoardRequest').its('response.statusCode').should('equal', 200)
|
||||
|
||||
cy.get('.app-navigation__list .app-navigation-entry__children .app-navigation-entry')
|
||||
.contains(board).should('be.visible')
|
||||
})
|
||||
})
|
||||
67
cypress/e2e/cardFeatures.js
Normal file
@@ -0,0 +1,67 @@
|
||||
import { randHash } from '../utils'
|
||||
const randUser = randHash()
|
||||
|
||||
const testBoardData = {
|
||||
title: 'MyBoardTest',
|
||||
color: '00ff00',
|
||||
stacks: [
|
||||
{
|
||||
title: 'TestList',
|
||||
cards: [
|
||||
{
|
||||
title: 'Hello world',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
|
||||
describe('Card', function() {
|
||||
before(function() {
|
||||
cy.nextcloudCreateUser(randUser, randUser)
|
||||
cy.createExampleBoard({
|
||||
user: randUser,
|
||||
password: randUser,
|
||||
board: testBoardData,
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login(randUser, randUser)
|
||||
})
|
||||
|
||||
it('Can show card details modal', function() {
|
||||
cy.openLeftSidebar()
|
||||
cy.getNavigationEntry(testBoardData.title)
|
||||
.first().click({ force: true })
|
||||
|
||||
cy.get('.board .stack').eq(0).within(() => {
|
||||
cy.get('.card:contains("Hello world")').should('be.visible').click()
|
||||
})
|
||||
|
||||
cy.get('.modal__card').should('be.visible')
|
||||
cy.get('.app-sidebar-header__maintitle').contains('Hello world')
|
||||
})
|
||||
|
||||
it('Can add a card', function() {
|
||||
const newCardTitle = 'Write some cypress tests'
|
||||
|
||||
cy.openLeftSidebar()
|
||||
cy.getNavigationEntry(testBoardData.title)
|
||||
.first().click({ force: true })
|
||||
|
||||
cy.get('.board .stack').eq(0).within(() => {
|
||||
cy.get('.card:contains("Hello world")').should('be.visible')
|
||||
|
||||
cy.get('.button-vue[aria-label*="Add card"]')
|
||||
.first().click()
|
||||
|
||||
cy.get('.stack__card-add form input#new-stack-input-main')
|
||||
.type(newCardTitle)
|
||||
cy.get('.stack__card-add form input[type=submit]')
|
||||
.first().click()
|
||||
cy.get(`.card:contains("${newCardTitle}")`).should('be.visible')
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
31
cypress/e2e/deckDashboard.js
Normal file
@@ -0,0 +1,31 @@
|
||||
import { randHash } from '../utils'
|
||||
const randUser = randHash()
|
||||
|
||||
describe('Deck dashboard', function() {
|
||||
const password = 'pass123'
|
||||
|
||||
before(function() {
|
||||
cy.nextcloudCreateUser(randUser, password)
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
cy.login(randUser, password)
|
||||
})
|
||||
|
||||
it('Can show the right title on the dashboard', function() {
|
||||
cy.get('.board-title h2')
|
||||
.should('have.length', 1).first()
|
||||
.should('have.text', 'Upcoming cards')
|
||||
})
|
||||
|
||||
it('Can see the default "Personal Board" created for user by default', function() {
|
||||
const defaultBoard = 'Personal'
|
||||
|
||||
cy.openLeftSidebar()
|
||||
cy.get('.app-navigation-entry-wrapper[icon=icon-deck]')
|
||||
.find('ul.app-navigation-entry__children .app-navigation-entry:contains(' + defaultBoard + ')')
|
||||
.first()
|
||||
.contains(defaultBoard)
|
||||
.should('be.visible')
|
||||
})
|
||||
})
|
||||
30
cypress/e2e/stackFeatures.js
Normal file
@@ -0,0 +1,30 @@
|
||||
import { randHash } from '../utils'
|
||||
const randUser = randHash()
|
||||
|
||||
describe('Stack', function() {
|
||||
const board = 'TestBoard'
|
||||
const password = 'pass123'
|
||||
const stack = 'List 1'
|
||||
|
||||
before(function() {
|
||||
cy.nextcloudCreateUser(randUser, password)
|
||||
cy.deckCreateBoard({ user: randUser, password }, board)
|
||||
})
|
||||
|
||||
beforeEach(function() {
|
||||
cy.logout()
|
||||
cy.login(randUser, password)
|
||||
})
|
||||
|
||||
it('Can create a stack', function() {
|
||||
cy.openLeftSidebar()
|
||||
cy.getNavigationEntry(board)
|
||||
.click({ force: true })
|
||||
|
||||
cy.get('#stack-add button').first().click()
|
||||
cy.get('#stack-add form input#new-stack-input-main').type(stack)
|
||||
cy.get('#stack-add form input[type=submit]').first().click()
|
||||
|
||||
cy.get('.board .stack').eq(0).contains(stack).should('be.visible')
|
||||
})
|
||||
})
|
||||
5
cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
||||
22
cypress/plugins/index.js
Normal file
@@ -0,0 +1,22 @@
|
||||
/// <reference types="cypress" />
|
||||
// ***********************************************************
|
||||
// This example plugins/index.js can be used to load plugins
|
||||
//
|
||||
// You can change the location of this file or turn off loading
|
||||
// the plugins file with the 'pluginsFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/plugins-guide
|
||||
// ***********************************************************
|
||||
|
||||
// This function is called when a project is opened or re-opened (e.g. due to
|
||||
// the project's config changing)
|
||||
|
||||
/**
|
||||
* @type {Cypress.PluginConfig}
|
||||
*/
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
module.exports = (on, config) => {
|
||||
// `on` is used to hook into various events Cypress emits
|
||||
// `config` is the resolved Cypress config
|
||||
}
|
||||
159
cypress/support/commands.js
Normal file
@@ -0,0 +1,159 @@
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
const url = Cypress.config('baseUrl').replace(/\/index.php\/?$/g, '')
|
||||
Cypress.env('baseUrl', url)
|
||||
|
||||
Cypress.Commands.add('login', (user, password, route = '/apps/deck/') => {
|
||||
const session = `${user}-${Date.now()}`
|
||||
cy.session(session, function() {
|
||||
cy.visit(route)
|
||||
cy.get('input[name=user]').type(user)
|
||||
cy.get('input[name=password]').type(password)
|
||||
cy.get('form[name=login] [type=submit]').click()
|
||||
cy.url().should('include', route)
|
||||
})
|
||||
cy.visit(route)
|
||||
})
|
||||
|
||||
Cypress.Commands.add('logout', (route = '/') => {
|
||||
cy.session('_guest', function() {})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('nextcloudCreateUser', (user, password) => {
|
||||
cy.clearCookies()
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `${Cypress.env('baseUrl')}/ocs/v1.php/cloud/users?format=json`,
|
||||
form: true,
|
||||
body: {
|
||||
userid: user,
|
||||
password,
|
||||
},
|
||||
auth: { user: 'admin', pass: 'admin' },
|
||||
headers: {
|
||||
'OCS-ApiRequest': 'true',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}).then((response) => {
|
||||
cy.log(`Created user ${user}`, response.status)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('nextcloudUpdateUser', (user, password, key, value) => {
|
||||
cy.request({
|
||||
method: 'PUT',
|
||||
url: `${Cypress.env('baseUrl')}/ocs/v2.php/cloud/users/${user}`,
|
||||
form: true,
|
||||
body: { key, value },
|
||||
auth: { user, pass: password },
|
||||
headers: {
|
||||
'OCS-ApiRequest': 'true',
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
}).then((response) => {
|
||||
cy.log(`Updated user ${user} ${key} to ${value}`, response.status)
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('openLeftSidebar', () => {
|
||||
cy.get('.app-navigation button.app-navigation-toggle').click()
|
||||
})
|
||||
|
||||
Cypress.Commands.add('deckCreateBoard', ({ user, password }, title) => {
|
||||
cy.login(user, password)
|
||||
|
||||
cy.get('.app-navigation button.app-navigation-toggle').click()
|
||||
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry')
|
||||
.eq(3)
|
||||
.find('a')
|
||||
.first()
|
||||
.click({ force: true })
|
||||
|
||||
cy.get('.board-create form input[type=text]').type(title, { force: true })
|
||||
|
||||
cy.get('.board-create form input[type=submit]')
|
||||
.first()
|
||||
.click({ force: true })
|
||||
})
|
||||
|
||||
Cypress.Commands.add('deckCreateList', ({ user, password }, title) => {
|
||||
cy.login(user, password)
|
||||
|
||||
cy.get('.app-navigation button.app-navigation-toggle').click()
|
||||
cy.get('#app-navigation-vue .app-navigation__list .app-navigation-entry')
|
||||
.eq(3)
|
||||
.find('a.app-navigation-entry-link')
|
||||
.first()
|
||||
.click({ force: true })
|
||||
|
||||
cy.get('#stack-add button').first().click()
|
||||
cy.get('#stack-add form input#new-stack-input-main').type(title)
|
||||
cy.get('#stack-add form input[type=submit]').first().click()
|
||||
})
|
||||
|
||||
Cypress.Commands.add('createExampleBoard', ({ user, password, board }) => {
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `${Cypress.env('baseUrl')}/index.php/apps/deck/api/v1.0/boards`,
|
||||
auth: {
|
||||
user,
|
||||
password,
|
||||
},
|
||||
body: { title: board.title, color: board.color ?? 'ff0000' },
|
||||
}).then((boardResponse) => {
|
||||
expect(boardResponse.status).to.eq(200)
|
||||
const boardData = boardResponse.body
|
||||
for (const stackIndex in board.stacks) {
|
||||
const stack = board.stacks[stackIndex]
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `${Cypress.env('baseUrl')}/index.php/apps/deck/api/v1.0/boards/${boardData.id}/stacks`,
|
||||
auth: {
|
||||
user,
|
||||
password,
|
||||
},
|
||||
body: { title: stack.title, order: 0 },
|
||||
}).then((stackResponse) => {
|
||||
const stackData = stackResponse.body
|
||||
for (const cardIndex in stack.cards) {
|
||||
const card = stack.cards[cardIndex]
|
||||
cy.request({
|
||||
method: 'POST',
|
||||
url: `${Cypress.env('baseUrl')}/index.php/apps/deck/api/v1.0/boards/${boardData.id}/stacks/${stackData.id}/cards`,
|
||||
auth: {
|
||||
user,
|
||||
password,
|
||||
},
|
||||
body: { title: card.title },
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Cypress.Commands.add('getNavigationEntry', (boardTitle) => {
|
||||
return cy.get('.app-navigation-entry-wrapper[icon=icon-deck]')
|
||||
.find('ul.app-navigation-entry__children .app-navigation-entry:contains(' + boardTitle + ')')
|
||||
.find('a.app-navigation-entry-link')
|
||||
})
|
||||
20
cypress/support/e2e.js
Normal file
@@ -0,0 +1,20 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// Import commands.js using ES2015 syntax:
|
||||
import './commands'
|
||||
|
||||
// Alternatively you can use CommonJS syntax:
|
||||
// require('./commands')
|
||||
1
cypress/utils/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export const randHash = () => Math.random().toString(36).replace(/[^a-z]+/g, '').slice(0, 10)
|
||||
@@ -90,7 +90,7 @@ Steps:
|
||||
* Create the configuration file
|
||||
* Execute the import informing the import file path, data file and source as `Trello JSON`
|
||||
|
||||
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloJson-schema.json) for import `Trello JSON`
|
||||
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/Importer/fixtures/config-trelloJson-schema.json) for import `Trello JSON`
|
||||
|
||||
Example configuration file:
|
||||
```json
|
||||
@@ -120,7 +120,7 @@ https://api.trello.com/1/members/me/boards?key={yourKey}&token={yourToken}&field
|
||||
This ID you will use in the configuration file in the `board` property
|
||||
* Create the configuration file
|
||||
|
||||
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/fixtures/config-trelloApi-schema.json) for import `Trello JSON`
|
||||
Create the configuration file respecting the [JSON Schema](https://github.com/nextcloud/deck/blob/master/lib/Service/Importer/fixtures/config-trelloApi-schema.json) for import `Trello JSON`
|
||||
|
||||
Example configuration file:
|
||||
```json
|
||||
|
||||
4
img/activity-dark.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0" viewbox="0 0 32 32">
|
||||
<path d="m16 1-10 18h11l-1 12 10-18h-11z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 205 B |
4
img/activity.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0" viewBox="0 0 32 32">
|
||||
<path d="m16 1-10 18h11l-1 12 10-18h-11z" fill="#FFF"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 217 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><g transform="translate(0 -1036.362)" fill="#fff"><path d="M1.93 1041.296c-.185 0-.336.138-.336.31v9.842c0 .172.15.313.336.313h12.517c.185 0 .333-.14.333-.313v-9.842c0-.172-.148-.31-.333-.31H1.93zm4.124 1.507h4.223c.39 0 .705.314.705.704v.43c0 .39-.315.705-.705.705H6.054a.703.703 0 0 1-.705-.705v-.43c0-.39.314-.704.705-.704z"/><rect width="15.742" height="2.296" x=".136" y="1037.543" ry="0"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 488 B |
1
img/circles-dark.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58" width="512" height="512"><g fill="#000"><path d="M54.319 37.839C54.762 35.918 55 33.96 55 32c0-9.095-4.631-17.377-12.389-22.153a1 1 0 1 0-1.049 1.703C48.724 15.96 53 23.604 53 32c0 1.726-.2 3.451-.573 5.147A6.992 6.992 0 0 0 51 37c-3.86 0-7 3.141-7 7s3.14 7 7 7 7-3.141 7-7a7.006 7.006 0 0 0-3.681-6.161zM38.171 54.182A23.867 23.867 0 0 1 29 56a24.047 24.047 0 0 1-17.017-7.092A6.974 6.974 0 0 0 14 44c0-3.859-3.14-7-7-7s-7 3.141-7 7 3.14 7 7 7a6.952 6.952 0 0 0 3.381-.875C15.26 55.136 21.994 58 29 58c3.435 0 6.778-.663 9.936-1.971.51-.211.753-.796.542-1.307a1.001 1.001 0 0 0-1.307-.54zM4 31.213a1 1 0 0 0 1.068-.927c.712-10.089 7.586-18.52 17.22-21.314C23.142 11.874 25.825 14 29 14c3.86 0 7-3.141 7-7s-3.14-7-7-7c-3.851 0-6.985 3.127-6.999 6.975C11.42 9.922 3.851 19.12 3.073 30.146A.999.999 0 0 0 4 31.213z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 885 B |
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58" width="512" height="512"><g fill="#000"><path d="M54.319 37.839C54.762 35.918 55 33.96 55 32c0-9.095-4.631-17.377-12.389-22.153a1 1 0 1 0-1.049 1.703C48.724 15.96 53 23.604 53 32c0 1.726-.2 3.451-.573 5.147A6.992 6.992 0 0 0 51 37c-3.86 0-7 3.141-7 7s3.14 7 7 7 7-3.141 7-7a7.006 7.006 0 0 0-3.681-6.161zM38.171 54.182A23.867 23.867 0 0 1 29 56a24.047 24.047 0 0 1-17.017-7.092A6.974 6.974 0 0 0 14 44c0-3.859-3.14-7-7-7s-7 3.141-7 7 3.14 7 7 7a6.952 6.952 0 0 0 3.381-.875C15.26 55.136 21.994 58 29 58c3.435 0 6.778-.663 9.936-1.971.51-.211.753-.796.542-1.307a1.001 1.001 0 0 0-1.307-.54zM4 31.213a1 1 0 0 0 1.068-.927c.712-10.089 7.586-18.52 17.22-21.314C23.142 11.874 25.825 14 29 14c3.86 0 7-3.141 7-7s-3.14-7-7-7c-3.851 0-6.985 3.127-6.999 6.975C11.42 9.922 3.851 19.12 3.073 30.146A.999.999 0 0 0 4 31.213z"/></g></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58" width="512" height="512"><g fill="#fff"><path d="M54.319 37.839C54.762 35.918 55 33.96 55 32c0-9.095-4.631-17.377-12.389-22.153a1 1 0 1 0-1.049 1.703C48.724 15.96 53 23.604 53 32c0 1.726-.2 3.451-.573 5.147A6.992 6.992 0 0 0 51 37c-3.86 0-7 3.141-7 7s3.14 7 7 7 7-3.141 7-7a7.006 7.006 0 0 0-3.681-6.161zM38.171 54.182A23.867 23.867 0 0 1 29 56a24.047 24.047 0 0 1-17.017-7.092A6.974 6.974 0 0 0 14 44c0-3.859-3.14-7-7-7s-7 3.141-7 7 3.14 7 7 7a6.952 6.952 0 0 0 3.381-.875C15.26 55.136 21.994 58 29 58c3.435 0 6.778-.663 9.936-1.971.51-.211.753-.796.542-1.307a1.001 1.001 0 0 0-1.307-.54zM4 31.213a1 1 0 0 0 1.068-.927c.712-10.089 7.586-18.52 17.22-21.314C23.142 11.874 25.825 14 29 14c3.86 0 7-3.141 7-7s-3.14-7-7-7c-3.851 0-6.985 3.127-6.999 6.975C11.42 9.922 3.851 19.12 3.073 30.146A.999.999 0 0 0 4 31.213z"/></g></svg>
|
||||
|
Before Width: | Height: | Size: 885 B After Width: | Height: | Size: 885 B |
@@ -1 +0,0 @@
|
||||
<svg width="16" height="16" version="1.1" xmlns="http://www.w3.org/2000/svg"><path d="M11.8 13.8H2.2V4.2h9.6m1.2 0c0-.67-.53-1.2-1.2-1.2H2.2C1.53 3 1 3.53 1 4.2v9.6c0 .67.53 1.2 1.2 1.2h9.6c.67 0 1.2-.53 1.2-1.2"/><path d="m4.2 1c-0.67 0-1.2 0.54-1.2 1.2h10.8v10.8c0.67 0 1.2-0.53 1.2-1.2v-9.6c0-0.67-0.53-1.2-1.2-1.2z"/></svg>
|
||||
|
Before Width: | Height: | Size: 327 B |
@@ -1 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="16" width="16"><path d="M15 15s-.4-7.8-7-10V1L1 8l7 7v-4c5.1 0 7 4 7 4z"/></svg>
|
||||
|
Before Width: | Height: | Size: 128 B |
@@ -45,7 +45,9 @@ use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\Comments\IComment;
|
||||
use OCP\IUser;
|
||||
use OCP\Server;
|
||||
use OCP\L10N\IFactory;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class ActivityManager {
|
||||
public const DECK_NOAUTHOR_COMMENT_SYSTEM_ENFORCED = 'DECK_NOAUTHOR_COMMENT_SYSTEM_ENFORCED';
|
||||
@@ -53,14 +55,14 @@ class ActivityManager {
|
||||
public const SUBJECT_PARAMS_MAX_LENGTH = 4000;
|
||||
public const SHORTENED_DESCRIPTION_MAX_LENGTH = 2000;
|
||||
|
||||
private $manager;
|
||||
private $userId;
|
||||
private $permissionService;
|
||||
private $boardMapper;
|
||||
private $cardMapper;
|
||||
private $aclMapper;
|
||||
private $stackMapper;
|
||||
private $l10nFactory;
|
||||
private IManager $manager;
|
||||
private ?string $userId;
|
||||
private PermissionService $permissionService;
|
||||
private BoardMapper $boardMapper;
|
||||
private CardMapper $cardMapper;
|
||||
private AclMapper $aclMapper;
|
||||
private StackMapper $stackMapper;
|
||||
private IFactory $l10nFactory;
|
||||
|
||||
public const DECK_OBJECT_BOARD = 'deck_board';
|
||||
public const DECK_OBJECT_CARD = 'deck_card';
|
||||
@@ -114,7 +116,7 @@ class ActivityManager {
|
||||
StackMapper $stackMapper,
|
||||
AclMapper $aclMapper,
|
||||
IFactory $l10nFactory,
|
||||
$userId
|
||||
?string $userId
|
||||
) {
|
||||
$this->manager = $manager;
|
||||
$this->permissionService = $permissionsService;
|
||||
@@ -310,10 +312,10 @@ class ActivityManager {
|
||||
try {
|
||||
$object = $this->findObjectForEntity($objectType, $entity);
|
||||
} catch (DoesNotExistException $e) {
|
||||
\OC::$server->getLogger()->error('Could not create activity entry for ' . $subject . '. Entity not found.', (array)$entity);
|
||||
Server::get(LoggerInterface::class)->error('Could not create activity entry for ' . $subject . '. Entity not found.', (array)$entity);
|
||||
return null;
|
||||
} catch (MultipleObjectsReturnedException $e) {
|
||||
\OC::$server->getLogger()->error('Could not create activity entry for ' . $subject . '. Entity not found.', (array)$entity);
|
||||
Server::get(LoggerInterface::class)->error('Could not create activity entry for ' . $subject . '. Entity not found.', (array)$entity);
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -365,7 +367,15 @@ class ActivityManager {
|
||||
case self::SUBJECT_CARD_USER_ASSIGN:
|
||||
case self::SUBJECT_CARD_USER_UNASSIGN:
|
||||
$subjectParams = $this->findDetailsForCard($entity->getId(), $subject);
|
||||
break;
|
||||
|
||||
if (isset($additionalParams['after']) && $additionalParams['after'] instanceof \DateTimeInterface) {
|
||||
$additionalParams['after'] = $additionalParams['after']->format('c');
|
||||
}
|
||||
if (isset($additionalParams['before']) && $additionalParams['before'] instanceof \DateTimeInterface) {
|
||||
$additionalParams['before'] = $additionalParams['before']->format('c');
|
||||
}
|
||||
|
||||
break;
|
||||
case self::SUBJECT_ATTACHMENT_CREATE:
|
||||
case self::SUBJECT_ATTACHMENT_UPDATE:
|
||||
case self::SUBJECT_ATTACHMENT_DELETE:
|
||||
|
||||
@@ -312,12 +312,19 @@ class DeckProvider implements IProvider {
|
||||
$userLanguage = $this->config->getUserValue($event->getAuthor(), 'core', 'lang', $this->l10nFactory->findLanguage());
|
||||
$userLocale = $this->config->getUserValue($event->getAuthor(), 'core', 'locale', $this->l10nFactory->findLocale());
|
||||
$l10n = $this->l10nFactory->get('deck', $userLanguage, $userLocale);
|
||||
$date = new \DateTime($subjectParams['after']);
|
||||
$date->setTimezone(new \DateTimeZone(\date_default_timezone_get()));
|
||||
if (is_array($subjectParams['after'])) {
|
||||
// Unluckily there was a time when we stored jsonSerialized date objects in the database
|
||||
// Broken in 1.8.0 and fixed again in 1.8.1
|
||||
$date = new \DateTime($subjectParams['after']['date']);
|
||||
$date->setTimezone(new \DateTimeZone(\date_default_timezone_get()));
|
||||
} else {
|
||||
$date = new \DateTime($subjectParams['after']);
|
||||
$date->setTimezone(new \DateTimeZone(\date_default_timezone_get()));
|
||||
}
|
||||
$params['after'] = [
|
||||
'type' => 'highlight',
|
||||
'id' => 'dt:' . $subjectParams['after'],
|
||||
'name' => $l10n->l('datetime', $date)
|
||||
'name' => $l10n->l('datetime', $date),
|
||||
];
|
||||
}
|
||||
return $params;
|
||||
|
||||
@@ -47,6 +47,7 @@ use OCA\Deck\Listeners\FullTextSearchEventListener;
|
||||
use OCA\Deck\Middleware\DefaultBoardMiddleware;
|
||||
use OCA\Deck\Middleware\ExceptionMiddleware;
|
||||
use OCA\Deck\Notification\Notifier;
|
||||
use OCA\Deck\Reference\CardReferenceProvider;
|
||||
use OCA\Deck\Search\CardCommentProvider;
|
||||
use OCA\Deck\Search\DeckProvider;
|
||||
use OCA\Deck\Service\PermissionService;
|
||||
@@ -57,20 +58,22 @@ use OCP\AppFramework\Bootstrap\IBootContext;
|
||||
use OCP\AppFramework\Bootstrap\IBootstrap;
|
||||
use OCP\AppFramework\Bootstrap\IRegistrationContext;
|
||||
use OCP\AppFramework\Http\Events\BeforeTemplateRenderedEvent;
|
||||
use OCP\Collaboration\Reference\RenderReferenceEvent;
|
||||
use OCP\Collaboration\Resources\IProviderManager;
|
||||
use OCP\Comments\CommentsEntityEvent;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Group\Events\GroupDeletedEvent;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IServerContainer;
|
||||
use OCP\IUser;
|
||||
use OCP\IRequest;
|
||||
use OCP\Server;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Notification\IManager as NotificationManager;
|
||||
use OCP\Share\IManager;
|
||||
use OCP\User\Events\UserDeletedEvent;
|
||||
use OCP\Util;
|
||||
use Psr\Container\ContainerInterface;
|
||||
|
||||
@@ -79,13 +82,16 @@ class Application extends App implements IBootstrap {
|
||||
|
||||
public const COMMENT_ENTITY_TYPE = 'deckCard';
|
||||
|
||||
/** @var IServerContainer */
|
||||
private $server;
|
||||
|
||||
public function __construct(array $urlParams = []) {
|
||||
parent::__construct(self::APP_ID, $urlParams);
|
||||
|
||||
$this->server = \OC::$server;
|
||||
// TODO move this back to ::register after fixing the autoload issue
|
||||
// (and use a listener class)
|
||||
$container = $this->getContainer();
|
||||
$eventDispatcher = $container->get(IEventDispatcher::class);
|
||||
$eventDispatcher->addListener(RenderReferenceEvent::class, function () {
|
||||
Util::addScript(self::APP_ID, self::APP_ID . '-card-reference');
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
@@ -124,8 +130,12 @@ class Application extends App implements IBootstrap {
|
||||
$context->registerSearchProvider(CardCommentProvider::class);
|
||||
$context->registerDashboardWidget(DeckWidget::class);
|
||||
|
||||
// reference widget
|
||||
$context->registerReferenceProvider(CardReferenceProvider::class);
|
||||
// $context->registerEventListener(RenderReferenceEvent::class, CardReferenceListener::class);
|
||||
|
||||
$context->registerEventListener(BeforeTemplateRenderedEvent::class, BeforeTemplateRenderedListener::class);
|
||||
|
||||
|
||||
// Event listening for full text search indexing
|
||||
$context->registerEventListener(CardCreatedEvent::class, FullTextSearchEventListener::class);
|
||||
$context->registerEventListener(CardUpdatedEvent::class, FullTextSearchEventListener::class);
|
||||
@@ -141,33 +151,43 @@ class Application extends App implements IBootstrap {
|
||||
|
||||
private function registerUserGroupHooks(IUserManager $userManager, IGroupManager $groupManager): void {
|
||||
$container = $this->getContainer();
|
||||
/** @var IEventDispatcher $eventDispatcher */
|
||||
$eventDispatcher = $container->get(IEventDispatcher::class);
|
||||
// Delete user/group acl entries when they get deleted
|
||||
$userManager->listen('\OC\User', 'postDelete', static function (IUser $user) use ($container) {
|
||||
$eventDispatcher->addListener(UserDeletedEvent::class, static function (Event $event) use ($container): void {
|
||||
if (!($event instanceof UserDeletedEvent)) {
|
||||
return;
|
||||
}
|
||||
$user = $event->getUser();
|
||||
// delete existing acl entries for deleted user
|
||||
/** @var AclMapper $aclMapper */
|
||||
$aclMapper = $container->query(AclMapper::class);
|
||||
$aclMapper = $container->get(AclMapper::class);
|
||||
$acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_USER, $user->getUID());
|
||||
foreach ($acls as $acl) {
|
||||
$aclMapper->delete($acl);
|
||||
}
|
||||
// delete existing user assignments
|
||||
$assignmentMapper = $container->query(AssignmentMapper::class);
|
||||
$assignmentMapper = $container->get(AssignmentMapper::class);
|
||||
$assignments = $assignmentMapper->findByParticipant($user->getUID());
|
||||
foreach ($assignments as $assignment) {
|
||||
$assignmentMapper->delete($assignment);
|
||||
}
|
||||
|
||||
/** @var BoardMapper $boardMapper */
|
||||
$boardMapper = $container->query(BoardMapper::class);
|
||||
$boardMapper = $container->get(BoardMapper::class);
|
||||
$boards = $boardMapper->findAllByOwner($user->getUID());
|
||||
foreach ($boards as $board) {
|
||||
$boardMapper->delete($board);
|
||||
}
|
||||
});
|
||||
|
||||
$groupManager->listen('\OC\Group', 'postDelete', static function (IGroup $group) use ($container) {
|
||||
$eventDispatcher->addListener(GroupDeletedEvent::class, static function (Event $event) use ($container): void {
|
||||
if (!($event instanceof GroupDeletedEvent)) {
|
||||
return;
|
||||
}
|
||||
$group = $event->getGroup();
|
||||
/** @var AclMapper $aclMapper */
|
||||
$aclMapper = $container->query(AclMapper::class);
|
||||
$aclMapper = $container->get(AclMapper::class);
|
||||
$aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID());
|
||||
$acls = $aclMapper->findByParticipant(Acl::PERMISSION_TYPE_GROUP, $group->getGID());
|
||||
foreach ($acls as $acl) {
|
||||
@@ -181,6 +201,7 @@ class Application extends App implements IBootstrap {
|
||||
$event->addEntityCollection(self::COMMENT_ENTITY_TYPE, function ($name) {
|
||||
/** @var CardMapper */
|
||||
$cardMapper = $this->getContainer()->get(CardMapper::class);
|
||||
/** @var PermissionService $permissionService */
|
||||
$permissionService = $this->getContainer()->get(PermissionService::class);
|
||||
|
||||
try {
|
||||
@@ -203,7 +224,7 @@ class Application extends App implements IBootstrap {
|
||||
$resourceManager->registerResourceProvider(ResourceProviderCard::class);
|
||||
|
||||
$symfonyAdapter->addListener('\OCP\Collaboration\Resources::loadAdditionalScripts', static function () {
|
||||
if (strpos(\OC::$server->getRequest()->getPathInfo(), '/call/') === 0) {
|
||||
if (strpos(Server::get(IRequest::class)->getPathInfo(), '/call/') === 0) {
|
||||
// Talk integration has its own entrypoint which already includes collections handling
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -32,20 +32,23 @@ use OCP\AppFramework\QueryException;
|
||||
use OCP\Collaboration\Resources\IManager;
|
||||
use OCP\Collaboration\Resources\IProvider;
|
||||
use OCP\Collaboration\Resources\IResource;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\Server;
|
||||
|
||||
class ResourceProvider implements IProvider {
|
||||
public const RESOURCE_TYPE = 'deck';
|
||||
|
||||
private $boardMapper;
|
||||
private $permissionService;
|
||||
private BoardMapper $boardMapper;
|
||||
private PermissionService $permissionService;
|
||||
private IURLGenerator $urlGenerator;
|
||||
|
||||
/** @var array */
|
||||
protected $nodes = [];
|
||||
protected array $nodes = [];
|
||||
|
||||
public function __construct(BoardMapper $boardMapper, PermissionService $permissionService) {
|
||||
public function __construct(BoardMapper $boardMapper, PermissionService $permissionService, IURLGenerator $urlGenerator) {
|
||||
$this->boardMapper = $boardMapper;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,14 +73,14 @@ class ResourceProvider implements IProvider {
|
||||
*/
|
||||
public function getResourceRichObject(IResource $resource): array {
|
||||
$board = $this->getBoard($resource);
|
||||
$link = \OC::$server->getURLGenerator()->linkToRoute('deck.page.index') . '#/board/' . $resource->getId();
|
||||
$link = $this->urlGenerator->linkToRoute('deck.page.index') . '#/board/' . $resource->getId();
|
||||
|
||||
return [
|
||||
'type' => self::RESOURCE_TYPE,
|
||||
'id' => $resource->getId(),
|
||||
'name' => $board->getTitle(),
|
||||
'link' => $link,
|
||||
'iconUrl' => \OC::$server->getURLGenerator()->imagePath('deck', 'deck-dark.svg')
|
||||
'iconUrl' => $this->urlGenerator->imagePath('deck', 'deck-dark.svg')
|
||||
];
|
||||
}
|
||||
|
||||
@@ -118,7 +121,7 @@ class ResourceProvider implements IProvider {
|
||||
public function invalidateAccessCache($boardId = null) {
|
||||
try {
|
||||
/** @var IManager $resourceManager */
|
||||
$resourceManager = \OC::$server->query(IManager::class);
|
||||
$resourceManager = Server::get(IManager::class);
|
||||
} catch (QueryException $e) {
|
||||
}
|
||||
if ($boardId !== null) {
|
||||
|
||||
@@ -37,24 +37,16 @@ use OCP\Collaboration\Resources\IResource;
|
||||
use OCP\Collaboration\Resources\ResourceException;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\Server;
|
||||
|
||||
class ResourceProviderCard implements IProvider {
|
||||
public const RESOURCE_TYPE = 'deck-card';
|
||||
|
||||
/** @var CardMapper */
|
||||
private $cardMapper;
|
||||
|
||||
/** @var BoardMapper */
|
||||
private $boardMapper;
|
||||
|
||||
/** @var PermissionService */
|
||||
private $permissionService;
|
||||
|
||||
/** @var IURLGenerator */
|
||||
private $urlGenerator;
|
||||
|
||||
/** @var array */
|
||||
protected $nodes = [];
|
||||
private CardMapper $cardMapper;
|
||||
private BoardMapper $boardMapper;
|
||||
private PermissionService $permissionService;
|
||||
private IURLGenerator $urlGenerator;
|
||||
protected array $nodes = [];
|
||||
|
||||
public function __construct(CardMapper $cardMapper, BoardMapper $boardMapper, PermissionService $permissionService, IURLGenerator $urlGenerator) {
|
||||
$this->cardMapper = $cardMapper;
|
||||
@@ -147,7 +139,7 @@ class ResourceProviderCard implements IProvider {
|
||||
public function invalidateAccessCache($cardId = null) {
|
||||
try {
|
||||
/** @var IManager $resourceManager */
|
||||
$resourceManager = \OC::$server->query(IManager::class);
|
||||
$resourceManager = Server::get(IManager::class);
|
||||
} catch (QueryException $e) {
|
||||
}
|
||||
if ($cardId !== null) {
|
||||
|
||||
@@ -30,8 +30,7 @@ use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
|
||||
class BoardImport extends Command {
|
||||
/** @var BoardImportCommandService */
|
||||
private $boardImportCommandService;
|
||||
private BoardImportCommandService $boardImportCommandService;
|
||||
|
||||
public function __construct(
|
||||
BoardImportCommandService $boardImportCommandService
|
||||
|
||||
@@ -27,6 +27,7 @@ use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Db\StackMapper;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCA\Deck\Service\BoardService;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
@@ -101,7 +102,9 @@ class UserExport extends Command {
|
||||
$fullCard = $this->cardMapper->find($card->getId());
|
||||
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
|
||||
$fullCard->setAssignedUsers($assignedUsers);
|
||||
$data[$board->getId()]['stacks'][$stack->getId()]['cards'][] = (array)$fullCard->jsonSerialize();
|
||||
|
||||
$cardDetails = new CardDetails($fullCard, $fullBoard);
|
||||
$data[$board->getId()]['stacks'][$stack->getId()]['cards'][] = $cardDetails->jsonSerialize();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,9 @@ use OCA\Deck\Service\PermissionService;
|
||||
use OCA\Files\Event\LoadSidebar;
|
||||
use OCA\Viewer\Event\LoadViewer;
|
||||
use OCP\AppFramework\Http\ContentSecurityPolicy;
|
||||
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as CollaborationResourcesEvent;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
use OCP\IInitialStateService;
|
||||
use OCP\IRequest;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
@@ -41,16 +43,17 @@ use OCA\Deck\Db\Acl;
|
||||
use OCA\Deck\Service\CardService;
|
||||
|
||||
class PageController extends Controller {
|
||||
private $permissionService;
|
||||
private $initialState;
|
||||
private $configService;
|
||||
private $eventDispatcher;
|
||||
private $cardMapper;
|
||||
private $urlGenerator;
|
||||
private $cardService;
|
||||
private PermissionService $permissionService;
|
||||
private IInitialStateService $initialState;
|
||||
private ConfigService $configService;
|
||||
private IEventDispatcher $eventDispatcher;
|
||||
private CardMapper $cardMapper;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private CardService $cardService;
|
||||
private IConfig $config;
|
||||
|
||||
public function __construct(
|
||||
$AppName,
|
||||
string $AppName,
|
||||
IRequest $request,
|
||||
PermissionService $permissionService,
|
||||
IInitialStateService $initialStateService,
|
||||
@@ -58,7 +61,8 @@ class PageController extends Controller {
|
||||
IEventDispatcher $eventDispatcher,
|
||||
CardMapper $cardMapper,
|
||||
IURLGenerator $urlGenerator,
|
||||
CardService $cardService
|
||||
CardService $cardService,
|
||||
IConfig $config
|
||||
) {
|
||||
parent::__construct($AppName, $request);
|
||||
|
||||
@@ -69,6 +73,7 @@ class PageController extends Controller {
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->cardService = $cardService;
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,13 +89,17 @@ class PageController extends Controller {
|
||||
$this->initialState->provideInitialState(Application::APP_ID, 'config', $this->configService->getAll());
|
||||
|
||||
$this->eventDispatcher->dispatchTyped(new LoadSidebar());
|
||||
$this->eventDispatcher->dispatchTyped(new CollaborationResourcesEvent());
|
||||
if (class_exists(LoadViewer::class)) {
|
||||
$this->eventDispatcher->dispatchTyped(new LoadViewer());
|
||||
}
|
||||
|
||||
$response = new TemplateResponse('deck', 'main');
|
||||
$response = new TemplateResponse('deck', 'main', [
|
||||
'id-app-content' => '#app-content-vue',
|
||||
'id-app-navigation' => '#app-navigation-vue',
|
||||
]);
|
||||
|
||||
if (\OC::$server->getConfig()->getSystemValueBool('debug', false)) {
|
||||
if ($this->config->getSystemValueBool('debug', false)) {
|
||||
$csp = new ContentSecurityPolicy();
|
||||
$csp->addAllowedConnectDomain('*');
|
||||
$csp->addAllowedScriptDomain('*');
|
||||
|
||||
@@ -27,6 +27,7 @@ declare(strict_types=1);
|
||||
namespace OCA\Deck\Controller;
|
||||
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCA\Deck\Service\SearchService;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
@@ -50,9 +51,12 @@ class SearchController extends OCSController {
|
||||
public function search(string $term, ?int $limit = null, ?int $cursor = null): DataResponse {
|
||||
$cards = $this->searchService->searchCards($term, $limit, $cursor);
|
||||
return new DataResponse(array_map(function (Card $card) {
|
||||
$json = $card->jsonSerialize();
|
||||
$board = $card->getRelatedBoard();
|
||||
$json = (new CardDetails($card, $board))->jsonSerialize();
|
||||
|
||||
$json['relatedBoard'] = $board;
|
||||
$json['relatedStack'] = $card->getRelatedStack();
|
||||
$json['relatedBoard'] = $card->getRelatedBoard();
|
||||
|
||||
return $json;
|
||||
}, $cards));
|
||||
}
|
||||
|
||||
@@ -26,18 +26,34 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\Deck\Dashboard;
|
||||
|
||||
use OCP\Dashboard\IWidget;
|
||||
use DateTime;
|
||||
use OCA\Deck\AppInfo\Application;
|
||||
use OCA\Deck\Db\Label;
|
||||
use OCA\Deck\Service\OverviewService;
|
||||
use OCP\Dashboard\IAPIWidget;
|
||||
use OCP\Dashboard\IButtonWidget;
|
||||
use OCP\Dashboard\IIconWidget;
|
||||
use OCP\Dashboard\Model\WidgetButton;
|
||||
use OCP\Dashboard\Model\WidgetItem;
|
||||
use OCP\IDateTimeFormatter;
|
||||
use OCP\IL10N;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Util;
|
||||
|
||||
class DeckWidget implements IWidget {
|
||||
class DeckWidget implements IAPIWidget, IButtonWidget, IIconWidget {
|
||||
private IL10N $l10n;
|
||||
private OverviewService $dashboardService;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private IDateTimeFormatter $dateTimeFormatter;
|
||||
|
||||
/**
|
||||
* @var IL10N
|
||||
*/
|
||||
private $l10n;
|
||||
|
||||
public function __construct(IL10N $l10n) {
|
||||
public function __construct(IL10N $l10n,
|
||||
OverviewService $dashboardService,
|
||||
IDateTimeFormatter $dateTimeFormatter,
|
||||
IURLGenerator $urlGenerator) {
|
||||
$this->l10n = $l10n;
|
||||
$this->dashboardService = $dashboardService;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->dateTimeFormatter = $dateTimeFormatter;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,17 +84,88 @@ class DeckWidget implements IWidget {
|
||||
return 'icon-deck';
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getIconUrl(): string {
|
||||
return $this->urlGenerator->getAbsoluteURL(
|
||||
$this->urlGenerator->imagePath(Application::APP_ID, 'deck-dark.svg')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getUrl(): ?string {
|
||||
return null;
|
||||
return $this->urlGenerator->getAbsoluteURL(
|
||||
$this->urlGenerator->linkToRoute(Application::APP_ID . '.page.index')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function load(): void {
|
||||
\OCP\Util::addScript('deck', 'deck-dashboard');
|
||||
Util::addScript('deck', 'deck-dashboard');
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getItems(string $userId, ?string $since = null, int $limit = 7): array {
|
||||
$upcomingCards = $this->dashboardService->findUpcomingCards($userId);
|
||||
$nowTimestamp = (new Datetime())->getTimestamp();
|
||||
$sinceTimestamp = $since !== null ? (new Datetime($since))->getTimestamp() : null;
|
||||
$upcomingCards = array_filter($upcomingCards, static function (array $card) use ($nowTimestamp, $sinceTimestamp) {
|
||||
if ($card['duedate']) {
|
||||
$ts = (new Datetime($card['duedate']))->getTimestamp();
|
||||
return $ts > $nowTimestamp && ($sinceTimestamp === null || $ts > $sinceTimestamp);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
usort($upcomingCards, static function ($a, $b) {
|
||||
$a = new Datetime($a['duedate']);
|
||||
$ta = $a->getTimestamp();
|
||||
$b = new Datetime($b['duedate']);
|
||||
$tb = $b->getTimestamp();
|
||||
return ($ta > $tb) ? 1 : -1;
|
||||
});
|
||||
$upcomingCards = array_slice($upcomingCards, 0, $limit);
|
||||
$urlGenerator = $this->urlGenerator;
|
||||
$dateTimeFormatter = $this->dateTimeFormatter;
|
||||
return array_map(static function (array $card) use ($urlGenerator, $dateTimeFormatter) {
|
||||
$formattedDueDate = $dateTimeFormatter->formatDateTime(new DateTime($card['duedate']));
|
||||
return new WidgetItem(
|
||||
$card['title'] . ' (' . $formattedDueDate . ')',
|
||||
implode(
|
||||
', ',
|
||||
array_map(static function (Label $label) {
|
||||
return $label->jsonSerialize()['title'];
|
||||
}, $card['labels'])
|
||||
),
|
||||
$urlGenerator->getAbsoluteURL(
|
||||
$urlGenerator->linkToRoute(Application::APP_ID . '.page.redirectToCard', ['cardId' => $card['id']])
|
||||
),
|
||||
$urlGenerator->getAbsoluteURL(
|
||||
$urlGenerator->imagePath(Application::APP_ID, 'deck-dark.svg')
|
||||
),
|
||||
$card['duedate']
|
||||
);
|
||||
}, $upcomingCards);
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function getWidgetButtons(string $userId): array {
|
||||
return [
|
||||
new WidgetButton(
|
||||
WidgetButton::TYPE_MORE,
|
||||
$this->urlGenerator->getAbsoluteURL(
|
||||
$this->urlGenerator->linkToRoute(Application::APP_ID . '.page.index')
|
||||
),
|
||||
$this->l10n->t('Load more')
|
||||
),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,18 +33,46 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
|
||||
parent::__construct($db, 'deck_board_acl', Acl::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Acl[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAll($boardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT id, board_id, type, participant, permission_edit, permission_share, permission_manage FROM `*PREFIX*deck_board_acl` WHERE `board_id` = ? ';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('id', 'board_id', 'type', 'participant', 'permission_edit', 'permission_share', 'permission_manage')
|
||||
->from('deck_board_acl')
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function isOwner($userId, $aclId): bool {
|
||||
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_board_acl` WHERE id = ?)';
|
||||
$stmt = $this->execute($sql, [$aclId]);
|
||||
$row = $stmt->fetch();
|
||||
return ($row['owner'] === $userId);
|
||||
/**
|
||||
* @param numeric $userId
|
||||
* @param numeric $id
|
||||
* @return bool
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function isOwner($userId, $id): bool {
|
||||
$aclId = $id;
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('acl.id')
|
||||
->from($this->getTableName(), 'acl')
|
||||
->innerJoin('acl', 'deck_boards', 'b', 'acl.board_id = b.id')
|
||||
->where($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)))
|
||||
->andWhere($qb->expr()->eq('acl.id', $qb->createNamedParameter($aclId, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return count($qb->executeQuery()->fetchAll()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $id
|
||||
* @return int|null
|
||||
*/
|
||||
public function findBoardId($id): ?int {
|
||||
try {
|
||||
$entity = $this->find($id);
|
||||
@@ -54,9 +82,21 @@ class AclMapper extends DeckMapper implements IPermissionMapper {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $type
|
||||
* @param string $participant
|
||||
* @return Acl[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findByParticipant($type, $participant): array {
|
||||
$sql = 'SELECT * from *PREFIX*deck_board_acl WHERE type = ? AND participant = ?';
|
||||
return $this->findEntities($sql, [$type, $participant]);
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('type', $qb->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('participant', $qb->createNamedParameter($participant, IQueryBuilder::PARAM_STR)));
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -55,9 +55,6 @@ class AssignmentMapper extends QBMapper implements IPermissionMapper {
|
||||
$this->circleService = $circleService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Assignment[]
|
||||
*/
|
||||
public function findAll(int $cardId): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
@@ -80,8 +77,8 @@ class AssignmentMapper extends QBMapper implements IPermissionMapper {
|
||||
}
|
||||
|
||||
|
||||
public function isOwner($userId, $cardId): bool {
|
||||
return $this->cardMapper->isOwner($userId, $cardId);
|
||||
public function isOwner($userId, $id): bool {
|
||||
return $this->cardMapper->isOwner($userId, $id);
|
||||
}
|
||||
|
||||
public function findBoardId($id): ?int {
|
||||
|
||||
@@ -30,7 +30,6 @@ use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IUserManager;
|
||||
use PDO;
|
||||
|
||||
class AttachmentMapper extends DeckMapper implements IPermissionMapper {
|
||||
private $cardMapper;
|
||||
@@ -52,70 +51,53 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return Entity|Attachment
|
||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
* @param int $id
|
||||
* @return Attachment
|
||||
* @throws DoesNotExistException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function find($id) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('deck_attachment')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
$cursor = $qb->execute();
|
||||
$row = $cursor->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row === false) {
|
||||
$cursor->closeCursor();
|
||||
throw new DoesNotExistException('Did expect one result but found none when executing query: ' . $qb->getSQL());
|
||||
}
|
||||
|
||||
$row2 = $cursor->fetch();
|
||||
$cursor->closeCursor();
|
||||
if ($row2 !== false) {
|
||||
throw new MultipleObjectsReturnedException('Did not expect more than one result when executing query: ' . $qb->getSQL());
|
||||
}
|
||||
|
||||
return $this->mapRowToEntity($row);
|
||||
}
|
||||
|
||||
public function findByData($cardId, $data) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('deck_attachment')
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('data', $qb->createNamedParameter($data, IQueryBuilder::PARAM_STR)));
|
||||
$cursor = $qb->execute();
|
||||
$row = $cursor->fetch(PDO::FETCH_ASSOC);
|
||||
if ($row === false) {
|
||||
$cursor->closeCursor();
|
||||
throw new DoesNotExistException('Did expect one result but found none when executing query: ' . $qb->getSQL());
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
return $this->mapRowToEntity($row);
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param int $cardId
|
||||
* @param string $data
|
||||
* @return Attachment
|
||||
* @throws DoesNotExistException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findByData($cardId, $data) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('data', $qb->createNamedParameter($data, IQueryBuilder::PARAM_STR)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* Find all attachments for a card
|
||||
*
|
||||
* @param $cardId
|
||||
* @return array
|
||||
* @return Entity[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAll($cardId) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('deck_attachment')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
|
||||
$entities = [];
|
||||
$cursor = $qb->execute();
|
||||
while ($row = $cursor->fetch()) {
|
||||
$entities[] = $this->mapRowToEntity($row);
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
return $entities;
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -128,7 +110,7 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
|
||||
$timeLimit = time() - (60 * 5);
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('deck_attachment')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->gt('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)));
|
||||
if ($withOffset) {
|
||||
$qb
|
||||
@@ -139,13 +121,7 @@ class AttachmentMapper extends DeckMapper implements IPermissionMapper {
|
||||
->andWhere($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
|
||||
}
|
||||
|
||||
$entities = [];
|
||||
$cursor = $qb->execute();
|
||||
while ($row = $cursor->fetch()) {
|
||||
$entities[] = $this->mapRowToEntity($row);
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
return $entities;
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -23,6 +23,14 @@
|
||||
|
||||
namespace OCA\Deck\Db;
|
||||
|
||||
/**
|
||||
* @method int getId()
|
||||
* @method string getTitle()
|
||||
* @method int getShared()
|
||||
* @method bool getArchived()
|
||||
* @method int getDeletedAt()
|
||||
* @method int getLastModified()
|
||||
*/
|
||||
class Board extends RelationalEntity {
|
||||
protected $title;
|
||||
protected $owner;
|
||||
|
||||
@@ -42,9 +42,9 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
private $circlesService;
|
||||
private $logger;
|
||||
|
||||
/** @var CappedMemoryCache */
|
||||
/** @var CappedMemoryCache<Board[]> */
|
||||
private $userBoardCache;
|
||||
/** @var CappedMemoryCache */
|
||||
/** @var CappedMemoryCache<Board> */
|
||||
private $boardCache;
|
||||
|
||||
public function __construct(
|
||||
@@ -107,6 +107,47 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
return $this->boardCache[$id];
|
||||
}
|
||||
|
||||
public function findBoardIds(string $userId): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->selectDistinct('b.id')
|
||||
->from($this->getTableName(), 'b')
|
||||
->leftJoin('b', 'deck_board_acl', 'acl', $qb->expr()->eq('b.id', 'acl.board_id'));
|
||||
|
||||
// Owned by the user
|
||||
$qb->where($qb->expr()->andX(
|
||||
$qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)),
|
||||
));
|
||||
|
||||
// Shared to the user
|
||||
$qb->orWhere($qb->expr()->andX(
|
||||
$qb->expr()->eq('acl.participant', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)),
|
||||
$qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_USER, IQueryBuilder::PARAM_INT)),
|
||||
));
|
||||
|
||||
// Shared to user groups of the user
|
||||
$groupIds = $this->groupManager->getUserGroupIds($this->userManager->get($userId));
|
||||
if (count($groupIds) !== 0) {
|
||||
$qb->orWhere($qb->expr()->andX(
|
||||
$qb->expr()->in('acl.participant', $qb->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY)),
|
||||
$qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_GROUP, IQueryBuilder::PARAM_INT)),
|
||||
));
|
||||
}
|
||||
|
||||
// Shared to circles of the user
|
||||
$circles = $this->circlesService->getUserCircles($userId);
|
||||
if (count($circles) !== 0) {
|
||||
$qb->orWhere($qb->expr()->andX(
|
||||
$qb->expr()->in('acl.participant', $qb->createNamedParameter($circles, IQueryBuilder::PARAM_STR_ARRAY)),
|
||||
$qb->expr()->eq('acl.type', $qb->createNamedParameter(Acl::PERMISSION_TYPE_CIRCLE, IQueryBuilder::PARAM_INT)),
|
||||
));
|
||||
}
|
||||
|
||||
$result = $qb->executeQuery();
|
||||
return array_map(function (string $id) {
|
||||
return (int)$id;
|
||||
}, $result->fetchAll(\PDO::FETCH_COLUMN));
|
||||
}
|
||||
|
||||
public function findAllForUser(string $userId, ?int $since = null, bool $includeArchived = true, ?int $before = null,
|
||||
?string $term = null): array {
|
||||
$useCache = ($since === -1 && $includeArchived === true && $before === null && $term === null);
|
||||
@@ -131,14 +172,9 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
|
||||
/**
|
||||
* Find all boards for a given user
|
||||
*
|
||||
* @param $userId
|
||||
* @param null $limit
|
||||
* @param null $offset
|
||||
* @return array
|
||||
*/
|
||||
public function findAllByUser(string $userId, ?int $limit = null, ?int $offset = null, ?int $since = null,
|
||||
bool $includeArchived = true, ?int $before = null, ?string $term = null) {
|
||||
bool $includeArchived = true, ?int $before = null, ?string $term = null): array {
|
||||
// FIXME this used to be a UNION to get boards owned by $userId and the user shares in one single query
|
||||
// Is it possible with the query builder?
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
@@ -247,15 +283,9 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
|
||||
/**
|
||||
* Find all boards for a given user
|
||||
*
|
||||
* @param $userId
|
||||
* @param $groups
|
||||
* @param null $limit
|
||||
* @param null $offset
|
||||
* @return array
|
||||
*/
|
||||
public function findAllByGroups(string $userId, array $groups, ?int $limit = null, ?int $offset = null, ?int $since = null,
|
||||
bool $includeArchived = true, ?int $before = null, ?string $term = null) {
|
||||
bool $includeArchived = true, ?int $before = null, ?string $term = null): array {
|
||||
if (count($groups) <= 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -414,8 +444,8 @@ class BoardMapper extends QBMapper implements IPermissionMapper {
|
||||
return parent::delete($entity);
|
||||
}
|
||||
|
||||
public function isOwner($userId, $boardId): bool {
|
||||
$board = $this->find($boardId);
|
||||
public function isOwner($userId, $id): bool {
|
||||
$board = $this->find($id);
|
||||
return ($board->getOwner() === $userId);
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,44 @@ use DateTime;
|
||||
use DateTimeZone;
|
||||
use Sabre\VObject\Component\VCalendar;
|
||||
|
||||
/**
|
||||
* @method string getTitle()
|
||||
* @method string getDescription()
|
||||
* @method string getDescriptionPrev()
|
||||
* @method int getStackId()
|
||||
* @method int getOrder()
|
||||
* @method int getLastModified()
|
||||
* @method int getCreatedAt()
|
||||
* @method bool getArchived()
|
||||
* @method bool getNotified()
|
||||
*
|
||||
* @method void setLabels(Label[] $labels)
|
||||
* @method null|Label[] getLabels()
|
||||
*
|
||||
* @method void setAssignedUsers(Assignment[] $users)
|
||||
* @method null|User[] getAssignedUsers()
|
||||
*
|
||||
* @method void setAttachments(Attachment[] $attachments)
|
||||
* @method null|Attachment[] getAttachments()
|
||||
*
|
||||
* @method void setAttachmentCount(int $count)
|
||||
* @method null|int getAttachmentCount()
|
||||
*
|
||||
* @method void setCommentsUnread(int $count)
|
||||
* @method null|int getCommentsUnread()
|
||||
*
|
||||
* @method void setCommentsCount(int $count)
|
||||
* @method null|int getCommentsCount()
|
||||
*
|
||||
* @method void setOwner(string $user)
|
||||
* @method null|string getOwner()
|
||||
*
|
||||
* @method void setRelatedStack(Stack $stack)
|
||||
* @method null|Stack getRelatedStack()
|
||||
*
|
||||
* @method void setRelatedBoard(Board $board)
|
||||
* @method null|Board getRelatedBoard()
|
||||
*/
|
||||
class Card extends RelationalEntity {
|
||||
public const TITLE_MAX_LENGTH = 255;
|
||||
|
||||
@@ -70,6 +108,7 @@ class Card extends RelationalEntity {
|
||||
$this->addType('archived', 'boolean');
|
||||
$this->addType('notified', 'boolean');
|
||||
$this->addType('deletedAt', 'integer');
|
||||
$this->addType('duedate', 'datetime');
|
||||
$this->addRelation('labels');
|
||||
$this->addRelation('assignedUsers');
|
||||
$this->addRelation('attachments');
|
||||
@@ -87,50 +126,6 @@ class Card extends RelationalEntity {
|
||||
$this->databaseType = $type;
|
||||
}
|
||||
|
||||
public function getDuedate($isoFormat = false) {
|
||||
if ($this->duedate === null) {
|
||||
return null;
|
||||
}
|
||||
$dt = new DateTime($this->duedate);
|
||||
if (!$isoFormat && $this->databaseType === 'mysql') {
|
||||
return $dt->format('Y-m-d H:i:s');
|
||||
}
|
||||
return $dt->format('c');
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
$json = parent::jsonSerialize();
|
||||
$json['overdue'] = self::DUEDATE_FUTURE;
|
||||
$due = $this->duedate ? strtotime($this->duedate) : false;
|
||||
if ($due !== false) {
|
||||
$today = new DateTime();
|
||||
$today->setTime(0, 0);
|
||||
|
||||
$match_date = new DateTime($this->duedate);
|
||||
|
||||
$match_date->setTime(0, 0);
|
||||
|
||||
$diff = $today->diff($match_date);
|
||||
$diffDays = (integer) $diff->format('%R%a'); // Extract days count in interval
|
||||
|
||||
if ($diffDays === 1) {
|
||||
$json['overdue'] = self::DUEDATE_NEXT;
|
||||
}
|
||||
if ($diffDays === 0) {
|
||||
$json['overdue'] = self::DUEDATE_NOW;
|
||||
}
|
||||
if ($diffDays < 0) {
|
||||
$json['overdue'] = self::DUEDATE_OVERDUE;
|
||||
}
|
||||
}
|
||||
$json['duedate'] = $this->getDuedate(true);
|
||||
unset($json['notified']);
|
||||
unset($json['descriptionPrev']);
|
||||
unset($json['relatedStack']);
|
||||
unset($json['relatedBoard']);
|
||||
return $json;
|
||||
}
|
||||
|
||||
public function getCalendarObject(): VCalendar {
|
||||
$calendar = new VCalendar();
|
||||
$event = $calendar->createComponent('VTODO');
|
||||
@@ -139,7 +134,7 @@ class Card extends RelationalEntity {
|
||||
$creationDate = new DateTime();
|
||||
$creationDate->setTimestamp($this->createdAt);
|
||||
$event->DTSTAMP = $creationDate;
|
||||
$event->DUE = new DateTime($this->getDuedate(true), new DateTimeZone('UTC'));
|
||||
$event->DUE = new DateTime($this->getDuedate()->format('c'), new DateTimeZone('UTC'));
|
||||
}
|
||||
$event->add('RELATED-TO', 'deck-stack-' . $this->getStackId());
|
||||
|
||||
|
||||
@@ -227,6 +227,21 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function findAllByBoardId(int $boardId, ?int $limit = null, ?int $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('c.*')
|
||||
->from('deck_cards', 'c')
|
||||
->innerJoin('c', 'deck_stacks', 's', 's.id = c.stack_id')
|
||||
->innerJoin('s', 'deck_boards', 'b', 'b.id = s.board_id')
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('archived', $qb->createNamedParameter(false, IQueryBuilder::PARAM_BOOL)))
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset)
|
||||
->orderBy('c.lastmodified')
|
||||
->addOrderBy('c.id');
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function findAllWithDue($boardId) {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('c.*')
|
||||
@@ -549,10 +564,10 @@ class CardMapper extends QBMapper implements IPermissionMapper {
|
||||
$qb->execute();
|
||||
}
|
||||
|
||||
public function isOwner($userId, $cardId): bool {
|
||||
public function isOwner($userId, $id): bool {
|
||||
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_stacks` WHERE id IN (SELECT stack_id FROM `*PREFIX*deck_cards` WHERE id = ?))';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT, 0);
|
||||
$stmt->bindParam(1, $id, \PDO::PARAM_INT, 0);
|
||||
$stmt->execute();
|
||||
$row = $stmt->fetch();
|
||||
return ($row['owner'] === $userId);
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
namespace OCA\Deck\Db;
|
||||
|
||||
use OCP\ICacheFactory;
|
||||
use OCP\ICache;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IRequest;
|
||||
|
||||
@@ -31,13 +32,16 @@ class ChangeHelper {
|
||||
public const TYPE_BOARD = 'boardChanged';
|
||||
public const TYPE_CARD = 'cardChanged';
|
||||
|
||||
private $db;
|
||||
private IDBConnection $db;
|
||||
private ICache $cache;
|
||||
private IRequest $request;
|
||||
private ?string $userId;
|
||||
|
||||
public function __construct(
|
||||
IDBConnection $db,
|
||||
ICacheFactory $cacheFactory,
|
||||
IRequest $request,
|
||||
$userId
|
||||
?string $userId
|
||||
) {
|
||||
$this->db = $db;
|
||||
$this->cache = $cacheFactory->createDistributed('deck_changes');
|
||||
|
||||
@@ -23,17 +23,15 @@
|
||||
|
||||
namespace OCA\Deck\Db;
|
||||
|
||||
use OCP\AppFramework\Db\Mapper;
|
||||
use OCP\AppFramework\Db\QBMapper;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
|
||||
/**
|
||||
* Class DeckMapper
|
||||
*
|
||||
* @package OCA\Deck\Db
|
||||
* @deprecated use QBMapper
|
||||
*
|
||||
* TODO: Move to QBMapper once Nextcloud 14 is a minimum requirement
|
||||
*/
|
||||
class DeckMapper extends Mapper {
|
||||
class DeckMapper extends QBMapper {
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
@@ -42,11 +40,11 @@ class DeckMapper extends Mapper {
|
||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
||||
*/
|
||||
public function find($id) {
|
||||
$sql = 'SELECT * FROM `' . $this->tableName . '` ' . 'WHERE `id` = ?';
|
||||
return $this->findEntity($sql, [$id]);
|
||||
}
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
protected function execute($sql, array $params = [], $limit = null, $offset = null) {
|
||||
return parent::execute($sql, $params, $limit, $offset);
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace OCA\Deck\Db;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class LabelMapper extends DeckMapper implements IPermissionMapper {
|
||||
@@ -33,41 +34,105 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
|
||||
parent::__construct($db, 'deck_labels', Label::class);
|
||||
}
|
||||
|
||||
public function findAll($boardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_labels` WHERE `board_id` = ? ORDER BY `id`';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Label[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAll($boardId, $limit = null, $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function delete(\OCP\AppFramework\Db\Entity $entity) {
|
||||
/**
|
||||
* @param Entity $entity
|
||||
* @return Entity
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function delete(Entity $entity): Entity {
|
||||
// delete assigned labels
|
||||
$this->deleteLabelAssignments($entity->getId());
|
||||
// delete label
|
||||
return parent::delete($entity);
|
||||
}
|
||||
|
||||
public function findAssignedLabelsForCard($cardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT l.*,card_id FROM `*PREFIX*deck_assigned_labels` as al INNER JOIN *PREFIX*deck_labels as l ON l.id = al.label_id WHERE `card_id` = ? ORDER BY l.id';
|
||||
return $this->findEntities($sql, [$cardId], $limit, $offset);
|
||||
}
|
||||
public function findAssignedLabelsForBoard($boardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT c.id as card_id, l.id as id, l.title as title, l.color as color FROM `*PREFIX*deck_cards` as c ' .
|
||||
' INNER JOIN `*PREFIX*deck_assigned_labels` as al ON al.card_id = c.id INNER JOIN `*PREFIX*deck_labels` as l ON al.label_id = l.id WHERE board_id=? ORDER BY l.id';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
/**
|
||||
* @param numeric $cardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Label[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAssignedLabelsForCard($cardId, $limit = null, $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('l.*', 'card_id')
|
||||
->from($this->getTableName(), 'l')
|
||||
->innerJoin('l', 'deck_assigned_labels', 'al', 'l.id = al.label_id')
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)))
|
||||
->orderBy('l.id')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
public function insert(Entity $entity) {
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Label[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAssignedLabelsForBoard($boardId, $limit = null, $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('l.id as id', 'l.title as title', 'l.color as color')
|
||||
->selectAlias('c.id', 'card_id')
|
||||
->from($this->getTableName(), 'l')
|
||||
->innerJoin('l', 'deck_assigned_labels', 'al', 'al.label_id = l.id')
|
||||
->innerJoin('l', 'deck_cards', 'c', 'al.card_id = c.id')
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->orderBy('l.id')
|
||||
->setMaxResults($limit)
|
||||
->setFirstResult($offset);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Entity $entity
|
||||
* @return Entity
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function insert(Entity $entity): Entity {
|
||||
$entity->setLastModified(time());
|
||||
return parent::insert($entity);
|
||||
}
|
||||
|
||||
public function update(Entity $entity, $updateModified = true) {
|
||||
/**
|
||||
* @param Entity $entity
|
||||
* @param bool $updateModified
|
||||
* @return Entity
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function update(Entity $entity, $updateModified = true): Entity {
|
||||
if ($updateModified) {
|
||||
$entity->setLastModified(time());
|
||||
}
|
||||
return parent::update($entity);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @return array
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function getAssignedLabelsForBoard($boardId) {
|
||||
$labels = $this->findAssignedLabelsForBoard($boardId);
|
||||
$result = [];
|
||||
@@ -80,27 +145,51 @@ class LabelMapper extends DeckMapper implements IPermissionMapper {
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $labelId
|
||||
* @return void
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function deleteLabelAssignments($labelId) {
|
||||
$sql = 'DELETE FROM `*PREFIX*deck_assigned_labels` WHERE label_id = ?';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->bindParam(1, $labelId, \PDO::PARAM_INT, 0);
|
||||
$stmt->execute();
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete('deck_assigned_labels')
|
||||
->where($qb->expr()->eq('label_id', $qb->createNamedParameter($labelId, IQueryBuilder::PARAM_INT)));
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $cardId
|
||||
* @return void
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function deleteLabelAssignmentsForCard($cardId) {
|
||||
$sql = 'DELETE FROM `*PREFIX*deck_assigned_labels` WHERE card_id = ?';
|
||||
$stmt = $this->db->prepare($sql);
|
||||
$stmt->bindParam(1, $cardId, \PDO::PARAM_INT, 0);
|
||||
$stmt->execute();
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->delete('deck_assigned_labels')
|
||||
->where($qb->expr()->eq('card_id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
|
||||
$qb->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $userId
|
||||
* @param numeric $labelId
|
||||
* @return bool
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function isOwner($userId, $labelId): bool {
|
||||
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_labels` WHERE id = ?)';
|
||||
$stmt = $this->execute($sql, [$labelId]);
|
||||
$row = $stmt->fetch();
|
||||
return ($row['owner'] === $userId);
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('l.id')
|
||||
->from($this->getTableName(), 'l')
|
||||
->innerJoin('l', 'deck_boards', 'b', 'l.board_id = b.id')
|
||||
->where($qb->expr()->eq('l.id', $qb->createNamedParameter($labelId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('b.owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
|
||||
|
||||
return count($qb->executeQuery()->fetchAll()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $id
|
||||
* @return int|null
|
||||
*/
|
||||
public function findBoardId($id): ?int {
|
||||
try {
|
||||
$entity = $this->find($id);
|
||||
|
||||
@@ -72,6 +72,9 @@ class RelationalEntity extends Entity implements \JsonSerializable {
|
||||
$propertyReflection = $reflection->getProperty($property);
|
||||
if (!$propertyReflection->isPrivate() && !in_array($property, $this->_resolvedProperties, true)) {
|
||||
$json[$property] = $this->getter($property);
|
||||
if ($json[$property] instanceof \DateTimeInterface) {
|
||||
$json[$property] = $json[$property]->format('c');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ namespace OCA\Deck\Db;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\Entity;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\DB\QueryBuilder\IQueryBuilder;
|
||||
use OCP\IDBConnection;
|
||||
|
||||
class StackMapper extends DeckMapper implements IPermissionMapper {
|
||||
@@ -38,62 +39,112 @@ class StackMapper extends DeckMapper implements IPermissionMapper {
|
||||
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @param numeric $id
|
||||
* @return Stack
|
||||
* @throws DoesNotExistException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function find($id): Stack {
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_stacks` ' .
|
||||
'WHERE `id` = ?';
|
||||
return $this->findEntity($sql, [$id]);
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
return $this->findEntity($qb);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $cardId
|
||||
* @return Stack|null
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findStackFromCardId($cardId): ?Stack {
|
||||
$sql = <<<SQL
|
||||
SELECT s.*
|
||||
FROM `*PREFIX*deck_stacks` as `s`
|
||||
INNER JOIN `*PREFIX*deck_cards` as `c` ON s.id = c.stack_id
|
||||
WHERE c.id = ?
|
||||
SQL;
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('s.*')
|
||||
->from($this->getTableName(), 's')
|
||||
->innerJoin('s', 'deck_cards', 'c', 's.id = c.stack_id')
|
||||
->where($qb->expr()->eq('c.id', $qb->createNamedParameter($cardId, IQueryBuilder::PARAM_INT)));
|
||||
|
||||
try {
|
||||
return $this->findEntity($sql, [$cardId]);
|
||||
return $this->findEntity($qb);
|
||||
} catch (MultipleObjectsReturnedException|DoesNotExistException $e) {
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Stack[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findAll($boardId, $limit = null, $offset = null): array {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit);
|
||||
|
||||
public function findAll($boardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_stacks` WHERE `board_id` = ? AND deleted_at = 0 ORDER BY `order`, `id`';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @param numeric $boardId
|
||||
* @param int|null $limit
|
||||
* @param int|null $offset
|
||||
* @return Stack[]
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findDeleted($boardId, $limit = null, $offset = null) {
|
||||
$sql = 'SELECT * FROM `*PREFIX*deck_stacks` s
|
||||
WHERE `s`.`board_id` = ? AND NOT s.deleted_at = 0';
|
||||
return $this->findEntities($sql, [$boardId], $limit, $offset);
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from($this->getTableName())
|
||||
->where($qb->expr()->eq('board_id', $qb->createNamedParameter($boardId, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->neq('deleted_at', $qb->createNamedParameter(0, IQueryBuilder::PARAM_INT)))
|
||||
->setFirstResult($offset)
|
||||
->setMaxResults($limit);
|
||||
|
||||
return $this->findEntities($qb);
|
||||
}
|
||||
|
||||
|
||||
public function delete(Entity $entity) {
|
||||
/**
|
||||
* @param Entity $entity
|
||||
* @return Entity
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function delete(Entity $entity): Entity {
|
||||
// delete cards on stack
|
||||
$this->cardMapper->deleteByStack($entity->getId());
|
||||
return parent::delete($entity);
|
||||
}
|
||||
|
||||
public function isOwner($userId, $stackId): bool {
|
||||
$sql = 'SELECT owner FROM `*PREFIX*deck_boards` WHERE `id` IN (SELECT board_id FROM `*PREFIX*deck_stacks` WHERE id = ?)';
|
||||
$stmt = $this->execute($sql, [$stackId]);
|
||||
$row = $stmt->fetch();
|
||||
return ($row['owner'] === $userId);
|
||||
/**
|
||||
* @param numeric $userId
|
||||
* @param numeric $stackId
|
||||
* @return bool
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function isOwner($userId, $id): bool {
|
||||
$qb = $this->db->getQueryBuilder();
|
||||
$qb->select('s.id')
|
||||
->from($this->getTableName(), 's')
|
||||
->innerJoin('s', 'deck_boards', 'b', 'b.id = s.board_id')
|
||||
->where($qb->expr()->eq('s.id', $qb->createNamedParameter($id, IQueryBuilder::PARAM_INT)))
|
||||
->andWhere($qb->expr()->eq('owner', $qb->createNamedParameter($userId, IQueryBuilder::PARAM_STR)));
|
||||
|
||||
return count($qb->executeQuery()->fetchAll()) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param numeric $id
|
||||
* @return int|null
|
||||
* @throws \OCP\DB\Exception
|
||||
*/
|
||||
public function findBoardId($id): ?int {
|
||||
try {
|
||||
$entity = $this->find($id);
|
||||
|
||||
@@ -33,7 +33,6 @@ use OCP\AppFramework\OCS\OCSException;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\ILogger;
|
||||
use OCP\IRequest;
|
||||
use OCP\Util;
|
||||
use OCP\IConfig;
|
||||
|
||||
class ExceptionMiddleware extends Middleware {
|
||||
@@ -85,9 +84,9 @@ class ExceptionMiddleware extends Middleware {
|
||||
'message' => 'Permission denied'
|
||||
], 403);
|
||||
}
|
||||
|
||||
|
||||
if ($exception instanceof StatusException) {
|
||||
if ($this->config->getSystemValue('loglevel', Util::WARN) === Util::DEBUG) {
|
||||
if ($this->config->getSystemValue('loglevel', ILogger::WARN) === ILogger::DEBUG) {
|
||||
$this->logger->logException($exception);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
/*
|
||||
* @copyright Copyright (c) 2016 Julius Härtl <jus@bitgrid.net>
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Raul Ferreira Fuentes <raul@nextcloud.com>
|
||||
*
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Artem Anufrij <artem.anufrij@live.de>
|
||||
* @author Marin Treselj <marin@pixelipo.com>
|
||||
* @author Oskar Kurz <oskar.kurz@gmail.com>
|
||||
* @author Ryan Fletcher <ryan.fletcher@codepassion.ca>
|
||||
* @author Raul Ferreira Fuentes <raul@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
@@ -23,6 +20,26 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Deck\Model;
|
||||
|
||||
@import 'icons';
|
||||
@import 'print';
|
||||
use OCA\Deck\Db\Board;
|
||||
|
||||
class BoardSummary extends Board {
|
||||
private Board $board;
|
||||
|
||||
public function __construct(Board $board) {
|
||||
parent::__construct();
|
||||
$this->board = $board;
|
||||
}
|
||||
|
||||
public function jsonSerialize(): array {
|
||||
return [
|
||||
'id' => $this->getId(),
|
||||
'title' => $this->getTitle()
|
||||
];
|
||||
}
|
||||
|
||||
public function __call($name, $arguments) {
|
||||
return $this->board->__call($name, $arguments);
|
||||
}
|
||||
}
|
||||
92
lib/Model/CardDetails.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Raul Ferreira Fuentes <raul@nextcloud.com>
|
||||
*
|
||||
* @author Raul Ferreira Fuentes <raul@nextcloud.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Deck\Model;
|
||||
|
||||
use DateTime;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\Card;
|
||||
|
||||
class CardDetails extends Card {
|
||||
private Card $card;
|
||||
private ?Board $board;
|
||||
|
||||
public function __construct(Card $card, ?Board $board = null) {
|
||||
parent::__construct();
|
||||
$this->card = $card;
|
||||
$this->board = $board;
|
||||
}
|
||||
|
||||
public function setBoard(?Board $board): void {
|
||||
$this->board = $board;
|
||||
}
|
||||
|
||||
public function jsonSerialize(array $extras = []): array {
|
||||
$array = $this->card->jsonSerialize();
|
||||
unset($array['notified'], $array['descriptionPrev'], $array['relatedStack'], $array['relatedBoard']);
|
||||
|
||||
$array['overdue'] = $this->getDueStatus();
|
||||
$this->appendBoardDetails($array);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
private function getDueStatus(): int {
|
||||
$today = new DateTime();
|
||||
$today->setTime(0, 0);
|
||||
|
||||
$match_date = $this->card->getDuedate();
|
||||
if (!$match_date) {
|
||||
return Card::DUEDATE_FUTURE;
|
||||
}
|
||||
$match_date->setTime(0, 0);
|
||||
|
||||
$diff = $today->diff($match_date);
|
||||
$diffDays = (int) $diff->format('%R%a'); // Extract days count in interval
|
||||
|
||||
|
||||
if ($diffDays === 1) {
|
||||
return Card::DUEDATE_NEXT;
|
||||
}
|
||||
if ($diffDays === 0) {
|
||||
return Card::DUEDATE_NOW;
|
||||
}
|
||||
if ($diffDays < 0) {
|
||||
return Card::DUEDATE_OVERDUE;
|
||||
}
|
||||
|
||||
return Card::DUEDATE_FUTURE;
|
||||
}
|
||||
|
||||
private function appendBoardDetails(&$array): void {
|
||||
if (!$this->board) {
|
||||
return;
|
||||
}
|
||||
|
||||
$array['boardId'] = $this->board->id;
|
||||
$array['board'] = (new BoardSummary($this->board))->jsonSerialize();
|
||||
}
|
||||
|
||||
public function __call($name, $arguments) {
|
||||
return $this->card->__call($name, $arguments);
|
||||
}
|
||||
}
|
||||
@@ -129,7 +129,7 @@ class NotificationHelper {
|
||||
->setSubject('card-overdue', [
|
||||
$card->getTitle(), $board->getTitle()
|
||||
])
|
||||
->setDateTime(new DateTime($card->getDuedate()));
|
||||
->setDateTime($card->getDuedate());
|
||||
$this->notificationManager->notify($notification);
|
||||
}
|
||||
}
|
||||
@@ -242,7 +242,7 @@ class NotificationHelper {
|
||||
}
|
||||
return $this->boards[$boardId];
|
||||
}
|
||||
|
||||
|
||||
private function generateBoardShared(Board $board, string $userId): INotification {
|
||||
$notification = $this->notificationManager->createNotification();
|
||||
$notification
|
||||
|
||||
@@ -114,7 +114,7 @@ class Notifier implements INotifier {
|
||||
$dn = $params[2];
|
||||
}
|
||||
$notification->setParsedSubject(
|
||||
(string) $l->t('The card "%s" on "%s" has been assigned to you by %s.', [$params[0], $params[1], $dn])
|
||||
$l->t('The card "%s" on "%s" has been assigned to you by %s.', [$params[0], $params[1], $dn])
|
||||
);
|
||||
$notification->setRichSubject(
|
||||
$l->t('{user} has assigned the card {deck-card} on {deck-board} to you.'),
|
||||
@@ -151,7 +151,7 @@ class Notifier implements INotifier {
|
||||
}
|
||||
|
||||
$notification->setParsedSubject(
|
||||
(string) $l->t('The card "%s" on "%s" has reached its due date.', $params)
|
||||
$l->t('The card "%s" on "%s" has reached its due date.', $params)
|
||||
);
|
||||
$notification->setRichSubject(
|
||||
$l->t('The card {deck-card} on {deck-board} has reached its due date.'),
|
||||
@@ -189,7 +189,7 @@ class Notifier implements INotifier {
|
||||
$dn = $params[2];
|
||||
}
|
||||
$notification->setParsedSubject(
|
||||
(string) $l->t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])
|
||||
$l->t('%s has mentioned you in a comment on "%s".', [$dn, $params[0]])
|
||||
);
|
||||
$notification->setRichSubject(
|
||||
$l->t('{user} has mentioned you in a comment on {deck-card}.'),
|
||||
@@ -226,7 +226,7 @@ class Notifier implements INotifier {
|
||||
$dn = $params[1];
|
||||
}
|
||||
$notification->setParsedSubject(
|
||||
(string) $l->t('The board "%s" has been shared with you by %s.', [$params[0], $dn])
|
||||
$l->t('The board "%s" has been shared with you by %s.', [$params[0], $dn])
|
||||
);
|
||||
$notification->setRichSubject(
|
||||
$l->t('{user} has shared {deck-board} with you.'),
|
||||
|
||||
@@ -54,22 +54,11 @@ use OCP\IURLGenerator;
|
||||
class DeckProvider implements IFullTextSearchProvider {
|
||||
public const DECK_PROVIDER_ID = 'deck';
|
||||
|
||||
|
||||
/** @var IL10N */
|
||||
private $l10n;
|
||||
|
||||
/** @var IUrlGenerator */
|
||||
private $urlGenerator;
|
||||
|
||||
/** @var FullTextSearchService */
|
||||
private $fullTextSearchService;
|
||||
|
||||
|
||||
/** @var IRunner */
|
||||
private $runner;
|
||||
|
||||
/** @var IIndexOptions */
|
||||
private $indexOptions = [];
|
||||
private IL10N $l10n;
|
||||
private IUrlGenerator $urlGenerator;
|
||||
private FullTextSearchService $fullTextSearchService;
|
||||
private ?IRunner $runner = null;
|
||||
private ?IIndexOptions $indexOptions = null;
|
||||
|
||||
|
||||
/**
|
||||
|
||||
167
lib/Reference/CardReferenceProvider.php
Normal file
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 Julien Veyssier <eneiluj@posteo.net>
|
||||
*
|
||||
* @author Julien Veyssier <eneiluj@posteo.net>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
namespace OCA\Deck\Reference;
|
||||
|
||||
use OCA\Deck\AppInfo\Application;
|
||||
use OCA\Deck\Db\Assignment;
|
||||
use OCA\Deck\Db\Attachment;
|
||||
use OCA\Deck\Db\Label;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCA\Deck\Service\BoardService;
|
||||
use OCA\Deck\Service\CardService;
|
||||
use OCA\Deck\Service\StackService;
|
||||
use OCP\Collaboration\Reference\IReference;
|
||||
use OCP\Collaboration\Reference\IReferenceProvider;
|
||||
use OCP\Collaboration\Reference\Reference;
|
||||
use OCP\IURLGenerator;
|
||||
|
||||
class CardReferenceProvider implements IReferenceProvider {
|
||||
private CardService $cardService;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private BoardService $boardService;
|
||||
private StackService $stackService;
|
||||
|
||||
public function __construct(CardService $cardService,
|
||||
BoardService $boardService,
|
||||
StackService $stackService,
|
||||
IURLGenerator $urlGenerator,
|
||||
?string $userId) {
|
||||
$this->cardService = $cardService;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->boardService = $boardService;
|
||||
$this->stackService = $stackService;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function matchReference(string $referenceText): bool {
|
||||
$start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID);
|
||||
$startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID);
|
||||
|
||||
// link example: https://nextcloud.local/index.php/apps/deck/#/board/2/card/11
|
||||
$noIndexMatch = preg_match('/^' . preg_quote($start, '/') . '\/#\/board\/[0-9]+\/card\/[0-9]+$/', $referenceText) === 1;
|
||||
$indexMatch = preg_match('/^' . preg_quote($startIndex, '/') . '\/#\/board\/[0-9]+\/card\/[0-9]+$/', $referenceText) === 1;
|
||||
|
||||
return $noIndexMatch || $indexMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritDoc
|
||||
*/
|
||||
public function resolveReference(string $referenceText): ?IReference {
|
||||
if ($this->matchReference($referenceText)) {
|
||||
$ids = $this->getBoardCardId($referenceText);
|
||||
if ($ids !== null) {
|
||||
[$boardId, $cardId] = $ids;
|
||||
$card = $this->cardService->find((int) $cardId)->jsonSerialize();
|
||||
$board = $this->boardService->find((int) $boardId)->jsonSerialize();
|
||||
$stack = $this->stackService->find((int) $card['stackId'])->jsonSerialize();
|
||||
|
||||
$card = $this->sanitizeSerializedCard($card);
|
||||
$board = $this->sanitizeSerializedBoard($board);
|
||||
$stack = $this->sanitizeSerializedStack($stack);
|
||||
/** @var IReference $reference */
|
||||
$reference = new Reference($referenceText);
|
||||
$reference->setRichObject(Application::APP_ID . '-card', [
|
||||
'id' => $boardId . '/' . $cardId,
|
||||
'card' => $card,
|
||||
'board' => $board,
|
||||
'stack' => $stack,
|
||||
]);
|
||||
return $reference;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function sanitizeSerializedStack(array $stack): array {
|
||||
$stack['cards'] = array_map(function (CardDetails $cardDetails) {
|
||||
$result = $cardDetails->jsonSerialize();
|
||||
unset($result['assignedUsers']);
|
||||
return $result;
|
||||
}, $stack['cards']);
|
||||
|
||||
return $stack;
|
||||
}
|
||||
|
||||
private function sanitizeSerializedBoard(array $board): array {
|
||||
unset($board['labels']);
|
||||
$board['owner'] = $board['owner']->jsonSerialize();
|
||||
unset($board['acl']);
|
||||
unset($board['users']);
|
||||
|
||||
return $board;
|
||||
}
|
||||
|
||||
private function sanitizeSerializedCard(array $card): array {
|
||||
$card['labels'] = array_map(function (Label $label) {
|
||||
return $label->jsonSerialize();
|
||||
}, $card['labels']);
|
||||
$card['assignedUsers'] = array_map(function (Assignment $assignment) {
|
||||
$result = $assignment->jsonSerialize();
|
||||
$result['participant'] = $result['participant']->jsonSerialize();
|
||||
return $result;
|
||||
}, $card['assignedUsers']);
|
||||
$card['owner'] = $card['owner']->jsonSerialize();
|
||||
unset($card['relatedStack']);
|
||||
unset($card['relatedBoard']);
|
||||
$card['attachments'] = array_map(function (Attachment $attachment) {
|
||||
return $attachment->jsonSerialize();
|
||||
}, $card['attachments']);
|
||||
|
||||
return $card;
|
||||
}
|
||||
|
||||
private function getBoardCardId(string $url): ?array {
|
||||
$start = $this->urlGenerator->getAbsoluteURL('/apps/' . Application::APP_ID);
|
||||
$startIndex = $this->urlGenerator->getAbsoluteURL('/index.php/apps/' . Application::APP_ID);
|
||||
|
||||
preg_match('/^' . preg_quote($start, '/') . '\/#\/board\/([0-9]+)\/card\/([0-9]+)$/', $url, $matches);
|
||||
if ($matches && count($matches) > 2) {
|
||||
return [$matches[1], $matches[2]];
|
||||
}
|
||||
|
||||
preg_match('/^' . preg_quote($startIndex, '/') . '\/#\/board\/([0-9]+)\/card\/([0-9]+)$/', $url, $matches2);
|
||||
if ($matches2 && count($matches2) > 2) {
|
||||
return [$matches2[1], $matches2[2]];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function getCachePrefix(string $referenceId): string {
|
||||
$ids = $this->getBoardCardId($referenceId);
|
||||
if ($ids !== null) {
|
||||
[$boardId, $cardId] = $ids;
|
||||
return $boardId . '/' . $cardId;
|
||||
}
|
||||
|
||||
return $referenceId;
|
||||
}
|
||||
|
||||
public function getCacheKey(string $referenceId): ?string {
|
||||
return $this->userId ?? '';
|
||||
}
|
||||
}
|
||||
@@ -24,7 +24,6 @@
|
||||
|
||||
namespace OCA\Deck\Service;
|
||||
|
||||
use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
|
||||
use OCA\Deck\Activity\ActivityManager;
|
||||
use OCA\Deck\Activity\ChangeSet;
|
||||
use OCA\Deck\AppInfo\Application;
|
||||
@@ -43,39 +42,43 @@ use OCA\Deck\Event\AclUpdatedEvent;
|
||||
use OCA\Deck\NoPermissionException;
|
||||
use OCA\Deck\Notification\NotificationHelper;
|
||||
use OCP\AppFramework\Db\DoesNotExistException;
|
||||
use OCP\AppFramework\Db\MultipleObjectsReturnedException;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
use OCP\IDBConnection;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IL10N;
|
||||
use OCP\DB\Exception as DbException;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\LabelMapper;
|
||||
use OCP\IUserManager;
|
||||
use OCA\Deck\BadRequestException;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Server;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
|
||||
class BoardService {
|
||||
private $boardMapper;
|
||||
private $stackMapper;
|
||||
private $labelMapper;
|
||||
private $aclMapper;
|
||||
/** @var IConfig */
|
||||
private $config;
|
||||
private $l10n;
|
||||
private $permissionService;
|
||||
private $notificationHelper;
|
||||
private $assignedUsersMapper;
|
||||
private $userManager;
|
||||
private $groupManager;
|
||||
private $userId;
|
||||
private $activityManager;
|
||||
private $eventDispatcher;
|
||||
private $changeHelper;
|
||||
private $cardMapper;
|
||||
|
||||
private $boardsCache = null;
|
||||
private $urlGenerator;
|
||||
|
||||
private BoardMapper $boardMapper;
|
||||
private StackMapper $stackMapper;
|
||||
private LabelMapper $labelMapper;
|
||||
private AclMapper $aclMapper;
|
||||
private IConfig $config;
|
||||
private IL10N $l10n;
|
||||
private PermissionService $permissionService;
|
||||
private NotificationHelper $notificationHelper;
|
||||
private AssignmentMapper $assignedUsersMapper;
|
||||
private IUserManager $userManager;
|
||||
private IGroupManager $groupManager;
|
||||
private ?string $userId;
|
||||
private ActivityManager $activityManager;
|
||||
private IEventDispatcher $eventDispatcher;
|
||||
private ChangeHelper $changeHelper;
|
||||
private CardMapper $cardMapper;
|
||||
private ?array $boardsCache = null;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private IDBConnection $connection;
|
||||
|
||||
public function __construct(
|
||||
BoardMapper $boardMapper,
|
||||
@@ -94,7 +97,8 @@ class BoardService {
|
||||
IEventDispatcher $eventDispatcher,
|
||||
ChangeHelper $changeHelper,
|
||||
IURLGenerator $urlGenerator,
|
||||
$userId
|
||||
IDBConnection $connection,
|
||||
?string $userId
|
||||
) {
|
||||
$this->boardMapper = $boardMapper;
|
||||
$this->stackMapper = $stackMapper;
|
||||
@@ -113,6 +117,7 @@ class BoardService {
|
||||
$this->userId = $userId;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -534,7 +539,7 @@ class BoardService {
|
||||
|
||||
// TODO: use the dispatched event for this
|
||||
try {
|
||||
$resourceProvider = \OC::$server->query(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
|
||||
$resourceProvider = Server::get(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
|
||||
$resourceProvider->invalidateAccessCache($boardId);
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
@@ -590,18 +595,14 @@ class BoardService {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $id
|
||||
* @return \OCP\AppFramework\Db\Entity
|
||||
* @throws DbException
|
||||
* @throws DoesNotExistException
|
||||
* @throws \OCA\Deck\NoPermissionException
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
* @throws BadRequestException
|
||||
* @throws NoPermissionException
|
||||
* @throws MultipleObjectsReturnedException
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws NotFoundExceptionInterface
|
||||
*/
|
||||
public function deleteAcl($id) {
|
||||
if (is_numeric($id) === false) {
|
||||
throw new BadRequestException('id must be a number');
|
||||
}
|
||||
|
||||
public function deleteAcl(int $id): ?Acl {
|
||||
$this->permissionService->checkPermission($this->aclMapper, $id, Acl::PERMISSION_SHARE);
|
||||
/** @var Acl $acl */
|
||||
$acl = $this->aclMapper->find($id);
|
||||
@@ -620,16 +621,16 @@ class BoardService {
|
||||
$version = \OCP\Util::getVersion()[0];
|
||||
if ($version >= 16) {
|
||||
try {
|
||||
$resourceProvider = \OC::$server->query(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
|
||||
$resourceProvider = Server::get(\OCA\Deck\Collaboration\Resources\ResourceProvider::class);
|
||||
$resourceProvider->invalidateAccessCache($acl->getBoardId());
|
||||
} catch (\Exception $e) {
|
||||
}
|
||||
}
|
||||
$delete = $this->aclMapper->delete($acl);
|
||||
|
||||
$deletedAcl = $this->aclMapper->delete($acl);
|
||||
$this->eventDispatcher->dispatchTyped(new AclDeletedEvent($acl));
|
||||
|
||||
return $delete;
|
||||
return $deletedAcl;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -683,7 +684,7 @@ class BoardService {
|
||||
}
|
||||
|
||||
public function transferBoardOwnership(int $boardId, string $newOwner, bool $changeContent = false): Board {
|
||||
\OC::$server->getDatabaseConnection()->beginTransaction();
|
||||
$this->connection->beginTransaction();
|
||||
try {
|
||||
$board = $this->boardMapper->find($boardId);
|
||||
$previousOwner = $board->getOwner();
|
||||
@@ -692,7 +693,10 @@ class BoardService {
|
||||
if (!$changeContent) {
|
||||
try {
|
||||
$this->addAcl($boardId, Acl::PERMISSION_TYPE_USER, $previousOwner, true, true, true);
|
||||
} catch (UniqueConstraintViolationException $e) {
|
||||
} catch (DbException $e) {
|
||||
if ($e->getReason() !== DbException::REASON_UNIQUE_CONSTRAINT_VIOLATION) {
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
}
|
||||
$this->boardMapper->transferOwnership($previousOwner, $newOwner, $boardId);
|
||||
@@ -702,10 +706,10 @@ class BoardService {
|
||||
$this->assignedUsersMapper->remapAssignedUser($boardId, $previousOwner, $newOwner);
|
||||
$this->cardMapper->remapCardOwner($boardId, $previousOwner, $newOwner);
|
||||
}
|
||||
\OC::$server->getDatabaseConnection()->commit();
|
||||
$this->connection->commit();
|
||||
return $this->boardMapper->find($boardId);
|
||||
} catch (\Throwable $e) {
|
||||
\OC::$server->getDatabaseConnection()->rollBack();
|
||||
$this->connection->rollBack();
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,26 +45,30 @@ use OCA\Deck\StatusException;
|
||||
use OCA\Deck\BadRequestException;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserManager;
|
||||
use OCP\IURLGenerator;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class CardService {
|
||||
private $cardMapper;
|
||||
private $stackMapper;
|
||||
private $boardMapper;
|
||||
private $labelMapper;
|
||||
private $permissionService;
|
||||
private $boardService;
|
||||
private $notificationHelper;
|
||||
private $assignedUsersMapper;
|
||||
private $attachmentService;
|
||||
private $currentUser;
|
||||
private $activityManager;
|
||||
private $commentsManager;
|
||||
private $changeHelper;
|
||||
private $eventDispatcher;
|
||||
private $userManager;
|
||||
private $urlGenerator;
|
||||
private CardMapper $cardMapper;
|
||||
private StackMapper $stackMapper;
|
||||
private BoardMapper $boardMapper;
|
||||
private LabelMapper $labelMapper;
|
||||
private PermissionService $permissionService;
|
||||
private BoardService $boardService;
|
||||
private NotificationHelper $notificationHelper;
|
||||
private AssignmentMapper $assignedUsersMapper;
|
||||
private AttachmentService $attachmentService;
|
||||
private ?string $currentUser;
|
||||
private ActivityManager $activityManager;
|
||||
private ICommentsManager $commentsManager;
|
||||
private ChangeHelper $changeHelper;
|
||||
private IEventDispatcher $eventDispatcher;
|
||||
private IUserManager $userManager;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private LoggerInterface $logger;
|
||||
private IRequest $request;
|
||||
|
||||
public function __construct(
|
||||
CardMapper $cardMapper,
|
||||
@@ -82,7 +86,9 @@ class CardService {
|
||||
ChangeHelper $changeHelper,
|
||||
IEventDispatcher $eventDispatcher,
|
||||
IURLGenerator $urlGenerator,
|
||||
$userId
|
||||
LoggerInterface $logger,
|
||||
IRequest $request,
|
||||
?string $userId
|
||||
) {
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->stackMapper = $stackMapper;
|
||||
@@ -100,6 +106,8 @@ class CardService {
|
||||
$this->eventDispatcher = $eventDispatcher;
|
||||
$this->currentUser = $userId;
|
||||
$this->urlGenerator = $urlGenerator;
|
||||
$this->logger = $logger;
|
||||
$this->request = $request;
|
||||
}
|
||||
|
||||
public function enrich($card) {
|
||||
@@ -131,23 +139,18 @@ class CardService {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $cardId
|
||||
* @return \OCA\Deck\Db\RelationalEntity
|
||||
* @throws \OCA\Deck\NoPermissionException
|
||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
public function find($cardId) {
|
||||
if (is_numeric($cardId) === false) {
|
||||
throw new BadRequestException('card id must be a number');
|
||||
}
|
||||
|
||||
public function find(int $cardId) {
|
||||
$this->permissionService->checkPermission($this->cardMapper, $cardId, Acl::PERMISSION_READ);
|
||||
$card = $this->cardMapper->find($cardId);
|
||||
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
|
||||
$attachments = $this->attachmentService->findAll($cardId, true);
|
||||
if (\OC::$server->getRequest()->getParam('apiVersion') === '1.0') {
|
||||
if ($this->request->getParam('apiVersion') === '1.0') {
|
||||
$attachments = array_filter($attachments, function ($attachment) {
|
||||
return $attachment->getType() === 'deck_file';
|
||||
});
|
||||
@@ -162,7 +165,7 @@ class CardService {
|
||||
try {
|
||||
$this->permissionService->checkPermission($this->boardMapper, $boardId, Acl::PERMISSION_READ);
|
||||
} catch (NoPermissionException $e) {
|
||||
\OC::$server->getLogger()->error('Unable to check permission for a previously obtained board ' . $boardId, ['exception' => $e]);
|
||||
$this->logger->error('Unable to check permission for a previously obtained board ' . $boardId, ['exception' => $e]);
|
||||
return [];
|
||||
}
|
||||
$cards = $this->cardMapper->findCalendarEntries($boardId);
|
||||
@@ -334,11 +337,11 @@ class CardService {
|
||||
$card->setType($type);
|
||||
$card->setOrder($order);
|
||||
$card->setOwner($owner);
|
||||
$card->setDuedate($duedate);
|
||||
$card->setDuedate($duedate ? new \DateTime($duedate) : null);
|
||||
$resetDuedateNotification = false;
|
||||
if (
|
||||
$card->getDuedate() === null ||
|
||||
(new \DateTime($card->getDuedate())) != (new \DateTime($changes->getBefore()->getDuedate() ?? ''))
|
||||
($card->getDuedate()) != ($changes->getBefore()->getDuedate())
|
||||
) {
|
||||
$card->setNotified(false);
|
||||
$resetDuedateNotification = true;
|
||||
@@ -486,7 +489,7 @@ class CardService {
|
||||
* @throws StatusException
|
||||
* @throws \OCA\Deck\NoPermissionException
|
||||
* @throws \OCP\AppFramework\Db\DoesNotExistException
|
||||
* @throws \OCP\AppFramework\Db\
|
||||
* @throws \OCP\AppFramework\Db\MultipleObjectsReturnedException
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
public function archive($id) {
|
||||
|
||||
@@ -31,6 +31,7 @@ use OCA\Circles\Model\Circle;
|
||||
use OCA\Circles\Model\Member;
|
||||
use OCA\Circles\Model\Probes\CircleProbe;
|
||||
use OCP\App\IAppManager;
|
||||
use OCP\Server;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
@@ -38,7 +39,7 @@ use Throwable;
|
||||
* having the app disabled is properly handled
|
||||
*/
|
||||
class CirclesService {
|
||||
private $circlesEnabled;
|
||||
private bool $circlesEnabled;
|
||||
|
||||
private $userCircleCache = [];
|
||||
|
||||
@@ -58,8 +59,7 @@ class CirclesService {
|
||||
try {
|
||||
|
||||
// Enforce current user condition since we always want the full list of members
|
||||
/** @var CirclesManager $circlesManager */
|
||||
$circlesManager = \OC::$server->get(CirclesManager::class);
|
||||
$circlesManager = Server::get(CirclesManager::class);
|
||||
$circlesManager->startSuperSession();
|
||||
return $circlesManager->getCircle($circleId);
|
||||
} catch (Throwable $e) {
|
||||
@@ -77,8 +77,7 @@ class CirclesService {
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var CirclesManager $circlesManager */
|
||||
$circlesManager = \OC::$server->get(CirclesManager::class);
|
||||
$circlesManager = Server::get(CirclesManager::class);
|
||||
$federatedUser = $circlesManager->getFederatedUser($userId, Member::TYPE_USER);
|
||||
$circlesManager->startSession($federatedUser);
|
||||
$circle = $circlesManager->getCircle($circleId);
|
||||
@@ -106,8 +105,7 @@ class CirclesService {
|
||||
}
|
||||
|
||||
try {
|
||||
/** @var CirclesManager $circlesManager */
|
||||
$circlesManager = \OC::$server->get(CirclesManager::class);
|
||||
$circlesManager = Server::get(CirclesManager::class);
|
||||
$federatedUser = $circlesManager->getFederatedUser($userId, Member::TYPE_USER);
|
||||
$circlesManager->startSession($federatedUser);
|
||||
$probe = new CircleProbe();
|
||||
|
||||
@@ -40,20 +40,14 @@ use OutOfBoundsException;
|
||||
use function is_numeric;
|
||||
|
||||
class CommentService {
|
||||
private ICommentsManager $commentsManager;
|
||||
private IUserManager $userManager;
|
||||
private CardMapper $cardMapper;
|
||||
private PermissionService $permissionService;
|
||||
private ILogger $logger;
|
||||
private ?string $userId;
|
||||
|
||||
/**
|
||||
* @var ICommentsManager
|
||||
*/
|
||||
private $commentsManager;
|
||||
/**
|
||||
* @var IUserManager
|
||||
*/
|
||||
private $userManager;
|
||||
/** @var ILogger */
|
||||
private $logger;
|
||||
private $userId;
|
||||
|
||||
public function __construct(ICommentsManager $commentsManager, PermissionService $permissionService, CardMapper $cardMapper, IUserManager $userManager, ILogger $logger, $userId) {
|
||||
public function __construct(ICommentsManager $commentsManager, PermissionService $permissionService, CardMapper $cardMapper, IUserManager $userManager, ILogger $logger, ?string $userId) {
|
||||
$this->commentsManager = $commentsManager;
|
||||
$this->permissionService = $permissionService;
|
||||
$this->cardMapper = $cardMapper;
|
||||
|
||||
@@ -40,9 +40,9 @@ class ConfigService {
|
||||
public const SETTING_BOARD_NOTIFICATION_DUE_ALL = 'all';
|
||||
public const SETTING_BOARD_NOTIFICATION_DUE_DEFAULT = self::SETTING_BOARD_NOTIFICATION_DUE_ASSIGNED;
|
||||
|
||||
private $config;
|
||||
private $userId;
|
||||
private $groupManager;
|
||||
private IConfig $config;
|
||||
private ?string $userId = null;
|
||||
private IGroupManager $groupManager;
|
||||
|
||||
public function __construct(
|
||||
IConfig $config,
|
||||
@@ -52,11 +52,14 @@ class ConfigService {
|
||||
$this->config = $config;
|
||||
}
|
||||
|
||||
public function getUserId() {
|
||||
public function getUserId(): ?string {
|
||||
if (!$this->userId) {
|
||||
$user = \OC::$server->get(IUserSession::class)->getUser();
|
||||
// We cannot use DI for the userId or UserSession as the ConfigService
|
||||
// is initiated too early before the session is actually loaded
|
||||
$user = \OCP\Server::get(IUserSession::class)->getUser();
|
||||
$this->userId = $user ? $user->getUID() : null;
|
||||
}
|
||||
|
||||
return $this->userId;
|
||||
}
|
||||
|
||||
@@ -75,8 +78,11 @@ class ConfigService {
|
||||
return $data;
|
||||
}
|
||||
|
||||
public function get($key) {
|
||||
$result = null;
|
||||
/**
|
||||
* @return bool|array{id: string, displayname: string}[]
|
||||
* @throws NoPermissionException
|
||||
*/
|
||||
public function get(string $key) {
|
||||
[$scope] = explode(':', $key, 2);
|
||||
switch ($scope) {
|
||||
case 'groupLimit':
|
||||
@@ -90,11 +96,12 @@ class ConfigService {
|
||||
}
|
||||
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'calendar', true);
|
||||
case 'cardDetailsInModal':
|
||||
if ($this->getUserId() === null) {
|
||||
return false;
|
||||
}
|
||||
if ($this->getUserId() === null) {
|
||||
return false;
|
||||
}
|
||||
return (bool)$this->config->getUserValue($this->getUserId(), Application::APP_ID, 'cardDetailsInModal', true);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isCalendarEnabled(int $boardId = null): bool {
|
||||
@@ -157,7 +164,10 @@ class ConfigService {
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function setGroupLimit($value) {
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
private function setGroupLimit(array $value): array {
|
||||
$groups = [];
|
||||
foreach ($value as $group) {
|
||||
$groups[] = $group['id'];
|
||||
@@ -167,7 +177,7 @@ class ConfigService {
|
||||
return $groups;
|
||||
}
|
||||
|
||||
private function getGroupLimitList() {
|
||||
private function getGroupLimitList(): array {
|
||||
$value = $this->config->getAppValue(Application::APP_ID, 'groupLimit', '');
|
||||
$groups = explode(',', $value);
|
||||
if ($value === '') {
|
||||
@@ -176,9 +186,10 @@ class ConfigService {
|
||||
return $groups;
|
||||
}
|
||||
|
||||
/** @return array{id: string, displayname: string}[] */
|
||||
private function getGroupLimit() {
|
||||
$groups = $this->getGroupLimitList();
|
||||
$groups = array_map(function ($groupId) {
|
||||
$groups = array_map(function (string $groupId): ?array {
|
||||
/** @var IGroup $groups */
|
||||
$group = $this->groupManager->get($groupId);
|
||||
if ($group === null) {
|
||||
|
||||
@@ -88,18 +88,6 @@ class DefaultBoardService {
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
public function createDefaultBoard(string $title, string $userId, string $color) {
|
||||
if ($title === false || $title === null) {
|
||||
throw new BadRequestException('title must be provided');
|
||||
}
|
||||
|
||||
if ($userId === false || $userId === null) {
|
||||
throw new BadRequestException('userId must be provided');
|
||||
}
|
||||
|
||||
if ($color === false || $color === null) {
|
||||
throw new BadRequestException('color must be provided');
|
||||
}
|
||||
|
||||
$defaultBoard = $this->boardService->create($title, $userId, $color);
|
||||
$defaultStacks = [];
|
||||
$defaultCards = [];
|
||||
|
||||
@@ -219,8 +219,11 @@ class FileService implements IAttachmentService {
|
||||
throw new \Exception('no instance id!');
|
||||
}
|
||||
$name = 'appdata_' . $instanceId;
|
||||
/** @var \OCP\Files\Folder $appDataFolder */
|
||||
$appDataFolder = $this->rootFolder->get($name);
|
||||
/** @var \OCP\Files\Folder $appDataFolder */
|
||||
$appDataFolder = $appDataFolder->get('deck');
|
||||
/** @var \OCP\Files\Folder $cardFolder */
|
||||
$cardFolder = $appDataFolder->get($folderName);
|
||||
return $cardFolder->get($attachment->getData());
|
||||
}
|
||||
|
||||
@@ -44,18 +44,19 @@ use OCP\Share\IShare;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
private $request;
|
||||
private $rootFolder;
|
||||
private $shareProvider;
|
||||
private $shareManager;
|
||||
private $userId;
|
||||
private $configService;
|
||||
private $l10n;
|
||||
private $preview;
|
||||
private $mimeTypeDetector;
|
||||
private $permissionService;
|
||||
private $cardMapper;
|
||||
private $logger;
|
||||
private IRequest $request;
|
||||
private IRootFolder $rootFolder;
|
||||
private DeckShareProvider $shareProvider;
|
||||
private IManager $shareManager;
|
||||
private ?string $userId;
|
||||
private ConfigService $configService;
|
||||
private IL10N $l10n;
|
||||
private IPreview $preview;
|
||||
private IMimeTypeDetector $mimeTypeDetector;
|
||||
private PermissionService $permissionService;
|
||||
private CardMapper $cardMapper;
|
||||
private LoggerInterface $logger;
|
||||
private IDBConnection $connection;
|
||||
|
||||
public function __construct(
|
||||
IRequest $request,
|
||||
@@ -69,7 +70,8 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
PermissionService $permissionService,
|
||||
CardMapper $cardMapper,
|
||||
LoggerInterface $logger,
|
||||
string $userId = null
|
||||
IDBConnection $connection,
|
||||
?string $userId
|
||||
) {
|
||||
$this->request = $request;
|
||||
$this->l10n = $l10n;
|
||||
@@ -83,6 +85,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
$this->permissionService = $permissionService;
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->logger = $logger;
|
||||
$this->connection = $connection;
|
||||
}
|
||||
|
||||
public function listAttachments(int $cardId): array {
|
||||
@@ -108,9 +111,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
}
|
||||
|
||||
public function getAttachmentCount(int $cardId): int {
|
||||
/** @var IDBConnection $qb */
|
||||
$db = \OC::$server->getDatabaseConnection();
|
||||
$qb = $db->getQueryBuilder();
|
||||
$qb = $this->connection->getQueryBuilder();
|
||||
$qb->select('s.id', 'f.fileid', 'f.path')
|
||||
->selectAlias('st.id', 'storage_string_id')
|
||||
->from('share', 's')
|
||||
@@ -125,7 +126,7 @@ class FilesAppService implements IAttachmentService, ICustomAttachmentService {
|
||||
));
|
||||
|
||||
$count = 0;
|
||||
$cursor = $qb->execute();
|
||||
$cursor = $qb->executeQuery();
|
||||
while ($data = $cursor->fetch()) {
|
||||
if ($this->shareProvider->isAccessibleResult($data)) {
|
||||
$count++;
|
||||
|
||||
@@ -47,34 +47,22 @@ use OCP\Comments\ICommentsManager;
|
||||
use OCP\Comments\NotFoundException as CommentNotFoundException;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IUserManager;
|
||||
use OCP\Server;
|
||||
|
||||
class BoardImportService {
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
/** @var BoardMapper */
|
||||
private $boardMapper;
|
||||
/** @var AclMapper */
|
||||
private $aclMapper;
|
||||
/** @var LabelMapper */
|
||||
private $labelMapper;
|
||||
/** @var StackMapper */
|
||||
private $stackMapper;
|
||||
/** @var CardMapper */
|
||||
private $cardMapper;
|
||||
/** @var AssignmentMapper */
|
||||
private $assignmentMapper;
|
||||
/** @var AttachmentMapper */
|
||||
private $attachmentMapper;
|
||||
/** @var ICommentsManager */
|
||||
private $commentsManager;
|
||||
/** @var IEventDispatcher */
|
||||
private $eventDispatcher;
|
||||
/** @var string */
|
||||
private $system = '';
|
||||
/** @var null|ABoardImportService */
|
||||
private $systemInstance;
|
||||
/** @var array */
|
||||
private $allowedSystems = [];
|
||||
private IUserManager $userManager;
|
||||
private BoardMapper $boardMapper;
|
||||
private AclMapper $aclMapper;
|
||||
private LabelMapper $labelMapper;
|
||||
private StackMapper $stackMapper;
|
||||
private CardMapper $cardMapper;
|
||||
private AssignmentMapper $assignmentMapper;
|
||||
private AttachmentMapper $attachmentMapper;
|
||||
private ICommentsManager $commentsManager;
|
||||
private IEventDispatcher $eventDispatcher;
|
||||
private string $system = '';
|
||||
private ?ABoardImportService $systemInstance;
|
||||
private array $allowedSystems = [];
|
||||
/**
|
||||
* Data object created from config JSON
|
||||
*
|
||||
@@ -89,10 +77,7 @@ class BoardImportService {
|
||||
* @psalm-suppress PropertyNotSetInConstructor
|
||||
*/
|
||||
private $data;
|
||||
/**
|
||||
* @var Board
|
||||
*/
|
||||
private $board;
|
||||
private Board $board;
|
||||
|
||||
public function __construct(
|
||||
IUserManager $userManager,
|
||||
@@ -198,7 +183,7 @@ class BoardImportService {
|
||||
}
|
||||
if (!is_object($this->systemInstance)) {
|
||||
$systemClass = 'OCA\\Deck\\Service\\Importer\\Systems\\' . ucfirst($this->getSystem()) . 'Service';
|
||||
$this->systemInstance = \OC::$server->get($systemClass);
|
||||
$this->systemInstance = Server::get($systemClass);
|
||||
$this->systemInstance->setImportService($this);
|
||||
}
|
||||
return $this->systemInstance;
|
||||
@@ -343,7 +328,7 @@ class BoardImportService {
|
||||
}
|
||||
|
||||
public function insertAttachment(Attachment $attachment, string $content): Attachment {
|
||||
$service = \OC::$server->get(FileService::class);
|
||||
$service = Server::get(FileService::class);
|
||||
$folder = $service->getFolder($attachment);
|
||||
|
||||
if ($folder->fileExists($attachment->getData())) {
|
||||
|
||||
@@ -91,12 +91,10 @@ class LabelService {
|
||||
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_MANAGE);
|
||||
|
||||
$boardLabels = $this->labelMapper->findAll($boardId);
|
||||
if (\is_array($boardLabels)) {
|
||||
foreach ($boardLabels as $boardLabel) {
|
||||
if ($boardLabel->getTitle() === $title) {
|
||||
throw new BadRequestException('title must be unique');
|
||||
break;
|
||||
}
|
||||
foreach ($boardLabels as $boardLabel) {
|
||||
if ($boardLabel->getTitle() === $title) {
|
||||
throw new BadRequestException('title must be unique');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,15 +161,13 @@ class LabelService {
|
||||
$label = $this->find($id);
|
||||
|
||||
$boardLabels = $this->labelMapper->findAll($label->getBoardId());
|
||||
if (\is_array($boardLabels)) {
|
||||
foreach ($boardLabels as $boardLabel) {
|
||||
if ($boardLabel->getId() === $label->getId()) {
|
||||
continue;
|
||||
}
|
||||
if ($boardLabel->getTitle() === $title) {
|
||||
throw new BadRequestException('title must be unique');
|
||||
break;
|
||||
}
|
||||
foreach ($boardLabels as $boardLabel) {
|
||||
if ($boardLabel->getId() === $label->getId()) {
|
||||
continue;
|
||||
}
|
||||
if ($boardLabel->getTitle() === $title) {
|
||||
throw new BadRequestException('title must be unique');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,31 +30,20 @@ namespace OCA\Deck\Service;
|
||||
use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCP\Comments\ICommentsManager;
|
||||
use OCP\IGroupManager;
|
||||
use OCA\Deck\Db\Board;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\LabelMapper;
|
||||
use OCP\IUserManager;
|
||||
|
||||
class OverviewService {
|
||||
|
||||
/** @var BoardMapper */
|
||||
private $boardMapper;
|
||||
/** @var LabelMapper */
|
||||
private $labelMapper;
|
||||
/** @var CardMapper */
|
||||
private $cardMapper;
|
||||
/** @var AssignmentMapper */
|
||||
private $assignedUsersMapper;
|
||||
/** @var IUserManager */
|
||||
private $userManager;
|
||||
/** @var IGroupManager */
|
||||
private $groupManager;
|
||||
/** @var ICommentsManager */
|
||||
private $commentsManager;
|
||||
/** @var AttachmentService */
|
||||
private $attachmentService;
|
||||
private BoardMapper $boardMapper;
|
||||
private LabelMapper $labelMapper;
|
||||
private CardMapper $cardMapper;
|
||||
private AssignmentMapper $assignedUsersMapper;
|
||||
private IUserManager $userManager;
|
||||
private ICommentsManager $commentsManager;
|
||||
private AttachmentService $attachmentService;
|
||||
|
||||
public function __construct(
|
||||
BoardMapper $boardMapper,
|
||||
@@ -62,7 +51,6 @@ class OverviewService {
|
||||
CardMapper $cardMapper,
|
||||
AssignmentMapper $assignedUsersMapper,
|
||||
IUserManager $userManager,
|
||||
IGroupManager $groupManager,
|
||||
ICommentsManager $commentsManager,
|
||||
AttachmentService $attachmentService
|
||||
) {
|
||||
@@ -71,7 +59,6 @@ class OverviewService {
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->assignedUsersMapper = $assignedUsersMapper;
|
||||
$this->userManager = $userManager;
|
||||
$this->groupManager = $groupManager;
|
||||
$this->commentsManager = $commentsManager;
|
||||
$this->attachmentService = $attachmentService;
|
||||
}
|
||||
@@ -93,62 +80,37 @@ class OverviewService {
|
||||
}
|
||||
|
||||
public function findAllWithDue(string $userId): array {
|
||||
$userBoards = $this->findAllBoardsFromUser($userId);
|
||||
$userBoards = $this->boardMapper->findAllForUser($userId);
|
||||
$allDueCards = [];
|
||||
foreach ($userBoards as $userBoard) {
|
||||
$service = $this;
|
||||
$allDueCards[] = array_map(static function ($card) use ($service, $userBoard, $userId) {
|
||||
$service->enrich($card, $userId);
|
||||
$cardData = $card->jsonSerialize();
|
||||
$cardData['boardId'] = $userBoard->getId();
|
||||
return $cardData;
|
||||
$allDueCards[] = array_map(function ($card) use ($userBoard, $userId) {
|
||||
$this->enrich($card, $userId);
|
||||
return (new CardDetails($card, $userBoard))->jsonSerialize();
|
||||
}, $this->cardMapper->findAllWithDue($userBoard->getId()));
|
||||
}
|
||||
return $allDueCards;
|
||||
return array_merge(...$allDueCards);
|
||||
}
|
||||
|
||||
public function findUpcomingCards(string $userId): array {
|
||||
$userBoards = $this->findAllBoardsFromUser($userId);
|
||||
$findCards = [];
|
||||
$userBoards = $this->boardMapper->findAllForUser($userId);
|
||||
$foundCards = [];
|
||||
foreach ($userBoards as $userBoard) {
|
||||
$service = $this;
|
||||
|
||||
if (count($userBoard->getAcl()) === 0) {
|
||||
// private board: get cards with due date
|
||||
$findCards[] = array_map(static function ($card) use ($service, $userBoard, $userId) {
|
||||
$service->enrich($card, $userId);
|
||||
$cardData = $card->jsonSerialize();
|
||||
$cardData['boardId'] = $userBoard->getId();
|
||||
return $cardData;
|
||||
}, $this->cardMapper->findAllWithDue($userBoard->getId()));
|
||||
$cards = $this->cardMapper->findAllWithDue($userBoard->getId());
|
||||
} else {
|
||||
// shared board: get all my assigned or unassigned cards
|
||||
$findCards[] = array_map(static function ($card) use ($service, $userBoard, $userId) {
|
||||
$service->enrich($card, $userId);
|
||||
$cardData = $card->jsonSerialize();
|
||||
$cardData['boardId'] = $userBoard->getId();
|
||||
return $cardData;
|
||||
}, $this->cardMapper->findToMeOrNotAssignedCards($userBoard->getId(), $userId));
|
||||
$cards = $this->cardMapper->findToMeOrNotAssignedCards($userBoard->getId(), $userId);
|
||||
}
|
||||
|
||||
$foundCards[] = array_map(
|
||||
function (Card $card) use ($userBoard, $userId) {
|
||||
$this->enrich($card, $userId);
|
||||
return (new CardDetails($card, $userBoard))->jsonSerialize();
|
||||
},
|
||||
$cards
|
||||
);
|
||||
}
|
||||
return $findCards;
|
||||
}
|
||||
|
||||
// FIXME: This is duplicate code with the board service
|
||||
private function findAllBoardsFromUser(string $userId): array {
|
||||
$userInfo = $this->getBoardPrerequisites($userId);
|
||||
$userBoards = $this->boardMapper->findAllByUser($userInfo['user'], null, null);
|
||||
$groupBoards = $this->boardMapper->findAllByGroups($userInfo['user'], $userInfo['groups'], null, null);
|
||||
$circleBoards = $this->boardMapper->findAllByCircles($userInfo['user'], null, null);
|
||||
return array_unique(array_merge($userBoards, $groupBoards, $circleBoards));
|
||||
}
|
||||
|
||||
private function getBoardPrerequisites($userId): array {
|
||||
$user = $this->userManager->get($userId);
|
||||
$groups = $user !== null ? $this->groupManager->getUserGroupIds($user) : [];
|
||||
return [
|
||||
'user' => $userId,
|
||||
'groups' => $groups
|
||||
];
|
||||
return array_merge(...$foundCards);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
|
||||
namespace OCA\Deck\Service;
|
||||
|
||||
use OC\Cache\CappedMemoryCache;
|
||||
use OCP\Cache\CappedMemoryCache;
|
||||
use OCA\Circles\Model\Member;
|
||||
use OCA\Deck\Db\Acl;
|
||||
use OCA\Deck\Db\AclMapper;
|
||||
@@ -63,8 +63,8 @@ class PermissionService {
|
||||
/** @var array */
|
||||
private $users = [];
|
||||
|
||||
private $boardCache;
|
||||
private $permissionCache;
|
||||
private CappedMemoryCache $boardCache;
|
||||
private CappedMemoryCache $permissionCache;
|
||||
|
||||
public function __construct(
|
||||
ILogger $logger,
|
||||
|
||||
@@ -30,26 +30,30 @@ use OCA\Deck\BadRequestException;
|
||||
use OCA\Deck\Db\Acl;
|
||||
use OCA\Deck\Db\AssignmentMapper;
|
||||
use OCA\Deck\Db\BoardMapper;
|
||||
use OCA\Deck\Db\Card;
|
||||
use OCA\Deck\Db\CardMapper;
|
||||
use OCA\Deck\Db\ChangeHelper;
|
||||
use OCA\Deck\Db\LabelMapper;
|
||||
use OCA\Deck\Db\Stack;
|
||||
use OCA\Deck\Db\StackMapper;
|
||||
use OCA\Deck\Model\CardDetails;
|
||||
use OCA\Deck\NoPermissionException;
|
||||
use OCA\Deck\StatusException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class StackService {
|
||||
private $stackMapper;
|
||||
private $cardMapper;
|
||||
private $boardMapper;
|
||||
private $labelMapper;
|
||||
private $permissionService;
|
||||
private $boardService;
|
||||
private $cardService;
|
||||
private $assignedUsersMapper;
|
||||
private $attachmentService;
|
||||
private $activityManager;
|
||||
private $changeHelper;
|
||||
private StackMapper $stackMapper;
|
||||
private CardMapper $cardMapper;
|
||||
private BoardMapper $boardMapper;
|
||||
private LabelMapper $labelMapper;
|
||||
private PermissionService $permissionService;
|
||||
private BoardService $boardService;
|
||||
private CardService $cardService;
|
||||
private AssignmentMapper $assignedUsersMapper;
|
||||
private AttachmentService $attachmentService;
|
||||
private ActivityManager $activityManager;
|
||||
private ChangeHelper $changeHelper;
|
||||
private LoggerInterface $logger;
|
||||
|
||||
public function __construct(
|
||||
StackMapper $stackMapper,
|
||||
@@ -62,7 +66,8 @@ class StackService {
|
||||
AssignmentMapper $assignedUsersMapper,
|
||||
AttachmentService $attachmentService,
|
||||
ActivityManager $activityManager,
|
||||
ChangeHelper $changeHelper
|
||||
ChangeHelper $changeHelper,
|
||||
LoggerInterface $logger
|
||||
) {
|
||||
$this->stackMapper = $stackMapper;
|
||||
$this->boardMapper = $boardMapper;
|
||||
@@ -75,6 +80,7 @@ class StackService {
|
||||
$this->attachmentService = $attachmentService;
|
||||
$this->activityManager = $activityManager;
|
||||
$this->changeHelper = $changeHelper;
|
||||
$this->logger = $logger;
|
||||
}
|
||||
|
||||
private function enrichStackWithCards($stack, $since = -1) {
|
||||
@@ -84,9 +90,13 @@ class StackService {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($cards as $card) {
|
||||
$this->cardService->enrich($card);
|
||||
}
|
||||
$cards = array_map(
|
||||
function (Card $card): CardDetails {
|
||||
$this->cardService->enrich($card);
|
||||
return new CardDetails($card);
|
||||
},
|
||||
$cards
|
||||
);
|
||||
|
||||
$stack->setCards($cards);
|
||||
}
|
||||
@@ -112,12 +122,18 @@ class StackService {
|
||||
|
||||
$this->permissionService->checkPermission($this->stackMapper, $stackId, Acl::PERMISSION_READ);
|
||||
$stack = $this->stackMapper->find($stackId);
|
||||
$cards = $this->cardMapper->findAll($stackId);
|
||||
foreach ($cards as $cardIndex => $card) {
|
||||
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
|
||||
$card->setAssignedUsers($assignedUsers);
|
||||
$card->setAttachmentCount($this->attachmentService->count($card->getId()));
|
||||
}
|
||||
|
||||
$cards = array_map(
|
||||
function (Card $card): CardDetails {
|
||||
$assignedUsers = $this->assignedUsersMapper->findAll($card->getId());
|
||||
$card->setAssignedUsers($assignedUsers);
|
||||
$card->setAttachmentCount($this->attachmentService->count($card->getId()));
|
||||
|
||||
return new CardDetails($card);
|
||||
},
|
||||
$this->cardMapper->findAll($stackId)
|
||||
);
|
||||
|
||||
$stack->setCards($cards);
|
||||
|
||||
return $stack;
|
||||
@@ -146,7 +162,7 @@ class StackService {
|
||||
try {
|
||||
$this->permissionService->checkPermission(null, $boardId, Acl::PERMISSION_READ);
|
||||
} catch (NoPermissionException $e) {
|
||||
\OC::$server->getLogger()->error('Unable to check permission for a previously obtained board ' . $boardId, ['exception' => $e]);
|
||||
$this->logger->error('Unable to check permission for a previously obtained board ' . $boardId, ['exception' => $e]);
|
||||
return [];
|
||||
}
|
||||
return $this->stackMapper->findAll($boardId);
|
||||
@@ -202,7 +218,7 @@ class StackService {
|
||||
* @throws BadRequestException
|
||||
*/
|
||||
public function create($title, $boardId, $order) {
|
||||
if ($title === false || $title === null) {
|
||||
if ($title === false || $title === null || mb_strlen($title) === 0) {
|
||||
throw new BadRequestException('title must be provided');
|
||||
}
|
||||
|
||||
@@ -279,7 +295,7 @@ class StackService {
|
||||
throw new BadRequestException('stack id must be a number');
|
||||
}
|
||||
|
||||
if ($title === false || $title === null) {
|
||||
if ($title === false || $title === null || mb_strlen($title) === 0) {
|
||||
throw new BadRequestException('title must be provided');
|
||||
}
|
||||
|
||||
@@ -336,6 +352,7 @@ class StackService {
|
||||
$this->permissionService->checkPermission($this->stackMapper, $id, Acl::PERMISSION_MANAGE);
|
||||
$stackToSort = $this->stackMapper->find($id);
|
||||
$stacks = $this->stackMapper->findAll($stackToSort->getBoardId());
|
||||
usort($stacks, static fn (Stack $stackA, Stack $stackB) => $stackA->getOrder() - $stackB->getOrder());
|
||||
$result = [];
|
||||
$i = 0;
|
||||
foreach ($stacks as $stack) {
|
||||
|
||||
@@ -65,22 +65,16 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
|
||||
public const SHARE_TYPE_DECK_USER = IShare::TYPE_DECK_USER;
|
||||
|
||||
/** @var IDBConnection */
|
||||
private $dbConnection;
|
||||
/** @var IManager */
|
||||
private $shareManager;
|
||||
/** @var AttachmentCacheHelper */
|
||||
private $attachmentCacheHelper;
|
||||
/** @var BoardMapper */
|
||||
private $boardMapper;
|
||||
/** @var CardMapper */
|
||||
private $cardMapper;
|
||||
/** @var PermissionService */
|
||||
private $permissionService;
|
||||
/** @var ITimeFactory */
|
||||
private $timeFactory;
|
||||
/** @var IL10N */
|
||||
private $l;
|
||||
private IDBConnection $dbConnection;
|
||||
private IManager $shareManager;
|
||||
private AttachmentCacheHelper $attachmentCacheHelper;
|
||||
private BoardMapper $boardMapper;
|
||||
private CardMapper $cardMapper;
|
||||
private PermissionService $permissionService;
|
||||
private ITimeFactory $timeFactory;
|
||||
private IL10N $l;
|
||||
private IMimeTypeLoader $mimeTypeLoader;
|
||||
private ?string $userId;
|
||||
|
||||
public function __construct(
|
||||
IDBConnection $connection,
|
||||
@@ -89,7 +83,10 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
CardMapper $cardMapper,
|
||||
PermissionService $permissionService,
|
||||
AttachmentCacheHelper $attachmentCacheHelper,
|
||||
IL10N $l
|
||||
IL10N $l,
|
||||
ITimeFactory $timeFactory,
|
||||
IMimeTypeLoader $mimeTypeLoader,
|
||||
?string $userId
|
||||
) {
|
||||
$this->dbConnection = $connection;
|
||||
$this->shareManager = $shareManager;
|
||||
@@ -97,9 +94,10 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
$this->cardMapper = $cardMapper;
|
||||
$this->attachmentCacheHelper = $attachmentCacheHelper;
|
||||
$this->permissionService = $permissionService;
|
||||
|
||||
$this->l = $l;
|
||||
$this->timeFactory = \OC::$server->get(ITimeFactory::class);
|
||||
$this->timeFactory = $timeFactory;
|
||||
$this->mimeTypeLoader = $mimeTypeLoader;
|
||||
$this->userId = $userId;
|
||||
}
|
||||
|
||||
public static function register(IEventDispatcher $dispatcher): void {
|
||||
@@ -207,13 +205,13 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
->setValue('file_target', $qb->createNamedParameter($target))
|
||||
->setValue('permissions', $qb->createNamedParameter($permissions))
|
||||
->setValue('token', $qb->createNamedParameter($token))
|
||||
->setValue('stime', $qb->createNamedParameter(\OC::$server->get(ITimeFactory::class)->getTime()));
|
||||
->setValue('stime', $qb->createNamedParameter($this->timeFactory->getTime()));
|
||||
|
||||
if ($expirationDate !== null) {
|
||||
$qb->setValue('expiration', $qb->createNamedParameter($expirationDate, 'datetime'));
|
||||
}
|
||||
|
||||
$qb->execute();
|
||||
$qb->executeStatement();
|
||||
|
||||
return $qb->getLastInsertId();
|
||||
}
|
||||
@@ -281,7 +279,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
$entryData = $data;
|
||||
$entryData['permissions'] = $entryData['f_permissions'];
|
||||
$entryData['parent'] = $entryData['f_parent'];
|
||||
$share->setNodeCacheEntry(Cache::cacheEntryFromData($entryData, \OC::$server->get(IMimeTypeLoader::class)));
|
||||
$share->setNodeCacheEntry(Cache::cacheEntryFromData($entryData, $this->mimeTypeLoader));
|
||||
}
|
||||
return $share;
|
||||
}
|
||||
@@ -474,14 +472,14 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
'file_target' => $qb->createNamedParameter($share->getTarget()),
|
||||
'permissions' => $qb->createNamedParameter($share->getPermissions()),
|
||||
'stime' => $qb->createNamedParameter($share->getShareTime()->getTimestamp()),
|
||||
])->execute();
|
||||
])->executeStatement();
|
||||
} else {
|
||||
// Already a userroom share. Update it.
|
||||
$qb = $this->dbConnection->getQueryBuilder();
|
||||
$qb->update('share')
|
||||
->set('file_target', $qb->createNamedParameter($share->getTarget()))
|
||||
->where($qb->expr()->eq('id', $qb->createNamedParameter($data['id'])))
|
||||
->execute();
|
||||
->executeStatement();
|
||||
}
|
||||
|
||||
return $share;
|
||||
@@ -491,7 +489,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
* @inheritDoc
|
||||
* @returns
|
||||
*/
|
||||
public function getSharesInFolder($userId, Folder $node, $reshares) {
|
||||
public function getSharesInFolder($userId, Folder $node, $reshares, $shallow = true) {
|
||||
$qb = $this->dbConnection->getQueryBuilder();
|
||||
$qb->select('*')
|
||||
->from('share', 's')
|
||||
@@ -518,7 +516,11 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
}
|
||||
|
||||
$qb->innerJoin('s', 'filecache', 'f', $qb->expr()->eq('s.file_source', 'f.fileid'));
|
||||
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
|
||||
if ($shallow) {
|
||||
$qb->andWhere($qb->expr()->eq('f.parent', $qb->createNamedParameter($node->getId())));
|
||||
} else {
|
||||
$qb->andWhere($qb->expr()->like('f.path', $qb->createNamedParameter($this->dbConnection->escapeLikeParameter($node->getInternalPath()) . '/%')));
|
||||
}
|
||||
|
||||
$qb->orderBy('s.id');
|
||||
|
||||
@@ -632,8 +634,8 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
$start = 0;
|
||||
while (true) {
|
||||
/** @var IShare[] $shareSlice */
|
||||
$shareSlice = array_slice($shares, $start, 100);
|
||||
$start += 100;
|
||||
$shareSlice = array_slice($shares, $start, 1000);
|
||||
$start += 1000;
|
||||
|
||||
if ($shareSlice === []) {
|
||||
break;
|
||||
@@ -712,15 +714,15 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
* @return IShare[]
|
||||
*/
|
||||
public function getSharedWith($userId, $shareType, $node, $limit, $offset): array {
|
||||
$allBoards = $this->boardMapper->findAllForUser($userId);
|
||||
$allBoards = $this->boardMapper->findBoardIds($userId);
|
||||
|
||||
/** @var IShare[] $shares */
|
||||
$shares = [];
|
||||
|
||||
$start = 0;
|
||||
while (true) {
|
||||
$boards = array_slice($allBoards, $start, 100);
|
||||
$start += 100;
|
||||
$boards = array_slice($allBoards, $start, 1000);
|
||||
$start += 1000;
|
||||
|
||||
if ($boards === []) {
|
||||
break;
|
||||
@@ -750,10 +752,6 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
$qb->andWhere($qb->expr()->eq('s.file_source', $qb->createNamedParameter($node->getId())));
|
||||
}
|
||||
|
||||
$boards = array_map(function (Board $board) {
|
||||
return $board->getId();
|
||||
}, $boards);
|
||||
|
||||
$qb->andWhere($qb->expr()->eq('s.share_type', $qb->createNamedParameter(IShare::TYPE_DECK)))
|
||||
->andWhere($qb->expr()->in('db.id', $qb->createNamedParameter(
|
||||
$boards,
|
||||
@@ -821,7 +819,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
$qb->expr()->eq('s.item_type', $qb->createNamedParameter('folder'))
|
||||
));
|
||||
|
||||
$cursor = $qb->execute();
|
||||
$cursor = $qb->executeQuery();
|
||||
while ($data = $cursor->fetch()) {
|
||||
if (!$this->isAccessibleResult($data)) {
|
||||
continue;
|
||||
@@ -836,9 +834,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
}
|
||||
$cursor->closeCursor();
|
||||
|
||||
$shares = $this->resolveSharesForRecipient($shares, \OC::$server->getUserSession()->getUser()->getUID());
|
||||
|
||||
return $shares;
|
||||
return $this->resolveSharesForRecipient($shares, $this->userId);
|
||||
}
|
||||
|
||||
public function isAccessibleResult(array $data): bool {
|
||||
@@ -941,7 +937,7 @@ class DeckShareProvider implements \OCP\Share\IShareProvider {
|
||||
$qb->expr()->eq('item_type', $qb->createNamedParameter('file')),
|
||||
$qb->expr()->eq('item_type', $qb->createNamedParameter('folder'))
|
||||
));
|
||||
$cursor = $qb->execute();
|
||||
$cursor = $qb->executeQuery();
|
||||
|
||||
$users = [];
|
||||
while ($row = $cursor->fetch()) {
|
||||
|
||||
@@ -29,14 +29,13 @@ namespace OCA\Deck\Sharing;
|
||||
use OC\Files\Filesystem;
|
||||
use OCA\Deck\Service\ConfigService;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Server;
|
||||
use OCP\Share\Events\VerifyMountPointEvent;
|
||||
use OCP\Share\IShare;
|
||||
use Symfony\Component\EventDispatcher\GenericEvent;
|
||||
|
||||
class Listener {
|
||||
|
||||
/** @var ConfigService */
|
||||
private $configService;
|
||||
private ConfigService $configService;
|
||||
|
||||
public function __construct(ConfigService $configService) {
|
||||
$this->configService = $configService;
|
||||
@@ -52,13 +51,13 @@ class Listener {
|
||||
|
||||
public static function listenPreShare(GenericEvent $event): void {
|
||||
/** @var self $listener */
|
||||
$listener = \OC::$server->query(self::class);
|
||||
$listener = Server::get(self::class);
|
||||
$listener->overwriteShareTarget($event);
|
||||
}
|
||||
|
||||
public static function listenVerifyMountPointEvent(VerifyMountPointEvent $event): void {
|
||||
/** @var self $listener */
|
||||
$listener = \OC::$server->query(self::class);
|
||||
$listener = Server::get(self::class);
|
||||
$listener->overwriteMountPoint($event);
|
||||
}
|
||||
|
||||
|
||||
66645
package-lock.json
generated
202
package.json
@@ -1,100 +1,106 @@
|
||||
{
|
||||
"name": "deck",
|
||||
"description": "",
|
||||
"version": "1.7.2",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Julius Härtl",
|
||||
"email": "jus@bitgrid.net",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Michael Weimann",
|
||||
"email": "mail@michael-weimann.eu",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"license": "agpl",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production webpack --progress --config webpack.js",
|
||||
"dev": "NODE_ENV=development webpack --progress --config webpack.js",
|
||||
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"lint:fix": "eslint --ext .js,.vue src --fix",
|
||||
"stylelint": "stylelint src",
|
||||
"stylelint:fix": "stylelint src --fix",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/runtime": "^7.17.9",
|
||||
"@juliushaertl/vue-richtext": "^1.0.1",
|
||||
"@nextcloud/auth": "^1.3.0",
|
||||
"@nextcloud/axios": "^1.9.0",
|
||||
"@nextcloud/dialogs": "^3.1.2",
|
||||
"@nextcloud/event-bus": "^2.1.1",
|
||||
"@nextcloud/files": "^2.1.0",
|
||||
"@nextcloud/initial-state": "^1.2.1",
|
||||
"@nextcloud/l10n": "^1.4.1",
|
||||
"@nextcloud/moment": "^1.2.0",
|
||||
"@nextcloud/router": "^2.0.0",
|
||||
"@nextcloud/vue": "^5.3.1",
|
||||
"@nextcloud/vue-dashboard": "^2.0.1",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"dompurify": "^2.3.6",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^12.3.2",
|
||||
"markdown-it-link-attributes": "^4.0.0",
|
||||
"markdown-it-task-checkbox": "^1.0.6",
|
||||
"moment": "^2.29.2",
|
||||
"nextcloud-vue-collections": "^0.9.0",
|
||||
"p-queue": "^6.6.2",
|
||||
"url-search-params-polyfill": "^8.1.1",
|
||||
"vue": "^2.6.14",
|
||||
"vue-at": "^2.5.0-beta.2",
|
||||
"vue-click-outside": "^1.1.0",
|
||||
"vue-easymde": "^2.0.0",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-router": "^3.5.3",
|
||||
"vue-smooth-dnd": "^0.8.1",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^14.0.0",
|
||||
"npm": "^7.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextcloud/babel-config": "^1.0.0",
|
||||
"@nextcloud/browserslist-config": "^2.2.0",
|
||||
"@nextcloud/eslint-config": "^6.1.2",
|
||||
"@nextcloud/stylelint-config": "^2.1.2",
|
||||
"@nextcloud/webpack-vue-config": "^5.0.0",
|
||||
"@relative-ci/agent": "^3.1.2",
|
||||
"@vue/test-utils": "^1.3.0",
|
||||
"jest": "^27.5.1",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"vue-jest": "^3.0.7"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"vue"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^@/(.*)$": "<rootDir>/src/$1"
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
|
||||
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
|
||||
},
|
||||
"snapshotSerializers": [
|
||||
"<rootDir>/node_modules/jest-serializer-vue"
|
||||
]
|
||||
}
|
||||
"name": "deck",
|
||||
"description": "",
|
||||
"version": "1.9.0-beta.1",
|
||||
"authors": [
|
||||
{
|
||||
"name": "Julius Härtl",
|
||||
"email": "jus@bitgrid.net",
|
||||
"role": "Developer"
|
||||
},
|
||||
{
|
||||
"name": "Michael Weimann",
|
||||
"email": "mail@michael-weimann.eu",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"license": "agpl",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"build": "NODE_ENV=production webpack --progress --config webpack.js",
|
||||
"dev": "NODE_ENV=development webpack --progress --config webpack.js",
|
||||
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.js",
|
||||
"lint": "eslint --ext .js,.vue src",
|
||||
"lint:fix": "eslint --ext .js,.vue src --fix",
|
||||
"stylelint": "stylelint src",
|
||||
"stylelint:fix": "stylelint src --fix",
|
||||
"test": "jest",
|
||||
"test:coverage": "jest --coverage"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/polyfill": "^7.12.1",
|
||||
"@babel/runtime": "^7.19.4",
|
||||
"@juliushaertl/vue-richtext": "^1.0.1",
|
||||
"@nextcloud/auth": "^2.0.0",
|
||||
"@nextcloud/axios": "^2.1.0",
|
||||
"@nextcloud/dialogs": "^3.2.0",
|
||||
"@nextcloud/event-bus": "^3.0.2",
|
||||
"@nextcloud/files": "^2.1.0",
|
||||
"@nextcloud/initial-state": "^2.0.0",
|
||||
"@nextcloud/l10n": "^1.6.0",
|
||||
"@nextcloud/moment": "^1.2.1",
|
||||
"@nextcloud/router": "^2.0.0",
|
||||
"@nextcloud/vue": "^7.0.1",
|
||||
"@nextcloud/vue-dashboard": "^2.0.1",
|
||||
"@nextcloud/vue-richtext": "^2.0.4",
|
||||
"blueimp-md5": "^2.19.0",
|
||||
"dompurify": "^2.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-link-attributes": "^4.0.1",
|
||||
"markdown-it-task-checkbox": "^1.0.6",
|
||||
"moment": "^2.29.4",
|
||||
"nextcloud-vue-collections": "^0.10.0",
|
||||
"p-queue": "^7.3.0",
|
||||
"url-search-params-polyfill": "^8.1.1",
|
||||
"vue": "^2.7.13",
|
||||
"vue-at": "^2.5.1",
|
||||
"vue-click-outside": "^1.1.0",
|
||||
"vue-easymde": "^2.0.0",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-material-design-icons": "^5.1.2",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-smooth-dnd": "^0.8.1",
|
||||
"vuex": "^3.6.2",
|
||||
"vuex-router-sync": "^5.0.0"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @nextcloud/browserslist-config"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^16.0.0",
|
||||
"npm": "^7.0.0 || ^8.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nextcloud/babel-config": "^1.0.0",
|
||||
"@nextcloud/browserslist-config": "^2.3.0",
|
||||
"@nextcloud/eslint-config": "^8.0.0",
|
||||
"@nextcloud/stylelint-config": "^2.2.0",
|
||||
"@nextcloud/webpack-vue-config": "^5.3.0",
|
||||
"@relative-ci/agent": "^4.1.1",
|
||||
"@vue/test-utils": "^1.3.0",
|
||||
"cypress": "^10.10.0",
|
||||
"eslint-webpack-plugin": "^3.2.0",
|
||||
"jest": "^29.2.1",
|
||||
"jest-serializer-vue": "^2.0.2",
|
||||
"stylelint-webpack-plugin": "^3.3.0",
|
||||
"vue-jest": "^3.0.7",
|
||||
"vue-template-compiler": "^2.7.13"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"js",
|
||||
"vue"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^@/(.*)$": "<rootDir>/src/$1"
|
||||
},
|
||||
"transform": {
|
||||
"^.+\\.js$": "<rootDir>/node_modules/babel-jest",
|
||||
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
|
||||
},
|
||||
"snapshotSerializers": [
|
||||
"<rootDir>/node_modules/jest-serializer-vue"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
errorBaseline="tests/psalm-baseline.xml"
|
||||
>
|
||||
<stubs>
|
||||
<file name="tests/stub.phpstub" preloadClasses="true"/>
|
||||
</stubs>
|
||||
<projectFiles>
|
||||
<directory name="lib" />
|
||||
<ignoreFiles>
|
||||
|
||||
59
src/App.vue
@@ -21,14 +21,13 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Content id="content" app-name="deck" :class="{ 'nav-hidden': !navShown, 'sidebar-hidden': !sidebarRouterView }">
|
||||
<NcContent app-name="deck" :class="{ 'nav-hidden': !navShown, 'sidebar-hidden': !sidebarRouterView }">
|
||||
<AppNavigation />
|
||||
<AppContent>
|
||||
<NcAppContent>
|
||||
<router-view />
|
||||
</AppContent>
|
||||
</NcAppContent>
|
||||
|
||||
<Modal
|
||||
v-if="cardDetailsInModal && $route.params.cardId"
|
||||
<NcModal v-if="cardDetailsInModal && $route.params.cardId"
|
||||
:clear-view-delay="0"
|
||||
:title="t('deck', 'Card details')"
|
||||
size="large"
|
||||
@@ -36,18 +35,17 @@
|
||||
<div class="modal__content modal__card">
|
||||
<router-view name="sidebar" />
|
||||
</div>
|
||||
</Modal>
|
||||
</NcModal>
|
||||
|
||||
<router-view name="sidebar" :visible="!cardDetailsInModal || !$route.params.cardId" />
|
||||
</Content>
|
||||
</NcContent>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
||||
import { mapState } from 'vuex'
|
||||
import AppNavigation from './components/navigation/AppNavigation'
|
||||
import { Modal, Content, AppContent } from '@nextcloud/vue'
|
||||
import { BoardApi } from './services/BoardApi'
|
||||
import AppNavigation from './components/navigation/AppNavigation.vue'
|
||||
import { NcModal, NcContent, NcAppContent } from '@nextcloud/vue'
|
||||
import { BoardApi } from './services/BoardApi.js'
|
||||
import { emit, subscribe } from '@nextcloud/event-bus'
|
||||
|
||||
const boardApi = new BoardApi()
|
||||
@@ -56,9 +54,9 @@ export default {
|
||||
name: 'App',
|
||||
components: {
|
||||
AppNavigation,
|
||||
Modal,
|
||||
Content,
|
||||
AppContent,
|
||||
NcModal,
|
||||
NcContent,
|
||||
NcAppContent,
|
||||
},
|
||||
provide() {
|
||||
return {
|
||||
@@ -130,7 +128,7 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
#content {
|
||||
#content-vue {
|
||||
#app-content {
|
||||
transition: margin-left 100ms ease;
|
||||
position: relative;
|
||||
@@ -158,6 +156,37 @@ export default {
|
||||
</style>
|
||||
|
||||
<style lang="scss">
|
||||
@import '../css/print';
|
||||
|
||||
.icon-activity {
|
||||
background-image: url(../img/activity-dark.svg);
|
||||
|
||||
body[data-theme-dark] & {
|
||||
background-image: url(../img/activity.svg);
|
||||
}
|
||||
}
|
||||
|
||||
.avatardiv.circles {
|
||||
background: var(--color-primary);
|
||||
}
|
||||
|
||||
.icon-circles {
|
||||
background-image: url(../img/circles-dark.svg);
|
||||
opacity: 1;
|
||||
background-size: 20px;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.icon-circles-white, .icon-circles.icon-white {
|
||||
background-image: url(../img/circles.svg);
|
||||
opacity: 1;
|
||||
background-size: 20px;
|
||||
background-position: center center;
|
||||
}
|
||||
|
||||
.icon-colorpicker {
|
||||
background-image: url('../img/color_picker.svg');
|
||||
}
|
||||
|
||||
.multiselect {
|
||||
width: 100%;
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Modal @close="close">
|
||||
<NcModal @close="close">
|
||||
<div id="modal-inner" :class="{ 'icon-loading': loading }">
|
||||
<h1>{{ t('deck', 'Select the board to link to a project') }}</h1>
|
||||
<input v-model="filter" type="text" :placeholder="t('deck', 'Search by board title')">
|
||||
@@ -38,17 +38,17 @@
|
||||
{{ t('deck', 'Select board') }}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</NcModal>
|
||||
</template>
|
||||
<script>
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
import { NcModal } from '@nextcloud/vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
|
||||
export default {
|
||||
name: 'BoardSelector',
|
||||
components: {
|
||||
Modal,
|
||||
NcModal,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
||||
@@ -21,11 +21,11 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Modal class="card-selector" @close="close">
|
||||
<NcModal class="card-selector" @close="close">
|
||||
<div class="modal-scroller">
|
||||
<div v-if="!creating && !created" id="modal-inner" :class="{ 'icon-loading': loading }">
|
||||
<h3>{{ t('deck', 'Create a new card') }}</h3>
|
||||
<Multiselect v-model="selectedBoard"
|
||||
<NcMultiselect v-model="selectedBoard"
|
||||
:placeholder="t('deck', 'Select a board')"
|
||||
:options="boards"
|
||||
:disabled="loading"
|
||||
@@ -44,9 +44,9 @@
|
||||
<span>{{ props.option.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</NcMultiselect>
|
||||
|
||||
<Multiselect v-model="selectedStack"
|
||||
<NcMultiselect v-model="selectedStack"
|
||||
:placeholder="t('deck', 'Select a list')"
|
||||
:options="stacksFromBoard"
|
||||
:max-height="100"
|
||||
@@ -71,10 +71,10 @@
|
||||
</div>
|
||||
</div>
|
||||
<div v-else id="modal-inner">
|
||||
<EmptyContent v-if="creating" icon="icon-loading">
|
||||
<NcEmptyContent v-if="creating" icon="icon-loading">
|
||||
{{ t('deck', 'Creating the new card …') }}
|
||||
</EmptyContent>
|
||||
<EmptyContent v-else-if="created" icon="icon-checkmark">
|
||||
</NcEmptyContent>
|
||||
<NcEmptyContent v-else-if="created" icon="icon-checkmark">
|
||||
{{ t('deck', 'Card "{card}" was added to "{board}"', { card: pendingTitle, board: selectedBoard.title }) }}
|
||||
<template #desc>
|
||||
<button class="primary" @click="openNewCard">
|
||||
@@ -84,28 +84,26 @@
|
||||
{{ t('deck', 'Close') }}
|
||||
</button>
|
||||
</template>
|
||||
</EmptyContent>
|
||||
</NcEmptyContent>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
|
||||
import EmptyContent from '@nextcloud/vue/dist/Components/EmptyContent'
|
||||
import { NcModal, NcMultiselect, NcEmptyContent } from '@nextcloud/vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { CardApi } from './services/CardApi'
|
||||
import { CardApi } from './services/CardApi.js'
|
||||
|
||||
const cardApi = new CardApi()
|
||||
|
||||
export default {
|
||||
name: 'CardCreateDialog',
|
||||
components: {
|
||||
EmptyContent,
|
||||
Modal,
|
||||
Multiselect,
|
||||
NcEmptyContent,
|
||||
NcModal,
|
||||
NcMultiselect,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
|
||||
@@ -21,10 +21,10 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<Modal class="card-selector" @close="close">
|
||||
<NcModal class="card-selector" @close="close">
|
||||
<div id="modal-inner" :class="{ 'icon-loading': loading }">
|
||||
<h3>{{ title }}</h3>
|
||||
<Multiselect v-model="selectedBoard"
|
||||
<NcMultiselect v-model="selectedBoard"
|
||||
:placeholder="t('deck', 'Select a board')"
|
||||
:options="boards"
|
||||
:disabled="loading"
|
||||
@@ -42,9 +42,9 @@
|
||||
<span>{{ props.option.title }}</span>
|
||||
</span>
|
||||
</template>
|
||||
</Multiselect>
|
||||
</NcMultiselect>
|
||||
|
||||
<Multiselect v-model="selectedCard"
|
||||
<NcMultiselect v-model="selectedCard"
|
||||
:placeholder="t('deck', 'Select a card')"
|
||||
:options="cardsFromBoard"
|
||||
:disabled="loading || selectedBoard === ''"
|
||||
@@ -57,20 +57,19 @@
|
||||
{{ t('deck', 'Cancel') }}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</NcModal>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import Modal from '@nextcloud/vue/dist/Components/Modal'
|
||||
import Multiselect from '@nextcloud/vue/dist/Components/Multiselect'
|
||||
import { NcModal, NcMultiselect } from '@nextcloud/vue'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
export default {
|
||||
name: 'CardSelector',
|
||||
components: {
|
||||
Modal,
|
||||
Multiselect,
|
||||
NcModal,
|
||||
NcMultiselect,
|
||||
},
|
||||
props: {
|
||||
title: {
|
||||
|
||||
@@ -36,10 +36,10 @@
|
||||
|
||||
<script>
|
||||
import RichText from '@juliushaertl/vue-richtext'
|
||||
import { UserBubble } from '@nextcloud/vue'
|
||||
import { NcUserBubble } from '@nextcloud/vue'
|
||||
import moment from '@nextcloud/moment'
|
||||
import DOMPurify from 'dompurify'
|
||||
import relativeDate from '../mixins/relativeDate'
|
||||
import relativeDate from '../mixins/relativeDate.js'
|
||||
|
||||
const InternalLink = {
|
||||
name: 'InternalLink',
|
||||
@@ -93,7 +93,7 @@ export default {
|
||||
break
|
||||
case 'user':
|
||||
parameters[key] = {
|
||||
component: UserBubble,
|
||||
component: NcUserBubble,
|
||||
props: {
|
||||
user: parameters[key].id,
|
||||
displayName: parameters[key].name,
|
||||
|
||||
@@ -37,7 +37,7 @@
|
||||
<script>
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateOcsUrl } from '@nextcloud/router'
|
||||
import ActivityEntry from './ActivityEntry'
|
||||
import ActivityEntry from './ActivityEntry.vue'
|
||||
import InfiniteLoading from 'vue-infinite-loading'
|
||||
|
||||
const ACTIVITY_FETCH_LIMIT = 50
|
||||
|
||||
@@ -27,24 +27,21 @@
|
||||
@drop.prevent="handleDropFiles">
|
||||
<slot />
|
||||
<transition name="fade" mode="out-in">
|
||||
<div
|
||||
v-show="isDraggingOver"
|
||||
<div v-show="isDraggingOver"
|
||||
class="dragover">
|
||||
<div class="drop-hint">
|
||||
<div
|
||||
class="drop-hint__icon"
|
||||
<div class="drop-hint__icon"
|
||||
:class="{
|
||||
'icon-upload' : !isReadOnly,
|
||||
'icon-error' : isReadOnly}" />
|
||||
<h2
|
||||
class="drop-hint__text">
|
||||
<h2 class="drop-hint__text">
|
||||
{{ dropHintText }}
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
</transition>
|
||||
|
||||
<Modal v-if="modalShow" :title="t('deck', 'File already exists')" @close="modalShow=false">
|
||||
<NcModal v-if="modalShow" :title="t('deck', 'File already exists')" @close="modalShow=false">
|
||||
<div class="modal__content">
|
||||
<h2>{{ t('deck', 'File already exists') }}</h2>
|
||||
<p>
|
||||
@@ -60,13 +57,13 @@
|
||||
{{ t('deck', 'Keep existing file') }}
|
||||
</button>
|
||||
</div>
|
||||
</Modal>
|
||||
</NcModal>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Modal } from '@nextcloud/vue'
|
||||
import attachmentUpload from '../mixins/attachmentUpload'
|
||||
import { NcModal } from '@nextcloud/vue'
|
||||
import attachmentUpload from '../mixins/attachmentUpload.js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
let maxUploadSizeState
|
||||
@@ -78,7 +75,7 @@ try {
|
||||
|
||||
export default {
|
||||
name: 'AttachmentDragAndDrop',
|
||||
components: { Modal },
|
||||
components: { NcModal },
|
||||
mixins: [attachmentUpload],
|
||||
props: {
|
||||
cardId: {
|
||||
|
||||
@@ -25,11 +25,11 @@
|
||||
<div v-if="overviewName" class="board-title">
|
||||
<div class="board-bullet icon-calendar-dark" />
|
||||
<h2>{{ overviewName }}</h2>
|
||||
<Actions>
|
||||
<ActionButton icon="icon-add" @click="clickShowAddCardModel">
|
||||
<NcActions>
|
||||
<NcActionButton icon="icon-add" @click="clickShowAddCardModel">
|
||||
{{ t('deck', 'Add card') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<CardCreateDialog v-if="showAddCardModal" @close="clickHideAddCardModel" />
|
||||
</div>
|
||||
<div v-else-if="board" class="board-title">
|
||||
@@ -49,11 +49,11 @@
|
||||
<div v-if="board && canManage && !showArchived && !board.archived"
|
||||
id="stack-add"
|
||||
v-click-outside="hideAddStack">
|
||||
<Actions v-if="!isAddStackVisible">
|
||||
<ActionButton icon="icon-add" @click.stop="showAddStack">
|
||||
<NcActions v-if="!isAddStackVisible">
|
||||
<NcActionButton icon="icon-add" @click.stop="showAddStack">
|
||||
{{ t('deck', 'Add list') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<form v-else @submit.prevent="addNewStack()">
|
||||
<label for="new-stack-input-main" class="hidden-visually">{{ t('deck', 'Add list') }}</label>
|
||||
<input id="new-stack-input-main"
|
||||
@@ -70,137 +70,145 @@
|
||||
</form>
|
||||
</div>
|
||||
<div v-if="board" class="board-action-buttons">
|
||||
<Popover @show="filterVisible=true" @hide="filterVisible=false">
|
||||
<Actions slot="trigger" :title="t('deck', 'Apply filter')">
|
||||
<ActionButton v-if="isFilterActive" icon="icon-filter_set" />
|
||||
<ActionButton v-else icon="icon-filter" />
|
||||
</Actions>
|
||||
<div class="board-action-buttons__filter">
|
||||
<NcPopover container=".board-action-buttons__filter"
|
||||
:placement="'bottom-end'"
|
||||
:aria-label="t('deck', 'Active filters')"
|
||||
@show="filterVisible=true"
|
||||
@hide="filterVisible=false">
|
||||
<!-- We cannot use NcActions here are the popover trigger does not update on reactive icons -->
|
||||
<NcButton slot="trigger"
|
||||
:title="t('deck', 'Apply filter')"
|
||||
class="filter-button"
|
||||
type="tertiary-no-background">
|
||||
<template #icon>
|
||||
<FilterIcon v-if="isFilterActive" :size="20" decorative />
|
||||
<FilterOffIcon v-else :size="20" decorative />
|
||||
</template>
|
||||
</NcButton>
|
||||
|
||||
<div v-if="filterVisible" class="filter">
|
||||
<h3>{{ t('deck', 'Filter by tag') }}</h3>
|
||||
<div v-for="label in labelsSorted" :key="label.id" class="filter--item">
|
||||
<input
|
||||
:id="label.id"
|
||||
v-model="filter.tags"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:value="label.id"
|
||||
@change="setFilter">
|
||||
<label :for="label.id"><span class="label" :style="labelStyle(label)">{{ label.title }}</span></label>
|
||||
<div v-if="filterVisible" class="filter">
|
||||
<h3>{{ t('deck', 'Filter by tag') }}</h3>
|
||||
<div v-for="label in labelsSorted" :key="label.id" class="filter--item">
|
||||
<input :id="label.id"
|
||||
v-model="filter.tags"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:value="label.id"
|
||||
@change="setFilter">
|
||||
<label :for="label.id"><span class="label" :style="labelStyle(label)">{{ label.title }}</span></label>
|
||||
</div>
|
||||
|
||||
<h3>{{ t('deck', 'Filter by assigned user') }}</h3>
|
||||
<div class="filter--item">
|
||||
<input id="unassigned"
|
||||
v-model="filter.unassigned"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
value="unassigned"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="unassigned">{{ t('deck', 'Unassigned') }}</label>
|
||||
</div>
|
||||
<div v-for="user in board.users" :key="user.uid" class="filter--item">
|
||||
<input :id="user.uid"
|
||||
v-model="filter.users"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:value="user.uid"
|
||||
@change="setFilter">
|
||||
<label :for="user.uid"><NcAvatar :user="user.uid" :size="24" :disable-menu="true" /> {{ user.displayname }}</label>
|
||||
</div>
|
||||
|
||||
<h3>{{ t('deck', 'Filter by due date') }}</h3>
|
||||
|
||||
<div class="filter--item">
|
||||
<input id="overdue"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="overdue"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="overdue">{{ t('deck', 'Overdue') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input id="dueToday"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueToday"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueToday">{{ t('deck', 'Next 24 hours') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input id="dueWeek"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueWeek"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueWeek">{{ t('deck', 'Next 7 days') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input id="dueMonth"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueMonth"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueMonth">{{ t('deck', 'Next 30 days') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input id="noDue"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="noDue"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="noDue">{{ t('deck', 'No due date') }}</label>
|
||||
</div>
|
||||
|
||||
<NcButton :disabled="!isFilterActive" :wide="true" @click="clearFilter">
|
||||
{{ t('deck', 'Clear filter') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</NcPopover>
|
||||
</div>
|
||||
|
||||
<h3>{{ t('deck', 'Filter by assigned user') }}</h3>
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="unassigned"
|
||||
v-model="filter.unassigned"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
value="unassigned"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="unassigned">{{ t('deck', 'Unassigned') }}</label>
|
||||
</div>
|
||||
<div v-for="user in board.users" :key="user.uid" class="filter--item">
|
||||
<input
|
||||
:id="user.uid"
|
||||
v-model="filter.users"
|
||||
type="checkbox"
|
||||
class="checkbox"
|
||||
:value="user.uid"
|
||||
@change="setFilter">
|
||||
<label :for="user.uid"><Avatar :user="user.uid" :size="24" :disable-menu="true" /> {{ user.displayname }}</label>
|
||||
</div>
|
||||
|
||||
<h3>{{ t('deck', 'Filter by due date') }}</h3>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="overdue"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="overdue"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="overdue">{{ t('deck', 'Overdue') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueToday"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueToday"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueToday">{{ t('deck', 'Next 24 hours') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueWeek"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueWeek"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueWeek">{{ t('deck', 'Next 7 days') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="dueMonth"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="dueMonth"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="dueMonth">{{ t('deck', 'Next 30 days') }}</label>
|
||||
</div>
|
||||
|
||||
<div class="filter--item">
|
||||
<input
|
||||
id="noDue"
|
||||
v-model="filter.due"
|
||||
type="radio"
|
||||
class="radio"
|
||||
value="noDue"
|
||||
@change="setFilter"
|
||||
@click="beforeSetFilter">
|
||||
<label for="noDue">{{ t('deck', 'No due date') }}</label>
|
||||
</div>
|
||||
|
||||
<Button :disabled="!isFilterActive" @click="clearFilter">
|
||||
{{ t('deck', 'Clear filter') }}
|
||||
</Button>
|
||||
</div>
|
||||
</Popover>
|
||||
|
||||
<Actions>
|
||||
<ActionButton
|
||||
icon="icon-archive"
|
||||
@click="toggleShowArchived">
|
||||
<NcActions>
|
||||
<NcActionButton @click="toggleShowArchived">
|
||||
<template #icon>
|
||||
<ArchiveIcon :size="20" decorative />
|
||||
</template>
|
||||
{{ showArchived ? t('deck', 'Hide archived cards') : t('deck', 'Show archived cards') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-if="compactMode"
|
||||
icon="icon-toggle-compact-collapsed"
|
||||
</NcActionButton>
|
||||
<NcActionButton v-if="compactMode"
|
||||
@click="toggleCompactMode">
|
||||
<ArrowExpandVerticalIcon slot="icon" :size="20" decorative />
|
||||
{{ t('deck', 'Toggle compact mode') }}
|
||||
</ActionButton>
|
||||
<ActionButton v-else
|
||||
icon="icon-toggle-compact-expanded"
|
||||
</NcActionButton>
|
||||
<NcActionButton v-else
|
||||
@click="toggleCompactMode">
|
||||
<ArrowCollapseVerticalIcon slot="icon" :size="20" decorative />
|
||||
{{ t('deck', 'Toggle compact mode') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
<!-- FIXME: ActionRouter currently doesn't work as an inline action -->
|
||||
<Actions :title="t('deck', 'Details')">
|
||||
<ActionButton icon="icon-menu-sidebar" @click="toggleDetailsView" />
|
||||
</Actions>
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
<!-- FIXME: NcActionRouter currently doesn't work as an inline action -->
|
||||
<NcActions>
|
||||
<NcActionButton icon="icon-menu-sidebar"
|
||||
:aria-label="t('deck', 'Open details')"
|
||||
:title="t('deck', 'Details')"
|
||||
@click="toggleDetailsView" />
|
||||
</NcActions>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -208,14 +216,29 @@
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import { Actions, ActionButton, Popover, Avatar } from '@nextcloud/vue'
|
||||
import labelStyle from '../mixins/labelStyle'
|
||||
import CardCreateDialog from '../CardCreateDialog'
|
||||
import { NcActions, NcActionButton, NcAvatar, NcButton, NcPopover } from '@nextcloud/vue'
|
||||
import labelStyle from '../mixins/labelStyle.js'
|
||||
import CardCreateDialog from '../CardCreateDialog.vue'
|
||||
import ArchiveIcon from 'vue-material-design-icons/Archive.vue'
|
||||
import FilterIcon from 'vue-material-design-icons/Filter.vue'
|
||||
import FilterOffIcon from 'vue-material-design-icons/FilterOff.vue'
|
||||
import ArrowCollapseVerticalIcon from 'vue-material-design-icons/ArrowCollapseVertical.vue'
|
||||
import ArrowExpandVerticalIcon from 'vue-material-design-icons/ArrowExpandVertical.vue'
|
||||
|
||||
export default {
|
||||
name: 'Controls',
|
||||
components: {
|
||||
Actions, ActionButton, Popover, Avatar, CardCreateDialog,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
NcButton,
|
||||
NcPopover,
|
||||
NcAvatar,
|
||||
CardCreateDialog,
|
||||
ArchiveIcon,
|
||||
FilterIcon,
|
||||
FilterOffIcon,
|
||||
ArrowCollapseVerticalIcon,
|
||||
ArrowExpandVerticalIcon,
|
||||
},
|
||||
mixins: [labelStyle],
|
||||
props: {
|
||||
@@ -258,18 +281,12 @@ export default {
|
||||
}
|
||||
},
|
||||
isFilterActive() {
|
||||
if (this.filter.tags.length !== 0 || this.filter.users.length !== 0 || this.filter.due !== '') {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return this.filter.tags.length !== 0 || this.filter.users.length !== 0 || this.filter.due !== ''
|
||||
},
|
||||
labelsSorted() {
|
||||
return [...this.board.labels].sort((a, b) => (a.title < b.title) ? -1 : 1)
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.setPageTitle('')
|
||||
},
|
||||
watch: {
|
||||
board(current, previous) {
|
||||
if (current?.id !== previous?.id) {
|
||||
@@ -280,6 +297,9 @@ export default {
|
||||
}
|
||||
},
|
||||
},
|
||||
beforeDestroy() {
|
||||
this.setPageTitle('')
|
||||
},
|
||||
methods: {
|
||||
beforeSetFilter(e) {
|
||||
if (this.filter.due === e.target.value) {
|
||||
@@ -360,7 +380,7 @@ export default {
|
||||
<style lang="scss" scoped>
|
||||
.controls {
|
||||
display: flex;
|
||||
padding: 3px;
|
||||
margin: 5px;
|
||||
height: 44px;
|
||||
padding-left: 44px;
|
||||
|
||||
@@ -408,18 +428,15 @@ export default {
|
||||
|
||||
.board-action-buttons {
|
||||
display: flex;
|
||||
button {
|
||||
border: 0;
|
||||
width: 44px;
|
||||
margin: 0 0 0 -1px;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.deck-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
input[type=search] {
|
||||
background-position: 5px;
|
||||
padding-left: 24px;
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -441,8 +458,9 @@ export default {
|
||||
}
|
||||
|
||||
.filter {
|
||||
width: 250px;
|
||||
max-height: 80vh;
|
||||
width: 240px;
|
||||
max-height: calc(100vh - 150px);
|
||||
position: relative;
|
||||
overflow: auto;
|
||||
padding: 8px;
|
||||
}
|
||||
@@ -451,8 +469,23 @@ export default {
|
||||
margin-top: 0px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.filter-button {
|
||||
padding: 0;
|
||||
border-radius: 50%;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
|
||||
&:hover, &:focus {
|
||||
background-color: rgba(127,127,127,0.25) !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<style lang="scss">
|
||||
.popover:focus {
|
||||
outline: 2px solid var(--color-main-text);
|
||||
}
|
||||
|
||||
.tooltip-inner.popover-inner {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@ export default {
|
||||
#app-sidebar .icon-close {
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.app-deck .app-sidebar {
|
||||
z-index: 1500 !important;
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
<h2>{{ t('deck', 'Loading board') }}</h2>
|
||||
<p />
|
||||
</div>
|
||||
<EmptyContent v-else-if="isEmpty" key="empty" icon="icon-deck">
|
||||
<NcEmptyContent v-else-if="isEmpty" key="empty" icon="icon-deck">
|
||||
{{ t('deck', 'No lists available') }}
|
||||
<template v-if="canManage" #desc>
|
||||
{{ t('deck', 'Create a new list to add cards to this board') }}
|
||||
@@ -48,7 +48,7 @@
|
||||
value="">
|
||||
</form>
|
||||
</template>
|
||||
</EmptyContent>
|
||||
</NcEmptyContent>
|
||||
<div v-else-if="!isEmpty && !loading" key="board" class="board">
|
||||
<Container lock-axix="y"
|
||||
orientation="horizontal"
|
||||
@@ -73,11 +73,11 @@
|
||||
|
||||
import { Container, Draggable } from 'vue-smooth-dnd'
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import Controls from '../Controls'
|
||||
import Stack from './Stack'
|
||||
import { EmptyContent } from '@nextcloud/vue'
|
||||
import GlobalSearchResults from '../search/GlobalSearchResults'
|
||||
import { showError } from '../../helpers/errors'
|
||||
import Controls from '../Controls.vue'
|
||||
import Stack from './Stack.vue'
|
||||
import { NcEmptyContent } from '@nextcloud/vue'
|
||||
import GlobalSearchResults from '../search/GlobalSearchResults.vue'
|
||||
import { showError } from '../../helpers/errors.js'
|
||||
|
||||
export default {
|
||||
name: 'Board',
|
||||
@@ -87,7 +87,7 @@ export default {
|
||||
Container,
|
||||
Draggable,
|
||||
Stack,
|
||||
EmptyContent,
|
||||
NcEmptyContent,
|
||||
},
|
||||
inject: [
|
||||
'boardApi',
|
||||
@@ -163,8 +163,8 @@ export default {
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@import '../../css/animations.scss';
|
||||
@import '../../css/variables.scss';
|
||||
@import '../../css/animations';
|
||||
@import '../../css/variables';
|
||||
|
||||
form {
|
||||
text-align: center;
|
||||
@@ -222,6 +222,7 @@ export default {
|
||||
padding: $stack-spacing;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
scrollbar-gutter: stable;
|
||||
padding-top: 15px;
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
@@ -21,57 +21,57 @@
|
||||
-->
|
||||
|
||||
<template>
|
||||
<AppSidebar v-if="board != null"
|
||||
<NcAppSidebar v-if="board != null"
|
||||
:actions="[]"
|
||||
:title="board.title"
|
||||
@close="closeSidebar">
|
||||
<AppSidebarTab id="sharing"
|
||||
<NcAppSidebarTab id="sharing"
|
||||
:order="0"
|
||||
:name="t('deck', 'Sharing')"
|
||||
icon="icon-shared">
|
||||
<SharingTabSidebar :board="board" />
|
||||
</AppSidebarTab>
|
||||
</NcAppSidebarTab>
|
||||
|
||||
<AppSidebarTab id="tags"
|
||||
<NcAppSidebarTab id="tags"
|
||||
:order="1"
|
||||
:name="t('deck', 'Tags')"
|
||||
icon="icon-tag">
|
||||
<TagsTabSidebar :board="board" />
|
||||
</AppSidebarTab>
|
||||
</NcAppSidebarTab>
|
||||
|
||||
<AppSidebarTab v-if="canEdit"
|
||||
<NcAppSidebarTab v-if="canEdit"
|
||||
id="deleted"
|
||||
:order="2"
|
||||
:name="t('deck', 'Deleted items')"
|
||||
icon="icon-delete">
|
||||
<DeletedTabSidebar :board="board" />
|
||||
</AppSidebarTab>
|
||||
</NcAppSidebarTab>
|
||||
|
||||
<AppSidebarTab v-if="hasActivity"
|
||||
<NcAppSidebarTab v-if="hasActivity"
|
||||
id="activity"
|
||||
:order="3"
|
||||
:name="t('deck', 'Timeline')"
|
||||
icon="icon-activity">
|
||||
<TimelineTabSidebar :board="board" />
|
||||
</AppSidebarTab>
|
||||
</AppSidebar>
|
||||
</NcAppSidebarTab>
|
||||
</NcAppSidebar>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapState, mapGetters } from 'vuex'
|
||||
import SharingTabSidebar from './SharingTabSidebar'
|
||||
import TagsTabSidebar from './TagsTabSidebar'
|
||||
import DeletedTabSidebar from './DeletedTabSidebar'
|
||||
import TimelineTabSidebar from './TimelineTabSidebar'
|
||||
import { AppSidebar, AppSidebarTab } from '@nextcloud/vue'
|
||||
import SharingTabSidebar from './SharingTabSidebar.vue'
|
||||
import TagsTabSidebar from './TagsTabSidebar.vue'
|
||||
import DeletedTabSidebar from './DeletedTabSidebar.vue'
|
||||
import TimelineTabSidebar from './TimelineTabSidebar.vue'
|
||||
import { NcAppSidebar, NcAppSidebarTab } from '@nextcloud/vue'
|
||||
|
||||
const capabilities = window.OC.getCapabilities()
|
||||
|
||||
export default {
|
||||
name: 'BoardSidebar',
|
||||
components: {
|
||||
AppSidebar,
|
||||
AppSidebarTab,
|
||||
NcAppSidebar,
|
||||
NcAppSidebarTab,
|
||||
SharingTabSidebar,
|
||||
TagsTabSidebar,
|
||||
DeletedTabSidebar,
|
||||
|
||||
@@ -8,8 +8,7 @@
|
||||
<span>{{ deletedStack.title }}</span>
|
||||
<span class="timestamp">{{ relativeDate(deletedStack.deletedAt*1000) }}</span>
|
||||
</div>
|
||||
<button
|
||||
:title="t('settings', 'Undo')"
|
||||
<button :title="t('settings', 'Undo')"
|
||||
class="app-navigation-entry-deleted-button icon-history"
|
||||
@click="stackUndoDelete(deletedStack)" />
|
||||
</li>
|
||||
@@ -23,8 +22,7 @@
|
||||
<span>{{ deletedCard.title }}</span>
|
||||
<span class="timestamp">{{ relativeDate(deletedCard.deletedAt*1000) }}</span>
|
||||
</div>
|
||||
<button
|
||||
:title="t('settings', 'Undo')"
|
||||
<button :title="t('settings', 'Undo')"
|
||||
class="app-navigation-entry-deleted-button icon-history"
|
||||
@click="cardUndoDelete(deletedCard)" />
|
||||
</li>
|
||||
@@ -34,7 +32,7 @@
|
||||
|
||||
<script>
|
||||
import { mapState } from 'vuex'
|
||||
import relativeDate from '../../mixins/relativeDate'
|
||||
import relativeDate from '../../mixins/relativeDate.js'
|
||||
|
||||
export default {
|
||||
name: 'DeletedTabSidebar',
|
||||
@@ -88,8 +86,6 @@ export default {
|
||||
|
||||
li {
|
||||
display: flex;
|
||||
height: 44px;
|
||||
|
||||
&:hover, &:active, &.focus {
|
||||
button {
|
||||
opacity: 1;
|
||||
@@ -108,7 +104,10 @@ export default {
|
||||
.title {
|
||||
flex-grow: 2;
|
||||
padding: 3px 0px;
|
||||
|
||||
> span:not(.timestamp) {
|
||||
line-height: 1.2em;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.timestamp {
|
||||
font-size: 0.9em;
|
||||
color: var(--color-text-lighter);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
<Multiselect
|
||||
v-if="canShare"
|
||||
<NcMultiselect v-if="canShare"
|
||||
v-model="addAcl"
|
||||
:placeholder="t('deck', 'Share board with a user, group or circle …')"
|
||||
:options="formatedSharees"
|
||||
@@ -19,13 +18,12 @@
|
||||
<template #noResult>
|
||||
{{ isSearching ? t('deck', 'Searching for users, groups and circles …') : t('deck', 'No participants found') }}
|
||||
</template>
|
||||
</Multiselect>
|
||||
</NcMultiselect>
|
||||
|
||||
<ul
|
||||
id="shareWithList"
|
||||
<ul id="shareWithList"
|
||||
class="shareWithList">
|
||||
<li>
|
||||
<Avatar :user="board.owner.uid" />
|
||||
<NcAvatar :user="board.owner.uid" />
|
||||
<span class="has-tooltip username">
|
||||
{{ board.owner.displayname }}
|
||||
<span v-if="!isCurrentUser(board.owner.uid)" class="board-owner-label">
|
||||
@@ -34,7 +32,7 @@
|
||||
</span>
|
||||
</li>
|
||||
<li v-for="acl in board.acl" :key="acl.id">
|
||||
<Avatar v-if="acl.type===0" :user="acl.participant.uid" />
|
||||
<NcAvatar v-if="acl.type===0" :user="acl.participant.uid" />
|
||||
<div v-if="acl.type===1" class="avatardiv icon icon-group" />
|
||||
<div v-if="acl.type===7" class="avatardiv icon icon-circles" />
|
||||
<span class="has-tooltip username">
|
||||
@@ -43,10 +41,10 @@
|
||||
<span v-if="acl.type===7">{{ t('deck', '(Circle)') }}</span>
|
||||
</span>
|
||||
|
||||
<ActionCheckbox v-if="!(isCurrentUser(acl.participant.uid) && acl.type === 0) && (canManage || (canEdit && canShare))" :checked="acl.permissionEdit" @change="clickEditAcl(acl)">
|
||||
<NcActionCheckbox v-if="!(isCurrentUser(acl.participant.uid) && acl.type === 0) && (canManage || (canEdit && canShare))" :checked="acl.permissionEdit" @change="clickEditAcl(acl)">
|
||||
{{ t('deck', 'Can edit') }}
|
||||
</ActionCheckbox>
|
||||
<Actions v-if="!(isCurrentUser(acl.participant.uid) && acl.type === 0)" :force-menu="true">
|
||||
</NcActionCheckbox>
|
||||
<NcActions v-if="!(isCurrentUser(acl.participant.uid) && acl.type === 0)" :force-menu="true">
|
||||
<ActionCheckbox v-if="canManage || canShare" :checked="acl.permissionShare" @change="clickShareAcl(acl)">
|
||||
{{ t('deck', 'Can share') }}
|
||||
</ActionCheckbox>
|
||||
@@ -56,14 +54,18 @@
|
||||
<ActionCheckbox v-if="acl.type === 0 && isCurrentUser(board.owner.uid)" :checked="acl.owner" @change="clickTransferOwner(acl.participant.uid)">
|
||||
{{ t('deck', 'Owner') }}
|
||||
</ActionCheckbox>
|
||||
<ActionButton v-if="canManage" icon="icon-delete" @click="clickDeleteAcl(acl)">
|
||||
<NcActionButton v-if="canManage" icon="icon-delete" @click="clickDeleteAcl(acl)">
|
||||
{{ t('deck', 'Delete') }}
|
||||
</ActionButton>
|
||||
</Actions>
|
||||
</NcActionButton>
|
||||
</NcActions>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<CollectionList v-if="board.id"
|
||||
<NcRelatedResourcesPanel v-if="board.id"
|
||||
provider-id="deck"
|
||||
:item-id="board.id" />
|
||||
|
||||
<CollectionList v-if="projectsEnabled && board.id"
|
||||
:id="`${board.id}`"
|
||||
:name="board.title"
|
||||
type="deck" />
|
||||
@@ -71,22 +73,24 @@
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Avatar, Multiselect, Actions, ActionButton, ActionCheckbox } from '@nextcloud/vue'
|
||||
import { NcAvatar, NcMultiselect, NcActions, NcActionButton, NcActionCheckbox, NcRelatedResourcesPanel } from '@nextcloud/vue'
|
||||
import { CollectionList } from 'nextcloud-vue-collections'
|
||||
import { mapGetters, mapState } from 'vuex'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import debounce from 'lodash/debounce'
|
||||
|
||||
export default {
|
||||
name: 'SharingTabSidebar',
|
||||
components: {
|
||||
Avatar,
|
||||
Actions,
|
||||
ActionButton,
|
||||
ActionCheckbox,
|
||||
Multiselect,
|
||||
NcAvatar,
|
||||
NcActions,
|
||||
NcActionButton,
|
||||
NcActionCheckbox,
|
||||
NcMultiselect,
|
||||
CollectionList,
|
||||
NcRelatedResourcesPanel,
|
||||
},
|
||||
props: {
|
||||
board: {
|
||||
@@ -101,6 +105,7 @@ export default {
|
||||
addAcl: null,
|
||||
addAclForAPI: null,
|
||||
newOwner: null,
|
||||
projectsEnabled: loadState('core', 'projects_enabled', false),
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
@@ -200,7 +205,7 @@ export default {
|
||||
},
|
||||
clickTransferOwner(newOwner) {
|
||||
OC.dialogs.confirmDestructive(
|
||||
t('deck', 'Are you sure you want to transfer the board {title} for {user}?', { title: this.board.title, user: newOwner }),
|
||||
t('deck', 'Are you sure you want to transfer the board {title} to {user}?', { title: this.board.title, user: newOwner }),
|
||||
t('deck', 'Transfer the board.'),
|
||||
{
|
||||
type: OC.dialogs.YES_NO_BUTTONS,
|
||||
@@ -214,13 +219,13 @@ export default {
|
||||
this.isLoading = true
|
||||
await this.$store.dispatch('transferOwnership', {
|
||||
boardId: this.board.id,
|
||||
newOwner
|
||||
newOwner,
|
||||
})
|
||||
const successMessage = t('deck', 'Transfer the board for {user} successfully', { user: newOwner })
|
||||
const successMessage = t('deck', 'The board has been transferred to {user}', { user: newOwner })
|
||||
showSuccess(successMessage)
|
||||
this.$router.push({ name: 'main' })
|
||||
} catch (e) {
|
||||
const errorMessage = t('deck', 'Failed to transfer the board for {user}', { user: newOwner.user })
|
||||
const errorMessage = t('deck', 'Failed to transfer the board to {user}', { user: newOwner.user })
|
||||
showError(errorMessage)
|
||||
} finally {
|
||||
this.isLoading = false
|
||||
|
||||