mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-15 05:53:03 +00:00
Compare commits
865 Commits
dspace-7.6
...
dspace-7_x
Author | SHA1 | Date | |
---|---|---|---|
![]() |
aea7e7b1ec | ||
![]() |
28ed7060f2 | ||
![]() |
d6c4dad800 | ||
![]() |
15525b5df1 | ||
![]() |
92ffac6eaa | ||
![]() |
ab53a7e7db | ||
![]() |
b97f02ed7a | ||
![]() |
50123adc36 | ||
![]() |
a36131557f | ||
![]() |
2c4790c9ae | ||
![]() |
7154bc0689 | ||
![]() |
42419d12ee | ||
![]() |
65cd1df6ef | ||
![]() |
66cdc972d2 | ||
![]() |
be06c4fcd1 | ||
![]() |
ccd8d433ee | ||
![]() |
c186423ad7 | ||
![]() |
826157c1d4 | ||
![]() |
96422c2909 | ||
![]() |
57c75d06f9 | ||
![]() |
0a63a34533 | ||
![]() |
fffbe2d62c | ||
![]() |
b1bd60ca79 | ||
![]() |
2414e9b5a3 | ||
![]() |
a836e54293 | ||
![]() |
90a1ca6f5d | ||
![]() |
6071065c12 | ||
![]() |
86a18f31d9 | ||
![]() |
1ade04398a | ||
![]() |
9b7b9343bd | ||
![]() |
26df76fde6 | ||
![]() |
541a5da540 | ||
![]() |
d96fc72fe1 | ||
![]() |
f5314156f6 | ||
![]() |
a833ba3d3c | ||
![]() |
4392a4b67e | ||
![]() |
b4eaa90068 | ||
![]() |
4ad1f6b404 | ||
![]() |
0a99de2fd9 | ||
![]() |
75c9112023 | ||
![]() |
d98963c9d1 | ||
![]() |
d6a39f2164 | ||
![]() |
81648512e9 | ||
![]() |
414589ee89 | ||
![]() |
21d6c54183 | ||
![]() |
e92a96bd92 | ||
![]() |
37d6c0d61d | ||
![]() |
88e18a3559 | ||
![]() |
18e2446a7e | ||
![]() |
376a731beb | ||
![]() |
e41f576d3a | ||
![]() |
d919d255c1 | ||
![]() |
0988c41c19 | ||
![]() |
a7a543cfb0 | ||
![]() |
07d3c7aa15 | ||
![]() |
57b1de3b94 | ||
![]() |
5a08173f5e | ||
![]() |
be8def0064 | ||
![]() |
d9d0032378 | ||
![]() |
da99511ff3 | ||
![]() |
08eb6def3e | ||
![]() |
1419ed9a22 | ||
![]() |
9497decf07 | ||
![]() |
d4edbf2e25 | ||
![]() |
7376ae2b26 | ||
![]() |
f80992f1fc | ||
![]() |
bb3af14b6c | ||
![]() |
3a5f4565a5 | ||
![]() |
fc43e2473d | ||
![]() |
16da9855fc | ||
![]() |
821f16ea1d | ||
![]() |
f9fb6d06f1 | ||
![]() |
4fa71b801c | ||
![]() |
7a7e468492 | ||
![]() |
34fc08766d | ||
![]() |
13d6acaf76 | ||
![]() |
d9c0401bdd | ||
![]() |
5a2702c797 | ||
![]() |
ac5ef72bbe | ||
![]() |
dabc3b400b | ||
![]() |
5b19ebe48c | ||
![]() |
32511b0f50 | ||
![]() |
17e58ae5e3 | ||
![]() |
0d87a72f41 | ||
![]() |
a17d58acc5 | ||
![]() |
44f2ad7f4f | ||
![]() |
c7f8ed1f42 | ||
![]() |
c6ef2467bc | ||
![]() |
c07bc3c379 | ||
![]() |
b44f74a20a | ||
![]() |
50757247ab | ||
![]() |
1883d93198 | ||
![]() |
d9ab4bca65 | ||
![]() |
edeb2d2e3a | ||
![]() |
c2257ebc81 | ||
![]() |
cf2407ba90 | ||
![]() |
e0af01ff43 | ||
![]() |
7afe49d2b0 | ||
![]() |
c89ad9f22a | ||
![]() |
391e87a11f | ||
![]() |
a864260d00 | ||
![]() |
abfdf63482 | ||
![]() |
7f3b3370d9 | ||
![]() |
d56119793f | ||
![]() |
7677f2941d | ||
![]() |
7ef44c5e35 | ||
![]() |
a77f98e29e | ||
![]() |
5c66bb9d2b | ||
![]() |
7cc17a5544 | ||
![]() |
b7b1e368d6 | ||
![]() |
5e8dd3480f | ||
![]() |
f2131ec757 | ||
![]() |
c52c7e778a | ||
![]() |
b870c54c83 | ||
![]() |
f58ab77d5d | ||
![]() |
406b46ba3f | ||
![]() |
b6448a7caf | ||
![]() |
8a1daa5cf9 | ||
![]() |
fa9ddefb59 | ||
![]() |
8a1ca84d91 | ||
![]() |
06e30050a4 | ||
![]() |
ffda0d6521 | ||
![]() |
a692a6762d | ||
![]() |
2687ded0df | ||
![]() |
e387be1e10 | ||
![]() |
b0e56c4129 | ||
![]() |
9a11618d11 | ||
![]() |
5b47170b6d | ||
![]() |
983551aea8 | ||
![]() |
1727acab33 | ||
![]() |
bf585e77c3 | ||
![]() |
a226502c72 | ||
![]() |
4f79574a76 | ||
![]() |
cb0ac0c9e5 | ||
![]() |
d3876c7c0a | ||
![]() |
db119e4118 | ||
![]() |
0c9f8c02f6 | ||
![]() |
e42d4228ed | ||
![]() |
38812a3e04 | ||
![]() |
084cad6e46 | ||
![]() |
1aaa20ec73 | ||
![]() |
e7709b8091 | ||
![]() |
6971ac98c2 | ||
![]() |
280866a58f | ||
![]() |
c8b375d238 | ||
![]() |
37383acb6b | ||
![]() |
277eca837e | ||
![]() |
160f33f98a | ||
![]() |
ca92b4cf0e | ||
![]() |
46b58c369e | ||
![]() |
a0ebf4af63 | ||
![]() |
c58b398e43 | ||
![]() |
df2d18b5f1 | ||
![]() |
1e8c55faf4 | ||
![]() |
9273c83179 | ||
![]() |
e06caab25c | ||
![]() |
a21db5b887 | ||
![]() |
cae435a5c7 | ||
![]() |
974e3bf3f3 | ||
![]() |
f0ce4b2170 | ||
![]() |
830be1f15a | ||
![]() |
8ff943b084 | ||
![]() |
206df7781e | ||
![]() |
545b2ff8a7 | ||
![]() |
163cc75437 | ||
![]() |
e9940f4005 | ||
![]() |
2301a8fd52 | ||
![]() |
f9aa721ec8 | ||
![]() |
d47baab3a1 | ||
![]() |
c0e71a0e68 | ||
![]() |
21ac024423 | ||
![]() |
ce0f6153b9 | ||
![]() |
cafcee110e | ||
![]() |
a8fcad7b6a | ||
![]() |
5c446d18f7 | ||
![]() |
cd0aa134f8 | ||
![]() |
9aed649160 | ||
![]() |
fbbf16f387 | ||
![]() |
283b345cdc | ||
![]() |
da6ace1882 | ||
![]() |
aaa4b910e2 | ||
![]() |
926abe6241 | ||
![]() |
97fd42a2e1 | ||
![]() |
3cff971297 | ||
![]() |
49b27b5bc0 | ||
![]() |
f9f3d186c5 | ||
![]() |
05e1fc3505 | ||
![]() |
3635bf40fe | ||
![]() |
70d1e499c1 | ||
![]() |
e40a44c5ac | ||
![]() |
3d32715d25 | ||
![]() |
498fad9e76 | ||
![]() |
e9061a46b6 | ||
![]() |
a1d391576e | ||
![]() |
d985bfa091 | ||
![]() |
4eb827c089 | ||
![]() |
43db1731ec | ||
![]() |
e2b72201b2 | ||
![]() |
34248186e8 | ||
![]() |
9dca838f25 | ||
![]() |
ae23e68d12 | ||
![]() |
dc8b10593c | ||
![]() |
501ccfea1b | ||
![]() |
02be3e0ad5 | ||
![]() |
d34dafd838 | ||
![]() |
85610d4eb6 | ||
![]() |
c1bd65e8c6 | ||
![]() |
0dabc8ed8f | ||
![]() |
da12147043 | ||
![]() |
99e8c1044c | ||
![]() |
287d35cb26 | ||
![]() |
15a8008869 | ||
![]() |
e35adee7f8 | ||
![]() |
ee3154c069 | ||
![]() |
b491ed3c27 | ||
![]() |
b184db7a99 | ||
![]() |
eac787f1f4 | ||
![]() |
8dbdb27c67 | ||
![]() |
02f30ab331 | ||
![]() |
523c13ff3d | ||
![]() |
1ddd248d0e | ||
![]() |
11f251755b | ||
![]() |
7b25b1d63c | ||
![]() |
d43730a542 | ||
![]() |
1c1b129068 | ||
![]() |
ae563442e7 | ||
![]() |
08d56407ff | ||
![]() |
8bc2c8768f | ||
![]() |
9a0e4ec107 | ||
![]() |
3a04ea8f79 | ||
![]() |
c272e51aeb | ||
![]() |
c6a1401f34 | ||
![]() |
19c680ebf0 | ||
![]() |
9a025f6610 | ||
![]() |
ac940c1bb2 | ||
![]() |
1c4f650ef9 | ||
![]() |
6ef781822d | ||
![]() |
0115edaa07 | ||
![]() |
faebbbe680 | ||
![]() |
0ff0b5298a | ||
![]() |
87bb0d4e5a | ||
![]() |
eee9897312 | ||
![]() |
fdc23a3350 | ||
![]() |
c4104be75b | ||
![]() |
08a707477c | ||
![]() |
d4efd6a8ef | ||
![]() |
04516fad5a | ||
![]() |
377c7034ff | ||
![]() |
28eb709ef5 | ||
![]() |
cc202b7adb | ||
![]() |
f5df726a18 | ||
![]() |
077467efa2 | ||
![]() |
dc3e842747 | ||
![]() |
828648aa7e | ||
![]() |
7063c49072 | ||
![]() |
036fd1c871 | ||
![]() |
22db6745b8 | ||
![]() |
4f42c1e95f | ||
![]() |
b37a7a1456 | ||
![]() |
33146603df | ||
![]() |
d3a97d9d15 | ||
![]() |
2a1c762e73 | ||
![]() |
c94279edc0 | ||
![]() |
878b2dba4a | ||
![]() |
2eca99379f | ||
![]() |
8a4405c473 | ||
![]() |
0d2e49b12c | ||
![]() |
edd5496a4d | ||
![]() |
01becae7d0 | ||
![]() |
eef6bbe134 | ||
![]() |
ff5f23017a | ||
![]() |
7dd6ab79ff | ||
![]() |
ecc03343b7 | ||
![]() |
fa6b8cc21d | ||
![]() |
6c11621d91 | ||
![]() |
d65ff20777 | ||
![]() |
c2528a9c1f | ||
![]() |
3ad99804d1 | ||
![]() |
122d31b11b | ||
![]() |
7ab598d571 | ||
![]() |
bd06eded8c | ||
![]() |
99840ba3bc | ||
![]() |
204556de20 | ||
![]() |
8d7b4cd101 | ||
![]() |
ac5c5c8a14 | ||
![]() |
008fe0151b | ||
![]() |
f8bcce43b5 | ||
![]() |
e75052bbcf | ||
![]() |
3340192382 | ||
![]() |
dcd32c48c3 | ||
![]() |
cd825ac0da | ||
![]() |
ca80812d0a | ||
![]() |
4a74a3ac99 | ||
![]() |
62904747fc | ||
![]() |
4c30e2d0be | ||
![]() |
d05096c015 | ||
![]() |
b837f63b7c | ||
![]() |
e838873235 | ||
![]() |
e9fa2812a0 | ||
![]() |
885c52ed34 | ||
![]() |
9b0669f76c | ||
![]() |
3daa110116 | ||
![]() |
d9c6856d9c | ||
![]() |
3372110839 | ||
![]() |
ddd7c323ab | ||
![]() |
961bb11b59 | ||
![]() |
f82426b1dc | ||
![]() |
8b90d999ac | ||
![]() |
80948d98f0 | ||
![]() |
6446a52e46 | ||
![]() |
e58a66f849 | ||
![]() |
1167f8a414 | ||
![]() |
eee851f001 | ||
![]() |
b18ff86894 | ||
![]() |
e811083339 | ||
![]() |
e0590109c1 | ||
![]() |
ed1aa37100 | ||
![]() |
601850cb6a | ||
![]() |
ebe26a7eb0 | ||
![]() |
6eb5eec050 | ||
![]() |
1956e25a83 | ||
![]() |
59cb19ae80 | ||
![]() |
2e28a51b02 | ||
![]() |
7fab9633ea | ||
![]() |
7584b9e1c3 | ||
![]() |
7660dd8d95 | ||
![]() |
6f4c3860a3 | ||
![]() |
d73885137a | ||
![]() |
290c24b448 | ||
![]() |
21dac408ee | ||
![]() |
d118496075 | ||
![]() |
cb5deb644e | ||
![]() |
f9d8e4e0b7 | ||
![]() |
a3ffda3bf8 | ||
![]() |
8ca668159e | ||
![]() |
95064122d0 | ||
![]() |
d92424e2f4 | ||
![]() |
45cc177cab | ||
![]() |
59b39f12d6 | ||
![]() |
636fba40f7 | ||
![]() |
f8123fbbfd | ||
![]() |
8313b9a929 | ||
![]() |
51aa5fb61c | ||
![]() |
a5378e0cf5 | ||
![]() |
0abbf80c1d | ||
![]() |
d4536ffbf4 | ||
![]() |
6b73c75205 | ||
![]() |
f01e49f05f | ||
![]() |
073b2b0acd | ||
![]() |
d155d778ee | ||
![]() |
50e440bcf5 | ||
![]() |
af39526d77 | ||
![]() |
f77ce869f8 | ||
![]() |
bc06839926 | ||
![]() |
a3ebfc0f36 | ||
![]() |
362364136d | ||
![]() |
b892b13529 | ||
![]() |
b220e20053 | ||
![]() |
eaff0b1397 | ||
![]() |
0108f525ba | ||
![]() |
3f5de295a7 | ||
![]() |
62924cc5e0 | ||
![]() |
8453b667cb | ||
![]() |
3922130434 | ||
![]() |
c3b265d213 | ||
![]() |
2b58377830 | ||
![]() |
4536d9c74d | ||
![]() |
a3b7125ef9 | ||
![]() |
6f9c55d250 | ||
![]() |
9c6fb1794f | ||
![]() |
0f27ae1125 | ||
![]() |
73285284ce | ||
![]() |
53186988f5 | ||
![]() |
8f9236f06d | ||
![]() |
470ad80741 | ||
![]() |
c3f17b9754 | ||
![]() |
e296295df5 | ||
![]() |
795f300158 | ||
![]() |
38d5f87d3e | ||
![]() |
5a5c1f18e3 | ||
![]() |
bb3d2bb483 | ||
![]() |
8c92364d43 | ||
![]() |
1cc4756ee2 | ||
![]() |
08bda53a2e | ||
![]() |
cd2f8abd2d | ||
![]() |
8a4f24fc31 | ||
![]() |
868b021118 | ||
![]() |
c7d8949c82 | ||
![]() |
7f1ae419af | ||
![]() |
30d57174e6 | ||
![]() |
c155a60a7e | ||
![]() |
5d6bf0bb33 | ||
![]() |
147564b114 | ||
![]() |
ecd2faea4a | ||
![]() |
990f00b129 | ||
![]() |
483b97d9c1 | ||
![]() |
171a971572 | ||
![]() |
8a4e8f6045 | ||
![]() |
32364e4f68 | ||
![]() |
311f648c3f | ||
![]() |
2c28f75263 | ||
![]() |
53335658e0 | ||
![]() |
9467838066 | ||
![]() |
99656f1357 | ||
![]() |
8ee05f4352 | ||
![]() |
6756a78ba0 | ||
![]() |
b482b58d53 | ||
![]() |
312a2a7f58 | ||
![]() |
d6f0f02c1a | ||
![]() |
61eb14783e | ||
![]() |
126f3c71f4 | ||
![]() |
6cd092671f | ||
![]() |
39c5de6e18 | ||
![]() |
5868a3198f | ||
![]() |
c5fd4426cd | ||
![]() |
ea715ba9f5 | ||
![]() |
f097652594 | ||
![]() |
cecf87364e | ||
![]() |
afa0a9017f | ||
![]() |
7638511762 | ||
![]() |
31336efad2 | ||
![]() |
fc2d3f0a23 | ||
![]() |
6cb3e8bbfb | ||
![]() |
00ff0cc7e4 | ||
![]() |
81fa0ba446 | ||
![]() |
5342653b99 | ||
![]() |
d4b0cf21d3 | ||
![]() |
88fb275017 | ||
![]() |
d9614b9a1b | ||
![]() |
b55a318676 | ||
![]() |
b839252ed7 | ||
![]() |
e3f13f8c54 | ||
![]() |
2bd53d822a | ||
![]() |
b11543c3a5 | ||
![]() |
6035d9e925 | ||
![]() |
74d6dbb454 | ||
![]() |
d0b6571954 | ||
![]() |
39ddc5cbcd | ||
![]() |
9bc65b99a6 | ||
![]() |
9741ce3e69 | ||
![]() |
7472be0098 | ||
![]() |
f3df3cc96c | ||
![]() |
8ef53178db | ||
![]() |
395a5b2cb1 | ||
![]() |
ce5ab70fd4 | ||
![]() |
2abdf122b4 | ||
![]() |
31c9b854c6 | ||
![]() |
75e45d4c15 | ||
![]() |
d88c8ef986 | ||
![]() |
f07a62b8b6 | ||
![]() |
2b96983f9d | ||
![]() |
c8b2642f57 | ||
![]() |
fd166fe79a | ||
![]() |
6446911441 | ||
![]() |
9e1ce916f6 | ||
![]() |
ecceaf9ee1 | ||
![]() |
fcf3370d66 | ||
![]() |
813e965938 | ||
![]() |
3c16537fe4 | ||
![]() |
a57fd1a069 | ||
![]() |
6f5864d106 | ||
![]() |
fbe4ce5829 | ||
![]() |
112bed787a | ||
![]() |
05d5a0816d | ||
![]() |
58e9a60812 | ||
![]() |
76539dd8a1 | ||
![]() |
d0a8448de6 | ||
![]() |
9fce33f514 | ||
![]() |
04123f92aa | ||
![]() |
7538f49a36 | ||
![]() |
60586f6c33 | ||
![]() |
86a6897861 | ||
![]() |
68372b2cb8 | ||
![]() |
cefeb6a1d6 | ||
![]() |
9c859b1fdc | ||
![]() |
fbaa5068af | ||
![]() |
8515de3d10 | ||
![]() |
96cdab8d6a | ||
![]() |
ebc7799404 | ||
![]() |
d3d86f4f2d | ||
![]() |
a078932857 | ||
![]() |
a1b7c8d9c5 | ||
![]() |
894448e3dc | ||
![]() |
b25294c872 | ||
![]() |
ecf3298345 | ||
![]() |
18819b76a8 | ||
![]() |
4ffde928d4 | ||
![]() |
9406f7b085 | ||
![]() |
33fdee554e | ||
![]() |
cee9d0422b | ||
![]() |
5c8eabddab | ||
![]() |
a18225aa4d | ||
![]() |
d578222f8f | ||
![]() |
abc3c41c63 | ||
![]() |
a8e658a399 | ||
![]() |
81522586dd | ||
![]() |
a8ff6a41e0 | ||
![]() |
75260b00d9 | ||
![]() |
14680b013e | ||
![]() |
b601405e56 | ||
![]() |
c6ef2f1bd0 | ||
![]() |
2a1ef02d75 | ||
![]() |
25ab69ff21 | ||
![]() |
eb3db9ceb6 | ||
![]() |
84c0371aab | ||
![]() |
4bf4e18389 | ||
![]() |
2ff5350302 | ||
![]() |
36dd4762ed | ||
![]() |
655558538c | ||
![]() |
12fc5aff71 | ||
![]() |
cd500007b6 | ||
![]() |
686b61915a | ||
![]() |
996c877412 | ||
![]() |
c9d6c95563 | ||
![]() |
bee8bde87f | ||
![]() |
5a88cedc22 | ||
![]() |
442b4ea284 | ||
![]() |
c71c6667e0 | ||
![]() |
010b2f9693 | ||
![]() |
297fc01892 | ||
![]() |
f9e67dc513 | ||
![]() |
5ee721f2c4 | ||
![]() |
830ada37f5 | ||
![]() |
b5a8b56473 | ||
![]() |
2a72fac646 | ||
![]() |
6988df519e | ||
![]() |
ce17d23c08 | ||
![]() |
91acc957b1 | ||
![]() |
987ea5104a | ||
![]() |
135c085024 | ||
![]() |
ed4e794d46 | ||
![]() |
80e938b1f2 | ||
![]() |
c9df52cba2 | ||
![]() |
e975585350 | ||
![]() |
7eeeab4c26 | ||
![]() |
a53df4fed4 | ||
![]() |
cdec4880d2 | ||
![]() |
8849f140fb | ||
![]() |
c743387a26 | ||
![]() |
c38352ed22 | ||
![]() |
deb4a63c88 | ||
![]() |
ec016e80fb | ||
![]() |
ced163a25f | ||
![]() |
5a28e66b2f | ||
![]() |
35a25b372d | ||
![]() |
62b31ea8c5 | ||
![]() |
9ac92e0196 | ||
![]() |
287d028331 | ||
![]() |
cae13942e2 | ||
![]() |
b16cec631d | ||
![]() |
dc8a699e94 | ||
![]() |
b54e54da30 | ||
![]() |
fdfa6e2c06 | ||
![]() |
ecb00a95a0 | ||
![]() |
fe90d39943 | ||
![]() |
e2615a798e | ||
![]() |
ff42b65fc9 | ||
![]() |
1e5eff918d | ||
![]() |
e59674b5ac | ||
![]() |
5a1522deac | ||
![]() |
a5dff6df87 | ||
![]() |
790fa26953 | ||
![]() |
b2f9743f9f | ||
![]() |
66dedc5b2e | ||
![]() |
6330516630 | ||
![]() |
bd638f0356 | ||
![]() |
b11efbbf0c | ||
![]() |
68b6cc9bd4 | ||
![]() |
d586a8450d | ||
![]() |
33bc8ba1a3 | ||
![]() |
865268e820 | ||
![]() |
e5ea435cbe | ||
![]() |
6b32d04aec | ||
![]() |
06783364c4 | ||
![]() |
a812e6ae27 | ||
![]() |
841caec3e6 | ||
![]() |
9715eaba36 | ||
![]() |
bc50efec5e | ||
![]() |
25fd1a9d76 | ||
![]() |
64628f7e0d | ||
![]() |
4353c57cd0 | ||
![]() |
ce010725ac | ||
![]() |
7b4d5f9aad | ||
![]() |
9f879d3850 | ||
![]() |
87f5b50201 | ||
![]() |
bb7f0cd3a5 | ||
![]() |
3c92acb770 | ||
![]() |
04515591e2 | ||
![]() |
37455a8b6c | ||
![]() |
82fd9539b7 | ||
![]() |
52eabec70d | ||
![]() |
cad086c945 | ||
![]() |
6a49df59af | ||
![]() |
d224a2c47d | ||
![]() |
b72ce73931 | ||
![]() |
8f708d0e28 | ||
![]() |
38c7988274 | ||
![]() |
9c3363d465 | ||
![]() |
33262795ba | ||
![]() |
2aaa32ae5f | ||
![]() |
1b77530a31 | ||
![]() |
2eb120909b | ||
![]() |
26f4d1d329 | ||
![]() |
9d4eeb9c9f | ||
![]() |
1011468273 | ||
![]() |
1fad3cffc7 | ||
![]() |
c330e83095 | ||
![]() |
3b3fa4f643 | ||
![]() |
a6b45f777c | ||
![]() |
585bbec5d5 | ||
![]() |
59e5f71a73 | ||
![]() |
0a005c2d95 | ||
![]() |
dda9c6ec09 | ||
![]() |
5b8cc3c9da | ||
![]() |
45fb5aa611 | ||
![]() |
6f360a6a2d | ||
![]() |
4a394a2a38 | ||
![]() |
da9e856369 | ||
![]() |
eedaa2c120 | ||
![]() |
d4e6aa8660 | ||
![]() |
4d365936f2 | ||
![]() |
6655dec1ec | ||
![]() |
99d421caec | ||
![]() |
8d33d52174 | ||
![]() |
6a7c19ae4b | ||
![]() |
fc9eb1157b | ||
![]() |
c83b6ff036 | ||
![]() |
e93e66d3e4 | ||
![]() |
ef11340280 | ||
![]() |
d4bb79ca18 | ||
![]() |
40e805f90f | ||
![]() |
a719a2337a | ||
![]() |
0ed8c05e1c | ||
![]() |
3deef34f5b | ||
![]() |
425078dc4e | ||
![]() |
5c877f56e9 | ||
![]() |
9486ab5fa1 | ||
![]() |
7c6ecf8e9e | ||
![]() |
ee36bab60b | ||
![]() |
1a816228e8 | ||
![]() |
ccf6f36ade | ||
![]() |
5cb22bb1d0 | ||
![]() |
b237c7f5cb | ||
![]() |
30c3e3db0c | ||
![]() |
39cdd1692a | ||
![]() |
603e46283f | ||
![]() |
c17adef242 | ||
![]() |
bacd484832 | ||
![]() |
7eb0a47fca | ||
![]() |
cfa6d50583 | ||
![]() |
47db669191 | ||
![]() |
ab60ad2072 | ||
![]() |
081c573653 | ||
![]() |
e22ed77527 | ||
![]() |
4eca7c062c | ||
![]() |
bef3d37802 | ||
![]() |
2ab52dad8e | ||
![]() |
6bd66910a9 | ||
![]() |
cfecf0ff62 | ||
![]() |
1bea5f8e6c | ||
![]() |
7ee4fd5b06 | ||
![]() |
b90f8e1b49 | ||
![]() |
2a92215d82 | ||
![]() |
d03b3c3a48 | ||
![]() |
5ac126250e | ||
![]() |
a1919a7a1f | ||
![]() |
dfae913dea | ||
![]() |
627d8097ac | ||
![]() |
8dc777fc46 | ||
![]() |
e7430b9c1a | ||
![]() |
4543285a9d | ||
![]() |
86ea2dc5cb | ||
![]() |
43d4122bc5 | ||
![]() |
e87da70191 | ||
![]() |
4f24f2d12f | ||
![]() |
44fdc49e26 | ||
![]() |
610bd32869 | ||
![]() |
059da40c64 | ||
![]() |
6f7c64e8ca | ||
![]() |
1b99ffbf58 | ||
![]() |
36195a9ed1 | ||
![]() |
d784a28ddb | ||
![]() |
e1619e6a6b | ||
![]() |
e3be0cd794 | ||
![]() |
ac3bccc67d | ||
![]() |
b2d46190cb | ||
![]() |
532da9d21f | ||
![]() |
dc73f85458 | ||
![]() |
ac1adefeec | ||
![]() |
6eb407ff3b | ||
![]() |
2c152e632b | ||
![]() |
047b4629bf | ||
![]() |
1244f23522 | ||
![]() |
5275f8d13f | ||
![]() |
c3eaf8bd6f | ||
![]() |
53c6e89486 | ||
![]() |
1bfd68bd34 | ||
![]() |
9410114be3 | ||
![]() |
5a3b0c6d78 | ||
![]() |
3459bf68cb | ||
![]() |
ab7bded221 | ||
![]() |
14a6894540 | ||
![]() |
946a3afde8 | ||
![]() |
b0900d5aa9 | ||
![]() |
6644714bb7 | ||
![]() |
93f9341387 | ||
![]() |
5bb6f6d34c | ||
![]() |
8d93f22767 | ||
![]() |
2b1b9d83d7 | ||
![]() |
7fb4755aba | ||
![]() |
e8379db987 | ||
![]() |
0bdb5742e0 | ||
![]() |
1dcc5d1ec5 | ||
![]() |
b158c5c2a2 | ||
![]() |
0920a21876 | ||
![]() |
2e1b1489b6 | ||
![]() |
181ea6d7c9 | ||
![]() |
1f909dc6ea | ||
![]() |
d674bcc390 | ||
![]() |
e181e854cc | ||
![]() |
ad8dd8b648 | ||
![]() |
dfe2d4580e | ||
![]() |
7f1e267b2d | ||
![]() |
8c88f7052a | ||
![]() |
f749ade52c | ||
![]() |
533da6624f | ||
![]() |
f7063d4eaf | ||
![]() |
4de3c58e06 | ||
![]() |
b709ee0300 | ||
![]() |
4af88997e8 | ||
![]() |
e74dc22077 | ||
![]() |
7884de94e0 | ||
![]() |
169ae41d49 | ||
![]() |
6388613948 | ||
![]() |
34a4f5b597 | ||
![]() |
7cf7749367 | ||
![]() |
adcc6f564d | ||
![]() |
ba2570b83b | ||
![]() |
7ac14daf46 | ||
![]() |
caed26d191 | ||
![]() |
5c0c559685 | ||
![]() |
9079a968f0 | ||
![]() |
3df66e0257 | ||
![]() |
be36175bb8 | ||
![]() |
d0265eb3f6 | ||
![]() |
5479cc6274 | ||
![]() |
db891f2f16 | ||
![]() |
543cc42bf3 | ||
![]() |
2ad9a44b99 | ||
![]() |
ecd2275e1b | ||
![]() |
f904c0f67d | ||
![]() |
64855c9cf8 | ||
![]() |
9a7b242827 | ||
![]() |
e46c801554 | ||
![]() |
7be3f1fada | ||
![]() |
144b455eae | ||
![]() |
bd8659ac11 | ||
![]() |
5636db84a8 | ||
![]() |
5621b84284 | ||
![]() |
35cc7a8096 | ||
![]() |
a4e2c47c51 | ||
![]() |
7a60cd9aaf | ||
![]() |
2f94f49c06 | ||
![]() |
3306d0d699 | ||
![]() |
bf9590c1e6 | ||
![]() |
e2a84b2145 | ||
![]() |
3e01912eb2 | ||
![]() |
4bd6071071 | ||
![]() |
876d94e124 | ||
![]() |
a207fb51e9 | ||
![]() |
8a16597b69 | ||
![]() |
6a2c7d09d6 | ||
![]() |
eadbcdbe14 | ||
![]() |
be99cc5c23 | ||
![]() |
a230eee76d | ||
![]() |
6a8095d456 | ||
![]() |
79f3a3116e | ||
![]() |
8481604b1e | ||
![]() |
cc5b841a65 | ||
![]() |
d8b426d745 | ||
![]() |
3f4bf7ce0f | ||
![]() |
374a9ae14e | ||
![]() |
d85124c121 | ||
![]() |
1773a75e98 | ||
![]() |
a11bfc80ad | ||
![]() |
cf54af2c22 | ||
![]() |
a8e5b26744 | ||
![]() |
6c0514ab48 | ||
![]() |
bc84bc867b | ||
![]() |
98c49231b4 | ||
![]() |
751d689ff6 | ||
![]() |
cf5ac335ca | ||
![]() |
fe7d2a8a7e | ||
![]() |
5ffd405ba6 | ||
![]() |
edc738ae39 | ||
![]() |
46b241eda5 | ||
![]() |
f244d388be | ||
![]() |
59fd32ac68 | ||
![]() |
48ec0cdc34 | ||
![]() |
aed97c9dcc | ||
![]() |
26dcb6b8b0 | ||
![]() |
c1fa52ee64 | ||
![]() |
29c1b51073 | ||
![]() |
35946dcf7c | ||
![]() |
f49bdcd8c8 | ||
![]() |
cd51baa5f1 | ||
![]() |
35d29c8425 | ||
![]() |
e987c35450 | ||
![]() |
83a44ba924 | ||
![]() |
4a7ebeea16 | ||
![]() |
f65d6c5775 | ||
![]() |
685e6d83c5 | ||
![]() |
593e67fe72 | ||
![]() |
f7706760cd | ||
![]() |
350cbea6d5 | ||
![]() |
98d9f639f7 | ||
![]() |
f03ed89687 | ||
![]() |
c74c178533 | ||
![]() |
36f07604ae | ||
![]() |
b55686e187 | ||
![]() |
680ed3bccf | ||
![]() |
4ee1c83073 | ||
![]() |
e73139d977 | ||
![]() |
fa95f56be0 | ||
![]() |
d3019e4006 | ||
![]() |
70f0af6611 | ||
![]() |
ef4496fd21 | ||
![]() |
dc3bf37521 | ||
![]() |
b00470aca0 | ||
![]() |
c9ecc560ee | ||
![]() |
d697d0f7a7 | ||
![]() |
0945021378 | ||
![]() |
b5d301e53c | ||
![]() |
2d48cc0f69 | ||
![]() |
4527349dcf | ||
![]() |
43745d830b | ||
![]() |
63c76644c1 | ||
![]() |
d09f5297a1 | ||
![]() |
54d99a84c9 | ||
![]() |
655813a8af | ||
![]() |
a0515c4121 | ||
![]() |
fd2bc90ae2 | ||
![]() |
288c75851e | ||
![]() |
d5a375ee65 | ||
![]() |
5101677d9a | ||
![]() |
9135c6318d | ||
![]() |
d31e17894c | ||
![]() |
11fad8d3ce | ||
![]() |
f7f279c5ed | ||
![]() |
35b83cc168 | ||
![]() |
73b5d776da | ||
![]() |
b08b6cca8c | ||
![]() |
e2c886c9a4 | ||
![]() |
b00ce65e8b | ||
![]() |
bdea9a6d71 | ||
![]() |
bb770ba65b | ||
![]() |
1d8d3b3c27 | ||
![]() |
a5595015e6 | ||
![]() |
a23cdfbc2b | ||
![]() |
087c203f72 | ||
![]() |
970b19bf01 | ||
![]() |
99384a7060 | ||
![]() |
36c95db7bf | ||
![]() |
984c9bfc2a | ||
![]() |
be6dbdec66 |
@@ -231,10 +231,13 @@
|
|||||||
"*.json5"
|
"*.json5"
|
||||||
],
|
],
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:jsonc/recommended-with-jsonc"
|
"plugin:jsonc/recommended-with-json5"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-irregular-whitespace": "error",
|
// The ESLint core no-irregular-whitespace rule doesn't work well in JSON
|
||||||
|
// See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html
|
||||||
|
"no-irregular-whitespace": "off",
|
||||||
|
"jsonc/no-irregular-whitespace": "error",
|
||||||
"no-trailing-spaces": "error",
|
"no-trailing-spaces": "error",
|
||||||
"jsonc/comma-dangle": [
|
"jsonc/comma-dangle": [
|
||||||
"error",
|
"error",
|
||||||
|
121
.github/workflows/build.yml
vendored
121
.github/workflows/build.yml
vendored
@@ -8,6 +8,7 @@ on: [push, pull_request]
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
packages: read # to fetch private images from GitHub Container Registry (GHCR)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
tests:
|
tests:
|
||||||
@@ -28,6 +29,8 @@ jobs:
|
|||||||
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
||||||
# Tell Cypress to run e2e tests using the same UI URL
|
# Tell Cypress to run e2e tests using the same UI URL
|
||||||
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
||||||
|
# Disable the cookie consent banner in e2e tests to avoid errors because of elements hidden by it
|
||||||
|
DSPACE_INFO_ENABLECOOKIECONSENTPOPUP: false
|
||||||
# When Chrome version is specified, we pin to a specific version of Chrome
|
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||||
# Comment this out to use the latest release
|
# Comment this out to use the latest release
|
||||||
#CHROME_VERSION: "90.0.4430.212-1"
|
#CHROME_VERSION: "90.0.4430.212-1"
|
||||||
@@ -35,10 +38,13 @@ jobs:
|
|||||||
NODE_OPTIONS: '--max-old-space-size=4096'
|
NODE_OPTIONS: '--max-old-space-size=4096'
|
||||||
# Project name to use when running "docker compose" prior to e2e tests
|
# Project name to use when running "docker compose" prior to e2e tests
|
||||||
COMPOSE_PROJECT_NAME: 'ci'
|
COMPOSE_PROJECT_NAME: 'ci'
|
||||||
|
# Docker Registry to use for Docker compose scripts below.
|
||||||
|
# We use GitHub's Container Registry to avoid aggressive rate limits at DockerHub.
|
||||||
|
DOCKER_REGISTRY: ghcr.io
|
||||||
strategy:
|
strategy:
|
||||||
# Create a matrix of Node versions to test against (in parallel)
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [16.x, 18.x]
|
node-version: [18.x, 20.x]
|
||||||
# Do NOT exit immediately if one matrix job fails
|
# Do NOT exit immediately if one matrix job fails
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
# These are the actual CI steps to perform per job
|
# These are the actual CI steps to perform per job
|
||||||
@@ -108,6 +114,14 @@ jobs:
|
|||||||
path: 'coverage/dspace-angular/lcov.info'
|
path: 'coverage/dspace-angular/lcov.info'
|
||||||
retention-days: 14
|
retention-days: 14
|
||||||
|
|
||||||
|
# Login to our Docker registry, so that we can access private Docker images using "docker compose" below.
|
||||||
|
- name: Login to ${{ env.DOCKER_REGISTRY }}
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.DOCKER_REGISTRY }}
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# Using "docker compose" start backend using CI configuration
|
# Using "docker compose" start backend using CI configuration
|
||||||
# and load assetstore from a cached copy
|
# and load assetstore from a cached copy
|
||||||
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
- name: Start DSpace REST Backend via Docker (for e2e tests)
|
||||||
@@ -172,12 +186,115 @@ jobs:
|
|||||||
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
|
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
|
||||||
# If it does, then SSR is working, as this tag is created by our MetadataService.
|
# If it does, then SSR is working, as this tag is created by our MetadataService.
|
||||||
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
||||||
- name: Verify SSR (server-side rendering)
|
- name: Verify SSR (server-side rendering) on Homepage
|
||||||
run: |
|
run: |
|
||||||
result=$(wget -O- -q http://127.0.0.1:4000/home)
|
result=$(wget -O- -q http://127.0.0.1:4000/home)
|
||||||
echo "$result"
|
echo "$result"
|
||||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
||||||
|
|
||||||
|
# Get a specific community in our test data and verify that the "<h1>" tag includes "Publications" (the community name).
|
||||||
|
# If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Community page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/communities/0958c910-2037-42a9-81c7-dca80e3892b4)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Publications
|
||||||
|
|
||||||
|
# Get a specific collection in our test data and verify that the "<h1>" tag includes "Articles" (the collection name).
|
||||||
|
# If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Collection page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/collections/282164f5-d325-4740-8dd1-fa4d6d3e7200)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Articles
|
||||||
|
|
||||||
|
# Get a specific publication in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the title of this publication. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Publication page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/publication/6160810f-1e53-40db-81ef-f6621a727398)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "An Economic Model of Mortality Salience"
|
||||||
|
|
||||||
|
# Get a specific person in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the person. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Person page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/person/b1b2c768-bda1-448a-a073-fc541e8b24d9)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Simmons, Cameron"
|
||||||
|
|
||||||
|
# Get a specific project in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the project. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Project page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/project/46ccb608-a74c-4bf6-bc7a-e29cc7defea9)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "University Research Fellowship"
|
||||||
|
|
||||||
|
# Get a specific orgunit in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the orgunit. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on an OrgUnit page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/orgunit/9851674d-bd9a-467b-8d84-068deb568ccf)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Law and Development"
|
||||||
|
|
||||||
|
# Get a specific journal in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the journal. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Journal page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/journal/d4af6c3e-53d0-4757-81eb-566f3b45d63a)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology"
|
||||||
|
|
||||||
|
# Get a specific journal volume in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the volume. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Journal Volume page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalvolume/07c6249f-4bf7-494d-9ce3-6ffdb2aed538)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology Volume 28 (2017)"
|
||||||
|
|
||||||
|
# Get a specific journal issue in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the issue. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Journal Issue page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalissue/44c29473-5de2-48fa-b005-e5029aa1a50b)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology Vol. 28, No. 1"
|
||||||
|
|
||||||
|
# Verify 301 Handle redirect behavior
|
||||||
|
# Note: /handle/123456789/260 is the same test Publication used by our e2e tests
|
||||||
|
- name: Verify 301 redirect from '/handle' URLs
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/handle/123456789/260 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "301" ]]
|
||||||
|
|
||||||
|
# Verify 403 error code behavior
|
||||||
|
- name: Verify 403 error code from '/403'
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/403 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "403" ]]
|
||||||
|
|
||||||
|
# Verify 404 error code behavior
|
||||||
|
- name: Verify 404 error code from '/404' and on invalid pages
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/404 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
result2=$(wget --server-response --quiet http://127.0.0.1:4000/invalidurl 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result2"
|
||||||
|
[[ "$result" -eq "404" && "$result2" -eq "404" ]]
|
||||||
|
|
||||||
|
# Verify 500 error code behavior
|
||||||
|
- name: Verify 500 error code from '/500'
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/500 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "500" ]]
|
||||||
|
|
||||||
- name: Stop running app
|
- name: Stop running app
|
||||||
run: kill -9 $(lsof -t -i:4000)
|
run: kill -9 $(lsof -t -i:4000)
|
||||||
|
|
||||||
|
6
.github/workflows/codescan.yml
vendored
6
.github/workflows/codescan.yml
vendored
@@ -40,14 +40,14 @@ jobs:
|
|||||||
# Initializes the CodeQL tools for scanning.
|
# Initializes the CodeQL tools for scanning.
|
||||||
# https://github.com/github/codeql-action
|
# https://github.com/github/codeql-action
|
||||||
- name: Initialize CodeQL
|
- name: Initialize CodeQL
|
||||||
uses: github/codeql-action/init@v2
|
uses: github/codeql-action/init@v3
|
||||||
with:
|
with:
|
||||||
languages: javascript
|
languages: javascript
|
||||||
|
|
||||||
# Autobuild attempts to build any compiled languages
|
# Autobuild attempts to build any compiled languages
|
||||||
- name: Autobuild
|
- name: Autobuild
|
||||||
uses: github/codeql-action/autobuild@v2
|
uses: github/codeql-action/autobuild@v3
|
||||||
|
|
||||||
# Perform GitHub Code Scanning.
|
# Perform GitHub Code Scanning.
|
||||||
- name: Perform CodeQL Analysis
|
- name: Perform CodeQL Analysis
|
||||||
uses: github/codeql-action/analyze@v2
|
uses: github/codeql-action/analyze@v3
|
||||||
|
1
.github/workflows/docker.yml
vendored
1
.github/workflows/docker.yml
vendored
@@ -17,6 +17,7 @@ on:
|
|||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read # to fetch code (actions/checkout)
|
contents: read # to fetch code (actions/checkout)
|
||||||
|
packages: write # to write images to GitHub Container Registry (GHCR)
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
#############################################################
|
#############################################################
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# This image will be published as dspace/dspace-angular
|
# This image will be published as dspace/dspace-angular
|
||||||
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||||
|
|
||||||
FROM node:18-alpine
|
FROM docker.io/node:18-alpine
|
||||||
|
|
||||||
# Ensure Python and other build tools are available
|
# Ensure Python and other build tools are available
|
||||||
# These are needed to install some node modules, especially on linux/arm64
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
@@ -24,5 +24,5 @@ ENV NODE_OPTIONS="--max_old_space_size=4096"
|
|||||||
# Listen / accept connections from all IP addresses.
|
# Listen / accept connections from all IP addresses.
|
||||||
# NOTE: At this time it is only possible to run Docker container in Production mode
|
# NOTE: At this time it is only possible to run Docker container in Production mode
|
||||||
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
|
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
|
||||||
ENV NODE_ENV development
|
ENV NODE_ENV=development
|
||||||
CMD yarn serve --host 0.0.0.0
|
CMD yarn serve --host 0.0.0.0
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
# Test build:
|
# Test build:
|
||||||
# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
|
# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
|
||||||
|
|
||||||
FROM node:18-alpine as build
|
FROM docker.io/node:18-alpine AS build
|
||||||
|
|
||||||
# Ensure Python and other build tools are available
|
# Ensure Python and other build tools are available
|
||||||
# These are needed to install some node modules, especially on linux/arm64
|
# These are needed to install some node modules, especially on linux/arm64
|
||||||
@@ -26,6 +26,6 @@ COPY --chown=node:node docker/dspace-ui.json /app/dspace-ui.json
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
USER node
|
USER node
|
||||||
ENV NODE_ENV production
|
ENV NODE_ENV=production
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
CMD pm2-runtime start dspace-ui.json --json
|
CMD pm2-runtime start dspace-ui.json --json
|
||||||
|
@@ -30,7 +30,6 @@
|
|||||||
"lodash",
|
"lodash",
|
||||||
"jwt-decode",
|
"jwt-decode",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webfontloader",
|
|
||||||
"zone.js"
|
"zone.js"
|
||||||
],
|
],
|
||||||
"outputPath": "dist/browser",
|
"outputPath": "dist/browser",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
# NOTE: will log all redux actions and transfers in console
|
# NOTE: will log all redux actions and transfers in console
|
||||||
debug: false
|
debug: false
|
||||||
|
|
||||||
# Angular Universal server settings
|
# Angular User Inteface settings
|
||||||
# NOTE: these settings define where Node.js will start your UI application. Therefore, these
|
# NOTE: these settings define where Node.js will start your UI application. Therefore, these
|
||||||
# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
|
# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
|
||||||
ui:
|
ui:
|
||||||
@@ -17,12 +17,48 @@ ui:
|
|||||||
# Trust X-FORWARDED-* headers from proxies (default = true)
|
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||||
useProxies: true
|
useProxies: true
|
||||||
|
|
||||||
|
# Angular Universal / Server Side Rendering (SSR) settings
|
||||||
universal:
|
universal:
|
||||||
# Whether to inline "critical" styles into the server-side rendered HTML.
|
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
|
||||||
# Determining which styles are critical is a relatively expensive operation;
|
# Determining which styles are critical is a relatively expensive operation; this option is
|
||||||
# this option can be disabled to boost server performance at the expense of
|
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
||||||
# loading smoothness. For improved SSR performance, DSpace defaults this to false (disabled).
|
|
||||||
inlineCriticalCss: false
|
inlineCriticalCss: false
|
||||||
|
# Patterns to be run as regexes against the path of the page to check if SSR is allowed.
|
||||||
|
# If the path match any of the regexes it will be served directly in CSR.
|
||||||
|
# By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools.
|
||||||
|
excludePathPatterns:
|
||||||
|
- pattern: "^/communities/[a-f0-9-]{36}/browse(/.*)?$"
|
||||||
|
flag: "i"
|
||||||
|
- pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$"
|
||||||
|
flag: "i"
|
||||||
|
- pattern: "^/browse/"
|
||||||
|
- pattern: "^/search$"
|
||||||
|
- pattern: "^/community-list$"
|
||||||
|
- pattern: "^/admin/"
|
||||||
|
- pattern: "^/processes/?"
|
||||||
|
- pattern: "^/notifications/"
|
||||||
|
- pattern: "^/statistics/?"
|
||||||
|
- pattern: "^/access-control/"
|
||||||
|
- pattern: "^/health$"
|
||||||
|
|
||||||
|
# Whether to enable rendering of Search component on SSR.
|
||||||
|
# If set to true the component will be included in the HTML returned from the server side rendering.
|
||||||
|
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
||||||
|
enableSearchComponent: false
|
||||||
|
# Whether to enable rendering of Browse component on SSR.
|
||||||
|
# If set to true the component will be included in the HTML returned from the server side rendering.
|
||||||
|
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
||||||
|
enableBrowseComponent: false
|
||||||
|
# Enable state transfer from the server-side application to the client-side application.
|
||||||
|
# Defaults to true.
|
||||||
|
# Note: When using an external application cache layer, it's recommended not to transfer the state to avoid caching it.
|
||||||
|
# Disabling it ensures that dynamic state information is not inadvertently cached, which can improve security and
|
||||||
|
# ensure that users always use the most up-to-date state.
|
||||||
|
transferState: true
|
||||||
|
# When a different REST base URL is used for the server-side application, the generated state contains references to
|
||||||
|
# REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
|
||||||
|
# Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
|
||||||
|
replaceRestUrl: true
|
||||||
|
|
||||||
# The REST API server settings
|
# The REST API server settings
|
||||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||||
@@ -33,6 +69,9 @@ rest:
|
|||||||
port: 443
|
port: 443
|
||||||
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
nameSpace: /server
|
nameSpace: /server
|
||||||
|
# Provide a different REST url to be used during SSR execution. It must contain the whole url including protocol, server port and
|
||||||
|
# server namespace (uncomment to use it).
|
||||||
|
#ssrBaseUrl: http://localhost:8080/server
|
||||||
|
|
||||||
# Caching settings
|
# Caching settings
|
||||||
cache:
|
cache:
|
||||||
@@ -396,3 +435,31 @@ vocabularies:
|
|||||||
comcolSelectionSort:
|
comcolSelectionSort:
|
||||||
sortField: 'dc.title'
|
sortField: 'dc.title'
|
||||||
sortDirection: 'ASC'
|
sortDirection: 'ASC'
|
||||||
|
|
||||||
|
# Live Region configuration
|
||||||
|
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
|
||||||
|
# Live regions are perceivable regions of a web page that are typically updated as a
|
||||||
|
# result of an external event when user focus may be elsewhere.
|
||||||
|
#
|
||||||
|
# The DSpace live region is a component present at the bottom of all pages that is invisible by default, but is useful
|
||||||
|
# for screen readers. Any message pushed to the live region will be announced by the screen reader. These messages
|
||||||
|
# usually contain information about changes on the page that might not be in focus.
|
||||||
|
liveRegion:
|
||||||
|
# The duration after which messages disappear from the live region in milliseconds
|
||||||
|
messageTimeOutDurationMs: 30000
|
||||||
|
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
|
||||||
|
isVisible: false
|
||||||
|
|
||||||
|
|
||||||
|
# Search settings
|
||||||
|
search:
|
||||||
|
# Number used to render n UI elements called loading skeletons that act as placeholders.
|
||||||
|
# These elements indicate that some content will be loaded in their stead.
|
||||||
|
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
|
||||||
|
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
|
||||||
|
defaultFiltersCount: 5
|
||||||
|
|
||||||
|
# Configuration for storing accessibility settings, used by the AccessibilitySettingsService
|
||||||
|
accessibility:
|
||||||
|
# The duration in days after which the accessibility settings cookie expires
|
||||||
|
cookieExpirationDuration: 7
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
import { defineConfig } from 'cypress';
|
import { defineConfig } from 'cypress';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
video: true,
|
||||||
videosFolder: 'cypress/videos',
|
videosFolder: 'cypress/videos',
|
||||||
screenshotsFolder: 'cypress/screenshots',
|
screenshotsFolder: 'cypress/screenshots',
|
||||||
fixturesFolder: 'cypress/fixtures',
|
fixturesFolder: 'cypress/fixtures',
|
||||||
@@ -18,6 +19,7 @@ export default defineConfig({
|
|||||||
|
|
||||||
// Admin account used for administrative tests
|
// Admin account used for administrative tests
|
||||||
DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com',
|
DSPACE_TEST_ADMIN_USER: 'dspacedemo+admin@gmail.com',
|
||||||
|
DSPACE_TEST_ADMIN_USER_UUID: '335647b6-8a52-4ecb-a8c1-7ebabb199bda',
|
||||||
DSPACE_TEST_ADMIN_PASSWORD: 'dspace',
|
DSPACE_TEST_ADMIN_PASSWORD: 'dspace',
|
||||||
// Community/collection/publication used for view/edit tests
|
// Community/collection/publication used for view/edit tests
|
||||||
DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4',
|
DSPACE_TEST_COMMUNITY: '0958c910-2037-42a9-81c7-dca80e3892b4',
|
||||||
@@ -33,6 +35,8 @@ export default defineConfig({
|
|||||||
// Account used to test basic submission process
|
// Account used to test basic submission process
|
||||||
DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com',
|
DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com',
|
||||||
DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace',
|
DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace',
|
||||||
|
// Administrator users group
|
||||||
|
DSPACE_ADMINISTRATOR_GROUP: 'e59f5659-bff9-451e-b28f-439e7bd467e4'
|
||||||
},
|
},
|
||||||
e2e: {
|
e2e: {
|
||||||
// Setup our plugins for e2e tests
|
// Setup our plugins for e2e tests
|
||||||
|
54
cypress/e2e/admin-add-new-modals.cy.ts
Normal file
54
cypress/e2e/admin-add-new-modals.cy.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Add New Modals', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin for sidebar to appear
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add new Community modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-new-title').should('be.visible');
|
||||||
|
cy.get('#admin-menu-section-new-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.new_community"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-create-community-parent-selector> for accessibility
|
||||||
|
testA11y('ds-create-community-parent-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add new Collection modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-new-title').should('be.visible');
|
||||||
|
cy.get('#admin-menu-section-new-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.new_collection"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-create-collection-parent-selector> for accessibility
|
||||||
|
testA11y('ds-create-collection-parent-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Add new Item modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-new-title').should('be.visible');
|
||||||
|
cy.get('#admin-menu-section-new-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.new_item"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-create-item-parent-selector> for accessibility
|
||||||
|
testA11y('ds-create-item-parent-selector');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/admin-curation-tasks.cy.ts
Normal file
16
cypress/e2e/admin-curation-tasks.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Curation Tasks', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/curation-tasks');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-admin-curation-task').should('be.visible');
|
||||||
|
// Analyze <ds-admin-curation-task> for accessibility issues
|
||||||
|
testA11y('ds-admin-curation-task');
|
||||||
|
});
|
||||||
|
});
|
54
cypress/e2e/admin-edit-modals.cy.ts
Normal file
54
cypress/e2e/admin-edit-modals.cy.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Edit Modals', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin for sidebar to appear
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edit Community modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-edit-title').should('be.visible');
|
||||||
|
cy.get('#admin-menu-section-edit-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.edit_community"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-edit-community-selector> for accessibility
|
||||||
|
testA11y('ds-edit-community-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edit Collection modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-edit-title').should('be.visible');
|
||||||
|
cy.get('#admin-menu-section-edit-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.edit_collection"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-edit-collection-selector> for accessibility
|
||||||
|
testA11y('ds-edit-collection-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Edit Item modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-edit-title').should('be.visible');
|
||||||
|
cy.get('#admin-menu-section-edit-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.edit_item"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-edit-item-selector> for accessibility
|
||||||
|
testA11y('ds-edit-item-selector');
|
||||||
|
});
|
||||||
|
});
|
39
cypress/e2e/admin-export-modals.cy.ts
Normal file
39
cypress/e2e/admin-export-modals.cy.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Export Modals', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin for sidebar to appear
|
||||||
|
cy.visit('/login');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Export metadata modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-export-title').should('be.visible');
|
||||||
|
cy.get('#admin-menu-section-export-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.export_metadata"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-export-metadata-selector> for accessibility
|
||||||
|
testA11y('ds-export-metadata-selector');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Export batch modal should pass accessibility tests', () => {
|
||||||
|
// Pin the sidebar open
|
||||||
|
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
|
||||||
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
|
// Click on entry of menu
|
||||||
|
cy.get('#admin-menu-section-export-title').should('be.visible');
|
||||||
|
cy.get('#admin-menu-section-export-title').click();
|
||||||
|
|
||||||
|
cy.get('a[data-test="menu.section.export_batch"]').click();
|
||||||
|
|
||||||
|
// Analyze <ds-export-batch-selector> for accessibility
|
||||||
|
testA11y('ds-export-metadata-selector');
|
||||||
|
});
|
||||||
|
});
|
21
cypress/e2e/admin-search-page.cy.ts
Normal file
21
cypress/e2e/admin-search-page.cy.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Search Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/search');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
//Page must first be visible
|
||||||
|
cy.get('ds-admin-search-page').should('be.visible');
|
||||||
|
// At least one search result should be displayed
|
||||||
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
|
// Click each filter toggle to open *every* filter
|
||||||
|
// (As we want to scan filter section for accessibility issues as well)
|
||||||
|
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
// Analyze <ds-admin-search-page> for accessibility issues
|
||||||
|
testA11y('ds-admin-search-page');
|
||||||
|
});
|
||||||
|
});
|
21
cypress/e2e/admin-workflow-page.cy.ts
Normal file
21
cypress/e2e/admin-workflow-page.cy.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Admin Workflow Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/workflow');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-admin-workflow-page').should('be.visible');
|
||||||
|
// At least one search result should be displayed
|
||||||
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
|
// Click each filter toggle to open *every* filter
|
||||||
|
// (As we want to scan filter section for accessibility issues as well)
|
||||||
|
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
// Analyze <ds-admin-workflow-page> for accessibility issues
|
||||||
|
testA11y('ds-admin-workflow-page');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/batch-import-page.cy.ts
Normal file
16
cypress/e2e/batch-import-page.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Batch Import Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see processes
|
||||||
|
cy.visit('/admin/batch-import');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Batch import form must first be visible
|
||||||
|
cy.get('ds-batch-import-page').should('be.visible');
|
||||||
|
// Analyze <ds-batch-import-page> for accessibility issues
|
||||||
|
testA11y('ds-batch-import-page');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/bitstreams-format.cy.ts
Normal file
16
cypress/e2e/bitstreams-format.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Bitstreams Formats', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/registries/bitstream-formats');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-bitstream-formats').should('be.visible');
|
||||||
|
// Analyze <ds-bitstream-formats> for accessibility issues
|
||||||
|
testA11y('ds-bitstream-formats');
|
||||||
|
});
|
||||||
|
});
|
31
cypress/e2e/bulk-access.cy.ts
Normal file
31
cypress/e2e/bulk-access.cy.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
|
||||||
|
describe('Bulk Access', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/bulk-access');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-bulk-access').should('be.visible');
|
||||||
|
// At least one search result should be displayed
|
||||||
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
|
// Click each filter toggle to open *every* filter
|
||||||
|
// (As we want to scan filter section for accessibility issues as well)
|
||||||
|
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
// Analyze <ds-bulk-access> for accessibility issues
|
||||||
|
testA11y('ds-bulk-access', {
|
||||||
|
rules: {
|
||||||
|
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
|
||||||
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
|
'aria-required-children': { enabled: false },
|
||||||
|
'nested-interactive': { enabled: false },
|
||||||
|
// Card titles fail this test currently
|
||||||
|
'heading-order': { enabled: false },
|
||||||
|
},
|
||||||
|
} as Options);
|
||||||
|
});
|
||||||
|
});
|
@@ -3,8 +3,15 @@ import { testA11y } from 'cypress/support/utils';
|
|||||||
describe('Collection Page', () => {
|
describe('Collection Page', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
|
||||||
|
|
||||||
|
// Visit Collections page
|
||||||
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
|
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
|
||||||
|
|
||||||
|
// Wait for the "viewevent" to trigger on the Collection page.
|
||||||
|
// This ensures our <ds-collection-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
|
||||||
|
cy.wait('@viewevent');
|
||||||
|
|
||||||
// <ds-collection-page> tag must be loaded
|
// <ds-collection-page> tag must be loaded
|
||||||
cy.get('ds-collection-page').should('be.visible');
|
cy.get('ds-collection-page').should('be.visible');
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ describe('Community Statistics Page', () => {
|
|||||||
|
|
||||||
it('should load if you click on "Statistics" from a Community page', () => {
|
it('should load if you click on "Statistics" from a Community page', () => {
|
||||||
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
||||||
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
16
cypress/e2e/create-eperson.cy.ts
Normal file
16
cypress/e2e/create-eperson.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Create Eperson', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/epeople/create');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Form must first be visible
|
||||||
|
cy.get('ds-eperson-form').should('be.visible');
|
||||||
|
// Analyze <ds-eperson-form> for accessibility issues
|
||||||
|
testA11y('ds-eperson-form');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/create-group.cy.ts
Normal file
16
cypress/e2e/create-group.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Create Group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/groups/create');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Form must first be visible
|
||||||
|
cy.get('ds-group-form').should('be.visible');
|
||||||
|
// Analyze <ds-group-form> for accessibility issues
|
||||||
|
testA11y('ds-group-form');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/edit-eperson.cy.ts
Normal file
16
cypress/e2e/edit-eperson.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Edit Eperson', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/epeople/'.concat(Cypress.env('DSPACE_TEST_ADMIN_USER_UUID')).concat('/edit'));
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Form must first be visible
|
||||||
|
cy.get('ds-eperson-form').should('be.visible');
|
||||||
|
// Analyze <ds-eperson-form> for accessibility issues
|
||||||
|
testA11y('ds-eperson-form');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/edit-group.cy.ts
Normal file
16
cypress/e2e/edit-group.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Edit Group', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/groups/'.concat(Cypress.env('DSPACE_ADMINISTRATOR_GROUP')).concat('/edit'));
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Form must first be visible
|
||||||
|
cy.get('ds-group-form').should('be.visible');
|
||||||
|
// Analyze <ds-group-form> for accessibility issues
|
||||||
|
testA11y('ds-group-form');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/e2e/end-user-agreement.cy.ts
Normal file
13
cypress/e2e/end-user-agreement.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('End User Agreement', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/info/end-user-agreement');
|
||||||
|
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-end-user-agreement').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-end-user-agreement> for accessibility
|
||||||
|
testA11y('ds-end-user-agreement');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/epeople-registry.cy.ts
Normal file
16
cypress/e2e/epeople-registry.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Epeople registry', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/epeople');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Epeople registry page must first be visible
|
||||||
|
cy.get('ds-epeople-registry').should('be.visible');
|
||||||
|
// Analyze <ds-epeople-registry> for accessibility issues
|
||||||
|
testA11y('ds-epeople-registry');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/e2e/feedback.cy.ts
Normal file
13
cypress/e2e/feedback.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Feedback', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/info/feedback');
|
||||||
|
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-feedback').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-feedback> for accessibility
|
||||||
|
testA11y('ds-feedback');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/groups-registry.cy.ts
Normal file
16
cypress/e2e/groups-registry.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Groups registry', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/access-control/groups');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Epeople registry page must first be visible
|
||||||
|
cy.get('ds-groups-registry').should('be.visible');
|
||||||
|
// Analyze <ds-groups-registry> for accessibility issues
|
||||||
|
testA11y('ds-groups-registry');
|
||||||
|
});
|
||||||
|
});
|
@@ -10,4 +10,29 @@ describe('Header', () => {
|
|||||||
// Analyze <ds-header> for accessibility
|
// Analyze <ds-header> for accessibility
|
||||||
testA11y('ds-header');
|
testA11y('ds-header');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should allow for changing language to German (for example)', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
// Click the language switcher (globe) in header
|
||||||
|
cy.get('button[data-test="lang-switch"]').click();
|
||||||
|
// Click on the "Deusch" language in dropdown
|
||||||
|
cy.get('#language-menu-list div[role="option"]').contains('Deutsch').click();
|
||||||
|
|
||||||
|
// HTML "lang" attribute should switch to "de"
|
||||||
|
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
|
||||||
|
|
||||||
|
// Login menu should now be in German
|
||||||
|
cy.get('[data-test="login-menu"]').contains('Anmelden');
|
||||||
|
|
||||||
|
// Change back to English from language switcher
|
||||||
|
cy.get('button[data-test="lang-switch"]').click();
|
||||||
|
cy.get('#language-menu-list div[role="option"]').contains('English').click();
|
||||||
|
|
||||||
|
// HTML "lang" attribute should switch to "en"
|
||||||
|
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
|
||||||
|
|
||||||
|
// Login menu should now be in English
|
||||||
|
cy.get('[data-test="login-menu"]').contains('Log In');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
62
cypress/e2e/health-page.cy.ts
Normal file
62
cypress/e2e/health-page.cy.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/health');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Health Page > Status Tab', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.intercept('GET', '/server/actuator/health').as('status');
|
||||||
|
cy.wait('@status');
|
||||||
|
|
||||||
|
cy.get('a[data-test="health-page.status-tab"]').click();
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-health-page').should('be.visible');
|
||||||
|
cy.get('ds-health-panel').should('be.visible');
|
||||||
|
|
||||||
|
// wait for all the ds-health-info-component components to be rendered
|
||||||
|
cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => {
|
||||||
|
cy.wrap($panel).find('ds-health-component').should('be.visible');
|
||||||
|
});
|
||||||
|
// Analyze <ds-health-page> for accessibility issues
|
||||||
|
testA11y('ds-health-page', {
|
||||||
|
rules: {
|
||||||
|
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
|
||||||
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
|
'aria-required-children': { enabled: false },
|
||||||
|
'nested-interactive': { enabled: false },
|
||||||
|
},
|
||||||
|
} as Options);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Health Page > Info Tab', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.intercept('GET', '/server/actuator/info').as('info');
|
||||||
|
cy.wait('@info');
|
||||||
|
|
||||||
|
cy.get('a[data-test="health-page.info-tab"]').click();
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-health-page').should('be.visible');
|
||||||
|
cy.get('ds-health-info').should('be.visible');
|
||||||
|
|
||||||
|
// wait for all the ds-health-info-component components to be rendered
|
||||||
|
cy.get('div[role="tabpanel"]').each(($panel: HTMLDivElement) => {
|
||||||
|
cy.wrap($panel).find('ds-health-info-component').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
// Analyze <ds-health-info> for accessibility issues
|
||||||
|
testA11y('ds-health-info', {
|
||||||
|
rules: {
|
||||||
|
// All panels are accordions & fail "aria-required-children" and "nested-interactive".
|
||||||
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
|
'aria-required-children': { enabled: false },
|
||||||
|
'nested-interactive': { enabled: false },
|
||||||
|
},
|
||||||
|
} as Options);
|
||||||
|
});
|
||||||
|
});
|
@@ -5,7 +5,7 @@ import '../support/commands';
|
|||||||
describe('Site Statistics Page', () => {
|
describe('Site Statistics Page', () => {
|
||||||
it('should load if you click on "Statistics" from homepage', () => {
|
it('should load if you click on "Statistics" from homepage', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', '/statistics');
|
cy.location('pathname').should('eq', '/statistics');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Options } from 'cypress-axe';
|
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
|
||||||
const ITEM_EDIT_PAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('/edit');
|
const ITEM_EDIT_PAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('/edit');
|
||||||
|
|
||||||
@@ -13,8 +13,13 @@ beforeEach(() => {
|
|||||||
|
|
||||||
describe('Edit Item > Edit Metadata tab', () => {
|
describe('Edit Item > Edit Metadata tab', () => {
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="metadata"]').should('be.visible');
|
||||||
cy.get('a[data-test="metadata"]').click();
|
cy.get('a[data-test="metadata"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="metadata"]').should('be.visible');
|
||||||
|
cy.get('a[data-test="metadata"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-edit-item-page> tag must be loaded
|
// <ds-edit-item-page> tag must be loaded
|
||||||
cy.get('ds-edit-item-page').should('be.visible');
|
cy.get('ds-edit-item-page').should('be.visible');
|
||||||
|
|
||||||
@@ -31,8 +36,13 @@ describe('Edit Item > Edit Metadata tab', () => {
|
|||||||
describe('Edit Item > Status tab', () => {
|
describe('Edit Item > Status tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="status"]').should('be.visible');
|
||||||
cy.get('a[data-test="status"]').click();
|
cy.get('a[data-test="status"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="status"]').should('be.visible');
|
||||||
|
cy.get('a[data-test="status"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-status> tag must be loaded
|
// <ds-item-status> tag must be loaded
|
||||||
cy.get('ds-item-status').should('be.visible');
|
cy.get('ds-item-status').should('be.visible');
|
||||||
|
|
||||||
@@ -44,8 +54,13 @@ describe('Edit Item > Status tab', () => {
|
|||||||
describe('Edit Item > Bitstreams tab', () => {
|
describe('Edit Item > Bitstreams tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="bitstreams"]').should('be.visible');
|
||||||
cy.get('a[data-test="bitstreams"]').click();
|
cy.get('a[data-test="bitstreams"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="bitstreams"]').should('be.visible');
|
||||||
|
cy.get('a[data-test="bitstreams"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-bitstreams> tag must be loaded
|
// <ds-item-bitstreams> tag must be loaded
|
||||||
cy.get('ds-item-bitstreams').should('be.visible');
|
cy.get('ds-item-bitstreams').should('be.visible');
|
||||||
|
|
||||||
@@ -59,8 +74,8 @@ describe('Edit Item > Bitstreams tab', () => {
|
|||||||
// Currently Bitstreams page loads a pagination component per Bundle
|
// Currently Bitstreams page loads a pagination component per Bundle
|
||||||
// and they all use the same 'id="p-dad"'.
|
// and they all use the same 'id="p-dad"'.
|
||||||
'duplicate-id': { enabled: false },
|
'duplicate-id': { enabled: false },
|
||||||
}
|
},
|
||||||
} as Options
|
} as Options,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -68,8 +83,13 @@ describe('Edit Item > Bitstreams tab', () => {
|
|||||||
describe('Edit Item > Curate tab', () => {
|
describe('Edit Item > Curate tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="curate"]').should('be.visible');
|
||||||
cy.get('a[data-test="curate"]').click();
|
cy.get('a[data-test="curate"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="curate"]').should('be.visible');
|
||||||
|
cy.get('a[data-test="curate"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-curate> tag must be loaded
|
// <ds-item-curate> tag must be loaded
|
||||||
cy.get('ds-item-curate').should('be.visible');
|
cy.get('ds-item-curate').should('be.visible');
|
||||||
|
|
||||||
@@ -81,8 +101,13 @@ describe('Edit Item > Curate tab', () => {
|
|||||||
describe('Edit Item > Relationships tab', () => {
|
describe('Edit Item > Relationships tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="relationships"]').should('be.visible');
|
||||||
cy.get('a[data-test="relationships"]').click();
|
cy.get('a[data-test="relationships"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="relationships"]').should('be.visible');
|
||||||
|
cy.get('a[data-test="relationships"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-relationships> tag must be loaded
|
// <ds-item-relationships> tag must be loaded
|
||||||
cy.get('ds-item-relationships').should('be.visible');
|
cy.get('ds-item-relationships').should('be.visible');
|
||||||
|
|
||||||
@@ -94,8 +119,13 @@ describe('Edit Item > Relationships tab', () => {
|
|||||||
describe('Edit Item > Version History tab', () => {
|
describe('Edit Item > Version History tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="versionhistory"]').should('be.visible');
|
||||||
cy.get('a[data-test="versionhistory"]').click();
|
cy.get('a[data-test="versionhistory"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="versionhistory"]').should('be.visible');
|
||||||
|
cy.get('a[data-test="versionhistory"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-version-history> tag must be loaded
|
// <ds-item-version-history> tag must be loaded
|
||||||
cy.get('ds-item-version-history').should('be.visible');
|
cy.get('ds-item-version-history').should('be.visible');
|
||||||
|
|
||||||
@@ -107,8 +137,13 @@ describe('Edit Item > Version History tab', () => {
|
|||||||
describe('Edit Item > Access Control tab', () => {
|
describe('Edit Item > Access Control tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="access-control"]').should('be.visible');
|
||||||
cy.get('a[data-test="access-control"]').click();
|
cy.get('a[data-test="access-control"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="access-control"]').should('be.visible');
|
||||||
|
cy.get('a[data-test="access-control"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-access-control> tag must be loaded
|
// <ds-item-access-control> tag must be loaded
|
||||||
cy.get('ds-item-access-control').should('be.visible');
|
cy.get('ds-item-access-control').should('be.visible');
|
||||||
|
|
||||||
@@ -120,8 +155,13 @@ describe('Edit Item > Access Control tab', () => {
|
|||||||
describe('Edit Item > Collection Mapper tab', () => {
|
describe('Edit Item > Collection Mapper tab', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.get('a[data-test="mapper"]').should('be.visible');
|
||||||
cy.get('a[data-test="mapper"]').click();
|
cy.get('a[data-test="mapper"]').click();
|
||||||
|
|
||||||
|
// Our selected tab should be both visible & active
|
||||||
|
cy.get('a[data-test="mapper"]').should('be.visible');
|
||||||
|
cy.get('a[data-test="mapper"]').should('have.class', 'active');
|
||||||
|
|
||||||
// <ds-item-collection-mapper> tag must be loaded
|
// <ds-item-collection-mapper> tag must be loaded
|
||||||
cy.get('ds-item-collection-mapper').should('be.visible');
|
cy.get('ds-item-collection-mapper').should('be.visible');
|
||||||
|
|
||||||
|
@@ -7,12 +7,19 @@ describe('Item Page', () => {
|
|||||||
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
||||||
it('should redirect to the entity page when navigating to an item page', () => {
|
it('should redirect to the entity page when navigating to an item page', () => {
|
||||||
cy.visit(ITEMPAGE);
|
cy.visit(ITEMPAGE);
|
||||||
|
cy.wait(1000);
|
||||||
cy.location('pathname').should('eq', ENTITYPAGE);
|
cy.location('pathname').should('eq', ENTITYPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
|
||||||
|
|
||||||
cy.visit(ENTITYPAGE);
|
cy.visit(ENTITYPAGE);
|
||||||
|
|
||||||
|
// Wait for the "viewevent" to trigger on the Item page.
|
||||||
|
// This ensures our <ds-item-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
|
||||||
|
cy.wait('@viewevent');
|
||||||
|
|
||||||
// <ds-item-page> tag must be loaded
|
// <ds-item-page> tag must be loaded
|
||||||
cy.get('ds-item-page').should('be.visible');
|
cy.get('ds-item-page').should('be.visible');
|
||||||
|
|
||||||
@@ -21,8 +28,14 @@ describe('Item Page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests on full item page', () => {
|
it('should pass accessibility tests on full item page', () => {
|
||||||
|
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
|
||||||
|
|
||||||
cy.visit(ENTITYPAGE + '/full');
|
cy.visit(ENTITYPAGE + '/full');
|
||||||
|
|
||||||
|
// Wait for the "viewevent" to trigger on the Item page.
|
||||||
|
// This ensures our <ds-item-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
|
||||||
|
cy.wait('@viewevent');
|
||||||
|
|
||||||
// <ds-full-item-page> tag must be loaded
|
// <ds-full-item-page> tag must be loaded
|
||||||
cy.get('ds-full-item-page').should('be.visible');
|
cy.get('ds-full-item-page').should('be.visible');
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ describe('Item Statistics Page', () => {
|
|||||||
|
|
||||||
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
||||||
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
||||||
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,8 +23,7 @@ describe('Item Statistics Page', () => {
|
|||||||
|
|
||||||
it('should contain a "Total visits per month" section', () => {
|
it('should contain a "Total visits per month" section', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
// Check just for existence because this table is empty in CI environment as it's historical data
|
cy.get('table[data-test="TotalVisitsPerMonth"]').should('be.visible');
|
||||||
cy.get('.'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('_TotalVisitsPerMonth')).should('exist');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
@@ -3,35 +3,35 @@ import { testA11y } from 'cypress/support/utils';
|
|||||||
const page = {
|
const page = {
|
||||||
openLoginMenu() {
|
openLoginMenu() {
|
||||||
// Click the "Log In" dropdown menu in header
|
// Click the "Log In" dropdown menu in header
|
||||||
cy.get('ds-themed-header [data-test="login-menu"]').click();
|
cy.get('[data-test="login-menu"]').click();
|
||||||
},
|
},
|
||||||
openUserMenu() {
|
openUserMenu() {
|
||||||
// Once logged in, click the User menu in header
|
// Once logged in, click the User menu in header
|
||||||
cy.get('ds-themed-header [data-test="user-menu"]').click();
|
cy.get('[data-test="user-menu"]').click();
|
||||||
},
|
},
|
||||||
submitLoginAndPasswordByPressingButton(email, password) {
|
submitLoginAndPasswordByPressingButton(email, password) {
|
||||||
// Enter email
|
// Enter email
|
||||||
cy.get('ds-themed-header [data-test="email"]').type(email);
|
cy.get('[data-test="email"]').type(email);
|
||||||
// Enter password
|
// Enter password
|
||||||
cy.get('ds-themed-header [data-test="password"]').type(password);
|
cy.get('[data-test="password"]').type(password);
|
||||||
// Click login button
|
// Click login button
|
||||||
cy.get('ds-themed-header [data-test="login-button"]').click();
|
cy.get('[data-test="login-button"]').click();
|
||||||
},
|
},
|
||||||
submitLoginAndPasswordByPressingEnter(email, password) {
|
submitLoginAndPasswordByPressingEnter(email, password) {
|
||||||
// In opened Login modal, fill out email & password, then click Enter
|
// In opened Login modal, fill out email & password, then click Enter
|
||||||
cy.get('ds-themed-header [data-test="email"]').type(email);
|
cy.get('[data-test="email"]').type(email);
|
||||||
cy.get('ds-themed-header [data-test="password"]').type(password);
|
cy.get('[data-test="password"]').type(password);
|
||||||
cy.get('ds-themed-header [data-test="password"]').type('{enter}');
|
cy.get('[data-test="password"]').type('{enter}');
|
||||||
},
|
},
|
||||||
submitLogoutByPressingButton() {
|
submitLogoutByPressingButton() {
|
||||||
// This is the POST command that will actually log us out
|
// This is the POST command that will actually log us out
|
||||||
cy.intercept('POST', '/server/api/authn/logout').as('logout');
|
cy.intercept('POST', '/server/api/authn/logout').as('logout');
|
||||||
// Click logout button
|
// Click logout button
|
||||||
cy.get('ds-themed-header [data-test="logout-button"]').click();
|
cy.get('[data-test="logout-button"]').click();
|
||||||
// Wait until above POST command responds before continuing
|
// Wait until above POST command responds before continuing
|
||||||
// (This ensures next action waits until logout completes)
|
// (This ensures next action waits until logout completes)
|
||||||
cy.wait('@logout');
|
cy.wait('@logout');
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Login Modal', () => {
|
describe('Login Modal', () => {
|
||||||
@@ -67,7 +67,7 @@ describe('Login Modal', () => {
|
|||||||
|
|
||||||
// Login, and the <ds-log-in> tag should no longer exist
|
// Login, and the <ds-log-in> tag should no longer exist
|
||||||
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
cy.get('.form-login').should('not.exist');
|
cy.get('ds-log-in').should('not.exist');
|
||||||
|
|
||||||
// Verify we are still on homepage
|
// Verify we are still on homepage
|
||||||
cy.url().should('include', '/home');
|
cy.url().should('include', '/home');
|
||||||
|
16
cypress/e2e/metadata-import-page.cy.ts
Normal file
16
cypress/e2e/metadata-import-page.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Metadata Import Page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/metadata-import');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Metadata import form must first be visible
|
||||||
|
cy.get('ds-metadata-import-page').should('be.visible');
|
||||||
|
// Analyze <ds-metadata-import-page> for accessibility issues
|
||||||
|
testA11y('ds-metadata-import-page');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/metadata-registry.cy.ts
Normal file
16
cypress/e2e/metadata-registry.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Metadata Registry', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/registries/metadata');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-metadata-registry').should('be.visible');
|
||||||
|
// Analyze <ds-metadata-registry> for accessibility issues
|
||||||
|
testA11y('ds-metadata-registry');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/metadata-schema.cy.ts
Normal file
16
cypress/e2e/metadata-schema.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Metadata Schema', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/registries/metadata/dc');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-metadata-schema').should('be.visible');
|
||||||
|
// Analyze <ds-metadata-schema> for accessibility issues
|
||||||
|
testA11y('ds-metadata-schema');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/new-process.cy.ts
Normal file
16
cypress/e2e/new-process.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('New Process', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/processes/new');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Process form must first be visible
|
||||||
|
cy.get('ds-new-process').should('be.visible');
|
||||||
|
// Analyze <ds-new-process> for accessibility issues
|
||||||
|
testA11y('ds-new-process');
|
||||||
|
});
|
||||||
|
});
|
13
cypress/e2e/privacy.cy.ts
Normal file
13
cypress/e2e/privacy.cy.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Privacy', () => {
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.visit('/info/privacy');
|
||||||
|
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-privacy').should('be.visible');
|
||||||
|
|
||||||
|
// Analyze <ds-privacy> for accessibility
|
||||||
|
testA11y('ds-privacy');
|
||||||
|
});
|
||||||
|
});
|
17
cypress/e2e/processes-overview.cy.ts
Normal file
17
cypress/e2e/processes-overview.cy.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Processes Overview', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/processes');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
|
||||||
|
// Process overview must first be visible
|
||||||
|
cy.get('ds-process-overview').should('be.visible');
|
||||||
|
// Analyze <ds-process-overview> for accessibility issues
|
||||||
|
testA11y('ds-process-overview');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/profile-page.cy.ts
Normal file
16
cypress/e2e/profile-page.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('Profile page', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/profile');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Process form must first be visible
|
||||||
|
cy.get('ds-profile-page').should('be.visible');
|
||||||
|
// Analyze <ds-profile-page> for accessibility issues
|
||||||
|
testA11y('ds-profile-page');
|
||||||
|
});
|
||||||
|
});
|
16
cypress/e2e/system-wide-alert.cy.ts
Normal file
16
cypress/e2e/system-wide-alert.cy.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('System Wide Alert', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
// Must login as an Admin to see the page
|
||||||
|
cy.visit('/admin/system-wide-alert');
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass accessibility tests', () => {
|
||||||
|
// Page must first be visible
|
||||||
|
cy.get('ds-system-wide-alert-form').should('be.visible');
|
||||||
|
// Analyze <ds-system-wide-alert-form> for accessibility issues
|
||||||
|
testA11y('ds-system-wide-alert-form');
|
||||||
|
});
|
||||||
|
});
|
@@ -95,11 +95,11 @@ Cypress.Commands.add('login', login);
|
|||||||
*/
|
*/
|
||||||
function loginViaForm(email: string, password: string): void {
|
function loginViaForm(email: string, password: string): void {
|
||||||
// Enter email
|
// Enter email
|
||||||
cy.get('ds-log-in [data-test="email"]').type(email);
|
cy.get('[data-test="email"]').type(email);
|
||||||
// Enter password
|
// Enter password
|
||||||
cy.get('ds-log-in [data-test="password"]').type(password);
|
cy.get('[data-test="password"]').type(password);
|
||||||
// Click login button
|
// Click login button
|
||||||
cy.get('ds-log-in [data-test="login-button"]').click();
|
cy.get('[data-test="login-button"]').click();
|
||||||
}
|
}
|
||||||
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
// Add as a Cypress command (i.e. assign to 'cy.loginViaForm')
|
||||||
Cypress.Commands.add('loginViaForm', loginViaForm);
|
Cypress.Commands.add('loginViaForm', loginViaForm);
|
||||||
|
@@ -21,7 +21,7 @@ networks:
|
|||||||
external: true
|
external: true
|
||||||
services:
|
services:
|
||||||
dspace-cli:
|
dspace-cli:
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
||||||
container_name: dspace-cli
|
container_name: dspace-cli
|
||||||
environment:
|
environment:
|
||||||
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||||
|
@@ -14,7 +14,7 @@
|
|||||||
# # Therefore, it should be kept in sync with that file
|
# # Therefore, it should be kept in sync with that file
|
||||||
services:
|
services:
|
||||||
dspacedb:
|
dspacedb:
|
||||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
image: ${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
||||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
|
@@ -33,7 +33,7 @@ services:
|
|||||||
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
||||||
solr__D__statistics__P__autoCommit: 'false'
|
solr__D__statistics__P__autoCommit: 'false'
|
||||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -60,7 +60,7 @@ services:
|
|||||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x-loadsql}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql"
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the LOADSQL in
|
# This LOADSQL should be kept in sync with the LOADSQL in
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
||||||
@@ -81,7 +81,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
|
@@ -26,7 +26,7 @@ services:
|
|||||||
DSPACE_REST_HOST: demo.dspace.org
|
DSPACE_REST_HOST: demo.dspace.org
|
||||||
DSPACE_REST_PORT: 443
|
DSPACE_REST_PORT: 443
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: dspace/dspace-angular:dspace-7_x-dist
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}-dist"
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile.dist
|
dockerfile: Dockerfile.dist
|
||||||
|
@@ -40,7 +40,7 @@ services:
|
|||||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -67,7 +67,7 @@ services:
|
|||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
# Uses a custom Postgres image with pgcrypto installed
|
# Uses a custom Postgres image with pgcrypto installed
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}"
|
||||||
environment:
|
environment:
|
||||||
PGDATA: /pgdata
|
PGDATA: /pgdata
|
||||||
POSTGRES_PASSWORD: dspace
|
POSTGRES_PASSWORD: dspace
|
||||||
@@ -84,7 +84,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
|
@@ -23,7 +23,7 @@ services:
|
|||||||
DSPACE_REST_HOST: localhost
|
DSPACE_REST_HOST: localhost
|
||||||
DSPACE_REST_PORT: 8080
|
DSPACE_REST_PORT: 8080
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: dspace/dspace-angular:dspace-7_x
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}"
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
143
package.json
143
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "7.6.2",
|
"version": "7.6.6-next",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
@@ -12,7 +12,6 @@
|
|||||||
"preserve": "yarn base-href",
|
"preserve": "yarn base-href",
|
||||||
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||||
"serve:ssr": "node dist/server/main",
|
"serve:ssr": "node dist/server/main",
|
||||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
|
||||||
"build": "ng build --configuration development",
|
"build": "ng build --configuration development",
|
||||||
"build:stats": "ng build --stats-json",
|
"build:stats": "ng build --stats-json",
|
||||||
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
|
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
|
||||||
@@ -49,27 +48,20 @@
|
|||||||
"https": false
|
"https": false
|
||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"resolutions": {
|
|
||||||
"minimist": "^1.2.5",
|
|
||||||
"webdriver-manager": "^12.1.8",
|
|
||||||
"ts-node": "10.2.1"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^15.2.8",
|
"@angular/animations": "^15.2.10",
|
||||||
"@angular/cdk": "^15.2.8",
|
"@angular/cdk": "^15.2.9",
|
||||||
"@angular/common": "^15.2.8",
|
"@angular/common": "^15.2.10",
|
||||||
"@angular/compiler": "^15.2.8",
|
"@angular/compiler": "^15.2.10",
|
||||||
"@angular/core": "^15.2.8",
|
"@angular/core": "^15.2.10",
|
||||||
"@angular/forms": "^15.2.8",
|
"@angular/forms": "^15.2.10",
|
||||||
"@angular/localize": "15.2.8",
|
"@angular/localize": "15.2.10",
|
||||||
"@angular/platform-browser": "^15.2.8",
|
"@angular/platform-browser": "^15.2.10",
|
||||||
"@angular/platform-browser-dynamic": "^15.2.8",
|
"@angular/platform-browser-dynamic": "^15.2.10",
|
||||||
"@angular/platform-server": "^15.2.8",
|
"@angular/platform-server": "^15.2.10",
|
||||||
"@angular/router": "^15.2.8",
|
"@angular/router": "^15.2.10",
|
||||||
"@babel/runtime": "7.21.0",
|
"@babel/runtime": "7.28.4",
|
||||||
"@kolkov/ngx-gallery": "^2.0.1",
|
"@kolkov/ngx-gallery": "^2.0.1",
|
||||||
"@material-ui/core": "^4.11.0",
|
|
||||||
"@material-ui/icons": "^4.11.3",
|
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@ng-dynamic-forms/core": "^15.0.0",
|
"@ng-dynamic-forms/core": "^15.0.0",
|
||||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0",
|
"@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0",
|
||||||
@@ -79,130 +71,129 @@
|
|||||||
"@nguniversal/express-engine": "^15.2.1",
|
"@nguniversal/express-engine": "^15.2.1",
|
||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
||||||
"@types/grecaptcha": "^3.0.4",
|
|
||||||
"angular-idle-preload": "3.0.0",
|
"angular-idle-preload": "3.0.0",
|
||||||
"angulartics2": "^12.2.0",
|
"angulartics2": "^12.2.1",
|
||||||
"axios": "^1.6.0",
|
"axios": "^1.11.0",
|
||||||
"bootstrap": "^4.6.1",
|
"bootstrap": "^4.6.1",
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression": "^1.7.4",
|
"compression": "^1.8.1",
|
||||||
"cookie-parser": "1.4.6",
|
"cookie-parser": "1.4.7",
|
||||||
"core-js": "^3.30.1",
|
"core-js": "^3.45.1",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.30.0",
|
||||||
"date-fns-tz": "^1.3.7",
|
"date-fns-tz": "^1.3.7",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
"express": "^4.19.2",
|
"express": "^4.21.2",
|
||||||
"express-rate-limit": "^5.1.3",
|
"express-rate-limit": "^5.1.3",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"http-proxy-middleware": "^2.0.9",
|
||||||
"http-terminator": "^3.2.0",
|
"http-terminator": "^3.2.0",
|
||||||
"isbot": "^3.6.10",
|
"isbot": "^5.1.30",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jsonschema": "1.4.1",
|
"jsonschema": "1.5.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.18",
|
"klaro": "^0.7.21",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lru-cache": "^7.14.1",
|
"lru-cache": "^7.14.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.2",
|
||||||
"markdown-it-mathjax3": "^4.3.2",
|
"markdown-it-mathjax3": "^4.3.2",
|
||||||
"mirador": "^3.3.0",
|
"mirador": "^3.4.3",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.11.0",
|
"mirador-share-plugin": "^0.16.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.1",
|
||||||
"ng-mocks": "^14.10.0",
|
|
||||||
"ng2-file-upload": "1.4.0",
|
"ng2-file-upload": "1.4.0",
|
||||||
"ng2-nouislider": "^2.0.0",
|
"ng2-nouislider": "^2.0.0",
|
||||||
"ngx-infinite-scroll": "^15.0.0",
|
"ngx-infinite-scroll": "^15.0.0",
|
||||||
"ngx-pagination": "6.0.3",
|
"ngx-pagination": "6.0.3",
|
||||||
|
"ngx-skeleton-loader": "^7.0.0",
|
||||||
"ngx-sortablejs": "^11.1.0",
|
"ngx-sortablejs": "^11.1.0",
|
||||||
"ngx-ui-switch": "^14.1.0",
|
"ngx-ui-switch": "^14.1.0",
|
||||||
"nouislider": "^15.7.1",
|
"nouislider": "^15.8.1",
|
||||||
"pem": "1.14.7",
|
"pem": "1.14.8",
|
||||||
"prop-types": "^15.8.1",
|
"reflect-metadata": "^0.2.2",
|
||||||
"react-copy-to-clipboard": "^5.1.0",
|
"rxjs": "^7.8.2",
|
||||||
"reflect-metadata": "^0.1.13",
|
"sanitize-html": "^2.17.0",
|
||||||
"rxjs": "^7.8.0",
|
"sortablejs": "1.15.6",
|
||||||
"sanitize-html": "^2.12.1",
|
|
||||||
"sortablejs": "1.15.0",
|
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webfontloader": "1.6.28",
|
"zone.js": "~0.13.3"
|
||||||
"zone.js": "~0.11.5"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "~15.0.0",
|
"@angular-builders/custom-webpack": "~15.0.0",
|
||||||
"@angular-devkit/build-angular": "^15.2.6",
|
"@angular-devkit/build-angular": "^15.2.11",
|
||||||
"@angular-eslint/builder": "15.2.1",
|
"@angular-eslint/builder": "15.2.1",
|
||||||
"@angular-eslint/eslint-plugin": "15.2.1",
|
"@angular-eslint/eslint-plugin": "15.2.1",
|
||||||
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
||||||
"@angular-eslint/schematics": "15.2.1",
|
"@angular-eslint/schematics": "15.2.1",
|
||||||
"@angular-eslint/template-parser": "15.2.1",
|
"@angular-eslint/template-parser": "15.2.1",
|
||||||
"@angular/cli": "^16.0.4",
|
"@angular/cli": "^16.2.16",
|
||||||
"@angular/compiler-cli": "^15.2.8",
|
"@angular/compiler-cli": "^15.2.10",
|
||||||
"@angular/language-service": "^15.2.8",
|
"@angular/language-service": "^15.2.10",
|
||||||
"@cypress/schematic": "^1.5.0",
|
"@cypress/schematic": "^1.5.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
|
"@material-ui/core": "^4.12.4",
|
||||||
|
"@material-ui/icons": "^4.11.3",
|
||||||
"@ngrx/store-devtools": "^15.4.0",
|
"@ngrx/store-devtools": "^15.4.0",
|
||||||
"@ngtools/webpack": "^15.2.6",
|
"@ngtools/webpack": "^15.2.6",
|
||||||
"@nguniversal/builders": "^15.2.1",
|
"@nguniversal/builders": "^15.2.1",
|
||||||
"@types/deep-freeze": "0.1.2",
|
"@types/deep-freeze": "0.1.5",
|
||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.21",
|
||||||
|
"@types/grecaptcha": "^3.0.9",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.14.194",
|
"@types/lodash": "^4.17.20",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.18.63",
|
||||||
"@types/sanitize-html": "^2.9.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/parser": "^5.59.1",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"axe-core": "^4.7.2",
|
"axe-core": "^4.10.3",
|
||||||
"compression-webpack-plugin": "^9.2.0",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cypress": "12.17.4",
|
"csstype": "^3.1.3",
|
||||||
"cypress-axe": "^1.4.0",
|
"cypress": "^13.17.0",
|
||||||
|
"cypress-axe": "^1.7.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
"eslint-plugin-deprecation": "^1.4.1",
|
"eslint-plugin-deprecation": "^1.5.0",
|
||||||
"eslint-plugin-import": "^2.27.5",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-jsdoc": "^45.0.0",
|
"eslint-plugin-jsdoc": "^45.0.0",
|
||||||
"eslint-plugin-jsonc": "^2.6.0",
|
"eslint-plugin-jsonc": "^2.20.1",
|
||||||
"eslint-plugin-lodash": "^7.4.0",
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
"eslint-plugin-unused-imports": "^2.0.0",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"express-static-gzip": "^2.1.7",
|
"express-static-gzip": "^2.2.0",
|
||||||
"jasmine-core": "^3.8.0",
|
"jasmine-core": "^3.8.0",
|
||||||
"jasmine-marbles": "0.9.2",
|
"jasmine-marbles": "0.9.2",
|
||||||
"karma": "^6.4.2",
|
"karma": "^6.4.4",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
|
"ng-mocks": "^14.13.5",
|
||||||
"ngx-mask": "^13.1.7",
|
"ngx-mask": "^13.1.7",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"postcss": "^8.4",
|
"postcss": "^8.5",
|
||||||
"postcss-apply": "0.12.0",
|
|
||||||
"postcss-import": "^14.0.0",
|
"postcss-import": "^14.0.0",
|
||||||
"postcss-loader": "^4.0.3",
|
"postcss-loader": "^4.0.3",
|
||||||
"postcss-preset-env": "^7.4.2",
|
"postcss-preset-env": "^7.4.2",
|
||||||
"postcss-responsive-type": "1.0.0",
|
"prop-types": "^15.8.1",
|
||||||
"react": "^16.14.0",
|
"react": "^16.14.0",
|
||||||
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"rxjs-spy": "^8.0.2",
|
"sass": "~1.92.1",
|
||||||
"sass": "~1.62.0",
|
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"sass-resources-loader": "^2.2.5",
|
"sass-resources-loader": "^2.2.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typescript": "~4.8.4",
|
"typescript": "~4.8.4",
|
||||||
"webpack": "5.76.1",
|
"webpack": "5.76.1",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
|
||||||
"webpack-cli": "^4.2.0",
|
"webpack-cli": "^4.2.0",
|
||||||
"webpack-dev-server": "^4.13.3"
|
"webpack-dev-server": "^5.2.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,8 +1,6 @@
|
|||||||
module.exports = {
|
module.exports = {
|
||||||
plugins: [
|
plugins: [
|
||||||
require('postcss-import')(),
|
require('postcss-import')(),
|
||||||
require('postcss-preset-env')(),
|
require('postcss-preset-env')()
|
||||||
require('postcss-apply')(),
|
|
||||||
require('postcss-responsive-type')()
|
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
61
server.ts
61
server.ts
@@ -28,7 +28,7 @@ import * as expressStaticGzip from 'express-static-gzip';
|
|||||||
/* eslint-enable import/no-namespace */
|
/* eslint-enable import/no-namespace */
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import LRU from 'lru-cache';
|
import LRU from 'lru-cache';
|
||||||
import isbot from 'isbot';
|
import { isbot } from 'isbot';
|
||||||
import { createCertificate } from 'pem';
|
import { createCertificate } from 'pem';
|
||||||
import { createServer } from 'https';
|
import { createServer } from 'https';
|
||||||
import { json } from 'body-parser';
|
import { json } from 'body-parser';
|
||||||
@@ -55,6 +55,7 @@ import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
|||||||
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||||
import { logStartupMessage } from './startup-message';
|
import { logStartupMessage } from './startup-message';
|
||||||
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
|
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
|
||||||
|
import { SsrExcludePatterns } from './src/config/universal-config.interface';
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -79,6 +80,9 @@ let anonymousCache: LRU<string, any>;
|
|||||||
// extend environment with app config for server
|
// extend environment with app config for server
|
||||||
extendEnvironmentWithAppConfig(environment, appConfig);
|
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||||
|
|
||||||
|
// The REST server base URL
|
||||||
|
const REST_BASE_URL = environment.rest.ssrBaseUrl || environment.rest.baseUrl;
|
||||||
|
|
||||||
// The Express app is exported so that it can be used by serverless Functions.
|
// The Express app is exported so that it can be used by serverless Functions.
|
||||||
export function app() {
|
export function app() {
|
||||||
|
|
||||||
@@ -176,7 +180,7 @@ export function app() {
|
|||||||
* Proxy the sitemaps
|
* Proxy the sitemaps
|
||||||
*/
|
*/
|
||||||
router.use('/sitemap**', createProxyMiddleware({
|
router.use('/sitemap**', createProxyMiddleware({
|
||||||
target: `${environment.rest.baseUrl}/sitemaps`,
|
target: `${REST_BASE_URL}/sitemaps`,
|
||||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}));
|
}));
|
||||||
@@ -185,7 +189,7 @@ export function app() {
|
|||||||
* Proxy the linksets
|
* Proxy the linksets
|
||||||
*/
|
*/
|
||||||
router.use('/signposting**', createProxyMiddleware({
|
router.use('/signposting**', createProxyMiddleware({
|
||||||
target: `${environment.rest.baseUrl}`,
|
target: `${REST_BASE_URL}`,
|
||||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||||
changeOrigin: true
|
changeOrigin: true
|
||||||
}));
|
}));
|
||||||
@@ -238,7 +242,7 @@ export function app() {
|
|||||||
* The callback function to serve server side angular
|
* The callback function to serve server side angular
|
||||||
*/
|
*/
|
||||||
function ngApp(req, res) {
|
function ngApp(req, res) {
|
||||||
if (environment.universal.preboot) {
|
if (environment.universal.preboot && req.method === 'GET' && (req.path === '/' || !isExcludedFromSsr(req.path, environment.universal.excludePathPatterns))) {
|
||||||
// Render the page to user via SSR (server side rendering)
|
// Render the page to user via SSR (server side rendering)
|
||||||
serverSideRender(req, res);
|
serverSideRender(req, res);
|
||||||
} else {
|
} else {
|
||||||
@@ -269,6 +273,11 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
|
|||||||
requestUrl: req.originalUrl,
|
requestUrl: req.originalUrl,
|
||||||
}, (err, data) => {
|
}, (err, data) => {
|
||||||
if (hasNoValue(err) && hasValue(data)) {
|
if (hasNoValue(err) && hasValue(data)) {
|
||||||
|
// Replace REST URL with UI URL
|
||||||
|
if (environment.universal.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
|
||||||
|
data = data.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
// save server side rendered page to cache (if any are enabled)
|
// save server side rendered page to cache (if any are enabled)
|
||||||
saveToCache(req, data);
|
saveToCache(req, data);
|
||||||
if (sendToUser) {
|
if (sendToUser) {
|
||||||
@@ -294,13 +303,24 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// Read file once at startup
|
||||||
* Send back response to user to trigger direct client-side rendering (CSR)
|
const indexHtmlContent = readFileSync(indexHtml, 'utf8');
|
||||||
* @param req current request
|
|
||||||
* @param res current response
|
|
||||||
*/
|
|
||||||
function clientSideRender(req, res) {
|
function clientSideRender(req, res) {
|
||||||
res.sendFile(indexHtml);
|
const namespace = environment.ui.nameSpace || '/';
|
||||||
|
let html = indexHtmlContent;
|
||||||
|
// Replace base href dynamically
|
||||||
|
html = html.replace(
|
||||||
|
/<base href="[^"]*">/,
|
||||||
|
`<base href="${namespace.endsWith('/') ? namespace : namespace + '/'}">`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Replace REST URL with UI URL
|
||||||
|
if (environment.universal.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
|
||||||
|
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.send(html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -550,8 +570,8 @@ function createHttpsServer(keys) {
|
|||||||
* Create an HTTP server with the configured port and host.
|
* Create an HTTP server with the configured port and host.
|
||||||
*/
|
*/
|
||||||
function run() {
|
function run() {
|
||||||
const port = environment.ui.port || 4000;
|
const port = environment.ui.port;
|
||||||
const host = environment.ui.host || '/';
|
const host = environment.ui.host;
|
||||||
|
|
||||||
// Start up the Node server
|
// Start up the Node server
|
||||||
const server = app();
|
const server = app();
|
||||||
@@ -617,11 +637,26 @@ function start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SSR should be skipped for path
|
||||||
|
*
|
||||||
|
* @param path
|
||||||
|
* @param excludePathPattern
|
||||||
|
*/
|
||||||
|
function isExcludedFromSsr(path: string, excludePathPattern: SsrExcludePatterns[]): boolean {
|
||||||
|
const patterns = excludePathPattern.map(p =>
|
||||||
|
new RegExp(p.pattern, p.flag || '')
|
||||||
|
);
|
||||||
|
return patterns.some((regex) => {
|
||||||
|
return regex.test(path)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The callback function to serve health check requests
|
* The callback function to serve health check requests
|
||||||
*/
|
*/
|
||||||
function healthCheck(req, res) {
|
function healthCheck(req, res) {
|
||||||
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
|
const baseUrl = `${REST_BASE_URL}${environment.actuators.endpointPath}`;
|
||||||
axios.get(baseUrl)
|
axios.get(baseUrl)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
res.status(response.status).send(response.data);
|
res.status(response.status).send(response.data);
|
||||||
|
@@ -10,7 +10,7 @@
|
|||||||
<button class="btn btn-outline-primary mr-3" (click)="reset()">
|
<button class="btn btn-outline-primary mr-3" (click)="reset()">
|
||||||
{{ 'access-control-cancel' | translate }}
|
{{ 'access-control-cancel' | translate }}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="!canExport()" (click)="submit()">
|
<button class="btn btn-primary" [dsBtnDisabled]="!canExport()" (click)="submit()">
|
||||||
{{ 'access-control-execute' | translate }}
|
{{ 'access-control-execute' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA, Component } from '@angular/core';
|
||||||
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
@@ -57,10 +57,15 @@ describe('BulkAccessComponent', () => {
|
|||||||
'file': { }
|
'file': { }
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockSettings: any = jasmine.createSpyObj('AccessControlFormContainerComponent', {
|
@Component({
|
||||||
getValue: jasmine.createSpy('getValue'),
|
selector: 'ds-bulk-access-settings',
|
||||||
reset: jasmine.createSpy('reset')
|
template: ''
|
||||||
});
|
})
|
||||||
|
class MockBulkAccessSettingsComponent {
|
||||||
|
isFormValid = jasmine.createSpy('isFormValid').and.returnValue(false);
|
||||||
|
getValue = jasmine.createSpy('getValue');
|
||||||
|
reset = jasmine.createSpy('reset');
|
||||||
|
}
|
||||||
const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }];
|
const selection: any[] = [{ indexableObject: { uuid: '1234' } }, { indexableObject: { uuid: '5678' } }];
|
||||||
const selectableListState: SelectableListState = { id: 'test', selection };
|
const selectableListState: SelectableListState = { id: 'test', selection };
|
||||||
const expectedIdList = ['1234', '5678'];
|
const expectedIdList = ['1234', '5678'];
|
||||||
@@ -73,7 +78,10 @@ describe('BulkAccessComponent', () => {
|
|||||||
RouterTestingModule,
|
RouterTestingModule,
|
||||||
TranslateModule.forRoot()
|
TranslateModule.forRoot()
|
||||||
],
|
],
|
||||||
declarations: [ BulkAccessComponent ],
|
declarations: [
|
||||||
|
BulkAccessComponent,
|
||||||
|
MockBulkAccessSettingsComponent,
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock },
|
{ provide: BulkAccessControlService, useValue: bulkAccessControlServiceMock },
|
||||||
{ provide: NotificationsService, useValue: NotificationsServiceStub },
|
{ provide: NotificationsService, useValue: NotificationsServiceStub },
|
||||||
@@ -102,7 +110,6 @@ describe('BulkAccessComponent', () => {
|
|||||||
|
|
||||||
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty));
|
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListStateEmpty));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.settings = mockSettings;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
@@ -119,13 +126,12 @@ describe('BulkAccessComponent', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when there are elements selected', () => {
|
describe('when there are elements selected and step two form is invalid', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
||||||
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
|
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.settings = mockSettings;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create', () => {
|
it('should create', () => {
|
||||||
@@ -136,9 +142,9 @@ describe('BulkAccessComponent', () => {
|
|||||||
expect(component.objectsSelected$.value).toEqual(expectedIdList);
|
expect(component.objectsSelected$.value).toEqual(expectedIdList);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should enable the execute button when there are objects selected', () => {
|
it('should not enable the execute button when there are objects selected and step two form is invalid', () => {
|
||||||
component.objectsSelected$.next(['1234']);
|
component.objectsSelected$.next(['1234']);
|
||||||
expect(component.canExport()).toBe(true);
|
expect(component.canExport()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call the settings reset method when reset is called', () => {
|
it('should call the settings reset method when reset is called', () => {
|
||||||
@@ -146,6 +152,23 @@ describe('BulkAccessComponent', () => {
|
|||||||
expect(component.settings.reset).toHaveBeenCalled();
|
expect(component.settings.reset).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when there are elements selectedted and the step two form is valid', () => {
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
|
||||||
|
(component as any).selectableListService.getSelectableList.and.returnValue(of(selectableListState));
|
||||||
|
fixture.detectChanges();
|
||||||
|
(component as any).settings.isFormValid.and.returnValue(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable the execute button when there are objects selected and step two form is valid', () => {
|
||||||
|
component.objectsSelected$.next(['1234']);
|
||||||
|
expect(component.canExport()).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
it('should call the bulkAccessControlService executeScript method when submit is called', () => {
|
it('should call the bulkAccessControlService executeScript method when submit is called', () => {
|
||||||
(component.settings as any).getValue.and.returnValue(mockFormState);
|
(component.settings as any).getValue.and.returnValue(mockFormState);
|
||||||
bulkAccessControlService.createPayloadFile.and.returnValue(mockFile);
|
bulkAccessControlService.createPayloadFile.and.returnValue(mockFile);
|
||||||
|
@@ -37,7 +37,7 @@ export class BulkAccessComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private bulkAccessControlService: BulkAccessControlService,
|
private bulkAccessControlService: BulkAccessControlService,
|
||||||
private selectableListService: SelectableListService
|
private selectableListService: SelectableListService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ export class BulkAccessComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
canExport(): boolean {
|
canExport(): boolean {
|
||||||
return this.objectsSelected$.value?.length > 0;
|
return this.objectsSelected$.value?.length > 0 && this.settings?.isFormValid();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -31,4 +31,8 @@ export class BulkAccessSettingsComponent {
|
|||||||
this.controlForm.reset();
|
this.controlForm.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isFormValid() {
|
||||||
|
return this.controlForm.isValid();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -61,7 +61,7 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
[ngClass]="{'table-primary' : (activeEPerson$ | async) === epersonDto.eperson}">
|
||||||
<td>{{epersonDto.eperson.id}}</td>
|
<td>{{epersonDto.eperson.id}}</td>
|
||||||
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
<td>{{ dsoNameService.getName(epersonDto.eperson) }}</td>
|
||||||
<td>{{epersonDto.eperson.email}}</td>
|
<td>{{epersonDto.eperson.email}}</td>
|
||||||
|
@@ -27,6 +27,7 @@ import { RequestService } from '../../core/data/request.service';
|
|||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { FindListOptions } from '../../core/data/find-list-options.model';
|
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||||
|
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('EPeopleRegistryComponent', () => {
|
describe('EPeopleRegistryComponent', () => {
|
||||||
let component: EPeopleRegistryComponent;
|
let component: EPeopleRegistryComponent;
|
||||||
@@ -131,7 +132,7 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
declarations: [EPeopleRegistryComponent],
|
declarations: [EPeopleRegistryComponent, BtnDisabledDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
@@ -239,7 +240,8 @@ describe('EPeopleRegistryComponent', () => {
|
|||||||
it('should be disabled', () => {
|
it('should be disabled', () => {
|
||||||
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
||||||
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
|
ePeopleDeleteButton.forEach((deleteButton: DebugElement) => {
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -46,6 +46,8 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);
|
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);
|
||||||
|
|
||||||
|
activeEPerson$: Observable<EPerson>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An observable for the pageInfo, needed to pass to the pagination component
|
* An observable for the pageInfo, needed to pass to the pagination component
|
||||||
*/
|
*/
|
||||||
@@ -111,6 +113,7 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.searching$.next(true);
|
this.searching$.next(true);
|
||||||
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
|
this.search({scope: this.currentSearchScope, query: this.currentSearchQuery});
|
||||||
|
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||||
this.subs.push(this.ePeople$.pipe(
|
this.subs.push(this.ePeople$.pipe(
|
||||||
switchMap((epeople: PaginatedList<EPerson>) => {
|
switchMap((epeople: PaginatedList<EPerson>) => {
|
||||||
if (epeople.pageInfo.totalElements > 0) {
|
if (epeople.pageInfo.totalElements > 0) {
|
||||||
@@ -178,23 +181,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given EPerson is active (being edited)
|
|
||||||
* @param eperson
|
|
||||||
*/
|
|
||||||
isActive(eperson: EPerson): Observable<boolean> {
|
|
||||||
return this.getActiveEPerson().pipe(
|
|
||||||
map((activeEPerson) => eperson === activeEPerson)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the active eperson (being edited)
|
|
||||||
*/
|
|
||||||
getActiveEPerson(): Observable<EPerson> {
|
|
||||||
return this.epersonService.getActiveEPerson();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
||||||
*/
|
*/
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editHeader; else createHeader"></div>
|
<div *ngIf="activeEPerson$ | async; then editHeader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
<h1 class="border-bottom pb-2">{{messagePrefix + '.create' | translate}}</h1>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div *ngIf="displayResetPassword" between class="btn-group">
|
<div *ngIf="displayResetPassword" between class="btn-group">
|
||||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
<button class="btn btn-primary" [dsBtnDisabled]="!(canReset$ | async)" type="button" (click)="resetPassword()">
|
||||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
|
|
||||||
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
<div *ngIf="activeEPerson$ | async">
|
||||||
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
<h2>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h2>
|
||||||
|
|
||||||
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
||||||
@@ -75,7 +75,9 @@
|
|||||||
{{ dsoNameService.getName(group) }}
|
{{ dsoNameService.getName(group) }}
|
||||||
</a>
|
</a>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">{{ dsoNameService.getName(undefined) }}</td>
|
<td class="align-middle">
|
||||||
|
{{ dsoNameService.getName((group.object | async)?.payload) }}
|
||||||
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
@@ -19,7 +19,6 @@ import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock'
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
|
||||||
import { AuthService } from '../../../core/auth/auth.service';
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
|
import { AuthServiceStub } from '../../../shared/testing/auth-service.stub';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
@@ -35,6 +34,8 @@ import { FollowLinkConfig } from '../../../shared/utils/follow-link-config.model
|
|||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import {BtnDisabledDirective} from '../../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -59,9 +60,6 @@ describe('EPersonFormComponent', () => {
|
|||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
activeEPerson: null,
|
activeEPerson: null,
|
||||||
allEpeople: mockEPeople,
|
allEpeople: mockEPeople,
|
||||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(null, this.allEpeople));
|
|
||||||
},
|
|
||||||
getActiveEPerson(): Observable<EPerson> {
|
getActiveEPerson(): Observable<EPerson> {
|
||||||
return observableOf(this.activeEPerson);
|
return observableOf(this.activeEPerson);
|
||||||
},
|
},
|
||||||
@@ -195,14 +193,10 @@ describe('EPersonFormComponent', () => {
|
|||||||
router = new RouterStub();
|
router = new RouterStub();
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
RouterTestingModule,
|
||||||
loader: {
|
TranslateModule.forRoot(),
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
declarations: [EPersonFormComponent],
|
declarations: [EPersonFormComponent, BtnDisabledDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
{ provide: GroupDataService, useValue: groupsDataService },
|
{ provide: GroupDataService, useValue: groupsDataService },
|
||||||
@@ -217,7 +211,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
EPeopleRegistryComponent
|
EPeopleRegistryComponent
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -236,37 +230,13 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('check form validation', () => {
|
describe('check form validation', () => {
|
||||||
let firstName;
|
let canLogIn: boolean;
|
||||||
let lastName;
|
let requireCertificate: boolean;
|
||||||
let email;
|
|
||||||
let canLogIn;
|
|
||||||
let requireCertificate;
|
|
||||||
|
|
||||||
let expected;
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
firstName = 'testName';
|
|
||||||
lastName = 'testLastName';
|
|
||||||
email = 'testEmail@test.com';
|
|
||||||
canLogIn = false;
|
canLogIn = false;
|
||||||
requireCertificate = false;
|
requireCertificate = false;
|
||||||
|
|
||||||
expected = Object.assign(new EPerson(), {
|
|
||||||
metadata: {
|
|
||||||
'eperson.firstname': [
|
|
||||||
{
|
|
||||||
value: firstName
|
|
||||||
}
|
|
||||||
],
|
|
||||||
'eperson.lastname': [
|
|
||||||
{
|
|
||||||
value: lastName
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
email: email,
|
|
||||||
canLogIn: canLogIn,
|
|
||||||
requireCertificate: requireCertificate,
|
|
||||||
});
|
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
component.canLogIn.value = canLogIn;
|
component.canLogIn.value = canLogIn;
|
||||||
component.requireCertificate.value = requireCertificate;
|
component.requireCertificate.value = requireCertificate;
|
||||||
@@ -340,15 +310,13 @@ describe('EPersonFormComponent', () => {
|
|||||||
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
expect(component.formGroup.controls.email.errors.emailTaken).toBeTruthy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
let firstName;
|
let firstName;
|
||||||
let lastName;
|
let lastName;
|
||||||
let email;
|
let email;
|
||||||
let canLogIn;
|
let canLogIn: boolean;
|
||||||
let requireCertificate;
|
let requireCertificate;
|
||||||
|
|
||||||
let expected;
|
let expected;
|
||||||
@@ -377,6 +345,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
requireCertificate: requireCertificate,
|
requireCertificate: requireCertificate,
|
||||||
});
|
});
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
|
component.ngOnInit();
|
||||||
component.firstName.value = firstName;
|
component.firstName.value = firstName;
|
||||||
component.lastName.value = lastName;
|
component.lastName.value = lastName;
|
||||||
component.email.value = email;
|
component.email.value = email;
|
||||||
@@ -416,9 +385,17 @@ describe('EPersonFormComponent', () => {
|
|||||||
email: email,
|
email: email,
|
||||||
canLogIn: canLogIn,
|
canLogIn: canLogIn,
|
||||||
requireCertificate: requireCertificate,
|
requireCertificate: requireCertificate,
|
||||||
_links: undefined
|
_links: {
|
||||||
|
groups: {
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
self: {
|
||||||
|
href: '',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||||
|
component.ngOnInit();
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -466,22 +443,19 @@ describe('EPersonFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
|
|
||||||
let ePersonId;
|
|
||||||
let eperson: EPerson;
|
let eperson: EPerson;
|
||||||
let modalService;
|
let modalService;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(authService, 'impersonate').and.callThrough();
|
spyOn(authService, 'impersonate').and.callThrough();
|
||||||
ePersonId = 'testEPersonId';
|
|
||||||
eperson = EPersonMock;
|
eperson = EPersonMock;
|
||||||
component.epersonInitial = eperson;
|
component.epersonInitial = eperson;
|
||||||
component.canDelete$ = observableOf(true);
|
component.canDelete$ = observableOf(true);
|
||||||
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
||||||
modalService = (component as any).modalService;
|
modalService = (component as any).modalService;
|
||||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||||
|
component.ngOnInit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('the delete button should be visible if the ePerson can be deleted', () => {
|
it('the delete button should be visible if the ePerson can be deleted', () => {
|
||||||
@@ -508,7 +482,8 @@ describe('EPersonFormComponent', () => {
|
|||||||
// ePersonDataServiceStub.activeEPerson = eperson;
|
// ePersonDataServiceStub.activeEPerson = eperson;
|
||||||
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
|
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(createSuccessfulRemoteDataObject$('No Content', 204));
|
||||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||||
expect(deleteButton.nativeElement.disabled).toBe(false);
|
expect(deleteButton.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||||
|
expect(deleteButton.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
deleteButton.triggerEventHandler('click', null);
|
deleteButton.triggerEventHandler('click', null);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
||||||
|
@@ -139,6 +139,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
canImpersonate$: Observable<boolean>;
|
canImpersonate$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current {@link EPerson}
|
||||||
|
*/
|
||||||
|
activeEPerson$: Observable<EPerson>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* List of subscriptions
|
* List of subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -199,7 +204,11 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
) {
|
) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.activeEPerson$ = this.epersonService.getActiveEPerson();
|
||||||
|
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||||
@@ -207,9 +216,6 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.submitLabel = 'form.submit';
|
this.submitLabel = 'form.submit';
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.initialisePage();
|
this.initialisePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,20 +223,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* This method will initialise the page
|
* This method will initialise the page
|
||||||
*/
|
*/
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
|
if (this.route.snapshot.params.id) {
|
||||||
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
this.subs.push(this.epersonService.findById(this.route.snapshot.params.id).subscribe((ePersonRD: RemoteData<EPerson>) => {
|
||||||
this.epersonService.editEPerson(ePersonRD.payload);
|
this.epersonService.editEPerson(ePersonRD.payload);
|
||||||
}));
|
}));
|
||||||
observableCombineLatest([
|
}
|
||||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.email`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.canLogIn`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.emailHint`),
|
|
||||||
]).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
|
|
||||||
this.firstName = new DynamicInputModel({
|
this.firstName = new DynamicInputModel({
|
||||||
id: 'firstName',
|
id: 'firstName',
|
||||||
label: firstName,
|
label: this.translateService.instant(`${this.messagePrefix}.firstName`),
|
||||||
name: 'firstName',
|
name: 'firstName',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
@@ -239,7 +239,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.lastName = new DynamicInputModel({
|
this.lastName = new DynamicInputModel({
|
||||||
id: 'lastName',
|
id: 'lastName',
|
||||||
label: lastName,
|
label: this.translateService.instant(`${this.messagePrefix}.lastName`),
|
||||||
name: 'lastName',
|
name: 'lastName',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
@@ -248,7 +248,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.email = new DynamicInputModel({
|
this.email = new DynamicInputModel({
|
||||||
id: 'email',
|
id: 'email',
|
||||||
label: email,
|
label: this.translateService.instant(`${this.messagePrefix}.email`),
|
||||||
name: 'email',
|
name: 'email',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
@@ -259,19 +259,19 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
emailTaken: 'error.validation.emailTaken',
|
emailTaken: 'error.validation.emailTaken',
|
||||||
pattern: 'error.validation.NotValidEmail'
|
pattern: 'error.validation.NotValidEmail'
|
||||||
},
|
},
|
||||||
hint: emailHint
|
hint: this.translateService.instant(`${this.messagePrefix}.emailHint`),
|
||||||
});
|
});
|
||||||
this.canLogIn = new DynamicCheckboxModel(
|
this.canLogIn = new DynamicCheckboxModel(
|
||||||
{
|
{
|
||||||
id: 'canLogIn',
|
id: 'canLogIn',
|
||||||
label: canLogIn,
|
label: this.translateService.instant(`${this.messagePrefix}.canLogIn`),
|
||||||
name: 'canLogIn',
|
name: 'canLogIn',
|
||||||
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
|
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
|
||||||
});
|
});
|
||||||
this.requireCertificate = new DynamicCheckboxModel(
|
this.requireCertificate = new DynamicCheckboxModel(
|
||||||
{
|
{
|
||||||
id: 'requireCertificate',
|
id: 'requireCertificate',
|
||||||
label: requireCertificate,
|
label: this.translateService.instant(`${this.messagePrefix}.requireCertificate`),
|
||||||
name: 'requireCertificate',
|
name: 'requireCertificate',
|
||||||
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
|
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
|
||||||
});
|
});
|
||||||
@@ -283,12 +283,12 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.requireCertificate,
|
this.requireCertificate,
|
||||||
];
|
];
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
if (eperson != null) {
|
if (eperson != null) {
|
||||||
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
elementsPerPage: this.config.pageSize
|
elementsPerPage: this.config.pageSize
|
||||||
});
|
}, undefined, undefined, followLink('object'));
|
||||||
}
|
}
|
||||||
this.formGroup.patchValue({
|
this.formGroup.patchValue({
|
||||||
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',
|
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',
|
||||||
@@ -306,9 +306,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const activeEPerson$ = this.epersonService.getActiveEPerson();
|
this.groups = this.activeEPerson$.pipe(
|
||||||
|
|
||||||
this.groups = activeEPerson$.pipe(
|
|
||||||
switchMap((eperson) => {
|
switchMap((eperson) => {
|
||||||
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
@@ -323,7 +321,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
this.canImpersonate$ = activeEPerson$.pipe(
|
this.canImpersonate$ = this.activeEPerson$.pipe(
|
||||||
switchMap((eperson) => {
|
switchMap((eperson) => {
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
||||||
@@ -332,11 +330,10 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
this.canDelete$ = activeEPerson$.pipe(
|
this.canDelete$ = this.activeEPerson$.pipe(
|
||||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
||||||
);
|
);
|
||||||
this.canReset$ = observableOf(true);
|
this.canReset$ = observableOf(true);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -355,7 +352,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
|
this.activeEPerson$.pipe(take(1)).subscribe(
|
||||||
(ePerson: EPerson) => {
|
(ePerson: EPerson) => {
|
||||||
const values = {
|
const values = {
|
||||||
metadata: {
|
metadata: {
|
||||||
@@ -474,7 +471,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
*/
|
*/
|
||||||
delete(): void {
|
delete(): void {
|
||||||
this.epersonService.getActiveEPerson().pipe(
|
this.activeEPerson$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((eperson: EPerson) => {
|
switchMap((eperson: EPerson) => {
|
||||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
@@ -578,7 +575,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Update the list of groups by fetching it from the rest api or cache
|
* Update the list of groups by fetching it from the rest api or cache
|
||||||
*/
|
*/
|
||||||
private updateGroups(options) {
|
private updateGroups(options) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.activeEPerson$.subscribe((eperson: EPerson) => {
|
||||||
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
<div class="group-form row">
|
<div class="group-form row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
|
|
||||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editHeader; else createHeader"></div>
|
<div *ngIf="activeGroup$ | async; then editHeader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
<h1 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h1>
|
||||||
@@ -23,11 +23,15 @@
|
|||||||
</h1>
|
</h1>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
|
||||||
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertTypeEnum.Warning"
|
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
|
||||||
|
<ds-alert *ngIf="groupBeingEdited?.permanent" [type]="AlertType.Warning"
|
||||||
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
[content]="messagePrefix + '.alert.permanent'"></ds-alert>
|
||||||
<ds-alert *ngIf="!(canEdit$ | async) && (groupDataService.getActiveGroup() | async)" [type]="AlertTypeEnum.Warning"
|
<ng-container *ngIf="(activeGroupLinkedDSO$ | async) as activeGroupLinkedDSO">
|
||||||
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName((getLinkedDSO(groupBeingEdited) | async)?.payload), comcol: (getLinkedDSO(groupBeingEdited) | async)?.payload?.type, comcolEditRolesRoute: (getLinkedEditRolesRoute(groupBeingEdited) | async) })">
|
<ds-alert *ngIf="!(canEdit$ | async)" [type]="AlertType.Warning"
|
||||||
|
[content]="(messagePrefix + '.alert.workflowGroup' | translate:{ name: dsoNameService.getName(activeGroupLinkedDSO), comcol: activeGroupLinkedDSO.type, comcolEditRolesRoute: (linkedEditRolesRoute$ | async) })">
|
||||||
</ds-alert>
|
</ds-alert>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
<ds-form [formId]="formId"
|
<ds-form [formId]="formId"
|
||||||
[formModel]="formModel"
|
[formModel]="formModel"
|
||||||
@@ -39,22 +43,21 @@
|
|||||||
<button (click)="onCancel()" type="button"
|
<button (click)="onCancel()" type="button"
|
||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div after *ngIf="(canEdit$ | async) && !groupBeingEdited?.permanent" class="btn-group">
|
<div after *ngIf="(canEdit$ | async) && !(activeGroup$ | async)?.permanent" class="btn-group">
|
||||||
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
<button (click)="delete()" class="btn btn-danger delete-button" type="button">
|
||||||
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
<i class="fa fa-trash"></i> {{ messagePrefix + '.actions.delete' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|
||||||
|
<ng-container *ngIf="(activeGroup$ | async) as groupBeingEdited">
|
||||||
<div class="mb-5">
|
<div class="mb-5">
|
||||||
<ds-members-list *ngIf="groupBeingEdited != null"
|
<ds-members-list *ngIf="groupBeingEdited != null"
|
||||||
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
[messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||||
</div>
|
</div>
|
||||||
<ds-subgroups-list *ngIf="groupBeingEdited != null"
|
<ds-subgroups-list *ngIf="groupBeingEdited != null"
|
||||||
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
[messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||||
|
</ng-container>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
import { UntypedFormControl, UntypedFormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { Observable, of as observableOf } from 'rxjs';
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
@@ -30,8 +30,6 @@ import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
|
|||||||
import { GroupFormComponent } from './group-form.component';
|
import { GroupFormComponent } from './group-form.component';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||||
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
|
|
||||||
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
|
|
||||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
@@ -39,23 +37,26 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
|
|||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
|
||||||
|
import { RouteService } from '../../../core/services/route.service';
|
||||||
|
|
||||||
describe('GroupFormComponent', () => {
|
describe('GroupFormComponent', () => {
|
||||||
let component: GroupFormComponent;
|
let component: GroupFormComponent;
|
||||||
let fixture: ComponentFixture<GroupFormComponent>;
|
let fixture: ComponentFixture<GroupFormComponent>;
|
||||||
let translateService: TranslateService;
|
|
||||||
let builderService: FormBuilderService;
|
let builderService: FormBuilderService;
|
||||||
let ePersonDataServiceStub: any;
|
let ePersonDataServiceStub: any;
|
||||||
let groupsDataServiceStub: any;
|
let groupsDataServiceStub: any;
|
||||||
let dsoDataServiceStub: any;
|
let dsoDataServiceStub: any;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let notificationService: NotificationsServiceStub;
|
let notificationService: NotificationsServiceStub;
|
||||||
let router;
|
let router: RouterMock;
|
||||||
|
let route: ActivatedRouteStub;
|
||||||
|
|
||||||
let groups;
|
let groups: Group[];
|
||||||
let groupName;
|
let groupName: string;
|
||||||
let groupDescription;
|
let groupDescription: string;
|
||||||
let expected;
|
let expected: Group;
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
groups = [GroupMock, GroupMock2];
|
groups = [GroupMock, GroupMock2];
|
||||||
@@ -70,6 +71,15 @@ describe('GroupFormComponent', () => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
object: createSuccessfulRemoteDataObject$(undefined),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
ePersonDataServiceStub = {};
|
ePersonDataServiceStub = {};
|
||||||
groupsDataServiceStub = {
|
groupsDataServiceStub = {
|
||||||
@@ -106,7 +116,14 @@ describe('GroupFormComponent', () => {
|
|||||||
create(group: Group): Observable<RemoteData<Group>> {
|
create(group: Group): Observable<RemoteData<Group>> {
|
||||||
this.allGroups = [...this.allGroups, group];
|
this.allGroups = [...this.allGroups, group];
|
||||||
this.createdGroup = Object.assign({}, group, {
|
this.createdGroup = Object.assign({}, group, {
|
||||||
_links: { self: { href: 'group-selflink' } }
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
||||||
},
|
},
|
||||||
@@ -188,17 +205,13 @@ describe('GroupFormComponent', () => {
|
|||||||
return typeof value === 'object' && value !== null;
|
return typeof value === 'object' && value !== null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
translateService = getMockTranslateService();
|
|
||||||
router = new RouterMock();
|
router = new RouterMock();
|
||||||
|
route = new ActivatedRouteStub();
|
||||||
notificationService = new NotificationsServiceStub();
|
notificationService = new NotificationsServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||||
TranslateModule.forRoot({
|
TranslateModule.forRoot(),
|
||||||
loader: {
|
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
declarations: [GroupFormComponent],
|
declarations: [GroupFormComponent],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -216,14 +229,12 @@ describe('GroupFormComponent', () => {
|
|||||||
{ provide: Store, useValue: {} },
|
{ provide: Store, useValue: {} },
|
||||||
{ provide: RemoteDataBuildService, useValue: {} },
|
{ provide: RemoteDataBuildService, useValue: {} },
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
{ provide: HALEndpointService, useValue: {} },
|
||||||
{
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
provide: ActivatedRoute,
|
|
||||||
useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) }
|
|
||||||
},
|
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -236,8 +247,8 @@ describe('GroupFormComponent', () => {
|
|||||||
describe('when submitting the form', () => {
|
describe('when submitting the form', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
component.groupName.value = groupName;
|
component.groupName.setValue(groupName);
|
||||||
component.groupDescription.value = groupDescription;
|
component.groupDescription.setValue(groupDescription);
|
||||||
});
|
});
|
||||||
describe('without active Group', () => {
|
describe('without active Group', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -245,14 +256,22 @@ describe('GroupFormComponent', () => {
|
|||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit a new group using the correct values', (async () => {
|
it('should emit a new group using the correct values', (() => {
|
||||||
await fixture.whenStable().then(() => {
|
expect(component.submitForm.emit).toHaveBeenCalledWith(jasmine.objectContaining({
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
name: groupName,
|
||||||
});
|
metadata: {
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
value: groupDescription,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}));
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('with active Group', () => {
|
describe('with active Group', () => {
|
||||||
let expected2;
|
let expected2: Group;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
expected2 = Object.assign(new Group(), {
|
expected2 = Object.assign(new Group(), {
|
||||||
name: 'newGroupName',
|
name: 'newGroupName',
|
||||||
@@ -263,15 +282,24 @@ describe('GroupFormComponent', () => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
object: createSuccessfulRemoteDataObject$(undefined),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
||||||
spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
||||||
component.groupName.value = 'newGroupName';
|
component.ngOnInit();
|
||||||
component.onSubmit();
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with name and description operations', () => {
|
it('should edit with name and description operations', () => {
|
||||||
|
component.groupName.setValue('newGroupName');
|
||||||
|
component.onSubmit();
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: '/metadata/dc.description',
|
path: '/metadata/dc.description',
|
||||||
@@ -285,9 +313,8 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with description operations', () => {
|
it('should edit with description operations', () => {
|
||||||
component.groupName.value = null;
|
component.groupName.setValue(null);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'add',
|
op: 'add',
|
||||||
path: '/metadata/dc.description',
|
path: '/metadata/dc.description',
|
||||||
@@ -297,9 +324,9 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should edit with name operations', () => {
|
it('should edit with name operations', () => {
|
||||||
component.groupDescription.value = null;
|
component.groupName.setValue('newGroupName');
|
||||||
|
component.groupDescription.setValue(null);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
|
||||||
const operations = [{
|
const operations = [{
|
||||||
op: 'replace',
|
op: 'replace',
|
||||||
path: '/name',
|
path: '/name',
|
||||||
@@ -308,12 +335,13 @@ describe('GroupFormComponent', () => {
|
|||||||
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
expect(groupsDataServiceStub.patch).toHaveBeenCalledWith(expected, operations);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit the existing group using the correct new values', (async () => {
|
it('should emit the existing group using the correct new values', () => {
|
||||||
await fixture.whenStable().then(() => {
|
component.onSubmit();
|
||||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
expect(component.submitForm.emit).toHaveBeenCalledWith(expected2);
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
it('should emit success notification', () => {
|
it('should emit success notification', () => {
|
||||||
|
component.onSubmit();
|
||||||
expect(notificationService.success).toHaveBeenCalled();
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -328,11 +356,8 @@ describe('GroupFormComponent', () => {
|
|||||||
|
|
||||||
|
|
||||||
describe('check form validation', () => {
|
describe('check form validation', () => {
|
||||||
let groupCommunity;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
groupName = 'testName';
|
groupName = 'testName';
|
||||||
groupCommunity = 'testgroupCommunity';
|
|
||||||
groupDescription = 'testgroupDescription';
|
groupDescription = 'testgroupDescription';
|
||||||
|
|
||||||
expected = Object.assign(new Group(), {
|
expected = Object.assign(new Group(), {
|
||||||
@@ -344,8 +369,17 @@ describe('GroupFormComponent', () => {
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'group-selflink',
|
||||||
|
},
|
||||||
|
object: {
|
||||||
|
href: 'group-objectlink',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
spyOn(component.submitForm, 'emit');
|
spyOn(component.submitForm, 'emit');
|
||||||
|
spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected));
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
component.initialisePage();
|
component.initialisePage();
|
||||||
@@ -395,21 +429,20 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('delete', () => {
|
describe('delete', () => {
|
||||||
let deleteButton;
|
let deleteButton: HTMLButtonElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
component.initialisePage();
|
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||||
|
component.activeGroup$ = observableOf({
|
||||||
|
id: 'active-group',
|
||||||
|
permanent: false,
|
||||||
|
} as Group);
|
||||||
component.canEdit$ = observableOf(true);
|
component.canEdit$ = observableOf(true);
|
||||||
component.groupBeingEdited = {
|
|
||||||
permanent: false
|
component.initialisePage();
|
||||||
} as Group;
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
||||||
|
|
||||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
|
||||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' }));
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if confirmed via modal', () => {
|
describe('if confirmed via modal', () => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
|
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup, AbstractControl } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import {
|
import {
|
||||||
@@ -12,10 +12,9 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import {
|
import {
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { catchError, map, switchMap, take, filter, debounceTime } from 'rxjs/operators';
|
import { map, switchMap, take, debounceTime } from 'rxjs/operators';
|
||||||
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
|
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
|
||||||
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
|
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
|
||||||
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
||||||
@@ -24,17 +23,16 @@ import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
|||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
|
||||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||||
import { Group } from '../../../core/eperson/models/group.model';
|
import { Group } from '../../../core/eperson/models/group.model';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { Community } from '../../../core/shared/community.model';
|
import { Community } from '../../../core/shared/community.model';
|
||||||
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
|
||||||
import {
|
import {
|
||||||
|
getAllCompletedRemoteData,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getFirstCompletedRemoteData,
|
getFirstCompletedRemoteData,
|
||||||
getFirstSucceededRemoteDataPayload
|
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { AlertType } from '../../../shared/alert/alert-type';
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||||
@@ -48,6 +46,7 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
|
|||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { environment } from '../../../../environments/environment';
|
import { environment } from '../../../../environments/environment';
|
||||||
import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths';
|
import { getGroupEditRoute, getGroupsRoute } from '../../access-control-routing-paths';
|
||||||
|
import { RouteService } from '../../../core/services/route.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-group-form',
|
selector: 'ds-group-form',
|
||||||
@@ -68,9 +67,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Dynamic models for the inputs of form
|
* Dynamic models for the inputs of form
|
||||||
*/
|
*/
|
||||||
groupName: DynamicInputModel;
|
groupName: AbstractControl;
|
||||||
groupCommunity: DynamicInputModel;
|
groupCommunity: AbstractControl;
|
||||||
groupDescription: DynamicTextAreaModel;
|
groupDescription: AbstractControl;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all dynamic input models
|
* A list of all dynamic input models
|
||||||
@@ -113,21 +112,30 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
subs: Subscription[] = [];
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
/**
|
|
||||||
* Group currently being edited
|
|
||||||
*/
|
|
||||||
groupBeingEdited: Group;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
* Observable whether or not the logged in user is allowed to delete the Group & doesn't have a linked object (community / collection linked to workspace group
|
||||||
*/
|
*/
|
||||||
canEdit$: Observable<boolean>;
|
canEdit$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The AlertType enumeration
|
* The current {@link Group}
|
||||||
* @type {AlertType}
|
|
||||||
*/
|
*/
|
||||||
public AlertTypeEnum = AlertType;
|
activeGroup$: Observable<Group>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The current {@link Group}'s linked {@link Community}/{@link Collection}
|
||||||
|
*/
|
||||||
|
activeGroupLinkedDSO$: Observable<DSpaceObject>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Link to the current {@link Group}'s {@link Community}/{@link Collection} edit role tab
|
||||||
|
*/
|
||||||
|
linkedEditRolesRoute$: Observable<string>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The AlertType enumeration
|
||||||
|
*/
|
||||||
|
public readonly AlertType = AlertType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscription to email field value change
|
* Subscription to email field value change
|
||||||
@@ -137,78 +145,77 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public groupDataService: GroupDataService,
|
public groupDataService: GroupDataService,
|
||||||
private ePersonDataService: EPersonDataService,
|
protected dSpaceObjectDataService: DSpaceObjectDataService,
|
||||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
protected formBuilderService: FormBuilderService,
|
||||||
private formBuilderService: FormBuilderService,
|
protected translateService: TranslateService,
|
||||||
private translateService: TranslateService,
|
protected notificationsService: NotificationsService,
|
||||||
private notificationsService: NotificationsService,
|
protected route: ActivatedRoute,
|
||||||
private route: ActivatedRoute,
|
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
private authorizationService: AuthorizationDataService,
|
protected authorizationService: AuthorizationDataService,
|
||||||
private modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
public requestService: RequestService,
|
public requestService: RequestService,
|
||||||
protected changeDetectorRef: ChangeDetectorRef,
|
protected changeDetectorRef: ChangeDetectorRef,
|
||||||
public dsoNameService: DSONameService,
|
public dsoNameService: DSONameService,
|
||||||
|
protected routeService: RouteService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit(): void {
|
||||||
|
if (this.route.snapshot.params.groupId !== 'newGroup') {
|
||||||
|
this.setActiveGroup(this.route.snapshot.params.groupId);
|
||||||
|
}
|
||||||
|
this.activeGroup$ = this.groupDataService.getActiveGroup();
|
||||||
|
this.activeGroupLinkedDSO$ = this.getActiveGroupLinkedDSO();
|
||||||
|
this.linkedEditRolesRoute$ = this.getLinkedEditRolesRoute();
|
||||||
|
this.canEdit$ = this.activeGroupLinkedDSO$.pipe(
|
||||||
|
switchMap((dso: DSpaceObject) => {
|
||||||
|
if (hasValue(dso)) {
|
||||||
|
return [false];
|
||||||
|
} else {
|
||||||
|
return this.activeGroup$.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
switchMap((group: Group) => this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
this.initialisePage();
|
this.initialisePage();
|
||||||
}
|
}
|
||||||
|
|
||||||
initialisePage() {
|
initialisePage() {
|
||||||
this.subs.push(this.route.params.subscribe((params) => {
|
const groupNameModel = new DynamicInputModel({
|
||||||
if (params.groupId !== 'newGroup') {
|
|
||||||
this.setActiveGroup(params.groupId);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
this.canEdit$ = this.groupDataService.getActiveGroup().pipe(
|
|
||||||
hasValueOperator(),
|
|
||||||
switchMap((group: Group) => {
|
|
||||||
return observableCombineLatest([
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, isNotEmpty(group) ? group.self : undefined),
|
|
||||||
this.hasLinkedDSO(group),
|
|
||||||
]).pipe(
|
|
||||||
map(([isAuthorized, hasLinkedDSO]: [boolean, boolean]) => isAuthorized && !hasLinkedDSO),
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
observableCombineLatest([
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupCommunity`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.groupDescription`)
|
|
||||||
]).subscribe(([groupName, groupCommunity, groupDescription]) => {
|
|
||||||
this.groupName = new DynamicInputModel({
|
|
||||||
id: 'groupName',
|
id: 'groupName',
|
||||||
label: groupName,
|
label: this.translateService.instant(`${this.messagePrefix}.groupName`),
|
||||||
name: 'groupName',
|
name: 'groupName',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
},
|
},
|
||||||
required: true,
|
required: true,
|
||||||
});
|
});
|
||||||
this.groupCommunity = new DynamicInputModel({
|
const groupCommunityModel = new DynamicInputModel({
|
||||||
id: 'groupCommunity',
|
id: 'groupCommunity',
|
||||||
label: groupCommunity,
|
label: this.translateService.instant(`${this.messagePrefix}.groupCommunity`),
|
||||||
name: 'groupCommunity',
|
name: 'groupCommunity',
|
||||||
required: false,
|
required: false,
|
||||||
readOnly: true,
|
readOnly: true,
|
||||||
});
|
});
|
||||||
this.groupDescription = new DynamicTextAreaModel({
|
const groupDescriptionModel = new DynamicTextAreaModel({
|
||||||
id: 'groupDescription',
|
id: 'groupDescription',
|
||||||
label: groupDescription,
|
label: this.translateService.instant(`${this.messagePrefix}.groupDescription`),
|
||||||
name: 'groupDescription',
|
name: 'groupDescription',
|
||||||
required: false,
|
required: false,
|
||||||
spellCheck: environment.form.spellCheck,
|
spellCheck: environment.form.spellCheck,
|
||||||
});
|
});
|
||||||
this.formModel = [
|
this.formModel = [
|
||||||
this.groupName,
|
groupNameModel,
|
||||||
this.groupDescription,
|
groupDescriptionModel,
|
||||||
];
|
];
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
this.groupName = this.formGroup.get('groupName');
|
||||||
|
this.groupDescription = this.formGroup.get('groupDescription');
|
||||||
|
|
||||||
if (!!this.formGroup.controls.groupName) {
|
if (hasValue(this.groupName)) {
|
||||||
this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
this.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
||||||
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||||
this.changeDetectorRef.detectChanges();
|
this.changeDetectorRef.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -216,10 +223,9 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest([
|
observableCombineLatest([
|
||||||
this.groupDataService.getActiveGroup(),
|
this.activeGroup$,
|
||||||
this.canEdit$,
|
this.canEdit$,
|
||||||
this.groupDataService.getActiveGroup()
|
this.activeGroupLinkedDSO$,
|
||||||
.pipe(filter((activeGroup) => hasValue(activeGroup)),switchMap((activeGroup) => this.getLinkedDSO(activeGroup).pipe(getFirstSucceededRemoteDataPayload())))
|
|
||||||
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
]).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||||
|
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
@@ -227,36 +233,34 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
// Disable group name exists validator
|
// Disable group name exists validator
|
||||||
this.formGroup.controls.groupName.clearAsyncValidators();
|
this.formGroup.controls.groupName.clearAsyncValidators();
|
||||||
|
|
||||||
this.groupBeingEdited = activeGroup;
|
if (isNotEmpty(linkedObject?.name)) {
|
||||||
|
|
||||||
if (linkedObject?.name) {
|
|
||||||
if (!this.formGroup.controls.groupCommunity) {
|
if (!this.formGroup.controls.groupCommunity) {
|
||||||
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, this.groupCommunity);
|
this.formBuilderService.insertFormGroupControl(1, this.formGroup, this.formModel, groupCommunityModel);
|
||||||
|
this.groupDescription = this.formGroup.get('groupCommunity');
|
||||||
|
}
|
||||||
this.formGroup.patchValue({
|
this.formGroup.patchValue({
|
||||||
groupName: activeGroup.name,
|
groupName: activeGroup.name,
|
||||||
groupCommunity: linkedObject?.name ?? '',
|
groupCommunity: linkedObject?.name ?? '',
|
||||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
this.formModel = [
|
this.formModel = [
|
||||||
this.groupName,
|
groupNameModel,
|
||||||
this.groupDescription,
|
groupDescriptionModel,
|
||||||
];
|
];
|
||||||
this.formGroup.patchValue({
|
this.formGroup.patchValue({
|
||||||
groupName: activeGroup.name,
|
groupName: activeGroup.name,
|
||||||
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
groupDescription: activeGroup.firstMetadataValue('dc.description'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setTimeout(() => {
|
|
||||||
if (!canEdit || activeGroup.permanent) {
|
if (!canEdit || activeGroup.permanent) {
|
||||||
this.formGroup.disable();
|
this.formGroup.disable();
|
||||||
|
} else {
|
||||||
|
this.formGroup.enable();
|
||||||
}
|
}
|
||||||
}, 200);
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -265,7 +269,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
onCancel() {
|
onCancel() {
|
||||||
this.groupDataService.cancelEditGroup();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.cancelForm.emit();
|
this.cancelForm.emit();
|
||||||
void this.router.navigate([getGroupsRoute()]);
|
this.routeService.getPreviousUrl().pipe(
|
||||||
|
take(1),
|
||||||
|
).subscribe((previousURL) => {
|
||||||
|
void this.router.navigate([previousURL && previousURL.trim().length > 0 ? previousURL : getGroupsRoute()]);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -275,25 +283,22 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit() {
|
onSubmit() {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe(
|
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||||
(group: Group) => {
|
if (group === null) {
|
||||||
const values = {
|
this.createNewGroup({
|
||||||
name: this.groupName.value,
|
name: this.groupName.value,
|
||||||
metadata: {
|
metadata: {
|
||||||
'dc.description': [
|
'dc.description': [
|
||||||
{
|
{
|
||||||
value: this.groupDescription.value
|
value: this.groupDescription.value,
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
};
|
],
|
||||||
if (group === null) {
|
},
|
||||||
this.createNewGroup(values);
|
});
|
||||||
} else {
|
} else {
|
||||||
this.editGroup(group);
|
this.editGroup(group);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -399,7 +404,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* @param groupSelfLink SelfLink of group to set as active
|
* @param groupSelfLink SelfLink of group to set as active
|
||||||
*/
|
*/
|
||||||
setActiveGroupWithLink(groupSelfLink: string) {
|
setActiveGroupWithLink(groupSelfLink: string) {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
this.activeGroup$.pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||||
if (activeGroup === null) {
|
if (activeGroup === null) {
|
||||||
this.groupDataService.cancelEditGroup();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
this.groupDataService.findByHref(groupSelfLink, false, false, followLink('subgroups'), followLink('epersons'), followLink('object'))
|
||||||
@@ -418,7 +423,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||||
*/
|
*/
|
||||||
delete() {
|
delete() {
|
||||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((group: Group) => {
|
this.activeGroup$.pipe(take(1)).subscribe((group: Group) => {
|
||||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||||
modalRef.componentInstance.dso = group;
|
modalRef.componentInstance.dso = group;
|
||||||
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
modalRef.componentInstance.headerLabel = this.messagePrefix + '.delete-group.modal.header';
|
||||||
@@ -462,52 +467,38 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if group has a linked object (community or collection linked to a workflow group)
|
* Get the active {@link Group}'s linked object if it has one ({@link Community} or {@link Collection} linked to a
|
||||||
* @param group
|
* workflow group)
|
||||||
*/
|
*/
|
||||||
hasLinkedDSO(group: Group): Observable<boolean> {
|
getActiveGroupLinkedDSO(): Observable<DSpaceObject> {
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
return this.activeGroup$.pipe(
|
||||||
return this.getLinkedDSO(group).pipe(
|
hasValueOperator(),
|
||||||
map((rd: RemoteData<DSpaceObject>) => {
|
switchMap((group: Group) => {
|
||||||
return hasValue(rd) && hasValue(rd.payload);
|
|
||||||
}),
|
|
||||||
catchError(() => observableOf(false)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get group's linked object if it has one (community or collection linked to a workflow group)
|
|
||||||
* @param group
|
|
||||||
*/
|
|
||||||
getLinkedDSO(group: Group): Observable<RemoteData<DSpaceObject>> {
|
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
|
||||||
if (group.object === undefined) {
|
if (group.object === undefined) {
|
||||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
return this.dSpaceObjectDataService.findByHref(group._links.object.href);
|
||||||
}
|
}
|
||||||
return group.object;
|
return group.object;
|
||||||
}
|
}),
|
||||||
|
getAllCompletedRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the route to the edit roles tab of the group's linked object (community or collection linked to a workflow group) if it has one
|
* Get the route to the edit roles tab of the active {@link Group}'s linked object (community or collection linked
|
||||||
* @param group
|
* to a workflow group) if it has one
|
||||||
*/
|
*/
|
||||||
getLinkedEditRolesRoute(group: Group): Observable<string> {
|
getLinkedEditRolesRoute(): Observable<string> {
|
||||||
if (hasValue(group) && hasValue(group._links.object.href)) {
|
return this.activeGroupLinkedDSO$.pipe(
|
||||||
return this.getLinkedDSO(group).pipe(
|
hasValueOperator(),
|
||||||
map((rd: RemoteData<DSpaceObject>) => {
|
map((dso: DSpaceObject) => {
|
||||||
if (hasValue(rd) && hasValue(rd.payload)) {
|
|
||||||
const dso = rd.payload;
|
|
||||||
switch ((dso as any).type) {
|
switch ((dso as any).type) {
|
||||||
case Community.type.value:
|
case Community.type.value:
|
||||||
return getCommunityEditRolesRoute(rd.payload.id);
|
return getCommunityEditRolesRoute(dso.id);
|
||||||
case Collection.type.value:
|
case Collection.type.value:
|
||||||
return getCollectionEditRolesRoute(rd.payload.id);
|
return getCollectionEditRolesRoute(dso.id);
|
||||||
}
|
}
|
||||||
}
|
}),
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,7 @@
|
|||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="deleteMemberFromGroup(eperson)"
|
<button (click)="deleteMemberFromGroup(eperson)"
|
||||||
[disabled]="actionConfig.remove.disabled"
|
[dsBtnDisabled]="actionConfig.remove.disabled"
|
||||||
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
[ngClass]="['btn btn-sm', actionConfig.remove.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||||
<i [ngClass]="actionConfig.remove.icon"></i>
|
<i [ngClass]="actionConfig.remove.icon"></i>
|
||||||
@@ -114,7 +114,7 @@
|
|||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="btn-group edit-field">
|
<div class="btn-group edit-field">
|
||||||
<button (click)="addMemberToGroup(eperson)"
|
<button (click)="addMemberToGroup(eperson)"
|
||||||
[disabled]="actionConfig.add.disabled"
|
[dsBtnDisabled]="actionConfig.add.disabled"
|
||||||
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
[ngClass]="['btn btn-sm', actionConfig.add.css]"
|
||||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
title="{{messagePrefix + '.table.edit.buttons.add' | translate: { name: dsoNameService.getName(eperson) } }}">
|
||||||
<i [ngClass]="actionConfig.add.icon"></i>
|
<i [ngClass]="actionConfig.add.icon"></i>
|
||||||
|
@@ -69,7 +69,7 @@
|
|||||||
<i class="fas fa-edit fa-fw"></i>
|
<i class="fas fa-edit fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
<button *ngSwitchCase="false"
|
<button *ngSwitchCase="false"
|
||||||
[disabled]="true"
|
[dsBtnDisabled]="true"
|
||||||
class="btn btn-outline-primary btn-sm btn-edit"
|
class="btn btn-outline-primary btn-sm btn-edit"
|
||||||
placement="left"
|
placement="left"
|
||||||
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
[ngbTooltip]="'admin.access-control.epeople.table.edit.buttons.edit-disabled' | translate"
|
||||||
|
@@ -34,6 +34,7 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|||||||
import { NoContent } from '../../core/shared/NoContent.model';
|
import { NoContent } from '../../core/shared/NoContent.model';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock, UNDEFINED_NAME } from '../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock, UNDEFINED_NAME } from '../../shared/mocks/dso-name.service.mock';
|
||||||
|
import {BtnDisabledDirective} from '../../shared/btn-disabled.directive';
|
||||||
|
|
||||||
describe('GroupsRegistryComponent', () => {
|
describe('GroupsRegistryComponent', () => {
|
||||||
let component: GroupsRegistryComponent;
|
let component: GroupsRegistryComponent;
|
||||||
@@ -171,7 +172,7 @@ describe('GroupsRegistryComponent', () => {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
],
|
],
|
||||||
declarations: [GroupsRegistryComponent],
|
declarations: [GroupsRegistryComponent, BtnDisabledDirective],
|
||||||
providers: [GroupsRegistryComponent,
|
providers: [GroupsRegistryComponent,
|
||||||
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
{ provide: DSONameService, useValue: new DSONameServiceMock() },
|
||||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||||
@@ -231,7 +232,8 @@ describe('GroupsRegistryComponent', () => {
|
|||||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
expect(editButtonsFound.length).toEqual(2);
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
editButtonsFound.forEach((editButtonFound) => {
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||||
|
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -265,7 +267,8 @@ describe('GroupsRegistryComponent', () => {
|
|||||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
expect(editButtonsFound.length).toEqual(2);
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
editButtonsFound.forEach((editButtonFound) => {
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
expect(editButtonFound.nativeElement.disabled).toBeFalse();
|
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBeNull();
|
||||||
|
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -284,7 +287,8 @@ describe('GroupsRegistryComponent', () => {
|
|||||||
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
const editButtonsFound = fixture.debugElement.queryAll(By.css('#groups tr td:nth-child(5) button.btn-edit'));
|
||||||
expect(editButtonsFound.length).toEqual(2);
|
expect(editButtonsFound.length).toEqual(2);
|
||||||
editButtonsFound.forEach((editButtonFound) => {
|
editButtonsFound.forEach((editButtonFound) => {
|
||||||
expect(editButtonFound.nativeElement.disabled).toBeTrue();
|
expect(editButtonFound.nativeElement.getAttribute('aria-disabled')).toBe('true');
|
||||||
|
expect(editButtonFound.nativeElement.classList.contains('disabled')).toBeTrue();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
11
src/app/accessibility/accessibility-settings.config.ts
Normal file
11
src/app/accessibility/accessibility-settings.config.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Config } from '../../config/config.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration interface used by the AccessibilitySettingsService
|
||||||
|
*/
|
||||||
|
export class AccessibilitySettingsConfig implements Config {
|
||||||
|
/**
|
||||||
|
* The duration in days after which the accessibility settings cookie expires
|
||||||
|
*/
|
||||||
|
cookieExpirationDuration: number;
|
||||||
|
}
|
410
src/app/accessibility/accessibility-settings.service.spec.ts
Normal file
410
src/app/accessibility/accessibility-settings.service.spec.ts
Normal file
@@ -0,0 +1,410 @@
|
|||||||
|
import {
|
||||||
|
AccessibilitySettingsService,
|
||||||
|
AccessibilitySettings,
|
||||||
|
ACCESSIBILITY_SETTINGS_METADATA_KEY,
|
||||||
|
ACCESSIBILITY_COOKIE, AccessibilitySettingsFormValues, FullAccessibilitySettings
|
||||||
|
} from './accessibility-settings.service';
|
||||||
|
import { CookieService } from '../core/services/cookie.service';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||||
|
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||||
|
import { AuthServiceStub } from '../shared/testing/auth-service.stub';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { fakeAsync, flush } from '@angular/core/testing';
|
||||||
|
import { createSuccessfulRemoteDataObject$, createFailedRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { KlaroServiceStub } from '../shared/cookies/klaro.service.stub';
|
||||||
|
import { AppConfig } from '../../config/app-config.interface';
|
||||||
|
|
||||||
|
|
||||||
|
describe('accessibilitySettingsService', () => {
|
||||||
|
let service: AccessibilitySettingsService;
|
||||||
|
let cookieService: CookieServiceMock;
|
||||||
|
let authService: AuthServiceStub;
|
||||||
|
let ePersonService: EPersonDataService;
|
||||||
|
let klaroService: KlaroServiceStub;
|
||||||
|
let appConfig: AppConfig;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cookieService = new CookieServiceMock();
|
||||||
|
authService = new AuthServiceStub();
|
||||||
|
klaroService = new KlaroServiceStub();
|
||||||
|
appConfig = { accessibility: { cookieExpirationDuration: 10 }} as AppConfig;
|
||||||
|
|
||||||
|
klaroService.getSavedPreferences.and.returnValue(of({ accessibility: true }));
|
||||||
|
|
||||||
|
ePersonService = jasmine.createSpyObj('ePersonService', {
|
||||||
|
createPatchFromCache: of([{
|
||||||
|
op: 'add',
|
||||||
|
value: null,
|
||||||
|
}]),
|
||||||
|
patch: of({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
service = new AccessibilitySettingsService(
|
||||||
|
cookieService as unknown as CookieService,
|
||||||
|
authService as unknown as AuthService,
|
||||||
|
ePersonService,
|
||||||
|
klaroService,
|
||||||
|
appConfig,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('get', () => {
|
||||||
|
it('should return the setting if it is set', () => {
|
||||||
|
const settings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.getAll = jasmine.createSpy('getAll').and.returnValue(of(settings));
|
||||||
|
|
||||||
|
service.get('notificationTimeOut', 'default').subscribe(value =>
|
||||||
|
expect(value).toEqual('1000')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the default value if the setting is not set', () => {
|
||||||
|
const settings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.getAll = jasmine.createSpy('getAll').and.returnValue(of(settings));
|
||||||
|
|
||||||
|
service.get('liveRegionTimeOut', 'default').subscribe(value =>
|
||||||
|
expect(value).toEqual('default')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAsNumber', () => {
|
||||||
|
it('should return the setting as number if the value for the setting can be parsed to a number', () => {
|
||||||
|
service.get = jasmine.createSpy('get').and.returnValue(of('1000'));
|
||||||
|
|
||||||
|
service.getAsNumber('notificationTimeOut').subscribe(value =>
|
||||||
|
expect(value).toEqual(1000)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the default value if no value is set for the setting', () => {
|
||||||
|
service.get = jasmine.createSpy('get').and.returnValue(of(null));
|
||||||
|
|
||||||
|
service.getAsNumber('notificationTimeOut', 123).subscribe(value =>
|
||||||
|
expect(value).toEqual(123)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the default value if the value for the setting can not be parsed to a number', () => {
|
||||||
|
service.get = jasmine.createSpy('get').and.returnValue(of('text'));
|
||||||
|
|
||||||
|
service.getAsNumber('notificationTimeOut', 123).subscribe(value =>
|
||||||
|
expect(value).toEqual(123)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAll', () => {
|
||||||
|
it('should attempt to get the settings from metadata first', () => {
|
||||||
|
service.getAllSettingsFromAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ }));
|
||||||
|
|
||||||
|
service.getAll().subscribe();
|
||||||
|
expect(service.getAllSettingsFromAuthenticatedUserMetadata).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attempt to get the settings from the cookie if the settings from metadata are empty', () => {
|
||||||
|
service.getAllSettingsFromAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ }));
|
||||||
|
|
||||||
|
service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ });
|
||||||
|
|
||||||
|
service.getAll().subscribe();
|
||||||
|
expect(service.getAllSettingsFromCookie).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not attempt to get the settings from the cookie if the settings from metadata are not empty', () => {
|
||||||
|
const settings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.getAllSettingsFromAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of(settings));
|
||||||
|
|
||||||
|
service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ });
|
||||||
|
|
||||||
|
service.getAll().subscribe();
|
||||||
|
expect(service.getAllSettingsFromCookie).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty object if both are empty', () => {
|
||||||
|
service.getAllSettingsFromAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ }));
|
||||||
|
|
||||||
|
service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ });
|
||||||
|
|
||||||
|
service.getAll().subscribe(value => expect(value).toEqual({}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllSettingsFromCookie', () => {
|
||||||
|
it('should retrieve the settings from the cookie', () => {
|
||||||
|
cookieService.get = jasmine.createSpy();
|
||||||
|
|
||||||
|
service.getAllSettingsFromCookie();
|
||||||
|
expect(cookieService.get).toHaveBeenCalledWith(ACCESSIBILITY_COOKIE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getAllSettingsFromAuthenticatedUserMetadata', () => {
|
||||||
|
it('should retrieve all settings from the user\'s metadata', () => {
|
||||||
|
const settings = { 'liveRegionTimeOut': '1000' };
|
||||||
|
|
||||||
|
const user = new EPerson();
|
||||||
|
user.setMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY, null, JSON.stringify(settings));
|
||||||
|
|
||||||
|
authService.getAuthenticatedUserFromStoreIfAuthenticated =
|
||||||
|
jasmine.createSpy('getAuthenticatedUserFromStoreIfAuthenticated').and.returnValue(of(user));
|
||||||
|
|
||||||
|
service.getAllSettingsFromAuthenticatedUserMetadata().subscribe(value =>
|
||||||
|
expect(value).toEqual(settings)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('set', () => {
|
||||||
|
it('should correctly update the chosen setting', () => {
|
||||||
|
service.updateSettings = jasmine.createSpy('updateSettings');
|
||||||
|
|
||||||
|
service.set('liveRegionTimeOut', '500');
|
||||||
|
expect(service.updateSettings).toHaveBeenCalledWith({ liveRegionTimeOut: '500' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSettings', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.setSettingsInCookie = jasmine.createSpy('setSettingsInCookie').and.returnValue(of('cookie'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should attempt to set settings in metadata', () => {
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of('failed'));
|
||||||
|
|
||||||
|
const settings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setSettings(settings).subscribe();
|
||||||
|
expect(service.setSettingsInAuthenticatedUserMetadata).toHaveBeenCalledWith(settings);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set settings in cookie if metadata failed', () => {
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(false));
|
||||||
|
|
||||||
|
const settings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setSettings(settings).subscribe();
|
||||||
|
expect(service.setSettingsInCookie).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set settings in cookie if metadata succeeded', () => {
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of('metadata'));
|
||||||
|
|
||||||
|
const settings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setSettings(settings).subscribe();
|
||||||
|
expect(service.setSettingsInCookie).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return \'metadata\' if settings are stored in metadata', () => {
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of('metadata'));
|
||||||
|
|
||||||
|
const settings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setSettings(settings).subscribe(value =>
|
||||||
|
expect(value).toEqual('metadata')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return \'cookie\' if settings are stored in cookie', () => {
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata =
|
||||||
|
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(false));
|
||||||
|
|
||||||
|
const settings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.setSettings(settings).subscribe(value =>
|
||||||
|
expect(value).toEqual('cookie')
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('updateSettings', () => {
|
||||||
|
it('should call setSettings with the updated settings', () => {
|
||||||
|
const beforeSettings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.getAll = jasmine.createSpy('getAll').and.returnValue(of(beforeSettings));
|
||||||
|
service.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('cookie'));
|
||||||
|
|
||||||
|
const newSettings: AccessibilitySettings = {
|
||||||
|
liveRegionTimeOut: '2000',
|
||||||
|
};
|
||||||
|
|
||||||
|
const combinedSettings: AccessibilitySettings = {
|
||||||
|
notificationTimeOut: '1000',
|
||||||
|
liveRegionTimeOut: '2000',
|
||||||
|
};
|
||||||
|
|
||||||
|
service.updateSettings(newSettings).subscribe();
|
||||||
|
expect(service.setSettings).toHaveBeenCalledWith(combinedSettings);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSettingsInAuthenticatedUserMetadata', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.setSettingsInMetadata = jasmine.createSpy('setSettingsInMetadata').and.returnValue(of(null));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should store settings in metadata when the user is authenticated', fakeAsync(() => {
|
||||||
|
const user = new EPerson();
|
||||||
|
authService.getAuthenticatedUserFromStoreIfAuthenticated = jasmine.createSpy().and.returnValue(of(user));
|
||||||
|
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata({}).subscribe();
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(service.setSettingsInMetadata).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should emit "failed" when the user is not authenticated', fakeAsync(() => {
|
||||||
|
authService.getAuthenticatedUserFromStoreIfAuthenticated = jasmine.createSpy().and.returnValue(of(null));
|
||||||
|
|
||||||
|
service.setSettingsInAuthenticatedUserMetadata({})
|
||||||
|
.subscribe(value => expect(value).toEqual('failed'));
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(service.setSettingsInMetadata).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSettingsInMetadata', () => {
|
||||||
|
const ePerson = new EPerson();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ePerson.setMetadata = jasmine.createSpy('setMetadata');
|
||||||
|
ePerson.removeMetadata = jasmine.createSpy('removeMetadata');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set the settings in metadata', () => {
|
||||||
|
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe();
|
||||||
|
expect(ePerson.setMetadata).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the metadata when the settings are emtpy', () => {
|
||||||
|
service.setSettingsInMetadata(ePerson, {}).subscribe();
|
||||||
|
expect(ePerson.setMetadata).not.toHaveBeenCalled();
|
||||||
|
expect(ePerson.removeMetadata).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a patch with the metadata changes', () => {
|
||||||
|
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe();
|
||||||
|
expect(ePersonService.createPatchFromCache).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should send the patch request', () => {
|
||||||
|
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe();
|
||||||
|
expect(ePersonService.patch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit "metadata" when the update succeeded', fakeAsync(() => {
|
||||||
|
ePersonService.patch = jasmine.createSpy().and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
|
|
||||||
|
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' })
|
||||||
|
.subscribe(value => {
|
||||||
|
expect(value).toEqual('metadata');
|
||||||
|
});
|
||||||
|
|
||||||
|
flush();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should emit "failed" when the update failed', fakeAsync(() => {
|
||||||
|
ePersonService.patch = jasmine.createSpy().and.returnValue(createFailedRemoteDataObject$());
|
||||||
|
|
||||||
|
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' })
|
||||||
|
.subscribe(value => {
|
||||||
|
expect(value).toEqual('failed');
|
||||||
|
});
|
||||||
|
|
||||||
|
flush();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setSettingsInCookie', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cookieService.set = jasmine.createSpy('set');
|
||||||
|
cookieService.remove = jasmine.createSpy('remove');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fail to store settings in the cookie when the user has not accepted the cookie', fakeAsync(() => {
|
||||||
|
klaroService.getSavedPreferences.and.returnValue(of({ accessibility: false }));
|
||||||
|
|
||||||
|
service.setSettingsInCookie({ ['liveRegionTimeOut']: '500' }).subscribe(value => {
|
||||||
|
expect(value).toEqual('failed');
|
||||||
|
});
|
||||||
|
flush();
|
||||||
|
expect(cookieService.set).not.toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should store the settings in a cookie', fakeAsync(() => {
|
||||||
|
service.setSettingsInCookie({ ['liveRegionTimeOut']: '500' }).subscribe(value => {
|
||||||
|
expect(value).toEqual('cookie');
|
||||||
|
});
|
||||||
|
flush();
|
||||||
|
expect(cookieService.set).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should remove the cookie when the settings are empty', fakeAsync(() => {
|
||||||
|
service.setSettingsInCookie({}).subscribe(value => {
|
||||||
|
expect(value).toEqual('cookie');
|
||||||
|
});
|
||||||
|
|
||||||
|
flush();
|
||||||
|
|
||||||
|
expect(cookieService.set).not.toHaveBeenCalled();
|
||||||
|
expect(cookieService.remove).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('convertFormValuesToStoredValues', () => {
|
||||||
|
it('should reset the notificationTimeOut when timeOut is enabled but set to "0"', () => {
|
||||||
|
const formValues: AccessibilitySettingsFormValues = {
|
||||||
|
notificationTimeOutEnabled: true,
|
||||||
|
notificationTimeOut: '0',
|
||||||
|
liveRegionTimeOut: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const storedValues: FullAccessibilitySettings = service.convertFormValuesToStoredValues(formValues);
|
||||||
|
expect('notificationTimeOut' in storedValues).toBeFalse();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the notificationTimeOut when timeOut is enabled and differs from "0"', () => {
|
||||||
|
const formValues: AccessibilitySettingsFormValues = {
|
||||||
|
notificationTimeOutEnabled: true,
|
||||||
|
notificationTimeOut: '3',
|
||||||
|
liveRegionTimeOut: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const storedValues: FullAccessibilitySettings = service.convertFormValuesToStoredValues(formValues);
|
||||||
|
expect('notificationTimeOut' in storedValues).toBeTrue();
|
||||||
|
});
|
||||||
|
});
|
44
src/app/accessibility/accessibility-settings.service.stub.ts
Normal file
44
src/app/accessibility/accessibility-settings.service.stub.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { of } from 'rxjs';
|
||||||
|
import { AccessibilitySettingsService } from './accessibility-settings.service';
|
||||||
|
|
||||||
|
export function getAccessibilitySettingsServiceStub(): AccessibilitySettingsService {
|
||||||
|
return new AccessibilitySettingsServiceStub() as unknown as AccessibilitySettingsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class AccessibilitySettingsServiceStub {
|
||||||
|
getAllAccessibilitySettingKeys = jasmine.createSpy('getAllAccessibilitySettingKeys').and.returnValue([]);
|
||||||
|
|
||||||
|
get = jasmine.createSpy('get').and.returnValue(of(null));
|
||||||
|
|
||||||
|
getAsNumber = jasmine.createSpy('getAsNumber').and.returnValue(of(0));
|
||||||
|
|
||||||
|
getAll = jasmine.createSpy('getAll').and.returnValue(of({}));
|
||||||
|
|
||||||
|
getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({});
|
||||||
|
|
||||||
|
getAllSettingsFromAuthenticatedUserMetadata = jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata')
|
||||||
|
.and.returnValue(of({}));
|
||||||
|
|
||||||
|
set = jasmine.createSpy('setSettings').and.returnValue(of('cookie'));
|
||||||
|
|
||||||
|
updateSettings = jasmine.createSpy('updateSettings').and.returnValue(of('cookie'));
|
||||||
|
|
||||||
|
setSettingsInAuthenticatedUserMetadata = jasmine.createSpy('setSettingsInAuthenticatedUserMetadata')
|
||||||
|
.and.returnValue(of(false));
|
||||||
|
|
||||||
|
setSettingsInMetadata = jasmine.createSpy('setSettingsInMetadata').and.returnValue(of(false));
|
||||||
|
|
||||||
|
setSettingsInCookie = jasmine.createSpy('setSettingsInCookie');
|
||||||
|
|
||||||
|
getInputType = jasmine.createSpy('getInputType').and.returnValue('text');
|
||||||
|
|
||||||
|
convertFormValuesToStoredValues = jasmine.createSpy('convertFormValuesToStoredValues').and.returnValue({});
|
||||||
|
|
||||||
|
convertStoredValuesToFormValues = jasmine.createSpy('convertStoredValuesToFormValues').and.returnValue({});
|
||||||
|
|
||||||
|
getDefaultValue = jasmine.createSpy('getPlaceholder').and.returnValue('placeholder');
|
||||||
|
|
||||||
|
isValid = jasmine.createSpy('isValid').and.returnValue(true);
|
||||||
|
|
||||||
|
formValuesValid = jasmine.createSpy('allValid').and.returnValue(true);
|
||||||
|
}
|
361
src/app/accessibility/accessibility-settings.service.ts
Normal file
361
src/app/accessibility/accessibility-settings.service.ts
Normal file
@@ -0,0 +1,361 @@
|
|||||||
|
import { Inject, Injectable, Optional } from '@angular/core';
|
||||||
|
import { Observable, of, switchMap, combineLatest } from 'rxjs';
|
||||||
|
import { map, take } from 'rxjs/operators';
|
||||||
|
import { CookieService } from '../core/services/cookie.service';
|
||||||
|
import { hasValue, isNotEmpty, hasNoValue } from '../shared/empty.util';
|
||||||
|
import { AuthService } from '../core/auth/auth.service';
|
||||||
|
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||||
|
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||||
|
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||||
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
|
import { environment } from '../../environments/environment';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||||
|
import { KlaroService } from '../shared/cookies/klaro.service';
|
||||||
|
import { AppConfig, APP_CONFIG } from '../../config/app-config.interface';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the cookie used to store the settings locally
|
||||||
|
*/
|
||||||
|
export const ACCESSIBILITY_COOKIE = 'dsAccessibilityCookie';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Name of the metadata field to store settings on the ePerson
|
||||||
|
*/
|
||||||
|
export const ACCESSIBILITY_SETTINGS_METADATA_KEY = 'dspace.accessibility.settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array containing all possible accessibility settings.
|
||||||
|
* When adding new settings, make sure to add the new setting to the accessibility-settings component form.
|
||||||
|
* The converter methods to convert from stored format to form format (and vice-versa) need to be updated as well.
|
||||||
|
*/
|
||||||
|
export const accessibilitySettingKeys = ['notificationTimeOut', 'liveRegionTimeOut'] as const;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representing the possible accessibility settings
|
||||||
|
*/
|
||||||
|
export type AccessibilitySetting = typeof accessibilitySettingKeys[number];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representing an object that contains accessibility settings values for all accessibility settings.
|
||||||
|
*/
|
||||||
|
export type FullAccessibilitySettings = { [key in AccessibilitySetting]: string };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type representing an object that contains accessibility settings values for some accessibility settings.
|
||||||
|
*/
|
||||||
|
export type AccessibilitySettings = Partial<FullAccessibilitySettings>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The accessibility settings object format used by the accessibility-settings component form.
|
||||||
|
*/
|
||||||
|
export interface AccessibilitySettingsFormValues {
|
||||||
|
notificationTimeOutEnabled: boolean,
|
||||||
|
notificationTimeOut: string,
|
||||||
|
liveRegionTimeOut: string,
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service handling the retrieval and configuration of accessibility settings.
|
||||||
|
*
|
||||||
|
* This service stores the configured settings in either a cookie or on the user's metadata depending on whether
|
||||||
|
* the user is authenticated.
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class AccessibilitySettingsService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected cookieService: CookieService,
|
||||||
|
protected authService: AuthService,
|
||||||
|
protected ePersonService: EPersonDataService,
|
||||||
|
@Optional() protected klaroService: KlaroService,
|
||||||
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stored value for the provided {@link AccessibilitySetting}. If the value does not exist or if it is empty,
|
||||||
|
* the provided defaultValue is emitted instead.
|
||||||
|
*/
|
||||||
|
get(setting: AccessibilitySetting, defaultValue: string = null): Observable<string> {
|
||||||
|
return this.getAll().pipe(
|
||||||
|
map(settings => settings[setting]),
|
||||||
|
map(value => isNotEmpty(value) ? value : defaultValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the stored value for the provided {@link AccessibilitySetting} as a number. If the stored value
|
||||||
|
* could not be converted to a number, the value of the defaultValue parameter is emitted instead.
|
||||||
|
*/
|
||||||
|
getAsNumber(setting: AccessibilitySetting, defaultValue: number = null): Observable<number> {
|
||||||
|
return this.get(setting).pipe(
|
||||||
|
map(value => hasValue(value) ? parseInt(value, 10) : NaN),
|
||||||
|
map(number => !isNaN(number) ? number : defaultValue),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all currently stored accessibility settings
|
||||||
|
*/
|
||||||
|
getAll(): Observable<AccessibilitySettings> {
|
||||||
|
return this.getAllSettingsFromAuthenticatedUserMetadata().pipe(
|
||||||
|
map(value => isNotEmpty(value) ? value : this.getAllSettingsFromCookie()),
|
||||||
|
map(value => isNotEmpty(value) ? value : {}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all settings from the accessibility settings cookie
|
||||||
|
*/
|
||||||
|
getAllSettingsFromCookie(): AccessibilitySettings {
|
||||||
|
return this.cookieService.get(ACCESSIBILITY_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to retrieve all settings from the authenticated user's metadata.
|
||||||
|
* Returns an empty object when no user is authenticated.
|
||||||
|
*/
|
||||||
|
getAllSettingsFromAuthenticatedUserMetadata(): Observable<AccessibilitySettings> {
|
||||||
|
return this.authService.getAuthenticatedUserFromStoreIfAuthenticated().pipe(
|
||||||
|
take(1),
|
||||||
|
map(user => hasValue(user) && hasValue(user.firstMetadataValue(ACCESSIBILITY_SETTINGS_METADATA_KEY)) ?
|
||||||
|
JSON.parse(user.firstMetadataValue(ACCESSIBILITY_SETTINGS_METADATA_KEY)) :
|
||||||
|
{}
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a single accessibility setting value, leaving all other settings unchanged.
|
||||||
|
* When setting all values, {@link AccessibilitySettingsService#setSettings} should be used.
|
||||||
|
* When updating multiple values, {@link AccessibilitySettingsService#updateSettings} should be used.
|
||||||
|
*
|
||||||
|
* Returns 'cookie' when the changes were stored in the cookie.
|
||||||
|
* Returns 'metadata' when the changes were stored in metadata.
|
||||||
|
* Returns 'failed' when both options failed.
|
||||||
|
*/
|
||||||
|
set(setting: AccessibilitySetting, value: string): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||||
|
return this.updateSettings({ [setting]: value });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set all accessibility settings simultaneously.
|
||||||
|
* This method removes existing settings if they are missing from the provided {@link AccessibilitySettings} object.
|
||||||
|
* Removes all settings if the provided object is empty.
|
||||||
|
*
|
||||||
|
* Returns 'cookie' when the changes were stored in the cookie.
|
||||||
|
* Returns 'metadata' when the changes were stored in metadata.
|
||||||
|
* Returns 'failed' when both options failed.
|
||||||
|
*/
|
||||||
|
setSettings(settings: AccessibilitySettings): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||||
|
return this.setSettingsInAuthenticatedUserMetadata(settings).pipe(
|
||||||
|
take(1),
|
||||||
|
map(saveLocation => saveLocation === 'metadata'),
|
||||||
|
switchMap((savedInMetadata) =>
|
||||||
|
savedInMetadata ? ofMetadata() : this.setSettingsInCookie(settings)
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update multiple accessibility settings simultaneously.
|
||||||
|
* This method does not change the settings that are missing from the provided {@link AccessibilitySettings} object.
|
||||||
|
*
|
||||||
|
* Returns 'cookie' when the changes were stored in the cookie.
|
||||||
|
* Returns 'metadata' when the changes were stored in metadata.
|
||||||
|
* Returns 'failed' when both options failed.
|
||||||
|
*/
|
||||||
|
updateSettings(settings: AccessibilitySettings): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||||
|
return this.getAll().pipe(
|
||||||
|
take(1),
|
||||||
|
map(currentSettings => Object.assign({}, currentSettings, settings)),
|
||||||
|
switchMap(newSettings => this.setSettings(newSettings))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to set the provided settings on the currently authorized user's metadata.
|
||||||
|
* Emits false when no user is authenticated or when the metadata update failed.
|
||||||
|
* Emits 'metadata' when the metadata update succeeded, and 'failed' otherwise.
|
||||||
|
*/
|
||||||
|
setSettingsInAuthenticatedUserMetadata(settings: AccessibilitySettings): Observable<'metadata' | 'failed'> {
|
||||||
|
return this.authService.getAuthenticatedUserFromStoreIfAuthenticated().pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap(user => {
|
||||||
|
if (hasValue(user)) {
|
||||||
|
// EPerson has to be cloned, otherwise the EPerson's metadata can't be modified
|
||||||
|
const clonedUser = cloneDeep(user);
|
||||||
|
return this.setSettingsInMetadata(clonedUser, settings);
|
||||||
|
} else {
|
||||||
|
return ofFailed();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to set the provided settings on the user's metadata.
|
||||||
|
* Emits false when the update failed, true when the update succeeded.
|
||||||
|
*/
|
||||||
|
setSettingsInMetadata(
|
||||||
|
user: EPerson,
|
||||||
|
settings: AccessibilitySettings,
|
||||||
|
): Observable<'metadata' | 'failed'> {
|
||||||
|
if (isNotEmpty(settings)) {
|
||||||
|
user.setMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY, null, JSON.stringify(settings));
|
||||||
|
} else {
|
||||||
|
user.removeMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.ePersonService.createPatchFromCache(user).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap(operations =>
|
||||||
|
isNotEmpty(operations) ? this.ePersonService.patch(user, operations) : createSuccessfulRemoteDataObject$({})),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap(rd => rd.hasSucceeded ? ofMetadata() : ofFailed()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempts to set the provided settings in a cookie.
|
||||||
|
* Emits 'failed' when setting in a cookie failed due to the cookie not being accepted, 'cookie' when it succeeded.
|
||||||
|
*/
|
||||||
|
setSettingsInCookie(settings: AccessibilitySettings): Observable<'cookie' | 'failed'> {
|
||||||
|
if (hasNoValue(this.klaroService)) {
|
||||||
|
return of('failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.klaroService.getSavedPreferences().pipe(
|
||||||
|
map(preferences => preferences.accessibility),
|
||||||
|
map((accessibilityCookieAccepted: boolean) => {
|
||||||
|
if (accessibilityCookieAccepted) {
|
||||||
|
if (isNotEmpty(settings)) {
|
||||||
|
this.cookieService.set(ACCESSIBILITY_COOKIE, settings, { expires: this.appConfig.accessibility.cookieExpirationDuration });
|
||||||
|
} else {
|
||||||
|
this.cookieService.remove(ACCESSIBILITY_COOKIE);
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'cookie';
|
||||||
|
} else {
|
||||||
|
return 'failed';
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears all settings in the cookie and attempts to clear settings in metadata.
|
||||||
|
* Emits an array mentioning which settings succeeded or failed.
|
||||||
|
*/
|
||||||
|
clearSettings(): Observable<['cookie' | 'failed', 'metadata' | 'failed']> {
|
||||||
|
return combineLatest([
|
||||||
|
this.setSettingsInCookie({}),
|
||||||
|
this.setSettingsInAuthenticatedUserMetadata({}),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the default value to be used for the provided AccessibilitySetting.
|
||||||
|
* Returns an empty string when no default value is specified for the provided setting.
|
||||||
|
*/
|
||||||
|
getDefaultValue(setting: AccessibilitySetting): string {
|
||||||
|
switch (setting) {
|
||||||
|
case 'notificationTimeOut':
|
||||||
|
return millisecondsToSeconds(environment.notifications.timeOut.toString());
|
||||||
|
case 'liveRegionTimeOut':
|
||||||
|
return millisecondsToSeconds(environment.liveRegion.messageTimeOutDurationMs.toString());
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert values in the provided accessibility settings object to values ready to be stored.
|
||||||
|
*/
|
||||||
|
convertFormValuesToStoredValues(settings: AccessibilitySettingsFormValues): FullAccessibilitySettings {
|
||||||
|
const storedValues = {
|
||||||
|
notificationTimeOut: settings.notificationTimeOutEnabled ?
|
||||||
|
secondsToMilliseconds(settings.notificationTimeOut) : '0',
|
||||||
|
liveRegionTimeOut: secondsToMilliseconds(settings.liveRegionTimeOut),
|
||||||
|
};
|
||||||
|
|
||||||
|
// When the user enables the timeout but does not change the timeout duration from 0,
|
||||||
|
// it is removed from the values to be stored so the default value is used.
|
||||||
|
// Keeping it at 0 would mean the notifications are not automatically removed.
|
||||||
|
if (settings.notificationTimeOutEnabled && settings.notificationTimeOut === '0') {
|
||||||
|
delete storedValues.notificationTimeOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
return storedValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert values in the provided accessibility settings object to values ready to show in the form.
|
||||||
|
*/
|
||||||
|
convertStoredValuesToFormValues(settings: AccessibilitySettings): AccessibilitySettingsFormValues {
|
||||||
|
return {
|
||||||
|
notificationTimeOutEnabled: parseFloat(settings.notificationTimeOut) !== 0,
|
||||||
|
notificationTimeOut: millisecondsToSeconds(settings.notificationTimeOut),
|
||||||
|
liveRegionTimeOut: millisecondsToSeconds(settings.liveRegionTimeOut),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the provided AccessibilitySetting is valid in regard to the provided formValues.
|
||||||
|
*/
|
||||||
|
isValid(setting: AccessibilitySetting, formValues: AccessibilitySettingsFormValues): boolean {
|
||||||
|
switch (setting) {
|
||||||
|
case 'notificationTimeOut':
|
||||||
|
return formValues.notificationTimeOutEnabled ?
|
||||||
|
hasNoValue(formValues.notificationTimeOut) || parseFloat(formValues.notificationTimeOut) > 0 :
|
||||||
|
true;
|
||||||
|
case 'liveRegionTimeOut':
|
||||||
|
return hasNoValue(formValues.liveRegionTimeOut) || parseFloat(formValues.liveRegionTimeOut) > 0;
|
||||||
|
default:
|
||||||
|
throw new Error(`Unhandled accessibility setting during validity check: ${setting}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if all settings in the provided AccessibilitySettingsFormValues object are valid
|
||||||
|
*/
|
||||||
|
formValuesValid(formValues: AccessibilitySettingsFormValues) {
|
||||||
|
return accessibilitySettingKeys.every(setting => this.isValid(setting, formValues));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string representing seconds to a string representing milliseconds
|
||||||
|
* Returns null if the input could not be parsed to a float
|
||||||
|
*/
|
||||||
|
function secondsToMilliseconds(secondsStr: string): string {
|
||||||
|
const seconds = parseFloat(secondsStr);
|
||||||
|
if (isNaN(seconds)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (seconds * 1000).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a string representing milliseconds to a string representing seconds
|
||||||
|
* Returns null if the input could not be parsed to a float
|
||||||
|
*/
|
||||||
|
function millisecondsToSeconds(millisecondsStr: string): string {
|
||||||
|
const milliseconds = parseFloat(millisecondsStr);
|
||||||
|
if (isNaN(milliseconds)) {
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return (milliseconds / 1000).toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ofMetadata(): Observable<'metadata'> {
|
||||||
|
return of('metadata');
|
||||||
|
}
|
||||||
|
|
||||||
|
function ofFailed(): Observable<'failed'> {
|
||||||
|
return of('failed');
|
||||||
|
}
|
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
|
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
*ngIf="(bitstreamFormats$ | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="pageConfig"
|
[paginationOptions]="pageConfig"
|
||||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
[collectionSize]="(bitstreamFormats$ | async)?.payload?.totalElements"
|
||||||
[hideGear]="false"
|
[hideGear]="false"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
@@ -26,12 +26,12 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
<tr *ngFor="let bitstreamFormat of (bitstreamFormats$ | async)?.payload?.page">
|
||||||
<td>
|
<td>
|
||||||
<label class="mb-0">
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
[attr.aria-label]="'admin.registries.bitstream-formats.select' | translate"
|
||||||
[checked]="isSelected(bitstreamFormat) | async"
|
[checked]="(selectedBitstreamFormatIDs$ | async)?.includes(bitstreamFormat.id)"
|
||||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
||||||
@@ -46,13 +46,13 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</ds-pagination>
|
</ds-pagination>
|
||||||
<div *ngIf="(bitstreamFormats | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
<div *ngIf="(bitstreamFormats$ | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
||||||
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
|
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" class="btn btn-primary deselect" (click)="deselectAll()">{{'admin.registries.bitstream-formats.table.deselect-all' | translate}}</button>
|
||||||
<button *ngIf="(bitstreamFormats | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
<button *ngIf="(bitstreamFormats$ | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -16,8 +16,7 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications-
|
|||||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level';
|
import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level';
|
||||||
import { XSRFService } from '../../../core/xsrf/xsrf.service';
|
import { XSRFService } from '../../../core/xsrf/xsrf.service';
|
||||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
import { hot } from 'jasmine-marbles';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
|
||||||
import {
|
import {
|
||||||
createNoContentRemoteDataObject$,
|
createNoContentRemoteDataObject$,
|
||||||
createSuccessfulRemoteDataObject,
|
createSuccessfulRemoteDataObject,
|
||||||
@@ -32,7 +31,6 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
let comp: BitstreamFormatsComponent;
|
let comp: BitstreamFormatsComponent;
|
||||||
let fixture: ComponentFixture<BitstreamFormatsComponent>;
|
let fixture: ComponentFixture<BitstreamFormatsComponent>;
|
||||||
let bitstreamFormatService;
|
let bitstreamFormatService;
|
||||||
let scheduler: TestScheduler;
|
|
||||||
let notificationsServiceStub;
|
let notificationsServiceStub;
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
@@ -87,8 +85,6 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
const initAsync = () => {
|
const initAsync = () => {
|
||||||
notificationsServiceStub = new NotificationsServiceStub();
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
scheduler = getTestScheduler();
|
|
||||||
|
|
||||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||||
findAll: observableOf(mockFormatsRD),
|
findAll: observableOf(mockFormatsRD),
|
||||||
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
||||||
@@ -180,17 +176,17 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
beforeEach(waitForAsync(initAsync));
|
beforeEach(waitForAsync(initAsync));
|
||||||
beforeEach(initBeforeEach);
|
beforeEach(initBeforeEach);
|
||||||
it('should return an observable of true if the provided bistream is in the list returned by the service', () => {
|
it('should return an observable of true if the provided bistream is in the list returned by the service', () => {
|
||||||
const result = comp.isSelected(bitstreamFormat1);
|
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||||
|
expect(selectedBitstreamFormatIDs).toContain(bitstreamFormat1.id);
|
||||||
expect(result).toBeObservable(cold('b', { b: true }));
|
});
|
||||||
});
|
});
|
||||||
it('should return an observable of false if the provided bistream is not in the list returned by the service', () => {
|
it('should return an observable of false if the provided bistream is not in the list returned by the service', () => {
|
||||||
const format = new BitstreamFormat();
|
const format = new BitstreamFormat();
|
||||||
format.uuid = 'new';
|
format.uuid = 'new';
|
||||||
|
|
||||||
const result = comp.isSelected(format);
|
comp.selectedBitstreamFormatIDs().subscribe((selectedBitstreamFormatIDs: string[]) => {
|
||||||
|
expect(selectedBitstreamFormatIDs).not.toContain(format.id);
|
||||||
expect(result).toBeObservable(cold('b', { b: false }));
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -216,8 +212,6 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
notificationsServiceStub = new NotificationsServiceStub();
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
scheduler = getTestScheduler();
|
|
||||||
|
|
||||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||||
findAll: observableOf(mockFormatsRD),
|
findAll: observableOf(mockFormatsRD),
|
||||||
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
||||||
@@ -265,8 +259,6 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
notificationsServiceStub = new NotificationsServiceStub();
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
scheduler = getTestScheduler();
|
|
||||||
|
|
||||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||||
findAll: observableOf(mockFormatsRD),
|
findAll: observableOf(mockFormatsRD),
|
||||||
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable} from 'rxjs';
|
import { Observable} from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
@@ -7,7 +7,6 @@ import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
|||||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
|
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
@@ -26,7 +25,12 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* A paginated list of bitstream formats to be shown on the page
|
* A paginated list of bitstream formats to be shown on the page
|
||||||
*/
|
*/
|
||||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
bitstreamFormats$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The currently selected {@link BitstreamFormat} IDs
|
||||||
|
*/
|
||||||
|
selectedBitstreamFormatIDs$: Observable<string[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current pagination configuration for the page
|
* The current pagination configuration for the page
|
||||||
@@ -39,7 +43,6 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
|
|
||||||
constructor(private notificationsService: NotificationsService,
|
constructor(private notificationsService: NotificationsService,
|
||||||
private router: Router,
|
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private bitstreamFormatService: BitstreamFormatDataService,
|
private bitstreamFormatService: BitstreamFormatDataService,
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
@@ -94,14 +97,11 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks whether a given bitstream format is selected in the list (checkbox)
|
* Returns the list of all the bitstream formats that are selected in the list (checkbox)
|
||||||
* @param bitstreamFormat
|
|
||||||
*/
|
*/
|
||||||
isSelected(bitstreamFormat: BitstreamFormat): Observable<boolean> {
|
selectedBitstreamFormatIDs(): Observable<string[]> {
|
||||||
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||||
map((bitstreamFormats: BitstreamFormat[]) => {
|
map((bitstreamFormats: BitstreamFormat[]) => bitstreamFormats.map((selectedFormat) => selectedFormat.id)),
|
||||||
return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null;
|
|
||||||
})
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,27 +125,23 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
const prefix = 'admin.registries.bitstream-formats.delete';
|
const prefix = 'admin.registries.bitstream-formats.delete';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
|
|
||||||
const messages = observableCombineLatest(
|
const head: string = this.translateService.instant(`${prefix}.${suffix}.head`);
|
||||||
this.translateService.get(`${prefix}.${suffix}.head`),
|
const content: string = this.translateService.instant(`${prefix}.${suffix}.amount`, { amount: amount });
|
||||||
this.translateService.get(`${prefix}.${suffix}.amount`, {amount: amount})
|
|
||||||
);
|
|
||||||
messages.subscribe(([head, content]) => {
|
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notificationsService.success(head, content);
|
this.notificationsService.success(head, content);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(head, content);
|
this.notificationsService.error(head, content);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.bitstreamFormats$ = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
||||||
this.bitstreamFormats = this.paginationService.getFindListOptions(this.pageConfig.id, this.pageConfig).pipe(
|
|
||||||
switchMap((findListOptions: FindListOptions) => {
|
switchMap((findListOptions: FindListOptions) => {
|
||||||
return this.bitstreamFormatService.findAll(findListOptions);
|
return this.bitstreamFormatService.findAll(findListOptions);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
this.selectedBitstreamFormatIDs$ = this.selectedBitstreamFormatIDs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@@ -27,14 +27,15 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
<tr *ngFor="let schema of (metadataSchemas | async)?.payload?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(schema) | async}">
|
[ngClass]="{'table-primary' : (activeMetadataSchema$ | async)?.id === schema.id}">
|
||||||
<td>
|
<td>
|
||||||
<label class="mb-0">
|
<label class="mb-0">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[checked]="isSelected(schema) | async"
|
[checked]="(selectedMetadataSchemaIDs$ | async)?.includes(schema.id)"
|
||||||
(change)="selectMetadataSchema(schema, $event)"
|
(change)="selectMetadataSchema(schema, $event)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{((isSelected(schema) | async) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
|
<span class="sr-only">{{(((selectedMetadataSchemaIDs$ | async)?.includes(schema.id)) ? 'admin.registries.metadata.schemas.deselect' : 'admin.registries.metadata.schemas.select') | translate}}</span>
|
||||||
|
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
<td class="selectable-row" (click)="editSchema(schema)"><a [routerLink]="[schema.prefix]">{{schema.id}}</a></td>
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { MetadataRegistryComponent } from './metadata-registry.component';
|
import { MetadataRegistryComponent } from './metadata-registry.component';
|
||||||
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
@@ -15,18 +14,21 @@ import { HostWindowService } from '../../../shared/host-window.service';
|
|||||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
|
||||||
describe('MetadataRegistryComponent', () => {
|
describe('MetadataRegistryComponent', () => {
|
||||||
let comp: MetadataRegistryComponent;
|
let comp: MetadataRegistryComponent;
|
||||||
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
let fixture: ComponentFixture<MetadataRegistryComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
let paginationService;
|
let paginationService: PaginationServiceStub;
|
||||||
const mockSchemasList = [
|
let registryService: RegistryServiceStub;
|
||||||
|
|
||||||
|
const mockSchemasList: MetadataSchema[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -47,32 +49,18 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
prefix: 'mock',
|
prefix: 'mock',
|
||||||
namespace: 'http://dspace.org/mockschema'
|
namespace: 'http://dspace.org/mockschema'
|
||||||
}
|
}
|
||||||
];
|
] as MetadataSchema[];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getMetadataSchemas: () => mockSchemas,
|
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
|
||||||
getSelectedMetadataSchemas: () => observableOf([]),
|
|
||||||
editMetadataSchema: (schema) => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
deleteMetadataSchema: () => observableOf(new RestResponse(true, 200, 'OK')),
|
|
||||||
deselectAllMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
paginationService = new PaginationServiceStub();
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
spyOn(registryService, 'getMetadataSchemas').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList(mockSchemasList)));
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [MetadataRegistryComponent, PaginationComponent, EnumKeysPipe],
|
declarations: [MetadataRegistryComponent, PaginationComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||||
@@ -123,7 +111,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should cancel editing the selected schema when clicked again', waitForAsync(() => {
|
it('should cancel editing the selected schema when clicked again', waitForAsync(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema));
|
comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema);
|
||||||
spyOn(registryService, 'cancelEditMetadataSchema');
|
spyOn(registryService, 'cancelEditMetadataSchema');
|
||||||
row.click();
|
row.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -138,7 +126,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
||||||
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[]));
|
comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id));
|
||||||
comp.deleteSchemas();
|
comp.deleteSchemas();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,11 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
|
import { BehaviorSubject, Observable, zip, Subscription } from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||||
@@ -24,13 +22,23 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
|||||||
* A component used for managing all existing metadata schemas within the repository.
|
* A component used for managing all existing metadata schemas within the repository.
|
||||||
* The admin can create, edit or delete metadata schemas here.
|
* The admin can create, edit or delete metadata schemas here.
|
||||||
*/
|
*/
|
||||||
export class MetadataRegistryComponent {
|
export class MetadataRegistryComponent implements OnDestroy, OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all the current metadata schemas within the repository
|
* A list of all the current metadata schemas within the repository
|
||||||
*/
|
*/
|
||||||
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
|
metadataSchemas: Observable<RemoteData<PaginatedList<MetadataSchema>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link MetadataSchema}that is being edited
|
||||||
|
*/
|
||||||
|
activeMetadataSchema$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected {@link MetadataSchema} IDs
|
||||||
|
*/
|
||||||
|
selectedMetadataSchemaIDs$: Observable<number[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Pagination config used to display the list of metadata schemas
|
* Pagination config used to display the list of metadata schemas
|
||||||
*/
|
*/
|
||||||
@@ -40,15 +48,25 @@ export class MetadataRegistryComponent {
|
|||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not the list of MetadataSchemas needs an update
|
* Whether the list of MetadataSchemas needs an update
|
||||||
*/
|
*/
|
||||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
subscriptions: Subscription[] = [];
|
||||||
private notificationsService: NotificationsService,
|
|
||||||
private router: Router,
|
constructor(
|
||||||
private paginationService: PaginationService,
|
protected registryService: RegistryService,
|
||||||
private translateService: TranslateService) {
|
protected notificationsService: NotificationsService,
|
||||||
|
protected paginationService: PaginationService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema();
|
||||||
|
this.selectedMetadataSchemaIDs$ = this.registryService.getSelectedMetadataSchemas().pipe(
|
||||||
|
map((schemas: MetadataSchema[]) => schemas.map((schema: MetadataSchema) => schema.id)),
|
||||||
|
);
|
||||||
this.updateSchemas();
|
this.updateSchemas();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,30 +95,13 @@ export class MetadataRegistryComponent {
|
|||||||
* @param schema
|
* @param schema
|
||||||
*/
|
*/
|
||||||
editSchema(schema: MetadataSchema) {
|
editSchema(schema: MetadataSchema) {
|
||||||
this.getActiveSchema().pipe(take(1)).subscribe((activeSchema) => {
|
this.subscriptions.push(this.activeMetadataSchema$.pipe(take(1)).subscribe((activeSchema: MetadataSchema) => {
|
||||||
if (schema === activeSchema) {
|
if (schema === activeSchema) {
|
||||||
this.registryService.cancelEditMetadataSchema();
|
this.registryService.cancelEditMetadataSchema();
|
||||||
} else {
|
} else {
|
||||||
this.registryService.editMetadataSchema(schema);
|
this.registryService.editMetadataSchema(schema);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given metadata schema is active (being edited)
|
|
||||||
* @param schema
|
|
||||||
*/
|
|
||||||
isActive(schema: MetadataSchema): Observable<boolean> {
|
|
||||||
return this.getActiveSchema().pipe(
|
|
||||||
map((activeSchema) => schema === activeSchema)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the active metadata schema (being edited)
|
|
||||||
*/
|
|
||||||
getActiveSchema(): Observable<MetadataSchema> {
|
|
||||||
return this.registryService.getActiveMetadataSchema();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -114,31 +115,16 @@ export class MetadataRegistryComponent {
|
|||||||
this.registryService.deselectMetadataSchema(schema);
|
this.registryService.deselectMetadataSchema(schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given metadata schema is selected in the list (checkbox)
|
|
||||||
* @param schema
|
|
||||||
*/
|
|
||||||
isSelected(schema: MetadataSchema): Observable<boolean> {
|
|
||||||
return this.registryService.getSelectedMetadataSchemas().pipe(
|
|
||||||
map((schemas) => schemas.find((selectedSchema) => selectedSchema === schema) != null)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all the selected metadata schemas
|
* Delete all the selected metadata schemas
|
||||||
*/
|
*/
|
||||||
deleteSchemas() {
|
deleteSchemas() {
|
||||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
this.subscriptions.push(this.selectedMetadataSchemaIDs$.pipe(
|
||||||
(schemas) => {
|
take(1),
|
||||||
const tasks$ = [];
|
switchMap((schemaIDs: number[]) => zip(schemaIDs.map((schemaID: number) => this.registryService.deleteMetadataSchema(schemaID).pipe(getFirstCompletedRemoteData())))),
|
||||||
for (const schema of schemas) {
|
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||||
if (hasValue(schema.id)) {
|
const successResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||||
tasks$.push(this.registryService.deleteMetadataSchema(schema.id).pipe(getFirstCompletedRemoteData()));
|
const failedResponses: RemoteData<NoContent>[] = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
}
|
|
||||||
}
|
|
||||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
|
||||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
|
||||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
|
||||||
if (successResponses.length > 0) {
|
if (successResponses.length > 0) {
|
||||||
this.showNotification(true, successResponses.length);
|
this.showNotification(true, successResponses.length);
|
||||||
}
|
}
|
||||||
@@ -147,9 +133,7 @@ export class MetadataRegistryComponent {
|
|||||||
}
|
}
|
||||||
this.registryService.deselectAllMetadataSchema();
|
this.registryService.deselectAllMetadataSchema();
|
||||||
this.registryService.cancelEditMetadataSchema();
|
this.registryService.cancelEditMetadataSchema();
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,20 +144,20 @@ export class MetadataRegistryComponent {
|
|||||||
showNotification(success: boolean, amount: number) {
|
showNotification(success: boolean, amount: number) {
|
||||||
const prefix = 'admin.registries.schema.notification';
|
const prefix = 'admin.registries.schema.notification';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
const messages = observableCombineLatest(
|
|
||||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
const head: string = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||||
this.translateService.get(`${prefix}.deleted.${suffix}`, {amount: amount})
|
const content: string = this.translateService.instant(`${prefix}.deleted.${suffix}`, {amount: amount});
|
||||||
);
|
|
||||||
messages.subscribe(([head, content]) => {
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notificationsService.success(head, content);
|
this.notificationsService.success(head, content);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(head, content);
|
this.notificationsService.error(head, content);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config.id);
|
||||||
|
this.subscriptions.map((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div *ngIf="registryService.getActiveMetadataSchema() | async; then editheader; else createHeader"></div>
|
<div *ngIf="activeMetadataSchema$ | async; then editheader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
import { MetadataSchemaFormComponent } from './metadata-schema-form.component';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
@@ -10,41 +10,26 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
|||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
|
import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub';
|
||||||
|
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||||
|
|
||||||
describe('MetadataSchemaFormComponent', () => {
|
describe('MetadataSchemaFormComponent', () => {
|
||||||
let component: MetadataSchemaFormComponent;
|
let component: MetadataSchemaFormComponent;
|
||||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
let registryService: RegistryServiceStub;
|
||||||
const registryServiceStub = {
|
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
|
||||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
|
||||||
};
|
|
||||||
const formBuilderServiceStub = {
|
|
||||||
createFormGroup: () => {
|
|
||||||
return {
|
|
||||||
patchValue: () => {
|
|
||||||
},
|
|
||||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [MetadataSchemaFormComponent, EnumKeysPipe],
|
declarations: [MetadataSchemaFormComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: FormBuilderService, useValue: formBuilderServiceStub }
|
{ provide: FormBuilderService, useValue: getMockFormBuilderService() }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -75,7 +60,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
|
|
||||||
describe('without an active schema', () => {
|
describe('without an active schema', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(undefined));
|
component.activeMetadataSchema$ = observableOf(undefined);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -94,7 +79,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
} as MetadataSchema);
|
} as MetadataSchema);
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(expectedWithId));
|
component.activeMetadataSchema$ = observableOf(expectedWithId);
|
||||||
component.onSubmit();
|
component.onSubmit();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -8,10 +8,10 @@ import {
|
|||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { switchMap, take, tap } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable, combineLatest } from 'rxjs';
|
|
||||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
|
import { Subscription, Observable } from 'rxjs';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-metadata-schema-form',
|
selector: 'ds-metadata-schema-form',
|
||||||
@@ -73,17 +73,24 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
constructor(public registryService: RegistryService, private formBuilderService: FormBuilderService, private translateService: TranslateService) {
|
/**
|
||||||
|
* The {@link MetadataSchema} that is currently being edited
|
||||||
|
*/
|
||||||
|
activeMetadataSchema$: Observable<MetadataSchema>;
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected registryService: RegistryService,
|
||||||
|
protected formBuilderService: FormBuilderService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
combineLatest([
|
|
||||||
this.translateService.get(`${this.messagePrefix}.name`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.namespace`)
|
|
||||||
]).subscribe(([name, namespace]) => {
|
|
||||||
this.name = new DynamicInputModel({
|
this.name = new DynamicInputModel({
|
||||||
id: 'name',
|
id: 'name',
|
||||||
label: name,
|
label: this.translateService.instant(`${this.messagePrefix}.name`),
|
||||||
name: 'name',
|
name: 'name',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
@@ -98,7 +105,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.namespace = new DynamicInputModel({
|
this.namespace = new DynamicInputModel({
|
||||||
id: 'namespace',
|
id: 'namespace',
|
||||||
label: namespace,
|
label: this.translateService.instant(`${this.messagePrefix}.namespace`),
|
||||||
name: 'namespace',
|
name: 'namespace',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
@@ -117,7 +124,8 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
this.registryService.getActiveMetadataSchema().subscribe((schema: MetadataSchema) => {
|
this.activeMetadataSchema$ = this.registryService.getActiveMetadataSchema();
|
||||||
|
this.subscriptions.push(this.activeMetadataSchema$.subscribe((schema: MetadataSchema) => {
|
||||||
if (schema == null) {
|
if (schema == null) {
|
||||||
this.clearFields();
|
this.clearFields();
|
||||||
} else {
|
} else {
|
||||||
@@ -129,8 +137,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.name.disabled = true;
|
this.name.disabled = true;
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -147,44 +154,25 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
* Emit the updated/created schema using the EventEmitter submitForm
|
* Emit the updated/created schema using the EventEmitter submitForm
|
||||||
*/
|
*/
|
||||||
onSubmit(): void {
|
onSubmit(): void {
|
||||||
this.registryService
|
this.activeMetadataSchema$.pipe(
|
||||||
.getActiveMetadataSchema()
|
|
||||||
.pipe(
|
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((schema: MetadataSchema) => {
|
switchMap((schema: MetadataSchema) => {
|
||||||
const metadataValues = {
|
const metadataValues = {
|
||||||
prefix: this.name.value,
|
prefix: this.name.value,
|
||||||
namespace: this.namespace.value,
|
namespace: this.namespace.value,
|
||||||
};
|
};
|
||||||
|
|
||||||
let createOrUpdate$: Observable<MetadataSchema>;
|
|
||||||
|
|
||||||
if (schema == null) {
|
if (schema == null) {
|
||||||
createOrUpdate$ =
|
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), metadataValues));
|
||||||
this.registryService.createOrUpdateMetadataSchema(
|
|
||||||
Object.assign(new MetadataSchema(), metadataValues)
|
|
||||||
);
|
|
||||||
} else {
|
} else {
|
||||||
const updatedSchema = Object.assign(
|
return this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||||
new MetadataSchema(),
|
|
||||||
schema,
|
|
||||||
{
|
|
||||||
namespace: metadataValues.namespace,
|
namespace: metadataValues.namespace,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
);
|
|
||||||
createOrUpdate$ =
|
|
||||||
this.registryService.createOrUpdateMetadataSchema(
|
|
||||||
updatedSchema
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return createOrUpdate$;
|
|
||||||
}),
|
}),
|
||||||
tap(() => {
|
switchMap((updatedOrCreatedSchema: MetadataSchema) => this.registryService.clearMetadataSchemaRequests().pipe(
|
||||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
map(() => updatedOrCreatedSchema),
|
||||||
})
|
)),
|
||||||
)
|
).subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
||||||
.subscribe((updatedOrCreatedSchema: MetadataSchema) => {
|
|
||||||
this.submitForm.emit(updatedOrCreatedSchema);
|
this.submitForm.emit(updatedOrCreatedSchema);
|
||||||
this.clearFields();
|
this.clearFields();
|
||||||
this.registryService.cancelEditMetadataSchema();
|
this.registryService.cancelEditMetadataSchema();
|
||||||
@@ -204,5 +192,6 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.onCancel();
|
this.onCancel();
|
||||||
|
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<div *ngIf="registryService.getActiveMetadataField() | async; then editheader; else createHeader"></div>
|
<div *ngIf="activeMetadataField$ | async; then editheader; else createHeader"></div>
|
||||||
|
|
||||||
<ng-template #createHeader>
|
<ng-template #createHeader>
|
||||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||||
|
@@ -9,14 +9,17 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../../../shared/utils/enum-keys-pipe';
|
||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
||||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
|
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||||
|
import { RegistryServiceStub } from '../../../../shared/testing/registry.service.stub';
|
||||||
|
|
||||||
describe('MetadataFieldFormComponent', () => {
|
describe('MetadataFieldFormComponent', () => {
|
||||||
let component: MetadataFieldFormComponent;
|
let component: MetadataFieldFormComponent;
|
||||||
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
let fixture: ComponentFixture<MetadataFieldFormComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
|
let registryService: RegistryServiceStub;
|
||||||
|
|
||||||
const metadataSchema = Object.assign(new MetadataSchema(), {
|
const metadataSchema = Object.assign(new MetadataSchema(), {
|
||||||
id: 1,
|
id: 1,
|
||||||
@@ -24,38 +27,17 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
prefix: 'fake'
|
prefix: 'fake'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
|
||||||
createMetadataField: (field: MetadataField) => observableOf(field),
|
|
||||||
updateMetadataField: (field: MetadataField) => observableOf(field),
|
|
||||||
cancelEditMetadataField: () => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataSchema: () => {
|
|
||||||
},
|
|
||||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
|
||||||
};
|
|
||||||
const formBuilderServiceStub = {
|
|
||||||
createFormGroup: () => {
|
|
||||||
return {
|
|
||||||
patchValue: () => {
|
|
||||||
},
|
|
||||||
reset(_value?: any, _options?: { onlySelf?: boolean; emitEvent?: boolean; }): void {
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
|
||||||
return TestBed.configureTestingModule({
|
return TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [MetadataFieldFormComponent, EnumKeysPipe],
|
declarations: [MetadataFieldFormComponent, EnumKeysPipe],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: FormBuilderService, useValue: formBuilderServiceStub }
|
{ provide: FormBuilderService, useValue: getMockFormBuilderService() }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -11,7 +11,7 @@ import { RegistryService } from '../../../../core/registry/registry.service';
|
|||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
import { take } from 'rxjs/operators';
|
import { take } from 'rxjs/operators';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { combineLatest } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
||||||
|
|
||||||
@@ -90,6 +90,8 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||||
|
|
||||||
|
activeMetadataField$: Observable<MetadataField>;
|
||||||
|
|
||||||
constructor(public registryService: RegistryService,
|
constructor(public registryService: RegistryService,
|
||||||
private formBuilderService: FormBuilderService,
|
private formBuilderService: FormBuilderService,
|
||||||
private translateService: TranslateService) {
|
private translateService: TranslateService) {
|
||||||
@@ -99,14 +101,9 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
* Initialize the component, setting up the necessary Models for the dynamic form
|
* Initialize the component, setting up the necessary Models for the dynamic form
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
combineLatest([
|
|
||||||
this.translateService.get(`${this.messagePrefix}.element`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.qualifier`),
|
|
||||||
this.translateService.get(`${this.messagePrefix}.scopenote`)
|
|
||||||
]).subscribe(([element, qualifier, scopenote]) => {
|
|
||||||
this.element = new DynamicInputModel({
|
this.element = new DynamicInputModel({
|
||||||
id: 'element',
|
id: 'element',
|
||||||
label: element,
|
label: this.translateService.instant(`${this.messagePrefix}.element`),
|
||||||
name: 'element',
|
name: 'element',
|
||||||
validators: {
|
validators: {
|
||||||
required: null,
|
required: null,
|
||||||
@@ -121,7 +118,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.qualifier = new DynamicInputModel({
|
this.qualifier = new DynamicInputModel({
|
||||||
id: 'qualifier',
|
id: 'qualifier',
|
||||||
label: qualifier,
|
label: this.translateService.instant(`${this.messagePrefix}.qualifier`),
|
||||||
name: 'qualifier',
|
name: 'qualifier',
|
||||||
validators: {
|
validators: {
|
||||||
pattern: '^[^. ,]*$',
|
pattern: '^[^. ,]*$',
|
||||||
@@ -135,7 +132,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
this.scopeNote = new DynamicTextAreaModel({
|
this.scopeNote = new DynamicTextAreaModel({
|
||||||
id: 'scopeNote',
|
id: 'scopeNote',
|
||||||
label: scopenote,
|
label: this.translateService.instant(`${this.messagePrefix}.scopenote`),
|
||||||
name: 'scopeNote',
|
name: 'scopeNote',
|
||||||
required: false,
|
required: false,
|
||||||
rows: 5,
|
rows: 5,
|
||||||
@@ -163,7 +160,6 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
|||||||
this.qualifier.disabled = true;
|
this.qualifier.disabled = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -31,8 +31,8 @@
|
|||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let field of fields?.page"
|
<tr *ngFor="let field of fields?.page"
|
||||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
[ngClass]="{'table-primary' : (activeField$ | async)?.id === field.id}">
|
||||||
<td *ngVar="(isSelected(field) | async) as selected">
|
<td *ngVar="(selectedMetadataFieldIDs$ | async)?.includes(field.id) as selected">
|
||||||
<input type="checkbox"
|
<input type="checkbox"
|
||||||
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
[attr.aria-label]="(selected ? 'admin.registries.schema.fields.deselect' : 'admin.registries.schema.fields.select') | translate"
|
||||||
[checked]="selected"
|
[checked]="selected"
|
||||||
|
@@ -4,7 +4,7 @@ import { of as observableOf } from 'rxjs';
|
|||||||
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { RegistryService } from '../../../core/registry/registry.service';
|
import { RegistryService } from '../../../core/registry/registry.service';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
@@ -12,25 +12,28 @@ import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
|||||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||||
import { HostWindowService } from '../../../shared/host-window.service';
|
import { HostWindowService } from '../../../shared/host-window.service';
|
||||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
import { RegistryServiceStub } from '../../../shared/testing/registry.service.stub';
|
||||||
|
|
||||||
describe('MetadataSchemaComponent', () => {
|
describe('MetadataSchemaComponent', () => {
|
||||||
let comp: MetadataSchemaComponent;
|
let comp: MetadataSchemaComponent;
|
||||||
let fixture: ComponentFixture<MetadataSchemaComponent>;
|
let fixture: ComponentFixture<MetadataSchemaComponent>;
|
||||||
let registryService: RegistryService;
|
|
||||||
const mockSchemasList = [
|
let registryService: RegistryServiceStub;
|
||||||
|
let activatedRoute: ActivatedRouteStub;
|
||||||
|
let paginationService: PaginationServiceStub;
|
||||||
|
|
||||||
|
const mockSchemasList: MetadataSchema[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -51,8 +54,8 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
prefix: 'mock',
|
prefix: 'mock',
|
||||||
namespace: 'http://dspace.org/mockschema'
|
namespace: 'http://dspace.org/mockschema'
|
||||||
}
|
}
|
||||||
];
|
] as MetadataSchema[];
|
||||||
const mockFieldsList = [
|
const mockFieldsList: MetadataField[] = [
|
||||||
{
|
{
|
||||||
id: 1,
|
id: 1,
|
||||||
_links: {
|
_links: {
|
||||||
@@ -101,47 +104,29 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
scopeNote: null,
|
scopeNote: null,
|
||||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1])
|
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1])
|
||||||
}
|
}
|
||||||
];
|
] as MetadataField[];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
|
||||||
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
|
||||||
const registryServiceStub = {
|
|
||||||
getMetadataSchemas: () => mockSchemas,
|
|
||||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
|
||||||
getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
|
||||||
getSelectedMetadataFields: () => observableOf([]),
|
|
||||||
editMetadataField: (schema) => {
|
|
||||||
},
|
|
||||||
cancelEditMetadataField: () => {
|
|
||||||
},
|
|
||||||
deleteMetadataField: () => observableOf(new RestResponse(true, 200, 'OK')),
|
|
||||||
deselectAllMetadataField: () => {
|
|
||||||
},
|
|
||||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
|
||||||
};
|
|
||||||
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
|
||||||
const schemaNameParam = 'mock';
|
const schemaNameParam = 'mock';
|
||||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
|
||||||
params: observableOf({
|
|
||||||
schemaName: schemaNameParam
|
|
||||||
})
|
|
||||||
});
|
|
||||||
|
|
||||||
const paginationService = new PaginationServiceStub();
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
activatedRoute = new ActivatedRouteStub({
|
||||||
|
schemaName: schemaNameParam,
|
||||||
|
});
|
||||||
|
paginationService = new PaginationServiceStub();
|
||||||
|
registryService = new RegistryServiceStub();
|
||||||
|
spyOn(registryService, 'getMetadataFieldsBySchema').and.returnValue(createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))));
|
||||||
|
spyOn(registryService, 'getMetadataSchemaByPrefix').and.callFake((schemaName) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]));
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||||
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective],
|
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: RegistryService, useValue: registryServiceStub },
|
{ provide: RegistryService, useValue: registryService },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRoute },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: Router, useValue: new RouterStub() },
|
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -190,7 +175,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
it('should cancel editing the selected field when clicked again', waitForAsync(() => {
|
it('should cancel editing the selected field when clicked again', waitForAsync(() => {
|
||||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField));
|
comp.activeField$ = observableOf(mockFieldsList[2] as MetadataField);
|
||||||
spyOn(registryService, 'cancelEditMetadataField');
|
spyOn(registryService, 'cancelEditMetadataField');
|
||||||
row.click();
|
row.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -205,7 +190,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
spyOn(registryService, 'deleteMetadataField').and.callThrough();
|
||||||
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[]));
|
comp.selectedMetadataFieldIDs$ = observableOf(selectedFields.map((metadataField: MetadataField) => metadataField.id));
|
||||||
comp.deleteFields();
|
comp.deleteFields();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -3,17 +3,16 @@ import { RegistryService } from '../../../core/registry/registry.service';
|
|||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
|
||||||
combineLatest,
|
combineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
zip
|
zip,
|
||||||
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
import { map, switchMap, take } from 'rxjs/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||||
@@ -32,7 +31,7 @@ import { PaginationService } from '../../../core/pagination/pagination.service';
|
|||||||
* A component used for managing all existing metadata fields within the current metadata schema.
|
* A component used for managing all existing metadata fields within the current metadata schema.
|
||||||
* The admin can create, edit or delete metadata fields here.
|
* The admin can create, edit or delete metadata fields here.
|
||||||
*/
|
*/
|
||||||
export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
export class MetadataSchemaComponent implements OnDestroy, OnInit {
|
||||||
/**
|
/**
|
||||||
* The metadata schema
|
* The metadata schema
|
||||||
*/
|
*/
|
||||||
@@ -57,26 +56,33 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||||
|
|
||||||
constructor(private registryService: RegistryService,
|
/**
|
||||||
private route: ActivatedRoute,
|
* The current {@link MetadataField} that is being edited
|
||||||
private notificationsService: NotificationsService,
|
*/
|
||||||
private paginationService: PaginationService,
|
activeField$: Observable<MetadataField>;
|
||||||
private translateService: TranslateService) {
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The selected {@link MetadataField} IDs
|
||||||
|
*/
|
||||||
|
selectedMetadataFieldIDs$: Observable<number[]>;
|
||||||
|
|
||||||
|
subscriptions: Subscription[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected registryService: RegistryService,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected paginationService: PaginationService,
|
||||||
|
protected translateService: TranslateService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.route.params.subscribe((params) => {
|
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(this.route.snapshot.params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||||
this.initialize(params);
|
this.activeField$ = this.registryService.getActiveMetadataField();
|
||||||
});
|
this.selectedMetadataFieldIDs$ = this.registryService.getSelectedMetadataFields().pipe(
|
||||||
}
|
map((metadataFields: MetadataField[]) => metadataFields.map((metadataField: MetadataField) => metadataField.id)),
|
||||||
|
);
|
||||||
/**
|
|
||||||
* Initialize the component using the params within the url (schemaName)
|
|
||||||
* @param params
|
|
||||||
*/
|
|
||||||
initialize(params) {
|
|
||||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
|
||||||
this.updateFields();
|
this.updateFields();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,30 +115,13 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
* @param field
|
* @param field
|
||||||
*/
|
*/
|
||||||
editField(field: MetadataField) {
|
editField(field: MetadataField) {
|
||||||
this.getActiveField().pipe(take(1)).subscribe((activeField) => {
|
this.subscriptions.push(this.activeField$.pipe(take(1)).subscribe((activeField) => {
|
||||||
if (field === activeField) {
|
if (field === activeField) {
|
||||||
this.registryService.cancelEditMetadataField();
|
this.registryService.cancelEditMetadataField();
|
||||||
} else {
|
} else {
|
||||||
this.registryService.editMetadataField(field);
|
this.registryService.editMetadataField(field);
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether the given metadata field is active (being edited)
|
|
||||||
* @param field
|
|
||||||
*/
|
|
||||||
isActive(field: MetadataField): Observable<boolean> {
|
|
||||||
return this.getActiveField().pipe(
|
|
||||||
map((activeField) => field === activeField)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the active metadata field (being edited)
|
|
||||||
*/
|
|
||||||
getActiveField(): Observable<MetadataField> {
|
|
||||||
return this.registryService.getActiveMetadataField();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -146,29 +135,14 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
this.registryService.deselectMetadataField(field);
|
this.registryService.deselectMetadataField(field);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks whether a given metadata field is selected in the list (checkbox)
|
|
||||||
* @param field
|
|
||||||
*/
|
|
||||||
isSelected(field: MetadataField): Observable<boolean> {
|
|
||||||
return this.registryService.getSelectedMetadataFields().pipe(
|
|
||||||
map((fields) => fields.find((selectedField) => selectedField === field) != null)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete all the selected metadata fields
|
* Delete all the selected metadata fields
|
||||||
*/
|
*/
|
||||||
deleteFields() {
|
deleteFields() {
|
||||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
this.subscriptions.push(this.selectedMetadataFieldIDs$.pipe(
|
||||||
(fields) => {
|
take(1),
|
||||||
const tasks$ = [];
|
switchMap((fieldIDs) => zip(fieldIDs.map((fieldID) => this.registryService.deleteMetadataField(fieldID).pipe(getFirstCompletedRemoteData())))),
|
||||||
for (const field of fields) {
|
).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||||
if (hasValue(field.id)) {
|
|
||||||
tasks$.push(this.registryService.deleteMetadataField(field.id).pipe(getFirstCompletedRemoteData()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
zip(...tasks$).subscribe((responses: RemoteData<NoContent>[]) => {
|
|
||||||
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
const successResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
if (successResponses.length > 0) {
|
if (successResponses.length > 0) {
|
||||||
@@ -179,9 +153,7 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
this.registryService.deselectAllMetadataField();
|
this.registryService.deselectAllMetadataField();
|
||||||
this.registryService.cancelEditMetadataField();
|
this.registryService.cancelEditMetadataField();
|
||||||
});
|
}));
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -192,21 +164,19 @@ export class MetadataSchemaComponent implements OnInit, OnDestroy {
|
|||||||
showNotification(success: boolean, amount: number) {
|
showNotification(success: boolean, amount: number) {
|
||||||
const prefix = 'admin.registries.schema.notification';
|
const prefix = 'admin.registries.schema.notification';
|
||||||
const suffix = success ? 'success' : 'failure';
|
const suffix = success ? 'success' : 'failure';
|
||||||
const messages = observableCombineLatest([
|
const head = this.translateService.instant(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`);
|
||||||
this.translateService.get(success ? `${prefix}.${suffix}` : `${prefix}.${suffix}`),
|
const content = this.translateService.instant(`${prefix}.field.deleted.${suffix}`, { amount: amount });
|
||||||
this.translateService.get(`${prefix}.field.deleted.${suffix}`, { amount: amount })
|
|
||||||
]);
|
|
||||||
messages.subscribe(([head, content]) => {
|
|
||||||
if (success) {
|
if (success) {
|
||||||
this.notificationsService.success(head, content);
|
this.notificationsService.success(head, content);
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(head, content);
|
this.notificationsService.error(head, content);
|
||||||
}
|
}
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.paginationService.clearPagination(this.config.id);
|
this.paginationService.clearPagination(this.config.id);
|
||||||
this.registryService.deselectAllMetadataField();
|
this.registryService.deselectAllMetadataField();
|
||||||
|
this.subscriptions.forEach((subscription: Subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { RouterModule } from '@angular/router';
|
import { RouterModule } from '@angular/router';
|
||||||
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
|
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
|
||||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
import { ThemedAdminSearchPageComponent } from './admin-search-page/themed-admin-search-page.component';
|
||||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
|
||||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||||
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||||
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||||
|
import { ThemedAdminWorkflowPageComponent } from './admin-workflow-page/themed-admin-workflow-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -20,13 +20,13 @@ import { BatchImportPageComponent } from './admin-import-batch-page/batch-import
|
|||||||
{
|
{
|
||||||
path: 'search',
|
path: 'search',
|
||||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
component: AdminSearchPageComponent,
|
component: ThemedAdminSearchPageComponent,
|
||||||
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
|
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: 'workflow',
|
path: 'workflow',
|
||||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
component: AdminWorkflowPageComponent,
|
component: ThemedAdminWorkflowPageComponent,
|
||||||
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
|
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { ThemedAdminSearchPageComponent } from './themed-admin-search-page.component';
|
||||||
import { AdminSearchPageComponent } from './admin-search-page.component';
|
import { AdminSearchPageComponent } from './admin-search-page.component';
|
||||||
import { ItemAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component';
|
import { ItemAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/item-search-result/item-admin-search-result-list-element.component';
|
||||||
import { CommunityAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
|
import { CommunityAdminSearchResultListElementComponent } from './admin-search-results/admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
|
||||||
@@ -31,6 +32,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
ResearchEntitiesModule.withEntryComponents()
|
ResearchEntitiesModule.withEntryComponents()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
ThemedAdminSearchPageComponent,
|
||||||
AdminSearchPageComponent,
|
AdminSearchPageComponent,
|
||||||
...ENTRY_COMPONENTS
|
...ENTRY_COMPONENTS
|
||||||
]
|
]
|
||||||
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { AdminSearchPageComponent } from './admin-search-page.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link AdminSearchPageComponent}
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-admin-search-page',
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedAdminSearchPageComponent extends ThemedComponent<AdminSearchPageComponent> {
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'AdminSearchPageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/admin/admin-search-page/admin-search-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./admin-search-page.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -67,8 +67,8 @@ export class ExpandableAdminSidebarSectionComponent extends AdminSidebarSectionC
|
|||||||
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
this.sidebarActiveBg$ = this.variableService.getVariable('--ds-admin-sidebar-active-bg');
|
||||||
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
|
this.isSidebarCollapsed$ = this.menuService.isMenuCollapsed(this.menuID);
|
||||||
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
this.isSidebarPreviewCollapsed$ = this.menuService.isMenuPreviewCollapsed(this.menuID);
|
||||||
this.isExpanded$ = combineLatestObservable([this.active, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe(
|
this.isExpanded$ = combineLatestObservable([this.active$, this.isSidebarCollapsed$, this.isSidebarPreviewCollapsed$]).pipe(
|
||||||
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed)))
|
map(([active, sidebarCollapsed, sidebarPreviewCollapsed]) => (active && (!sidebarCollapsed || !sidebarPreviewCollapsed))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -27,6 +27,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
SupervisionOrderStatusComponent
|
SupervisionOrderStatusComponent
|
||||||
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component';
|
} from './admin-workflow-search-results/actions/workspace-item/supervision-order-status/supervision-order-status.component';
|
||||||
|
import { ThemedAdminWorkflowPageComponent } from './themed-admin-workflow-page.component';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -42,6 +43,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
SharedModule.withEntryComponents()
|
SharedModule.withEntryComponents()
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
ThemedAdminWorkflowPageComponent,
|
||||||
AdminWorkflowPageComponent,
|
AdminWorkflowPageComponent,
|
||||||
SupervisionOrderGroupSelectorComponent,
|
SupervisionOrderGroupSelectorComponent,
|
||||||
SupervisionOrderStatusComponent,
|
SupervisionOrderStatusComponent,
|
||||||
|
@@ -0,0 +1,26 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { AdminWorkflowPageComponent } from './admin-workflow-page.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for {@link AdminWorkflowPageComponent}
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-admin-workflow-page',
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedAdminWorkflowPageComponent extends ThemedComponent<AdminWorkflowPageComponent> {
|
||||||
|
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'AdminWorkflowPageComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/admin/admin-workflow-page/admin-workflow-page.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./admin-workflow-page.component');
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -40,6 +40,8 @@ import {
|
|||||||
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
import { ServerCheckGuard } from './core/server-check/server-check.guard';
|
||||||
import { MenuResolver } from './menu.resolver';
|
import { MenuResolver } from './menu.resolver';
|
||||||
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
|
||||||
|
import { HomePageResolver } from './home-page/home-page.resolver';
|
||||||
|
import { ViewTrackerResolverService } from './statistics/angulartics/dspace/view-tracker-resolver.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -63,7 +65,15 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
|||||||
path: 'home',
|
path: 'home',
|
||||||
loadChildren: () => import('./home-page/home-page.module')
|
loadChildren: () => import('./home-page/home-page.module')
|
||||||
.then((m) => m.HomePageModule),
|
.then((m) => m.HomePageModule),
|
||||||
data: { showBreadcrumbs: false },
|
data: {
|
||||||
|
showBreadcrumbs: false,
|
||||||
|
dsoPath: 'site'
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
site: HomePageResolver,
|
||||||
|
tracking: ViewTrackerResolverService,
|
||||||
|
},
|
||||||
|
|
||||||
canActivate: [EndUserAgreementCurrentUserGuard]
|
canActivate: [EndUserAgreementCurrentUserGuard]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -251,6 +261,7 @@ import { ThemedPageErrorComponent } from './page-error/themed-page-error.compone
|
|||||||
})
|
})
|
||||||
],
|
],
|
||||||
exports: [RouterModule],
|
exports: [RouterModule],
|
||||||
|
providers: [HomePageResolver, ViewTrackerResolverService],
|
||||||
})
|
})
|
||||||
export class AppRoutingModule {
|
export class AppRoutingModule {
|
||||||
|
|
||||||
|
@@ -30,6 +30,7 @@ import { EagerThemesModule } from '../themes/eager-themes.module';
|
|||||||
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
import { APP_CONFIG, AppConfig } from '../config/app-config.interface';
|
||||||
import { StoreDevModules } from '../config/store/devtools';
|
import { StoreDevModules } from '../config/store/devtools';
|
||||||
import { RootModule } from './root.module';
|
import { RootModule } from './root.module';
|
||||||
|
import { DspaceRestInterceptor } from './core/dspace-rest/dspace-rest.interceptor';
|
||||||
|
|
||||||
export function getConfig() {
|
export function getConfig() {
|
||||||
return environment;
|
return environment;
|
||||||
@@ -103,6 +104,12 @@ const PROVIDERS = [
|
|||||||
useClass: LogInterceptor,
|
useClass: LogInterceptor,
|
||||||
multi: true
|
multi: true
|
||||||
},
|
},
|
||||||
|
// register DspaceRestInterceptor as HttpInterceptor
|
||||||
|
{
|
||||||
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
useClass: DspaceRestInterceptor,
|
||||||
|
multi: true
|
||||||
|
},
|
||||||
// register the dynamic matcher used by form. MUST be provided by the app module
|
// register the dynamic matcher used by form. MUST be provided by the app module
|
||||||
...DYNAMIC_MATCHER_PROVIDERS,
|
...DYNAMIC_MATCHER_PROVIDERS,
|
||||||
];
|
];
|
||||||
|
@@ -8,11 +8,11 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-collection-authorizations',
|
selector: 'ds-bitstream-authorizations',
|
||||||
templateUrl: './bitstream-authorizations.component.html',
|
templateUrl: './bitstream-authorizations.component.html',
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component that handles the Collection Authorizations
|
* Component that handles the Bitstream Authorizations
|
||||||
*/
|
*/
|
||||||
export class BitstreamAuthorizationsComponent<TDomain extends DSpaceObject> implements OnInit {
|
export class BitstreamAuthorizationsComponent<TDomain extends DSpaceObject> implements OnInit {
|
||||||
|
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<ng-container *ngVar="(bitstreamRD$ | async) as bitstreamRD">
|
<ng-container *ngVar="(bitstreamRD$ | async) as bitstreamRD">
|
||||||
<div class="container" *ngVar="(bitstreamFormatsRD$ | async) as formatsRD">
|
<div class="container">
|
||||||
<div class="row" *ngIf="bitstreamRD?.hasSucceeded && formatsRD?.hasSucceeded">
|
<div class="row" *ngIf="bitstreamRD?.hasSucceeded">
|
||||||
<div class="col-md-2">
|
<div class="col-md-2">
|
||||||
<ds-themed-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-themed-thumbnail>
|
<ds-themed-thumbnail [thumbnail]="bitstreamRD?.payload"></ds-themed-thumbnail>
|
||||||
</div>
|
</div>
|
||||||
@@ -27,7 +27,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
<ds-error *ngIf="bitstreamRD?.hasFailed" message="{{'error.bitstream' | translate}}"></ds-error>
|
||||||
<ds-themed-loading *ngIf="!bitstreamRD || !formatsRD || bitstreamRD?.isLoading || formatsRD?.isLoading"
|
<ds-themed-loading *ngIf="!bitstreamRD || bitstreamRD?.isLoading"
|
||||||
message="{{'loading.bitstream' | translate}}"></ds-themed-loading>
|
message="{{'loading.bitstream' | translate}}"></ds-themed-loading>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -239,7 +239,7 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should select the correct format', () => {
|
it('should select the correct format', () => {
|
||||||
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id);
|
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.shortDescription);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should put the \"New Format\" input on invisible', () => {
|
it('should put the \"New Format\" input on invisible', () => {
|
||||||
@@ -270,7 +270,13 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
|
|
||||||
describe('when an unknown format is selected', () => {
|
describe('when an unknown format is selected', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.updateNewFormatLayout(allFormats[0].id);
|
comp.onChange({
|
||||||
|
model: {
|
||||||
|
id: 'selectedFormat',
|
||||||
|
value: allFormats[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
comp.updateNewFormatLayout();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should remove the invisible class from the \"New Format\" input', () => {
|
it('should remove the invisible class from the \"New Format\" input', () => {
|
||||||
@@ -372,10 +378,11 @@ describe('EditBitstreamPageComponent', () => {
|
|||||||
|
|
||||||
describe('when selected format has changed', () => {
|
describe('when selected format has changed', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.formGroup.patchValue({
|
comp.onChange({
|
||||||
formatContainer: {
|
model: {
|
||||||
selectedFormat: allFormats[2].id
|
id: 'selectedFormat',
|
||||||
}
|
value: allFormats[2],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
comp.onSubmit();
|
comp.onSubmit();
|
||||||
|
@@ -2,14 +2,20 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, OnIni
|
|||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
import { filter, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { combineLatest, combineLatest as observableCombineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
import {
|
||||||
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel, DynamicSelectModel } from '@ng-dynamic-forms/core';
|
combineLatest,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
Subscription, take
|
||||||
|
} from 'rxjs';
|
||||||
|
import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormLayout, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core';
|
||||||
import { UntypedFormGroup } from '@angular/forms';
|
import { UntypedFormGroup } from '@angular/forms';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
import { DynamicCustomSwitchModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/custom-switch/custom-switch.model';
|
||||||
import cloneDeep from 'lodash/cloneDeep';
|
import cloneDeep from 'lodash/cloneDeep';
|
||||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||||
import { getAllSucceededRemoteDataPayload, getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../core/shared/operators';
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteData, getFirstSucceededRemoteDataPayload, getRemoteDataPayload } from '../../core/shared/operators';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
||||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||||
@@ -18,7 +24,6 @@ import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../shared/em
|
|||||||
import { Metadata } from '../../core/shared/metadata.utils';
|
import { Metadata } from '../../core/shared/metadata.utils';
|
||||||
import { Location } from '@angular/common';
|
import { Location } from '@angular/common';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
|
||||||
import { getEntityEditRoute } from '../../item-page/item-page-routing-paths';
|
import { getEntityEditRoute } from '../../item-page/item-page-routing-paths';
|
||||||
import { Bundle } from '../../core/shared/bundle.model';
|
import { Bundle } from '../../core/shared/bundle.model';
|
||||||
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
@@ -26,6 +31,8 @@ import { Item } from '../../core/shared/item.model';
|
|||||||
import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
import { DsDynamicInputModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model';
|
||||||
import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
|
import { DsDynamicTextAreaModel } from '../../shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-textarea.model';
|
||||||
import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
|
import { PrimaryBitstreamService } from '../../core/data/primary-bitstream.service';
|
||||||
|
import { DynamicScrollableDropdownModel } from 'src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model';
|
||||||
|
import { FindAllDataImpl } from '../../core/data/base/find-all-data';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-bitstream-page',
|
selector: 'ds-edit-bitstream-page',
|
||||||
@@ -44,12 +51,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
bitstreamRD$: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
/**
|
|
||||||
* The formats their remote data observable
|
|
||||||
* Tracks changes and updates the view
|
|
||||||
*/
|
|
||||||
bitstreamFormatsRD$: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The UUID of the primary bitstream for this bundle
|
* The UUID of the primary bitstream for this bundle
|
||||||
*/
|
*/
|
||||||
@@ -65,11 +66,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
originalFormat: BitstreamFormat;
|
originalFormat: BitstreamFormat;
|
||||||
|
|
||||||
/**
|
|
||||||
* A list of all available bitstream formats
|
|
||||||
*/
|
|
||||||
formats: BitstreamFormat[];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @type {string} Key prefix used to generate form messages
|
* @type {string} Key prefix used to generate form messages
|
||||||
*/
|
*/
|
||||||
@@ -113,7 +109,10 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Options for fetching all bitstream formats
|
* Options for fetching all bitstream formats
|
||||||
*/
|
*/
|
||||||
findAllOptions = { elementsPerPage: 9999 };
|
findAllOptions = {
|
||||||
|
elementsPerPage: 20,
|
||||||
|
currentPage: 1
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Dynamic Input Model for the file's name
|
* The Dynamic Input Model for the file's name
|
||||||
@@ -153,9 +152,22 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* The Dynamic Input Model for the selected format
|
* The Dynamic Input Model for the selected format
|
||||||
*/
|
*/
|
||||||
selectedFormatModel = new DynamicSelectModel({
|
selectedFormatModel = new DynamicScrollableDropdownModel({
|
||||||
id: 'selectedFormat',
|
id: 'selectedFormat',
|
||||||
name: 'selectedFormat'
|
name: 'selectedFormat',
|
||||||
|
displayKey: 'shortDescription',
|
||||||
|
repeatable: false,
|
||||||
|
metadataFields: [],
|
||||||
|
submissionId: '',
|
||||||
|
hasSelectableMetadata: false,
|
||||||
|
findAllFactory: this.findAllFormatsServiceFactory(),
|
||||||
|
formatFunction: (format: BitstreamFormat | string) => {
|
||||||
|
if (format instanceof BitstreamFormat) {
|
||||||
|
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription;
|
||||||
|
} else {
|
||||||
|
return format;
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -370,6 +382,11 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
* @private
|
* @private
|
||||||
*/
|
*/
|
||||||
private bundle: Bundle;
|
private bundle: Bundle;
|
||||||
|
/**
|
||||||
|
* The currently selected format
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private selectedFormat: BitstreamFormat;
|
||||||
|
|
||||||
constructor(private route: ActivatedRoute,
|
constructor(private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -396,18 +413,12 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
this.itemId = this.route.snapshot.queryParams.itemId;
|
this.itemId = this.route.snapshot.queryParams.itemId;
|
||||||
this.entityType = this.route.snapshot.queryParams.entityType;
|
this.entityType = this.route.snapshot.queryParams.entityType;
|
||||||
this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream));
|
this.bitstreamRD$ = this.route.data.pipe(map((data: any) => data.bitstream));
|
||||||
this.bitstreamFormatsRD$ = this.bitstreamFormatService.findAll(this.findAllOptions);
|
|
||||||
|
|
||||||
const bitstream$ = this.bitstreamRD$.pipe(
|
const bitstream$ = this.bitstreamRD$.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const allFormats$ = this.bitstreamFormatsRD$.pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
);
|
|
||||||
|
|
||||||
const bundle$ = bitstream$.pipe(
|
const bundle$ = bitstream$.pipe(
|
||||||
switchMap((bitstream: Bitstream) => bitstream.bundle),
|
switchMap((bitstream: Bitstream) => bitstream.bundle),
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
@@ -423,24 +434,31 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
switchMap((bundle: Bundle) => bundle.item),
|
switchMap((bundle: Bundle) => bundle.item),
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
);
|
);
|
||||||
|
const format$ = bitstream$.pipe(
|
||||||
|
switchMap(bitstream => bitstream.format),
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
bitstream$,
|
bitstream$,
|
||||||
allFormats$,
|
|
||||||
bundle$,
|
bundle$,
|
||||||
primaryBitstream$,
|
primaryBitstream$,
|
||||||
item$,
|
item$,
|
||||||
).pipe()
|
format$,
|
||||||
.subscribe(([bitstream, allFormats, bundle, primaryBitstream, item]) => {
|
).subscribe(([bitstream, bundle, primaryBitstream, item, format]) => {
|
||||||
this.bitstream = bitstream as Bitstream;
|
this.bitstream = bitstream as Bitstream;
|
||||||
this.formats = allFormats.page;
|
|
||||||
this.bundle = bundle;
|
this.bundle = bundle;
|
||||||
|
this.selectedFormat = format;
|
||||||
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
|
// hasValue(primaryBitstream) because if there's no primaryBitstream on the bundle it will
|
||||||
// be a success response, but empty
|
// be a success response, but empty
|
||||||
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
|
this.primaryBitstreamUUID = hasValue(primaryBitstream) ? primaryBitstream.uuid : null;
|
||||||
this.itemId = item.uuid;
|
this.itemId = item.uuid;
|
||||||
this.setIiifStatus(this.bitstream);
|
this.setIiifStatus(this.bitstream);
|
||||||
})
|
}),
|
||||||
|
format$.pipe(take(1)).subscribe(
|
||||||
|
(format) => this.originalFormat = format,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
@@ -456,7 +474,6 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
setForm() {
|
setForm() {
|
||||||
this.formGroup = this.formService.createFormGroup(this.formModel);
|
this.formGroup = this.formService.createFormGroup(this.formModel);
|
||||||
this.updateFormatModel();
|
|
||||||
this.updateForm(this.bitstream);
|
this.updateForm(this.bitstream);
|
||||||
this.updateFieldTranslations();
|
this.updateFieldTranslations();
|
||||||
}
|
}
|
||||||
@@ -475,8 +492,9 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
description: bitstream.firstMetadataValue('dc.description')
|
description: bitstream.firstMetadataValue('dc.description')
|
||||||
},
|
},
|
||||||
formatContainer: {
|
formatContainer: {
|
||||||
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined
|
selectedFormat: this.selectedFormat.shortDescription,
|
||||||
}
|
newFormat: hasValue(bitstream.firstMetadata('dc.format')) ? bitstream.firstMetadata('dc.format').value : undefined,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
if (this.isIIIF) {
|
if (this.isIIIF) {
|
||||||
this.formGroup.patchValue({
|
this.formGroup.patchValue({
|
||||||
@@ -494,36 +512,16 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
this.bitstream.format.pipe(
|
this.updateNewFormatLayout();
|
||||||
getAllSucceededRemoteDataPayload()
|
|
||||||
).subscribe((format: BitstreamFormat) => {
|
|
||||||
this.originalFormat = format;
|
|
||||||
this.formGroup.patchValue({
|
|
||||||
formatContainer: {
|
|
||||||
selectedFormat: format.id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.updateNewFormatLayout(format.id);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the list of unknown format IDs an add options to the selectedFormatModel
|
|
||||||
*/
|
|
||||||
updateFormatModel() {
|
|
||||||
this.selectedFormatModel.options = this.formats.map((format: BitstreamFormat) =>
|
|
||||||
Object.assign({
|
|
||||||
value: format.id,
|
|
||||||
label: this.isUnknownFormat(format.id) ? this.translate.instant(this.KEY_PREFIX + 'selectedFormat.unknown') : format.shortDescription
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update the layout of the "Other Format" input depending on the selected format
|
* Update the layout of the "Other Format" input depending on the selected format
|
||||||
* @param selectedId
|
* @param selectedId
|
||||||
*/
|
*/
|
||||||
updateNewFormatLayout(selectedId: string) {
|
updateNewFormatLayout() {
|
||||||
if (this.isUnknownFormat(selectedId)) {
|
if (this.isUnknownFormat()) {
|
||||||
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout;
|
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout;
|
||||||
} else {
|
} else {
|
||||||
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible';
|
this.formLayout.newFormat.grid.host = this.newFormatBaseLayout + ' invisible';
|
||||||
@@ -534,9 +532,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
* Is the provided format (id) part of the list of unknown formats?
|
* Is the provided format (id) part of the list of unknown formats?
|
||||||
* @param id
|
* @param id
|
||||||
*/
|
*/
|
||||||
isUnknownFormat(id: string): boolean {
|
isUnknownFormat(): boolean {
|
||||||
const format = this.formats.find((f: BitstreamFormat) => f.id === id);
|
return hasValue(this.selectedFormat) && this.selectedFormat.supportLevel === BitstreamFormatSupportLevel.Unknown;
|
||||||
return hasValue(format) && format.supportLevel === BitstreamFormatSupportLevel.Unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -568,7 +565,8 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
onChange(event) {
|
onChange(event) {
|
||||||
const model = event.model;
|
const model = event.model;
|
||||||
if (model.id === this.selectedFormatModel.id) {
|
if (model.id === this.selectedFormatModel.id) {
|
||||||
this.updateNewFormatLayout(model.value);
|
this.selectedFormat = model.value;
|
||||||
|
this.updateNewFormatLayout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -578,8 +576,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
onSubmit() {
|
onSubmit() {
|
||||||
const updatedValues = this.formGroup.getRawValue();
|
const updatedValues = this.formGroup.getRawValue();
|
||||||
const updatedBitstream = this.formToBitstream(updatedValues);
|
const updatedBitstream = this.formToBitstream(updatedValues);
|
||||||
const selectedFormat = this.formats.find((f: BitstreamFormat) => f.id === updatedValues.formatContainer.selectedFormat);
|
const isNewFormat = this.selectedFormat.id !== this.originalFormat.id;
|
||||||
const isNewFormat = selectedFormat.id !== this.originalFormat.id;
|
|
||||||
const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream;
|
const isPrimary = updatedValues.fileNamePrimaryContainer.primaryBitstream;
|
||||||
const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid;
|
const wasPrimary = this.primaryBitstreamUUID === this.bitstream.uuid;
|
||||||
|
|
||||||
@@ -631,7 +628,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
bundle$ = observableOf(this.bundle);
|
bundle$ = observableOf(this.bundle);
|
||||||
}
|
}
|
||||||
if (isNewFormat) {
|
if (isNewFormat) {
|
||||||
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, selectedFormat).pipe(
|
bitstream$ = this.bitstreamService.updateFormat(this.bitstream, this.selectedFormat).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
map((formatResponse: RemoteData<Bitstream>) => {
|
map((formatResponse: RemoteData<Bitstream>) => {
|
||||||
if (hasValue(formatResponse) && formatResponse.hasFailed) {
|
if (hasValue(formatResponse) && formatResponse.hasFailed) {
|
||||||
@@ -789,4 +786,7 @@ export class EditBitstreamPageComponent implements OnInit, OnDestroy {
|
|||||||
.forEach((subscription) => subscription.unsubscribe());
|
.forEach((subscription) => subscription.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
findAllFormatsServiceFactory() {
|
||||||
|
return () => this.bitstreamFormatService as any as FindAllDataImpl<BitstreamFormat>;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user