mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Compare commits
1679 Commits
dspace-7.0
...
dspace-7.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
b7a57035c4 | ||
![]() |
c64760b0d3 | ||
![]() |
2fdb5a4ce2 | ||
![]() |
fc35bdfe33 | ||
![]() |
b7b7e7ae24 | ||
![]() |
d9ec9029ea | ||
![]() |
ac84b3f9c0 | ||
![]() |
44449e33cd | ||
![]() |
fc5395c1e5 | ||
![]() |
31c22d544e | ||
![]() |
d2ec2c65fd | ||
![]() |
36bd2169db | ||
![]() |
f35a5804fc | ||
![]() |
ad71796507 | ||
![]() |
6a828f9286 | ||
![]() |
9adafac6c7 | ||
![]() |
15e44ff46c | ||
![]() |
0e2f85ef77 | ||
![]() |
cea01d46f9 | ||
![]() |
6351a951e7 | ||
![]() |
f368da7a27 | ||
![]() |
2ecc1ca7fd | ||
![]() |
49e7b4b4b4 | ||
![]() |
d932928a7f | ||
![]() |
e56d3f717f | ||
![]() |
5e651881b5 | ||
![]() |
926f8e5a2f | ||
![]() |
cc402a0a02 | ||
![]() |
ddc0ed6321 | ||
![]() |
b493c9b0f2 | ||
![]() |
27f17b35f9 | ||
![]() |
1f984c4c40 | ||
![]() |
6850a47d70 | ||
![]() |
52d5beae1d | ||
![]() |
46a5a3eaca | ||
![]() |
59df043578 | ||
![]() |
ecf8e8915f | ||
![]() |
b1b8c05150 | ||
![]() |
06c6ed18a6 | ||
![]() |
48d7f6987e | ||
![]() |
0d38c75ea2 | ||
![]() |
b535d166e0 | ||
![]() |
7996555ec2 | ||
![]() |
521b0d3986 | ||
![]() |
02526753e2 | ||
![]() |
74af325e45 | ||
![]() |
b384a9400b | ||
![]() |
7cf0480098 | ||
![]() |
54b351c0d3 | ||
![]() |
240d2cc1a5 | ||
![]() |
10cd6182ae | ||
![]() |
441d8a07b5 | ||
![]() |
32546273e2 | ||
![]() |
103605c36e | ||
![]() |
173dcee427 | ||
![]() |
909158c30e | ||
![]() |
e10a37e414 | ||
![]() |
87f53d613d | ||
![]() |
78ab49c4a6 | ||
![]() |
5b76ed6d52 | ||
![]() |
b9401b46b1 | ||
![]() |
a8e0bd28fd | ||
![]() |
2ba1106e0a | ||
![]() |
6f75ab6237 | ||
![]() |
8e3699f6a3 | ||
![]() |
c7963e5126 | ||
![]() |
e0dc90ddd3 | ||
![]() |
6f4167c6cf | ||
![]() |
e6a454f863 | ||
![]() |
1d09a6b050 | ||
![]() |
08a6fa7c96 | ||
![]() |
e8049aac04 | ||
![]() |
c911ec902a | ||
![]() |
f49007e2f6 | ||
![]() |
256ee0f6f4 | ||
![]() |
7c4a84bc2b | ||
![]() |
58f2a5bb2a | ||
![]() |
4e9afc9f67 | ||
![]() |
2e574ce0bf | ||
![]() |
2f201a49f4 | ||
![]() |
32dfaa17b3 | ||
![]() |
30d46bedf2 | ||
![]() |
2cb6ba98ce | ||
![]() |
1853d1bda2 | ||
![]() |
a261034f9d | ||
![]() |
3fae13690f | ||
![]() |
1549872cb8 | ||
![]() |
a0b9ddcab7 | ||
![]() |
4f4c2d25ed | ||
![]() |
48efc90531 | ||
![]() |
c14f5bee55 | ||
![]() |
8f1df1921f | ||
![]() |
a8636b0e5c | ||
![]() |
5ec544ffea | ||
![]() |
18f50fb0c0 | ||
![]() |
41969ec3b1 | ||
![]() |
0400b98e97 | ||
![]() |
30c7e563f5 | ||
![]() |
1f02c2cd64 | ||
![]() |
f080a7568e | ||
![]() |
c6de988486 | ||
![]() |
7390999010 | ||
![]() |
92c5804af5 | ||
![]() |
9d3da6fdef | ||
![]() |
232b20689a | ||
![]() |
4ee634144d | ||
![]() |
930624fd19 | ||
![]() |
5849499569 | ||
![]() |
ed2c935f7e | ||
![]() |
2b6072492e | ||
![]() |
00db52494b | ||
![]() |
7fce3df3b4 | ||
![]() |
e76c6e4dc3 | ||
![]() |
b2234e45e9 | ||
![]() |
580346c2f7 | ||
![]() |
0f55ee8adb | ||
![]() |
5db2906dea | ||
![]() |
a72ef836f9 | ||
![]() |
48b80c6a38 | ||
![]() |
1ea9623efc | ||
![]() |
ede9c3d397 | ||
![]() |
33e395a7f6 | ||
![]() |
c932f41378 | ||
![]() |
d9e6d25da0 | ||
![]() |
5c096d5f66 | ||
![]() |
c1bd6938a7 | ||
![]() |
6b8e134e45 | ||
![]() |
404b1053a7 | ||
![]() |
f469253fe4 | ||
![]() |
0175b50d48 | ||
![]() |
20f8f913cf | ||
![]() |
a9cd9f0366 | ||
![]() |
c6c34b667a | ||
![]() |
795d638aa6 | ||
![]() |
383dff736c | ||
![]() |
adfffac730 | ||
![]() |
a970aeaab8 | ||
![]() |
7d26fd478d | ||
![]() |
892a985156 | ||
![]() |
1f1846c487 | ||
![]() |
004297fcfa | ||
![]() |
53c457689c | ||
![]() |
2922deff50 | ||
![]() |
ba25bd61cd | ||
![]() |
51e732e430 | ||
![]() |
401393c8bd | ||
![]() |
8fd46d9c88 | ||
![]() |
6c7780ca56 | ||
![]() |
7803c2a97b | ||
![]() |
f5a66c2266 | ||
![]() |
18eaf18108 | ||
![]() |
d5484e5c89 | ||
![]() |
60d2f386df | ||
![]() |
4bbbc93f6c | ||
![]() |
cb21cd47bb | ||
![]() |
a054938d80 | ||
![]() |
373acc6c82 | ||
![]() |
bae5c6652e | ||
![]() |
c087b8859f | ||
![]() |
496bedfe2e | ||
![]() |
b9c050c19c | ||
![]() |
18b9a41fe0 | ||
![]() |
9e31b61d42 | ||
![]() |
568b8ee1a7 | ||
![]() |
305c4ce882 | ||
![]() |
677b3f63de | ||
![]() |
525c333e09 | ||
![]() |
781a558458 | ||
![]() |
1f71274db6 | ||
![]() |
dffcd745af | ||
![]() |
32d0c8118b | ||
![]() |
a6b2ed607a | ||
![]() |
01231ef105 | ||
![]() |
98d44fd42e | ||
![]() |
000bfb85c0 | ||
![]() |
6606520c2f | ||
![]() |
5e95f1df1a | ||
![]() |
0e1d3e4048 | ||
![]() |
3cd8178f65 | ||
![]() |
2262a115fc | ||
![]() |
64c96c78f0 | ||
![]() |
401065dba2 | ||
![]() |
be4d4ec88d | ||
![]() |
ccb3c98f81 | ||
![]() |
dbca93e3aa | ||
![]() |
18e17f0dad | ||
![]() |
df8ef2b5c3 | ||
![]() |
b2f0e9f620 | ||
![]() |
dbdc42fc73 | ||
![]() |
8b6b478df9 | ||
![]() |
aa5383ca5a | ||
![]() |
3d1e93f5ef | ||
![]() |
5d22ba7d36 | ||
![]() |
653cf8e921 | ||
![]() |
355d9984b4 | ||
![]() |
500f5c645f | ||
![]() |
3b89580caf | ||
![]() |
4814ed0c68 | ||
![]() |
5acbf4f216 | ||
![]() |
423cf7569a | ||
![]() |
9d988022c7 | ||
![]() |
802498e27e | ||
![]() |
e1f940668a | ||
![]() |
d2fe506299 | ||
![]() |
b554d40e9c | ||
![]() |
4ae8997ada | ||
![]() |
b4a63fccf4 | ||
![]() |
5b326aea92 | ||
![]() |
f92eb27c24 | ||
![]() |
1a192ecdc9 | ||
![]() |
d95d68dd7f | ||
![]() |
076ee8b26d | ||
![]() |
cbd36ce1f9 | ||
![]() |
56433d0776 | ||
![]() |
3f11ae9fa5 | ||
![]() |
254305652d | ||
![]() |
a6f1a6d1ec | ||
![]() |
f67387ed65 | ||
![]() |
725f20a9d0 | ||
![]() |
4ea264dd7f | ||
![]() |
413f798f71 | ||
![]() |
e60688b85e | ||
![]() |
91e3775135 | ||
![]() |
a0d5cbc36a | ||
![]() |
92ee38c9ff | ||
![]() |
ab224cced5 | ||
![]() |
3ded2b6973 | ||
![]() |
cda800f3be | ||
![]() |
0cb606877b | ||
![]() |
f52c1d0ba5 | ||
![]() |
3298c4e6c6 | ||
![]() |
775dfb5f87 | ||
![]() |
e614e9eca7 | ||
![]() |
ade2187492 | ||
![]() |
dea6638c1f | ||
![]() |
f7f6600806 | ||
![]() |
2c12446128 | ||
![]() |
29ff18264c | ||
![]() |
df9e6b67f5 | ||
![]() |
179fbd5276 | ||
![]() |
22ff110e9d | ||
![]() |
d7da83ad5a | ||
![]() |
173f14c41f | ||
![]() |
6824ccb307 | ||
![]() |
bc00c000a6 | ||
![]() |
d4ff4aab36 | ||
![]() |
08dedb2dc3 | ||
![]() |
c5e8074040 | ||
![]() |
39f1766391 | ||
![]() |
4d3f85fafe | ||
![]() |
dc73561575 | ||
![]() |
025948e3a0 | ||
![]() |
8551d730d8 | ||
![]() |
7f76769bff | ||
![]() |
10bb457897 | ||
![]() |
bc7c92f44c | ||
![]() |
884e94a08b | ||
![]() |
ab0f2c89e6 | ||
![]() |
b9432e7553 | ||
![]() |
e6c1069e19 | ||
![]() |
07998a8c08 | ||
![]() |
bffae34fcc | ||
![]() |
89b3d2c40e | ||
![]() |
3331a72b02 | ||
![]() |
d5b2fbff0c | ||
![]() |
6bffa68bbb | ||
![]() |
42d6527ca9 | ||
![]() |
be0158fc9c | ||
![]() |
fb153b7b13 | ||
![]() |
a52650e62a | ||
![]() |
f17e204712 | ||
![]() |
b2f966eb83 | ||
![]() |
1d31cae970 | ||
![]() |
56c3d12497 | ||
![]() |
44facb8dcb | ||
![]() |
ad4e8eeb8c | ||
![]() |
8af72cb1d3 | ||
![]() |
1102cda3b2 | ||
![]() |
347d42246c | ||
![]() |
d571c174f0 | ||
![]() |
516b23cfcc | ||
![]() |
3f48a5149b | ||
![]() |
a04eb28815 | ||
![]() |
e3ce775aee | ||
![]() |
c8be50614a | ||
![]() |
a18120d497 | ||
![]() |
04cb75e786 | ||
![]() |
78c6144f71 | ||
![]() |
87d7bc1a20 | ||
![]() |
c38bf3fd0c | ||
![]() |
3dd433a5da | ||
![]() |
a638055d12 | ||
![]() |
421b45b9ec | ||
![]() |
c1db19ee03 | ||
![]() |
573a9b8de3 | ||
![]() |
03c1b689f8 | ||
![]() |
1e67131902 | ||
![]() |
0452a9a8cd | ||
![]() |
cc4b7b215e | ||
![]() |
3bbd05f588 | ||
![]() |
618ac070e8 | ||
![]() |
5e93a89678 | ||
![]() |
cddd345fcc | ||
![]() |
90f8bf42a1 | ||
![]() |
210db7fac8 | ||
![]() |
b987a3d762 | ||
![]() |
bda3a30889 | ||
![]() |
2795c1ecc7 | ||
![]() |
de17f099ce | ||
![]() |
e891d65526 | ||
![]() |
b16607e597 | ||
![]() |
ddbc0e097a | ||
![]() |
db7ecb0f53 | ||
![]() |
6f4f0eab11 | ||
![]() |
ce5547db4b | ||
![]() |
4a46780d85 | ||
![]() |
246343d175 | ||
![]() |
19c788f126 | ||
![]() |
5585bcbc9b | ||
![]() |
a45273048d | ||
![]() |
d0680e2aa8 | ||
![]() |
58bfb9b1a7 | ||
![]() |
0387c5f15b | ||
![]() |
3a9277c415 | ||
![]() |
eba6bf505f | ||
![]() |
b352690cca | ||
![]() |
cff3a6f010 | ||
![]() |
1903ad0440 | ||
![]() |
cab971211f | ||
![]() |
ed11cb9f01 | ||
![]() |
34f79f1667 | ||
![]() |
cf15fbcca2 | ||
![]() |
14ea71f536 | ||
![]() |
b2ef5ee2fa | ||
![]() |
1472bd7df9 | ||
![]() |
31aff062eb | ||
![]() |
e6ffec0129 | ||
![]() |
68d3144bec | ||
![]() |
b768043bcc | ||
![]() |
b19aa64052 | ||
![]() |
fe3eea6079 | ||
![]() |
cc27ad5965 | ||
![]() |
3bc903f812 | ||
![]() |
a403732d45 | ||
![]() |
c8d516598a | ||
![]() |
2a4f8aaad8 | ||
![]() |
f531bdd976 | ||
![]() |
ddb787277b | ||
![]() |
eb866d85ef | ||
![]() |
29b2e89625 | ||
![]() |
1e99071907 | ||
![]() |
0eac9c6369 | ||
![]() |
afe70bc546 | ||
![]() |
49e59c44e1 | ||
![]() |
7b3a2e9dc8 | ||
![]() |
57b007ffe2 | ||
![]() |
f630898eab | ||
![]() |
1ca1fe746a | ||
![]() |
4f8f4de241 | ||
![]() |
323ac03e54 | ||
![]() |
3fd4ecbfc4 | ||
![]() |
6560d1d112 | ||
![]() |
17989667aa | ||
![]() |
cc1cc08d8e | ||
![]() |
010da52476 | ||
![]() |
4a2e7741b7 | ||
![]() |
6010316d91 | ||
![]() |
4c23d01567 | ||
![]() |
16831decca | ||
![]() |
fd3eea99b8 | ||
![]() |
32a7dcd787 | ||
![]() |
cd39d47214 | ||
![]() |
4e8ec5f4a2 | ||
![]() |
94a71b69d1 | ||
![]() |
1d6e9d5b96 | ||
![]() |
2e06357291 | ||
![]() |
813fcfbd30 | ||
![]() |
db4408274e | ||
![]() |
0271821305 | ||
![]() |
78f9d62e00 | ||
![]() |
1bc7182372 | ||
![]() |
9d71f0348b | ||
![]() |
d8bdd768ca | ||
![]() |
617bbbb90c | ||
![]() |
9563ceb348 | ||
![]() |
4ba3b01997 | ||
![]() |
02802c0de1 | ||
![]() |
2c1337031d | ||
![]() |
6e928eeb89 | ||
![]() |
a3b349b6a3 | ||
![]() |
0730c68654 | ||
![]() |
24015b2f95 | ||
![]() |
d47a8fc572 | ||
![]() |
ec9e0407df | ||
![]() |
ff80afb839 | ||
![]() |
11e4f02946 | ||
![]() |
70b2c8c6e1 | ||
![]() |
3a37f9be12 | ||
![]() |
de0992bf85 | ||
![]() |
3e51fd8598 | ||
![]() |
6f8f4b31bf | ||
![]() |
d3d2bb20d4 | ||
![]() |
c57810f00d | ||
![]() |
34fb8612f8 | ||
![]() |
7b337acc1c | ||
![]() |
4f57d7ae83 | ||
![]() |
3132da9b3d | ||
![]() |
4e679ec6db | ||
![]() |
f594fc9088 | ||
![]() |
cd2e640886 | ||
![]() |
894888fc8f | ||
![]() |
6801194cbb | ||
![]() |
feced9f893 | ||
![]() |
34a4f211f1 | ||
![]() |
5aca8cc87d | ||
![]() |
f9fa8f0347 | ||
![]() |
ad77640f7d | ||
![]() |
bb87bf5a0e | ||
![]() |
42d12a83ce | ||
![]() |
ebe64c2dcc | ||
![]() |
b1575b3336 | ||
![]() |
5de567b8b0 | ||
![]() |
dee06c69a6 | ||
![]() |
d2c2431c6c | ||
![]() |
347c9ca785 | ||
![]() |
d6e19254e6 | ||
![]() |
f7d4d89db2 | ||
![]() |
a4d9ce2ec9 | ||
![]() |
1ca4b95bef | ||
![]() |
015a0c76f8 | ||
![]() |
1a22f147fb | ||
![]() |
fb10c7764b | ||
![]() |
9ed2d4321e | ||
![]() |
375b51d0d4 | ||
![]() |
fdd05d2fec | ||
![]() |
972f0dfd60 | ||
![]() |
daeb475eb6 | ||
![]() |
75b4291789 | ||
![]() |
ee8293978f | ||
![]() |
c09010e151 | ||
![]() |
b18cfcbd25 | ||
![]() |
fcbb690b29 | ||
![]() |
e004098016 | ||
![]() |
ce004c2e58 | ||
![]() |
4dd66073fc | ||
![]() |
4b67dbf10f | ||
![]() |
6b5303faa5 | ||
![]() |
b510894f6f | ||
![]() |
65b648b000 | ||
![]() |
4f1dd88923 | ||
![]() |
01ba97af7a | ||
![]() |
7c39bf4b5f | ||
![]() |
9d185e8a15 | ||
![]() |
83fabb007a | ||
![]() |
62b555beee | ||
![]() |
ea05af74bd | ||
![]() |
e2c4e6d27b | ||
![]() |
d352443848 | ||
![]() |
8ecb215105 | ||
![]() |
850f474c28 | ||
![]() |
a5ed837180 | ||
![]() |
2a26159af6 | ||
![]() |
be445a7bde | ||
![]() |
66834bb279 | ||
![]() |
00039436b6 | ||
![]() |
bc72b1c2ae | ||
![]() |
6b123ad0a7 | ||
![]() |
2022268ab0 | ||
![]() |
890d60aa9c | ||
![]() |
904228127e | ||
![]() |
64eb5db05b | ||
![]() |
20b4da5c33 | ||
![]() |
b98cec011b | ||
![]() |
0dbea31b0d | ||
![]() |
38f3f583f6 | ||
![]() |
c480e96043 | ||
![]() |
c9987e3451 | ||
![]() |
a4988d580e | ||
![]() |
9ad78e1ff1 | ||
![]() |
e94c1adfb8 | ||
![]() |
2db42d0677 | ||
![]() |
ea4e9f8797 | ||
![]() |
8df0591272 | ||
![]() |
907358edc5 | ||
![]() |
de6abb7ce6 | ||
![]() |
149f59039e | ||
![]() |
c54faf69b1 | ||
![]() |
9df5a508f1 | ||
![]() |
9a921e9311 | ||
![]() |
23352412ce | ||
![]() |
46c9a77cd9 | ||
![]() |
c31b1725a8 | ||
![]() |
9e5508f9e5 | ||
![]() |
64944ebb71 | ||
![]() |
6914000e19 | ||
![]() |
9246cce7de | ||
![]() |
1402dd9129 | ||
![]() |
3a7fa8dbaf | ||
![]() |
cdbb9b6203 | ||
![]() |
b51218ad5f | ||
![]() |
a78e0512cb | ||
![]() |
393b2e0029 | ||
![]() |
f0e38e9e8c | ||
![]() |
dfb3c076d7 | ||
![]() |
a434b5706e | ||
![]() |
e13801ce81 | ||
![]() |
fda083137a | ||
![]() |
038483c8b8 | ||
![]() |
7a904f9bf7 | ||
![]() |
6ada3fae5b | ||
![]() |
0210be1a53 | ||
![]() |
7b31ad0345 | ||
![]() |
4368015567 | ||
![]() |
e77866a0d2 | ||
![]() |
0b368e8c25 | ||
![]() |
8cacad3264 | ||
![]() |
a0347d263f | ||
![]() |
33972cbaf7 | ||
![]() |
891415daae | ||
![]() |
2eac2a20bb | ||
![]() |
69d58c0881 | ||
![]() |
e9a8a25116 | ||
![]() |
5234a7b015 | ||
![]() |
558285da85 | ||
![]() |
b3e2041cdb | ||
![]() |
71a3a22a7c | ||
![]() |
ef1ed04fd2 | ||
![]() |
c48932431f | ||
![]() |
dc316dc6cb | ||
![]() |
c3ef2f8dee | ||
![]() |
78d1c5ee2b | ||
![]() |
6e0bd0c146 | ||
![]() |
edbc32604d | ||
![]() |
bc8e7d8fe6 | ||
![]() |
4896c98f9a | ||
![]() |
de4b32dcad | ||
![]() |
0048a97181 | ||
![]() |
afa61b11c8 | ||
![]() |
380e02a92d | ||
![]() |
820280f901 | ||
![]() |
06667e448f | ||
![]() |
d1ba3d9936 | ||
![]() |
82fcd47ac3 | ||
![]() |
16a16bedfe | ||
![]() |
9147a6b18f | ||
![]() |
23cbc98208 | ||
![]() |
25c3e53b63 | ||
![]() |
7755228b59 | ||
![]() |
424089312b | ||
![]() |
6b3a395e4a | ||
![]() |
f72343bd8e | ||
![]() |
870c98f368 | ||
![]() |
93b465c3b2 | ||
![]() |
dfa846a98e | ||
![]() |
9b711cc460 | ||
![]() |
1b978124d1 | ||
![]() |
7f44c7751b | ||
![]() |
8c2e63c2a7 | ||
![]() |
2ef02864f2 | ||
![]() |
c83c861e85 | ||
![]() |
d127e5f27c | ||
![]() |
afcf897bbe | ||
![]() |
c0962cb966 | ||
![]() |
36e80e3624 | ||
![]() |
db7ebfb16e | ||
![]() |
fcae21e944 | ||
![]() |
5ff634a26f | ||
![]() |
69a99e9381 | ||
![]() |
77f6294bde | ||
![]() |
b1f4a90a58 | ||
![]() |
9bd1933548 | ||
![]() |
eaf0911e2e | ||
![]() |
84326ef564 | ||
![]() |
adfe881a81 | ||
![]() |
aed4db6289 | ||
![]() |
ee140e623a | ||
![]() |
98c8eb558a | ||
![]() |
a4bf1a64c7 | ||
![]() |
48d893e975 | ||
![]() |
1400af3a5e | ||
![]() |
e950c23f40 | ||
![]() |
c99b6adb31 | ||
![]() |
f4686ea6cf | ||
![]() |
3997ed2c99 | ||
![]() |
3daf35e4a4 | ||
![]() |
79424ac108 | ||
![]() |
8e0280cb5a | ||
![]() |
514e9a98ed | ||
![]() |
2ece89db62 | ||
![]() |
69e8221867 | ||
![]() |
4feba32157 | ||
![]() |
ae476baa62 | ||
![]() |
8522c35186 | ||
![]() |
835ce735cb | ||
![]() |
b73799d28b | ||
![]() |
2d50598177 | ||
![]() |
c5d271b647 | ||
![]() |
757b9f9dcd | ||
![]() |
7c88616ba8 | ||
![]() |
23a378bd26 | ||
![]() |
168e74d1fa | ||
![]() |
cbfc396b29 | ||
![]() |
29c9682508 | ||
![]() |
b34e636379 | ||
![]() |
8c3416ce29 | ||
![]() |
745ac6eaad | ||
![]() |
eca4931c7b | ||
![]() |
599f66b57f | ||
![]() |
3c0adf9b12 | ||
![]() |
536ecbb6ed | ||
![]() |
1267b7fc2d | ||
![]() |
0f4c905628 | ||
![]() |
7bbb90cbfd | ||
![]() |
6097d089fd | ||
![]() |
d416437895 | ||
![]() |
33156547ea | ||
![]() |
ed3f7dfc7b | ||
![]() |
a825699af8 | ||
![]() |
55fe66676a | ||
![]() |
64c9d009a9 | ||
![]() |
87eb733d8b | ||
![]() |
63257eac3d | ||
![]() |
981c915975 | ||
![]() |
4ff887035e | ||
![]() |
9d124c1546 | ||
![]() |
3487231af1 | ||
![]() |
87d468f9b9 | ||
![]() |
52e6d5ca29 | ||
![]() |
da577756fe | ||
![]() |
3a84dc1800 | ||
![]() |
c7b3766399 | ||
![]() |
485a496938 | ||
![]() |
64978f2cfe | ||
![]() |
0c4a1bc610 | ||
![]() |
4887353450 | ||
![]() |
94329b0b52 | ||
![]() |
efc2c9933a | ||
![]() |
e2a2cad91b | ||
![]() |
86af0368b8 | ||
![]() |
558ff27d31 | ||
![]() |
a7ad6885e3 | ||
![]() |
3b4d8598d5 | ||
![]() |
2c4bbe874d | ||
![]() |
4debb54352 | ||
![]() |
4c8f9f82a4 | ||
![]() |
56d6965233 | ||
![]() |
1ba07c4683 | ||
![]() |
699c86c201 | ||
![]() |
5ba7cd2c2d | ||
![]() |
1c3f2e73e1 | ||
![]() |
f5234bab52 | ||
![]() |
3550cb9073 | ||
![]() |
d8dd045a10 | ||
![]() |
fe680150bd | ||
![]() |
94b99b9bb5 | ||
![]() |
eb4f7d557c | ||
![]() |
c2f13548ed | ||
![]() |
3381d5e8df | ||
![]() |
68d186ae8d | ||
![]() |
ce31ef0d0f | ||
![]() |
c2ae71b1b8 | ||
![]() |
78a29a8770 | ||
![]() |
4bddbb6c62 | ||
![]() |
aedd4b99ee | ||
![]() |
771041a3cf | ||
![]() |
4f7b7637cb | ||
![]() |
4a7c7a61f1 | ||
![]() |
ac58297cbf | ||
![]() |
3bea8f5a17 | ||
![]() |
840ed6acb7 | ||
![]() |
190203cb48 | ||
![]() |
166aab606d | ||
![]() |
bb5103518e | ||
![]() |
40d740640d | ||
![]() |
792455f007 | ||
![]() |
0cdafacf22 | ||
![]() |
857f796287 | ||
![]() |
2d3d7ae71e | ||
![]() |
576b5328c3 | ||
![]() |
449f66076b | ||
![]() |
85509e2159 | ||
![]() |
e7302bd8af | ||
![]() |
f289c1353e | ||
![]() |
d8a049c550 | ||
![]() |
5b37d90444 | ||
![]() |
6d3a52d2f9 | ||
![]() |
19510a88ef | ||
![]() |
7e62ce80c2 | ||
![]() |
1e94a02da0 | ||
![]() |
e04d13402b | ||
![]() |
575a142022 | ||
![]() |
b1ff5765a6 | ||
![]() |
36ec68fd33 | ||
![]() |
77826beb2a | ||
![]() |
7474c0a514 | ||
![]() |
7565507f69 | ||
![]() |
836492d2af | ||
![]() |
60f9692c29 | ||
![]() |
bf086db969 | ||
![]() |
959134fc89 | ||
![]() |
5a6a0eac6f | ||
![]() |
d255838265 | ||
![]() |
dc43e23301 | ||
![]() |
b62af58502 | ||
![]() |
e762ed4d9e | ||
![]() |
404729faa3 | ||
![]() |
0dd765d84a | ||
![]() |
d309b2081c | ||
![]() |
9a0a1645fb | ||
![]() |
d3a84c7e7f | ||
![]() |
c3095e37bf | ||
![]() |
18cf2a8aca | ||
![]() |
bbb2570f87 | ||
![]() |
3694c557e1 | ||
![]() |
dc9520a832 | ||
![]() |
dd5f40858d | ||
![]() |
468fc097c0 | ||
![]() |
ca38af3772 | ||
![]() |
0b2daf8cbf | ||
![]() |
9cad39ff36 | ||
![]() |
e26961322f | ||
![]() |
90436f3b2c | ||
![]() |
756134f773 | ||
![]() |
4efbbf1c99 | ||
![]() |
8059301906 | ||
![]() |
2e50d99fc4 | ||
![]() |
89f4d66012 | ||
![]() |
eea76746b1 | ||
![]() |
1896b14520 | ||
![]() |
24b308ace6 | ||
![]() |
61eccb2973 | ||
![]() |
ab117105b3 | ||
![]() |
5925b50141 | ||
![]() |
95b2feb868 | ||
![]() |
e4bcf3d4b0 | ||
![]() |
103425d7ee | ||
![]() |
d5198283ff | ||
![]() |
b550667bc9 | ||
![]() |
7e3ba86ccc | ||
![]() |
f53ebba096 | ||
![]() |
cbbc776922 | ||
![]() |
7adb50a9b8 | ||
![]() |
896e19990b | ||
![]() |
19a2ae6ecc | ||
![]() |
9113a08796 | ||
![]() |
9d5f1b09bc | ||
![]() |
5e6082c1c7 | ||
![]() |
a851a71f22 | ||
![]() |
9ae869936b | ||
![]() |
82367ea778 | ||
![]() |
227943b112 | ||
![]() |
83e7b662e3 | ||
![]() |
a30529e091 | ||
![]() |
9abc48960f | ||
![]() |
0df7bb57a6 | ||
![]() |
b9754764b3 | ||
![]() |
2e236cc98b | ||
![]() |
03c36ab233 | ||
![]() |
dfd1881f89 | ||
![]() |
6dca421256 | ||
![]() |
5088ec0628 | ||
![]() |
0b57dd738b | ||
![]() |
6231495444 | ||
![]() |
4805dd5abf | ||
![]() |
d894056d44 | ||
![]() |
eb96359d9e | ||
![]() |
41daaa34ad | ||
![]() |
ef3d4fb154 | ||
![]() |
5fa2827d1b | ||
![]() |
764546e4d5 | ||
![]() |
e9abafb141 | ||
![]() |
64a396a00f | ||
![]() |
9741fbe9f6 | ||
![]() |
eb099e588c | ||
![]() |
0aac997238 | ||
![]() |
94539cd61d | ||
![]() |
1fafc3a5d5 | ||
![]() |
ac32d2de11 | ||
![]() |
8ea2b79edf | ||
![]() |
8d939cd35c | ||
![]() |
8fbe6ade81 | ||
![]() |
792f7bf74d | ||
![]() |
6b26506d88 | ||
![]() |
c7bb6ab17c | ||
![]() |
ae6a6f28f7 | ||
![]() |
392b539036 | ||
![]() |
6a2b9dad26 | ||
![]() |
99517c084c | ||
![]() |
bc358ec954 | ||
![]() |
f79e210959 | ||
![]() |
bf5fb417e1 | ||
![]() |
6d253be878 | ||
![]() |
44228c3839 | ||
![]() |
e38556e821 | ||
![]() |
b695da8487 | ||
![]() |
85d179e27f | ||
![]() |
2d49f3e765 | ||
![]() |
5e40d7a4c1 | ||
![]() |
2aaab83427 | ||
![]() |
a1d21cd6af | ||
![]() |
5ec9b7c29f | ||
![]() |
efc91a4591 | ||
![]() |
ddbe0c825a | ||
![]() |
437dce9b15 | ||
![]() |
8fde909915 | ||
![]() |
ac851f3812 | ||
![]() |
94377487b7 | ||
![]() |
a59aafb1f4 | ||
![]() |
d962e40c58 | ||
![]() |
331596968b | ||
![]() |
26fd261d17 | ||
![]() |
ef4ae44898 | ||
![]() |
4b03f76db2 | ||
![]() |
040692455e | ||
![]() |
70a5e271f4 | ||
![]() |
2475de726f | ||
![]() |
255e17b1a9 | ||
![]() |
5c2b7767ed | ||
![]() |
2d9fdef974 | ||
![]() |
934f5a1bd0 | ||
![]() |
fbcffd8097 | ||
![]() |
34defdc5cd | ||
![]() |
121e564e12 | ||
![]() |
7bda63bc5a | ||
![]() |
8ae5e415db | ||
![]() |
2a1c85937c | ||
![]() |
a50e568899 | ||
![]() |
d449bd70a0 | ||
![]() |
6e3127c3a6 | ||
![]() |
d2897cec3f | ||
![]() |
96f8e905dd | ||
![]() |
150aace38e | ||
![]() |
b7b8eed6db | ||
![]() |
8f2a942536 | ||
![]() |
7ca88021c9 | ||
![]() |
6718bfdcfe | ||
![]() |
f1b4b57cdb | ||
![]() |
909b47425f | ||
![]() |
bafb2f3490 | ||
![]() |
c9f92ee7a8 | ||
![]() |
61d757493a | ||
![]() |
a7f48865ea | ||
![]() |
03a91adfab | ||
![]() |
896462ff10 | ||
![]() |
dfe1143184 | ||
![]() |
f7bd30cf12 | ||
![]() |
98cd2aa84c | ||
![]() |
7f50d7b23d | ||
![]() |
46b86ec6c7 | ||
![]() |
c46480c97f | ||
![]() |
be19a2a961 | ||
![]() |
c957031317 | ||
![]() |
3d8ffd6fe3 | ||
![]() |
a6ae487ebe | ||
![]() |
cc8a3c2dc1 | ||
![]() |
61edfea1de | ||
![]() |
593018021d | ||
![]() |
f9d9ca6191 | ||
![]() |
71fbf28984 | ||
![]() |
0fb12c4274 | ||
![]() |
e7b72d6df7 | ||
![]() |
5b776b605a | ||
![]() |
43933771c0 | ||
![]() |
da7b7f92a0 | ||
![]() |
95239e0590 | ||
![]() |
46878a7875 | ||
![]() |
254b7d1633 | ||
![]() |
4836fd9ec8 | ||
![]() |
bfb2ef021a | ||
![]() |
d45b4eedfd | ||
![]() |
c9ea990483 | ||
![]() |
0cd8a4ebcd | ||
![]() |
1508d6133d | ||
![]() |
f114897661 | ||
![]() |
a1f144aa0b | ||
![]() |
ac68893f6e | ||
![]() |
03db2a084b | ||
![]() |
604e99eee4 | ||
![]() |
0e75ef8c12 | ||
![]() |
c1f7d39290 | ||
![]() |
dc4becee0c | ||
![]() |
e6e9530b3f | ||
![]() |
0b6963aea1 | ||
![]() |
ac95a8b8d6 | ||
![]() |
2988146d59 | ||
![]() |
6cf8cc5fbe | ||
![]() |
1b48d3d1f6 | ||
![]() |
262b33261e | ||
![]() |
9badabade6 | ||
![]() |
556475318d | ||
![]() |
a1fea1d6a2 | ||
![]() |
e10a5bb8f8 | ||
![]() |
36d3ddd691 | ||
![]() |
97de2b8ef3 | ||
![]() |
b85c76f40e | ||
![]() |
5b6aa951ad | ||
![]() |
37a18fdaa7 | ||
![]() |
8a475f2523 | ||
![]() |
f3a032470d | ||
![]() |
e4bab45f3c | ||
![]() |
f8961d7647 | ||
![]() |
dc47d191ec | ||
![]() |
a74f256cea | ||
![]() |
292f7ddd25 | ||
![]() |
b89e57a1a6 | ||
![]() |
f85d4fa033 | ||
![]() |
d4d4a6121e | ||
![]() |
39aa284310 | ||
![]() |
eade3e7d37 | ||
![]() |
ecef1b35d1 | ||
![]() |
ed91e96d42 | ||
![]() |
a2695edbac | ||
![]() |
dd817c9159 | ||
![]() |
24be6c1544 | ||
![]() |
8fe37eeaf7 | ||
![]() |
e41b3083aa | ||
![]() |
c6d2cb66c7 | ||
![]() |
8652c4bce8 | ||
![]() |
b8a466d12b | ||
![]() |
3c8e35e4d2 | ||
![]() |
a2fb8a316b | ||
![]() |
eca35851d3 | ||
![]() |
824289cde1 | ||
![]() |
c060099ff8 | ||
![]() |
6bf086b707 | ||
![]() |
2789996000 | ||
![]() |
5353e889a3 | ||
![]() |
145ed346c8 | ||
![]() |
f83e31a1a2 | ||
![]() |
e68cbcd28c | ||
![]() |
20274bd4af | ||
![]() |
e584489eaf | ||
![]() |
eea250664d | ||
![]() |
69a0ae8722 | ||
![]() |
ef832d34dc | ||
![]() |
5dfc2fffca | ||
![]() |
71287b6b41 | ||
![]() |
dfcb5abd49 | ||
![]() |
1bb3bb6626 | ||
![]() |
f81de8ad0e | ||
![]() |
16223dfedc | ||
![]() |
314c33b011 | ||
![]() |
05d85c2d18 | ||
![]() |
e992dde287 | ||
![]() |
4f6b81ea9f | ||
![]() |
2aaa0cf726 | ||
![]() |
948fa5cfee | ||
![]() |
f152cad1fa | ||
![]() |
8936b96a25 | ||
![]() |
1a703af9cb | ||
![]() |
9844e9eff7 | ||
![]() |
cf980dfb47 | ||
![]() |
2c904f7e39 | ||
![]() |
d85cc0a0f2 | ||
![]() |
a87dc89ff1 | ||
![]() |
4357c19cad | ||
![]() |
0798701019 | ||
![]() |
52ef2acb08 | ||
![]() |
300aeb435e | ||
![]() |
b91220368c | ||
![]() |
ca048736b5 | ||
![]() |
7f3152b21a | ||
![]() |
6525516538 | ||
![]() |
ba133084c8 | ||
![]() |
6aee498d29 | ||
![]() |
f4a1d5eb8a | ||
![]() |
3d82a47e0b | ||
![]() |
3c03ec8ede | ||
![]() |
ea55e7333e | ||
![]() |
471ec25a58 | ||
![]() |
e56def92fd | ||
![]() |
d87f1777f9 | ||
![]() |
4d6e57337b | ||
![]() |
8cec08b29f | ||
![]() |
73dd0147bc | ||
![]() |
5341beae1e | ||
![]() |
e854fb85e5 | ||
![]() |
4a77dec870 | ||
![]() |
6b55f94405 | ||
![]() |
af3005c863 | ||
![]() |
01212750bf | ||
![]() |
e70b9c5994 | ||
![]() |
e76579ee00 | ||
![]() |
a56d551ef1 | ||
![]() |
fa1b1ad886 | ||
![]() |
3c3cff4398 | ||
![]() |
5917d92a07 | ||
![]() |
65ae16b70b | ||
![]() |
8fe617bd74 | ||
![]() |
6ae9efde59 | ||
![]() |
f429483c93 | ||
![]() |
d088f0a504 | ||
![]() |
b4d5bff1e6 | ||
![]() |
afc38dbff5 | ||
![]() |
f45bc3124a | ||
![]() |
4157844178 | ||
![]() |
95005ef110 | ||
![]() |
43c329988a | ||
![]() |
5fd8a12489 | ||
![]() |
358a6522c0 | ||
![]() |
ed958c1232 | ||
![]() |
95d1495397 | ||
![]() |
8ffd44f1ed | ||
![]() |
647bfb4e0d | ||
![]() |
4f6c3c0a9b | ||
![]() |
683c19bfbe | ||
![]() |
6230fef6b9 | ||
![]() |
7d947291e4 | ||
![]() |
25c6491962 | ||
![]() |
6c0aaa7bee | ||
![]() |
ef8f857fe8 | ||
![]() |
0b878af92a | ||
![]() |
4cc7432e91 | ||
![]() |
aea83ed662 | ||
![]() |
a00ceb3da5 | ||
![]() |
1d3ada20f9 | ||
![]() |
91c6b76230 | ||
![]() |
154fc83930 | ||
![]() |
b186ec635e | ||
![]() |
a1ac80e53a | ||
![]() |
62cbba5982 | ||
![]() |
1b0b1d7797 | ||
![]() |
8b5a3ac9b3 | ||
![]() |
02b007a0f6 | ||
![]() |
da0b70faa4 | ||
![]() |
4c5d4427dc | ||
![]() |
6425f6e7d1 | ||
![]() |
c3102b9d43 | ||
![]() |
d9fb68dce9 | ||
![]() |
0935bd4afd | ||
![]() |
f0813fcbc1 | ||
![]() |
54fc57d1f3 | ||
![]() |
fcaf01807c | ||
![]() |
c6156c5cbe | ||
![]() |
db326a706c | ||
![]() |
6360d5a88c | ||
![]() |
ee760908c5 | ||
![]() |
5553d046ce | ||
![]() |
ee76c256f8 | ||
![]() |
9e8dd6f349 | ||
![]() |
754abdd2ec | ||
![]() |
0b1bc9fe65 | ||
![]() |
1099c42eb1 | ||
![]() |
80706b9f61 | ||
![]() |
3c9fdec8dd | ||
![]() |
e518380025 | ||
![]() |
ddd1bb42cf | ||
![]() |
56c479f73d | ||
![]() |
af3cc28f43 | ||
![]() |
2b24caba34 | ||
![]() |
bf039978be | ||
![]() |
431d7e12b1 | ||
![]() |
92f8a5d522 | ||
![]() |
7fe0db7d7c | ||
![]() |
e90e550f8f | ||
![]() |
6ffd791f12 | ||
![]() |
3a68b6dd7b | ||
![]() |
6c4d461ade | ||
![]() |
adcefbae17 | ||
![]() |
f9c5768ee3 | ||
![]() |
f443a2bcce | ||
![]() |
5a3eb8c9df | ||
![]() |
c4c6de66de | ||
![]() |
e6a051e9c6 | ||
![]() |
67b09e3794 | ||
![]() |
04f57b753e | ||
![]() |
9723d929e1 | ||
![]() |
b634799ef9 | ||
![]() |
917783e34b | ||
![]() |
20bc4d1c2d | ||
![]() |
36f134f1c6 | ||
![]() |
2845578d07 | ||
![]() |
d6bcdda8a0 | ||
![]() |
50c3c4133f | ||
![]() |
94eecffa88 | ||
![]() |
6167012e32 | ||
![]() |
11ad52c120 | ||
![]() |
c428b1deef | ||
![]() |
77e2fde15a | ||
![]() |
f609476270 | ||
![]() |
ba89698509 | ||
![]() |
64db1c242e | ||
![]() |
79ccf2e2be | ||
![]() |
093ad47a3d | ||
![]() |
e59b0929ed | ||
![]() |
3018845d0a | ||
![]() |
4ace067265 | ||
![]() |
526fab8008 | ||
![]() |
e72e02773d | ||
![]() |
e27aadc432 | ||
![]() |
7432297dce | ||
![]() |
3375086c6f | ||
![]() |
7e292cdde4 | ||
![]() |
901951eaa8 | ||
![]() |
b68c59b835 | ||
![]() |
5142915e83 | ||
![]() |
343a1498ac | ||
![]() |
ee56de9e55 | ||
![]() |
c80542c280 | ||
![]() |
c8fafd067e | ||
![]() |
beb0f2d410 | ||
![]() |
22df3ec7f2 | ||
![]() |
addc6618ba | ||
![]() |
f955998d8b | ||
![]() |
fa86601f55 | ||
![]() |
3f0e5865f9 | ||
![]() |
466d01ce09 | ||
![]() |
c117af34c3 | ||
![]() |
7233482fe7 | ||
![]() |
3517e9bee9 | ||
![]() |
3ee20ad17a | ||
![]() |
ca1710c548 | ||
![]() |
c9142b604a | ||
![]() |
bde1d33b6f | ||
![]() |
85279c5601 | ||
![]() |
dc3302e73b | ||
![]() |
35056959bb | ||
![]() |
76636933e4 | ||
![]() |
713c40451f | ||
![]() |
f4f099995e | ||
![]() |
7b65270a0f | ||
![]() |
8a1fd78811 | ||
![]() |
89be29ad7f | ||
![]() |
4d67c98f04 | ||
![]() |
fc6f5fd331 | ||
![]() |
a641b20db7 | ||
![]() |
4afb35c53e | ||
![]() |
bd54d47037 | ||
![]() |
4b55d7507c | ||
![]() |
7903a93042 | ||
![]() |
02cf98c759 | ||
![]() |
ba3778acae | ||
![]() |
56c93ff27e | ||
![]() |
94050b655b | ||
![]() |
f49fab3599 | ||
![]() |
ba97ca9f3a | ||
![]() |
5d7a36e1e3 | ||
![]() |
047f9e75c3 | ||
![]() |
316ced7fef | ||
![]() |
064552188e | ||
![]() |
a35523f78d | ||
![]() |
5963c6cd95 | ||
![]() |
6f3a5448c5 | ||
![]() |
72c382d5c3 | ||
![]() |
86d3389438 | ||
![]() |
74c26c17ba | ||
![]() |
b73ad7a8f0 | ||
![]() |
0bf4c57a43 | ||
![]() |
c47ed59c96 | ||
![]() |
c1b241dc29 | ||
![]() |
5bcb361c6e | ||
![]() |
3651b16b2c | ||
![]() |
0cc130a8b9 | ||
![]() |
37072dc01e | ||
![]() |
8f93d723a5 | ||
![]() |
1c2203804a | ||
![]() |
edb6577d06 | ||
![]() |
165dc2ccda | ||
![]() |
2a6e3c08cc | ||
![]() |
16b3e8af35 | ||
![]() |
b9baecd573 | ||
![]() |
78638e77da | ||
![]() |
8659182795 | ||
![]() |
0eb47fd804 | ||
![]() |
1008bd16e2 | ||
![]() |
3912860f98 | ||
![]() |
897e303035 | ||
![]() |
4b59780117 | ||
![]() |
ff2083941a | ||
![]() |
a3116a3c5a | ||
![]() |
86555b936d | ||
![]() |
9fab40d7e9 | ||
![]() |
a3b6917aba | ||
![]() |
bf8e2078bc | ||
![]() |
8103321245 | ||
![]() |
90a9570e7d | ||
![]() |
1b9bc2f7a3 | ||
![]() |
764f532b99 | ||
![]() |
2d3dfde6bb | ||
![]() |
88c8fddbce | ||
![]() |
40b30b7aea | ||
![]() |
c95fa8fb96 | ||
![]() |
70503e1fd7 | ||
![]() |
34b165b7d2 | ||
![]() |
03ef13fd76 | ||
![]() |
7bd6c9acab | ||
![]() |
1d05680126 | ||
![]() |
d1dbe49333 | ||
![]() |
1b6c0a9e42 | ||
![]() |
7d0439b006 | ||
![]() |
737792a1df | ||
![]() |
d1dbd891b4 | ||
![]() |
62880336d9 | ||
![]() |
863189b2e5 | ||
![]() |
67feaf3178 | ||
![]() |
295bbc5c40 | ||
![]() |
46614f4562 | ||
![]() |
228a61e7ed | ||
![]() |
930da6dde3 | ||
![]() |
fc21dc2019 | ||
![]() |
e05f44d079 | ||
![]() |
9afcd48f12 | ||
![]() |
26e25069ad | ||
![]() |
1bf9693952 | ||
![]() |
7d4987da5c | ||
![]() |
2f0a5a4065 | ||
![]() |
1be7f4e550 | ||
![]() |
0ad505ba30 | ||
![]() |
2db0bf44f3 | ||
![]() |
64b657507c | ||
![]() |
2fc6f9ea77 | ||
![]() |
7406ab5d47 | ||
![]() |
827ffc2d79 | ||
![]() |
e846aa801f | ||
![]() |
3a7926eebe | ||
![]() |
9907087dc3 | ||
![]() |
ad863b62a7 | ||
![]() |
5c101d116a | ||
![]() |
719d0e791c | ||
![]() |
44aabe19d7 | ||
![]() |
746567ab52 | ||
![]() |
ad2b98e780 | ||
![]() |
8883793b06 | ||
![]() |
aca60642a9 | ||
![]() |
d3e93fed24 | ||
![]() |
3b6a20bf67 | ||
![]() |
bcc6daa39f | ||
![]() |
68968ca70a | ||
![]() |
b6e9c5b840 | ||
![]() |
c81f589338 | ||
![]() |
c325e6d61b | ||
![]() |
d1ba6ab610 | ||
![]() |
588fc484a4 | ||
![]() |
848b700f84 | ||
![]() |
5221615c42 | ||
![]() |
28b450498f | ||
![]() |
004f4ce38c | ||
![]() |
1fb38fe56d | ||
![]() |
dc868853a8 | ||
![]() |
73f67827f3 | ||
![]() |
92989a618a | ||
![]() |
07d876348e | ||
![]() |
9234f8bc46 | ||
![]() |
12941640a4 | ||
![]() |
de8bcce8be | ||
![]() |
c8f4db618e | ||
![]() |
279b80b4de | ||
![]() |
7a3155f2b4 | ||
![]() |
d25f12590d | ||
![]() |
0b4379abad | ||
![]() |
2e4b9d7811 | ||
![]() |
00858ae013 | ||
![]() |
aed3e5ae46 | ||
![]() |
3e8b12cb91 | ||
![]() |
8148a67e40 | ||
![]() |
8ece6bf79f | ||
![]() |
f086ee79a5 | ||
![]() |
fb44c02437 | ||
![]() |
8af15d06bc | ||
![]() |
8beece2f73 | ||
![]() |
71f281516a | ||
![]() |
7c243623c8 | ||
![]() |
39fd025252 | ||
![]() |
5d5f9c4046 | ||
![]() |
318c736083 | ||
![]() |
159ce66050 | ||
![]() |
26f01d6f8f | ||
![]() |
14419ca567 | ||
![]() |
a504d96897 | ||
![]() |
0b56b0d8dc | ||
![]() |
3d5b3ea9a1 | ||
![]() |
c4ab1c5180 | ||
![]() |
b9c10813ce | ||
![]() |
17e2918721 | ||
![]() |
4bcc54befd | ||
![]() |
70c9050fad | ||
![]() |
0355ff3e47 | ||
![]() |
4504fa1818 | ||
![]() |
8ed359cc7e | ||
![]() |
a36aa50d16 | ||
![]() |
9a2b22943e | ||
![]() |
2e9eb1d387 | ||
![]() |
99ffd43d95 | ||
![]() |
e8f7aa6201 | ||
![]() |
1cbbcaa800 | ||
![]() |
1186f24cea | ||
![]() |
0a4df44315 | ||
![]() |
3f13031ea3 | ||
![]() |
99576dc64a | ||
![]() |
ecdf585216 | ||
![]() |
41e238bad7 | ||
![]() |
20794381f1 | ||
![]() |
994d96bc95 | ||
![]() |
ef9447cb54 | ||
![]() |
3dfdc9bdc5 | ||
![]() |
73c046974b | ||
![]() |
086579b7f7 | ||
![]() |
ffc724bfbc | ||
![]() |
b4f3bf71ad | ||
![]() |
d6eab441a4 | ||
![]() |
52cfe846cb | ||
![]() |
70d95b4764 | ||
![]() |
10085ccf41 | ||
![]() |
6b25dcfa70 | ||
![]() |
78287f5504 | ||
![]() |
f4f6c21abf | ||
![]() |
2c3c07ce52 | ||
![]() |
140a194b0a | ||
![]() |
6043d16429 | ||
![]() |
22fa4c6dae | ||
![]() |
a891a319d8 | ||
![]() |
d320c703b1 | ||
![]() |
16b8184dad | ||
![]() |
01198c3ac5 | ||
![]() |
19e28bf367 | ||
![]() |
344f355995 | ||
![]() |
c7c150a7dd | ||
![]() |
0749cbcc0f | ||
![]() |
6f347868ff | ||
![]() |
3807a16f9a | ||
![]() |
e187778dbc | ||
![]() |
1f7b8b8210 | ||
![]() |
841c4eabf3 | ||
![]() |
b4d7311a1c | ||
![]() |
55a5b1891e | ||
![]() |
ee11944645 | ||
![]() |
24a8e06ee7 | ||
![]() |
509fd0d802 | ||
![]() |
861f8ddb6c | ||
![]() |
4f8fe9e611 | ||
![]() |
359cb10fa3 | ||
![]() |
16feb61ebf | ||
![]() |
45ab326355 | ||
![]() |
27ec828142 | ||
![]() |
64a60f561e | ||
![]() |
f93a104d68 | ||
![]() |
277aad26c8 | ||
![]() |
650b77081f | ||
![]() |
96bcb8c671 | ||
![]() |
b442690350 | ||
![]() |
697b8199e4 | ||
![]() |
3fe67b8da1 | ||
![]() |
50024c8c55 | ||
![]() |
2ff1b88bac | ||
![]() |
81b19a96d7 | ||
![]() |
64c653feb9 | ||
![]() |
4461e9025b | ||
![]() |
5ca011e5fe | ||
![]() |
f4ee930c4a | ||
![]() |
d335ab71af | ||
![]() |
0d34152398 | ||
![]() |
fa6fe668c3 | ||
![]() |
4f3ec612a1 | ||
![]() |
c190cedcea | ||
![]() |
82426700fd | ||
![]() |
0ab2ca1880 | ||
![]() |
49c341b05a | ||
![]() |
a2180c19ac | ||
![]() |
b0cf35e422 | ||
![]() |
062ff5b8a4 | ||
![]() |
7269be9142 | ||
![]() |
93b415a6be | ||
![]() |
ec105b35a9 | ||
![]() |
ac5c7d5230 | ||
![]() |
170377f209 | ||
![]() |
1fd3b04a43 | ||
![]() |
1530d7f3d1 | ||
![]() |
a8d9d74818 | ||
![]() |
2478d44b2e | ||
![]() |
3bb2ac15e9 | ||
![]() |
200b29f6f8 | ||
![]() |
5be883f9ce | ||
![]() |
aff3f3df03 | ||
![]() |
5c74c0221a | ||
![]() |
c2345c1562 | ||
![]() |
d16ee88641 | ||
![]() |
bd8177c17d | ||
![]() |
96f68695b0 | ||
![]() |
651b4100dd | ||
![]() |
cee7ffeb04 | ||
![]() |
2bfe6ceebb | ||
![]() |
a185019da5 | ||
![]() |
2f6ba1e219 | ||
![]() |
e4f992ba1d | ||
![]() |
735408c336 | ||
![]() |
65a66a104c | ||
![]() |
9a10abeff5 | ||
![]() |
9959176575 | ||
![]() |
f45f390c67 | ||
![]() |
d97fcc5c15 | ||
![]() |
d60ba8cb76 | ||
![]() |
8b70c1321e | ||
![]() |
e877560ec7 | ||
![]() |
1c738ac276 | ||
![]() |
f91e52f1ad | ||
![]() |
23a4a3f57b | ||
![]() |
1e56f03785 | ||
![]() |
7774c3c83c | ||
![]() |
615a47fe5b | ||
![]() |
f1efe21df6 | ||
![]() |
d3e0ffed47 | ||
![]() |
cf73625830 | ||
![]() |
03887e7c41 | ||
![]() |
7667cab772 | ||
![]() |
016afd84e4 | ||
![]() |
6856f95cb9 | ||
![]() |
4a1530eea5 | ||
![]() |
fe1e4931c3 | ||
![]() |
b665456b9d | ||
![]() |
ed959e492a | ||
![]() |
b1585ac7f2 | ||
![]() |
67cee55754 | ||
![]() |
1d98b8c29f | ||
![]() |
a573c507b4 | ||
![]() |
8188da0c75 | ||
![]() |
12f57e3975 | ||
![]() |
dbf31f767d | ||
![]() |
d20962e5cd | ||
![]() |
6d64478f4f | ||
![]() |
09c6a167fb | ||
![]() |
3d9af688e7 | ||
![]() |
42c690dfd4 | ||
![]() |
cdb2c30bd4 | ||
![]() |
7f3dab9fc0 | ||
![]() |
1a30744f5b | ||
![]() |
7653558cc0 | ||
![]() |
28f127122f | ||
![]() |
d55bcc93bb | ||
![]() |
f420cca78c | ||
![]() |
34e75a46e5 | ||
![]() |
8265942f18 | ||
![]() |
5c39d3c8d1 | ||
![]() |
998a7107a8 | ||
![]() |
d734ed108e | ||
![]() |
58f991aaa1 | ||
![]() |
417f79ab6a | ||
![]() |
2f6c0fb126 | ||
![]() |
14d7437da9 | ||
![]() |
96959b401c | ||
![]() |
74813aaedc | ||
![]() |
d91b3b85b0 | ||
![]() |
966bcd31f6 | ||
![]() |
0746aaed43 | ||
![]() |
deef684d70 | ||
![]() |
78737ed1ab | ||
![]() |
4106e7cb21 | ||
![]() |
f980b55c1c | ||
![]() |
395509a19a | ||
![]() |
c244575e89 | ||
![]() |
cc389e1149 | ||
![]() |
97374d47c5 | ||
![]() |
745245ef5d | ||
![]() |
9ed8722169 | ||
![]() |
bb76015aa1 | ||
![]() |
080ea9e7a4 | ||
![]() |
37fd04593b | ||
![]() |
e1fea7f8e3 | ||
![]() |
a259446a60 | ||
![]() |
e179596ac2 | ||
![]() |
c956c212be | ||
![]() |
cc7374351d | ||
![]() |
3868b0aaae | ||
![]() |
480a1b482a | ||
![]() |
47994f9557 | ||
![]() |
0a32d3f915 | ||
![]() |
22ae5a04e7 | ||
![]() |
d1bf380e46 | ||
![]() |
16bad82564 | ||
![]() |
50c6cd4b27 | ||
![]() |
9f3997f103 | ||
![]() |
1753d55d77 | ||
![]() |
20d15dab54 | ||
![]() |
509cea087f | ||
![]() |
5bd1e050fe | ||
![]() |
2b1ce1e39a | ||
![]() |
0040ff2920 | ||
![]() |
25edc3c952 | ||
![]() |
99ca2dedea | ||
![]() |
a6bb83b4f8 | ||
![]() |
2008c8bc92 | ||
![]() |
072537968f | ||
![]() |
7fbde90095 | ||
![]() |
c6f1867478 | ||
![]() |
a5d6131238 | ||
![]() |
fac7cc266e | ||
![]() |
743be6a2cd | ||
![]() |
56b2ee1f0d | ||
![]() |
d305adaa07 | ||
![]() |
ed128d1f17 | ||
![]() |
5a47c2f4a7 | ||
![]() |
99b43e0a4e | ||
![]() |
c58e6f6239 | ||
![]() |
9a915ae8c4 | ||
![]() |
4f8d60c4d2 | ||
![]() |
a7e8c279f2 | ||
![]() |
8bccdac99d | ||
![]() |
a7fd250f4b | ||
![]() |
bf469ef91b | ||
![]() |
4a1a64b728 | ||
![]() |
95bd8c8653 | ||
![]() |
984da54e1a | ||
![]() |
50effd3b70 | ||
![]() |
16ebaf1bfe | ||
![]() |
d93173e8d2 | ||
![]() |
1f9e014df0 | ||
![]() |
32e7b7a3b4 | ||
![]() |
eca6aa9b04 | ||
![]() |
b352137fb3 | ||
![]() |
7d75454623 | ||
![]() |
813a856ed8 | ||
![]() |
b846384ba7 | ||
![]() |
acf83f6261 | ||
![]() |
df730efbd0 | ||
![]() |
4143824bb8 | ||
![]() |
28fe62f918 | ||
![]() |
f763dcf752 | ||
![]() |
b5c98e6917 | ||
![]() |
49dc4b86e2 | ||
![]() |
a419e64cef | ||
![]() |
045b87c1c8 | ||
![]() |
2b1fd76365 | ||
![]() |
da4115f122 | ||
![]() |
41e14e176c | ||
![]() |
93075fc70f | ||
![]() |
74bf69f984 | ||
![]() |
898795fba1 | ||
![]() |
81ba49f220 | ||
![]() |
11d06e34b1 | ||
![]() |
cfcf496cc1 | ||
![]() |
8a32777802 | ||
![]() |
22928c3f23 | ||
![]() |
4a37759dea | ||
![]() |
3a7929369a | ||
![]() |
139edd2df1 | ||
![]() |
1d99d8a535 | ||
![]() |
a8aea654a1 | ||
![]() |
c0f3d38695 | ||
![]() |
2cc3dd0694 | ||
![]() |
856211d964 | ||
![]() |
a4a4be9983 | ||
![]() |
d55e234167 | ||
![]() |
395a78c360 | ||
![]() |
41e9770a61 | ||
![]() |
35f73708ef | ||
![]() |
a0a0830e5f | ||
![]() |
233909d6ca | ||
![]() |
fca9b4d481 | ||
![]() |
1fb6b45560 | ||
![]() |
24fdbaf0bd | ||
![]() |
c3bd151cc3 | ||
![]() |
8a5bffa881 | ||
![]() |
796afb3919 | ||
![]() |
b8822e996d | ||
![]() |
e9bfb77300 | ||
![]() |
2d07cc1913 | ||
![]() |
31c9694666 | ||
![]() |
098298c1ea | ||
![]() |
37a8b9f253 | ||
![]() |
54371503ae | ||
![]() |
38625d2cd5 | ||
![]() |
7ef769431a | ||
![]() |
461eacff45 | ||
![]() |
2166e0633c | ||
![]() |
80c7b86084 | ||
![]() |
af2d376fb9 | ||
![]() |
369a6dfaea | ||
![]() |
41e55d8d44 | ||
![]() |
67e5578bba | ||
![]() |
b2ceb8e9d6 | ||
![]() |
af291845ec | ||
![]() |
6438c65381 | ||
![]() |
15ed0cc8fa | ||
![]() |
a5086b8d11 | ||
![]() |
5a06c9195e | ||
![]() |
dacf813676 | ||
![]() |
eeb2c790c1 | ||
![]() |
bbbd6959a8 | ||
![]() |
01b60dbf34 | ||
![]() |
9f27a89dc4 | ||
![]() |
b78b2e5b82 | ||
![]() |
0cf4cdc1c5 | ||
![]() |
bb68373489 | ||
![]() |
1e31fadb70 | ||
![]() |
3d9b3c68f3 | ||
![]() |
7a74c3355b | ||
![]() |
a99fa4d4a2 | ||
![]() |
4a749cf91d | ||
![]() |
cc862af249 | ||
![]() |
47294d37bf | ||
![]() |
ff924abcfb | ||
![]() |
04ad79e416 | ||
![]() |
0b164b33b9 | ||
![]() |
532cd4d717 | ||
![]() |
eb7b4cbf0e | ||
![]() |
aeba5d683d | ||
![]() |
6e7024e31e | ||
![]() |
13c1a553a1 | ||
![]() |
69f4846407 | ||
![]() |
f2bfdbcf84 | ||
![]() |
1f19324ff6 | ||
![]() |
90b4a0bf2d | ||
![]() |
c242ba0412 | ||
![]() |
8bbc89d8f4 | ||
![]() |
0193893e75 | ||
![]() |
1e62262050 | ||
![]() |
791325f584 | ||
![]() |
e2420c56d3 | ||
![]() |
a698b56ae8 | ||
![]() |
03d789560c | ||
![]() |
32db97e67d | ||
![]() |
8498a16979 | ||
![]() |
b36a2ea66b | ||
![]() |
eaa297cd90 | ||
![]() |
930af49030 | ||
![]() |
2053078a16 | ||
![]() |
849396459e | ||
![]() |
55e37a63bc | ||
![]() |
4411c312e5 | ||
![]() |
4307cb0ff1 | ||
![]() |
e275fe590b | ||
![]() |
aa172b6c68 | ||
![]() |
7f0e7980cd | ||
![]() |
44d18d2fb6 | ||
![]() |
3e75bc5591 | ||
![]() |
1c013bc85b | ||
![]() |
904ee2cab4 | ||
![]() |
8d396f3832 | ||
![]() |
08063154e7 | ||
![]() |
b11a168e72 | ||
![]() |
512b678b73 | ||
![]() |
e61467bb7f | ||
![]() |
31fe5efaa1 | ||
![]() |
469b424cfe | ||
![]() |
2a4eebcfc4 | ||
![]() |
7bd8fae72a | ||
![]() |
0d89eb6cce | ||
![]() |
9902209fb9 | ||
![]() |
dd36913496 | ||
![]() |
3d9e4a66ff | ||
![]() |
d26bba8e14 | ||
![]() |
8570ff2578 | ||
![]() |
fc96700475 | ||
![]() |
8f0d7b6a4e | ||
![]() |
497f089c2f | ||
![]() |
378fbe86f4 | ||
![]() |
dd38e61230 | ||
![]() |
0cf91fb05e | ||
![]() |
0b3b5d3965 | ||
![]() |
c4203f25d5 | ||
![]() |
7021527f5c | ||
![]() |
c25340fca0 | ||
![]() |
7d9afeefea | ||
![]() |
fb0e1d81e4 | ||
![]() |
05a4918ef0 | ||
![]() |
ec96962a87 | ||
![]() |
6bc4d061f1 | ||
![]() |
3a809cc671 | ||
![]() |
ba4d2861f5 | ||
![]() |
24f6f982e9 | ||
![]() |
5f56d5959d | ||
![]() |
199e6c7299 | ||
![]() |
7934b87336 | ||
![]() |
8b99ea44b2 | ||
![]() |
c3add84d86 | ||
![]() |
95b4635291 | ||
![]() |
44caed878c | ||
![]() |
e1c734e113 | ||
![]() |
0a8a6bb720 | ||
![]() |
ba8be3f57d | ||
![]() |
7b74d9c077 | ||
![]() |
6f64b943f5 | ||
![]() |
4731da83cd | ||
![]() |
ce134bbd10 | ||
![]() |
09a84edb09 | ||
![]() |
5040d230fb | ||
![]() |
9440401ca3 | ||
![]() |
3dc09062d0 | ||
![]() |
315b6a9690 | ||
![]() |
6b986c8c91 |
@@ -1,21 +1,28 @@
|
||||
.git
|
||||
node-modules
|
||||
__build__
|
||||
__server_build__
|
||||
.idea
|
||||
.vscode
|
||||
.DS_Store
|
||||
*.iml
|
||||
|
||||
# Build folders
|
||||
node_modules
|
||||
build
|
||||
dist
|
||||
typings
|
||||
tsd_typings
|
||||
npm-debug.log
|
||||
dist
|
||||
coverage
|
||||
.idea
|
||||
*.iml
|
||||
__build__
|
||||
__server_build__
|
||||
|
||||
# Node
|
||||
*.log
|
||||
npm-debug.log.*
|
||||
|
||||
# Angular files
|
||||
*.ngfactory.ts
|
||||
*.css.shim.ts
|
||||
*.scss.shim.ts
|
||||
.DS_Store
|
||||
|
||||
# Webpack files
|
||||
webpack.records.json
|
||||
npm-debug.log.*
|
||||
morgan.log
|
||||
yarn-error.log
|
||||
*.css
|
||||
package-lock.json
|
||||
|
28
.github/pull_request_template.md
vendored
Normal file
28
.github/pull_request_template.md
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
## References
|
||||
_Add references/links to any related tickets or PRs. These may include:_
|
||||
* Link to [Angular issue or PR](https://github.com/DSpace/dspace-angular/issues) related to this PR, if any
|
||||
* Link to [JIRA](https://jira.lyrasis.org/projects/DS/summary) ticket(s), if any
|
||||
|
||||
## Description
|
||||
Short summary of changes (1-2 sentences).
|
||||
|
||||
## Instructions for Reviewers
|
||||
Please add a more detailed description of the changes made by your PR. At a minimum, providing a bulleted list of changes in your PR is helpful to reviewers.
|
||||
|
||||
List of changes in this PR:
|
||||
* First, ...
|
||||
* Second, ...
|
||||
|
||||
**Include guidance for how to test or review your PR.** This may include: steps to reproduce a bug, screenshots or description of a new feature, or reasons behind specific changes.
|
||||
|
||||
## Checklist
|
||||
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
|
||||
|
||||
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
||||
- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
|
||||
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
||||
- [ ] My PR passes all specs/tests and includes new/updated specs for any bug fixes, improvements or new features. A few reminders about what constitutes good tests:
|
||||
* Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
|
||||
* Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
|
||||
* For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
|
||||
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/master/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,6 +5,8 @@
|
||||
/tsd_typings/
|
||||
npm-debug.log
|
||||
|
||||
/build/
|
||||
|
||||
/config/environment.dev.js
|
||||
/config/environment.prod.js
|
||||
|
||||
@@ -32,3 +34,5 @@ yarn-error.log
|
||||
*.css
|
||||
|
||||
package-lock.json
|
||||
|
||||
.java-version
|
||||
|
60
.travis.yml
60
.travis.yml
@@ -1,32 +1,58 @@
|
||||
sudo: required
|
||||
dist: trusty
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- google-chrome
|
||||
packages:
|
||||
- google-chrome-stable
|
||||
dist: bionic
|
||||
|
||||
env:
|
||||
# Install the latest docker-compose version for ci testing.
|
||||
# The default installation in travis is not compatible with the latest docker-compose file version.
|
||||
COMPOSE_VERSION: 1.24.1
|
||||
# The ci step will test the dspace-angular code against DSpace REST.
|
||||
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
||||
DSPACE_REST_HOST: localhost
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: '/server/api'
|
||||
DSPACE_REST_SSL: false
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
|
||||
before_install:
|
||||
# Docker Compose Install
|
||||
- curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
|
||||
- chmod +x docker-compose
|
||||
- sudo mv docker-compose /usr/local/bin
|
||||
|
||||
install:
|
||||
# update chrome
|
||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install google-chrome-stable
|
||||
# Start up DSpace 7 using the entities database dump
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
||||
# Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update
|
||||
- docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||
- travis_retry yarn install
|
||||
|
||||
before_script:
|
||||
# The following line could be enabled to verify that the rest server is responding.
|
||||
# Currently, "yarn run build" takes enough time to run to allow the service to be available
|
||||
#- curl http://localhost:8080/
|
||||
|
||||
after_script:
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml down
|
||||
|
||||
language: node_js
|
||||
|
||||
node_js:
|
||||
- "8"
|
||||
- "9"
|
||||
- "10"
|
||||
- "12"
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
|
||||
bundler_args: --retry 5
|
||||
|
||||
before_install:
|
||||
- travis_retry yarn run global
|
||||
|
||||
install:
|
||||
- travis_retry yarn install
|
||||
|
||||
script:
|
||||
# Use Chromium instead of Chrome.
|
||||
- export CHROME_BIN=chromium-browser
|
||||
- yarn run build
|
||||
- yarn run ci
|
||||
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
||||
|
@@ -1,10 +1,12 @@
|
||||
# This image will be published as dspace/dspace-angular
|
||||
# See https://dspace-labs.github.io/DSpace-Docker-Images/ for usage details
|
||||
|
||||
FROM node:8-alpine
|
||||
FROM node:12-alpine
|
||||
WORKDIR /app
|
||||
ADD . /app/
|
||||
EXPOSE 3000
|
||||
|
||||
RUN yarn install
|
||||
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
||||
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||
RUN yarn install --network-timeout 300000
|
||||
CMD yarn run watch
|
||||
|
39
LICENSE
Normal file
39
LICENSE
Normal file
@@ -0,0 +1,39 @@
|
||||
DSpace source code BSD License:
|
||||
|
||||
Copyright (c) 2002-2020, LYRASIS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
- Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
|
||||
- Redistributions in binary form must reproduce the above copyright
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name DuraSpace nor the name of the DSpace Foundation
|
||||
nor the names of its contributors may be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
||||
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
|
||||
OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
||||
|
||||
DSpace uses third-party libraries which may be distributed under
|
||||
different licenses to the above. Information about these licenses
|
||||
is detailed in the LICENSES_THIRD_PARTY file at the root of the source
|
||||
tree. You must agree to the terms of these licenses, in addition to
|
||||
the above DSpace source code license, in order to use this software.
|
15
LICENSES_THIRD_PARTY
Normal file
15
LICENSES_THIRD_PARTY
Normal file
@@ -0,0 +1,15 @@
|
||||
|
||||
DSpace uses third-party libraries which may be distributed under different licenses.
|
||||
A summary of all third-party, production dependencies used by this user interface may be found by running:
|
||||
|
||||
npx license-checker --production --summary
|
||||
|
||||
(Additional license-checker options may be found in its documentation: https://github.com/davglass/license-checker)
|
||||
|
||||
You must agree to the terms of these licenses, in addition to the DSpace source code license, in order to use this
|
||||
software.
|
||||
|
||||
PLEASE NOTE: Some third-party dependencies may be listed under multiple licenses if they are dual-licensed.
|
||||
This is especially true of anything listed as GPL (or similar), as DSpace does NOT allow for the inclusion of
|
||||
any dependencies that are solely released under GPL (or similar) terms. For more info see:
|
||||
https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions
|
98
README.md
98
README.md
@@ -3,18 +3,17 @@
|
||||
dspace-angular
|
||||
==============
|
||||
|
||||
> The next UI for DSpace, based on Angular Universal.
|
||||
> The next UI for DSpace 7, based on Angular Universal.
|
||||
|
||||
This project is currently in pre-alpha.
|
||||
This project is currently under active development. For more information on the DSpace 7 release see the [DSpace 7.0 Release Status wiki page](https://wiki.lyrasis.org/display/DSPACE/DSpace+Release+7.0+Status)
|
||||
|
||||
You can find additional information on the [wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+UI) or [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular).
|
||||
You can find additional information on the DSpace 7 Angular UI on the [wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development).
|
||||
|
||||
If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [here](https://github.com/DSpace-Labs/angular2-ui-prototype)
|
||||
|
||||
Quick start
|
||||
-----------
|
||||
|
||||
**Ensure you're running [Node](https://nodejs.org) >= `v8.0.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
|
||||
**Ensure you're running [Node](https://nodejs.org) `v10.x` or `v12.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
|
||||
|
||||
```bash
|
||||
# clone the repo
|
||||
@@ -32,8 +31,6 @@ yarn start
|
||||
|
||||
Then go to [http://localhost:3000](http://localhost:3000) in your browser
|
||||
|
||||
NOTE: currently there's not much to see at that URL. We really do need your help. If you're interested in jumping in, and you've made it this far, please look at the [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular), grab a card, and get to work. Thanks!
|
||||
|
||||
Not sure where to start? watch the training videos linked in the [Introduction to the technology](#introduction-to-the-technology) section below.
|
||||
|
||||
Table of Contents
|
||||
@@ -42,30 +39,33 @@ Table of Contents
|
||||
- [Introduction to the technology](#introduction-to-the-technology)
|
||||
- [Requirements](#requirements)
|
||||
- [Installing](#installing)
|
||||
- [Configuring](#configuring)
|
||||
- [Configuring](#configuring)
|
||||
- [Running the app](#running-the-app)
|
||||
- [Running in production mode](#running-in-production-mode)
|
||||
- [Running in production mode](#running-in-production-mode)
|
||||
- [Deploy](#deploy)
|
||||
- [Running the application with Docker](#running-the-application-with-docker)
|
||||
- [Cleaning](#cleaning)
|
||||
- [Testing](#testing)
|
||||
- [Test a Pull Request](#test-a-pull-request)
|
||||
- [Documentation](#documentation)
|
||||
- [Other commands](#other-commands)
|
||||
- [Recommended Editors/IDEs](#recommended-editorsides)
|
||||
- [Collaborating](#collaborating)
|
||||
- [File Structure](#file-structure)
|
||||
- [3rd Party Library Installation](#3rd-party-library-installation)
|
||||
- [Managing Dependencies (via yarn)](#managing-dependencies-via-yarn)
|
||||
- [Frequently asked questions](#frequently-asked-questions)
|
||||
- [License](#license)
|
||||
|
||||
Introduction to the technology
|
||||
------------------------------
|
||||
|
||||
You can find more information on the technologies used in this project (Angular 2, Typescript, Angular Universal, RxJS, etc) on the [DuraSpace wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+UI+Technology+Stack)
|
||||
You can find more information on the technologies used in this project (Angular.io, Typescript, Angular Universal, RxJS, etc) on the [LYRASIS wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+UI+Technology+Stack)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- [Node.js](https://nodejs.org), [npm](https://www.npmjs.com/), and [yarn](https://yarnpkg.com)
|
||||
- Ensure you're running node >= `v8.x`, npm >= `v5.x` and yarn >= `v1.x`
|
||||
- Ensure you're running node `v10.x` or `v12.x`, npm >= `v5.x` and yarn >= `v1.x`
|
||||
|
||||
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
||||
|
||||
@@ -75,8 +75,7 @@ Installing
|
||||
- `yarn run global` to install the required global dependencies
|
||||
- `yarn install` to install the local dependencies
|
||||
|
||||
Configuring
|
||||
-----------
|
||||
### Configuring
|
||||
|
||||
Default configuration file is located in `config/` folder.
|
||||
|
||||
@@ -98,8 +97,7 @@ Running the app
|
||||
|
||||
After you have installed all dependencies you can now run the app. Run `yarn run watch` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:3000`.
|
||||
|
||||
Running in production mode
|
||||
--------------------------
|
||||
### Running in production mode
|
||||
|
||||
When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload.
|
||||
|
||||
@@ -117,6 +115,19 @@ yarn run build:prod
|
||||
|
||||
This will build the application and put the result in the `dist` folder
|
||||
|
||||
### Deploy
|
||||
```bash
|
||||
# deploy production in standalone pm2 container
|
||||
yarn run deploy
|
||||
|
||||
# remove production from standalone pm2 container
|
||||
yarn run undeploy
|
||||
```
|
||||
|
||||
### Running the application with Docker
|
||||
See [Docker Runtime Options](docker/README.md)
|
||||
|
||||
|
||||
Cleaning
|
||||
--------
|
||||
|
||||
@@ -131,6 +142,7 @@ yarn run clean:prod
|
||||
yarn run clean:dist
|
||||
```
|
||||
|
||||
|
||||
Testing
|
||||
-------
|
||||
|
||||
@@ -184,21 +196,14 @@ To run all the tests (e.g.: to run tests with Continuous Integration software) y
|
||||
Documentation
|
||||
--------------
|
||||
|
||||
See [`./docs`](docs) for further documentation.
|
||||
|
||||
### Building code documentation
|
||||
|
||||
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts informations from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
|
||||
|
||||
Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder.
|
||||
|
||||
Deploy
|
||||
------
|
||||
|
||||
```bash
|
||||
# deploy production in standalone pm2 container
|
||||
yarn run deploy
|
||||
|
||||
# remove production from standalone pm2 container
|
||||
yarn run undeploy
|
||||
```
|
||||
|
||||
Other commands
|
||||
--------------
|
||||
|
||||
@@ -224,7 +229,7 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've
|
||||
Collaborating
|
||||
-------------
|
||||
|
||||
See [the guide on the wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+2+UI#DSpace7-Angular2UI-Howtocontribute)
|
||||
See [the guide on the wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development#DSpace7-AngularUIDevelopment-Howtocontribute)
|
||||
|
||||
File Structure
|
||||
--------------
|
||||
@@ -330,10 +335,20 @@ dspace-angular
|
||||
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
|
||||
```
|
||||
|
||||
3rd Party Library Installation
|
||||
------------------------------
|
||||
Managing Dependencies (via yarn)
|
||||
-------------
|
||||
|
||||
Install your library via `yarn add lib-name --save` and import it in your code. `--save` will add it to `package.json`.
|
||||
This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it.
|
||||
|
||||
* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn.
|
||||
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`.
|
||||
* If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev`
|
||||
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version`
|
||||
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it.
|
||||
|
||||
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
|
||||
|
||||
### Adding Typings for libraries
|
||||
|
||||
If the library does not include typings, you can install them using yarn:
|
||||
|
||||
@@ -365,24 +380,6 @@ If you're importing a module that uses CommonJS you need to import as
|
||||
import * as _ from 'lodash';
|
||||
```
|
||||
|
||||
Managing Dependencies (via yarn)
|
||||
-------------
|
||||
|
||||
This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it.
|
||||
|
||||
* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn.
|
||||
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`.
|
||||
* If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev`
|
||||
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version`
|
||||
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it.
|
||||
|
||||
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
|
||||
|
||||
Further Documentation
|
||||
---------------------
|
||||
|
||||
See [`./docs`](docs) for further documentation.
|
||||
|
||||
Frequently asked questions
|
||||
--------------------------
|
||||
|
||||
@@ -406,5 +403,4 @@ Frequently asked questions
|
||||
|
||||
License
|
||||
-------
|
||||
|
||||
http://www.dspace.org/license
|
||||
This project's source code is made available under the DSpace BSD License: http://www.dspace.org/license
|
||||
|
@@ -13,7 +13,7 @@ module.exports = {
|
||||
host: 'dspace7.4science.cloud',
|
||||
port: 443,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: '/dspace-spring-rest/api'
|
||||
nameSpace: '/server/api'
|
||||
},
|
||||
// Caching settings
|
||||
cache: {
|
||||
@@ -140,7 +140,19 @@ module.exports = {
|
||||
}, {
|
||||
code: 'nl',
|
||||
label: 'Nederlands',
|
||||
active: false,
|
||||
active: true,
|
||||
}, {
|
||||
code: 'pt',
|
||||
label: 'Português',
|
||||
active: true,
|
||||
}, {
|
||||
code: 'fr',
|
||||
label: 'Français',
|
||||
active: true,
|
||||
}, {
|
||||
code: 'lv',
|
||||
label: 'Latviešu',
|
||||
active: true,
|
||||
}],
|
||||
// Browse-By Pages
|
||||
browseBy: {
|
||||
@@ -149,11 +161,44 @@ module.exports = {
|
||||
// Limit for years to display using jumps of five years (current year - fiveYearLimit)
|
||||
fiveYearLimit: 30,
|
||||
// The absolute lowest year to display in the dropdown (only used when no lowest date can be found for all items)
|
||||
defaultLowerLimit: 1900
|
||||
defaultLowerLimit: 1900,
|
||||
// List of all the active Browse-By types
|
||||
// Adding a type will activate their Browse-By page and add them to the global navigation menu, as well as community and collection pages
|
||||
// Allowed fields and their purpose:
|
||||
// id: The browse id to use for fetching info from the rest api
|
||||
// type: The type of Browse-By page to display
|
||||
// metadataField: The metadata-field used to create starts-with options (only necessary when the type is set to 'date')
|
||||
types: [
|
||||
{
|
||||
id: 'title',
|
||||
type: 'title'
|
||||
},
|
||||
{
|
||||
id: 'dateissued',
|
||||
type: 'date',
|
||||
metadataField: 'dc.date.issued'
|
||||
},
|
||||
{
|
||||
id: 'author',
|
||||
type: 'metadata'
|
||||
},
|
||||
{
|
||||
id: 'subject',
|
||||
type: 'metadata'
|
||||
}
|
||||
]
|
||||
},
|
||||
item: {
|
||||
edit: {
|
||||
undoTimeout: 10000 // 10 seconds
|
||||
}
|
||||
},
|
||||
collection: {
|
||||
edit: {
|
||||
undoTimeout: 10000 // 10 seconds
|
||||
}
|
||||
},
|
||||
theme: {
|
||||
name: 'default',
|
||||
}
|
||||
};
|
||||
|
@@ -1,3 +1,4 @@
|
||||
// This configuration is currently only being used for unit tests, end-to-end tests use environment.dev.ts
|
||||
module.exports = {
|
||||
|
||||
};
|
||||
|
79
docker/README.md
Normal file
79
docker/README.md
Normal file
@@ -0,0 +1,79 @@
|
||||
# Docker Compose files
|
||||
|
||||
## docker directory
|
||||
- docker-compose.yml
|
||||
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
||||
- docker-compose-rest.yml
|
||||
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
|
||||
- docker-compose-travis.yml
|
||||
- Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
|
||||
- cli.yml
|
||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||
- cli.assetstore.yml
|
||||
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
|
||||
- environment.dev.js
|
||||
- Environment file for running DSpace Angular in Docker
|
||||
- local.cfg
|
||||
- Environment file for running the DSpace 7 REST API in Docker.
|
||||
|
||||
|
||||
## To refresh / pull DSpace images from Dockerhub
|
||||
```
|
||||
docker-compose -f docker/docker-compose.yml pull
|
||||
```
|
||||
|
||||
## To build DSpace images using code in your branch
|
||||
```
|
||||
docker-compose -f docker/docker-compose.yml build
|
||||
```
|
||||
|
||||
## To start DSpace (REST and Angular) from your branch
|
||||
|
||||
```
|
||||
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
|
||||
```
|
||||
|
||||
## Run DSpace REST and DSpace Angular from local branches.
|
||||
_The system will be started in 2 steps. Each step shares the same docker network._
|
||||
|
||||
From DSpace/DSpace (build as needed)
|
||||
```
|
||||
docker-compose -p d7 up -d
|
||||
```
|
||||
|
||||
From DSpace/DSpace-angular
|
||||
```
|
||||
docker-compose -p d7 -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Ingest test data from AIPDIR
|
||||
|
||||
Create an administrator
|
||||
```
|
||||
docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
|
||||
```
|
||||
|
||||
Load content from AIP files
|
||||
```
|
||||
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
||||
```
|
||||
|
||||
## Alternative Ingest - Use Entities dataset
|
||||
_Delete your docker volumes or use a unique project (-p) name_
|
||||
|
||||
Start DSpace with Database Content from a database dump
|
||||
```
|
||||
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
|
||||
```
|
||||
|
||||
Load assetstore content and trigger a re-index of the repository
|
||||
```
|
||||
docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
|
||||
```
|
||||
|
||||
## End to end testing of the rest api (runs in travis).
|
||||
_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._
|
||||
|
||||
```
|
||||
docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
|
||||
```
|
23
docker/cli.assetstore.yml
Normal file
23
docker/cli.assetstore.yml
Normal file
@@ -0,0 +1,23 @@
|
||||
version: "3.7"
|
||||
|
||||
networks:
|
||||
dspacenet:
|
||||
|
||||
services:
|
||||
dspace-cli:
|
||||
networks:
|
||||
dspacenet: {}
|
||||
environment:
|
||||
- LOADASSETS=https://www.dropbox.com/s/zv7lj8j2lp3egjs/assetstore.tar.gz?dl=1
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
if [ ! -z $${LOADASSETS} ]
|
||||
then
|
||||
curl $${LOADASSETS} -L -s --output /tmp/assetstore.tar.gz
|
||||
cd /dspace
|
||||
tar xvfz /tmp/assetstore.tar.gz
|
||||
fi
|
||||
|
||||
/dspace/bin/dspace index-discovery
|
32
docker/cli.ingest.yml
Normal file
32
docker/cli.ingest.yml
Normal file
@@ -0,0 +1,32 @@
|
||||
#
|
||||
# The contents of this file are subject to the license and copyright
|
||||
# detailed in the LICENSE and NOTICE files at the root of the source
|
||||
# tree and available online at
|
||||
#
|
||||
# http://www.dspace.org/license/
|
||||
#
|
||||
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
dspace-cli:
|
||||
environment:
|
||||
- AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/master/dogAndReport.zip
|
||||
- ADMIN_EMAIL=test@test.edu
|
||||
- AIPDIR=/tmp/aip-dir
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
rm -rf $${AIPDIR}
|
||||
mkdir $${AIPDIR} /dspace/upload
|
||||
cd $${AIPDIR}
|
||||
pwd
|
||||
curl $${AIPZIP} -L -s --output aip.zip
|
||||
unzip aip.zip
|
||||
cd $${AIPDIR}
|
||||
|
||||
/dspace/bin/dspace packager -r -a -t AIP -e $${ADMIN_EMAIL} -f -u SITE*.zip
|
||||
/dspace/bin/dspace database update-sequences
|
||||
|
||||
/dspace/bin/dspace index-discovery
|
22
docker/cli.yml
Normal file
22
docker/cli.yml
Normal file
@@ -0,0 +1,22 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
dspace-cli:
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
||||
container_name: dspace-cli
|
||||
#environment:
|
||||
volumes:
|
||||
- "assetstore:/dspace/assetstore"
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
entrypoint: /dspace/bin/dspace
|
||||
command: help
|
||||
networks:
|
||||
- dspacenet
|
||||
tty: true
|
||||
stdin_open: true
|
||||
|
||||
volumes:
|
||||
assetstore:
|
||||
|
||||
networks:
|
||||
dspacenet:
|
16
docker/db.entities.yml
Normal file
16
docker/db.entities.yml
Normal file
@@ -0,0 +1,16 @@
|
||||
#
|
||||
# The contents of this file are subject to the license and copyright
|
||||
# detailed in the LICENSE and NOTICE files at the root of the source
|
||||
# tree and available online at
|
||||
#
|
||||
# http://www.dspace.org/license/
|
||||
#
|
||||
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
dspacedb:
|
||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||
environment:
|
||||
# Double underbars in env names will be replaced with periods for apache commons
|
||||
- LOADSQL=https://www.dropbox.com/s/xh3ack0vg0922p2/configurable-entities-2019-05-08.sql?dl=1
|
59
docker/docker-compose-rest.yml
Normal file
59
docker/docker-compose-rest.yml
Normal file
@@ -0,0 +1,59 @@
|
||||
networks:
|
||||
dspacenet:
|
||||
services:
|
||||
dspace:
|
||||
container_name: dspace
|
||||
depends_on:
|
||||
- dspacedb
|
||||
image: dspace/dspace:dspace-7_x-test
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
- published: 8080
|
||||
target: 8080
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- assetstore:/dspace/assetstore
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
# Ensure that the database is ready before starting tomcat
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
/dspace/bin/dspace database migrate
|
||||
catalina.sh run
|
||||
dspacedb:
|
||||
container_name: dspacedb
|
||||
image: dspace/dspace-postgres-pgcrypto
|
||||
environment:
|
||||
PGDATA: /pgdata
|
||||
networks:
|
||||
dspacenet:
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- pgdata:/pgdata
|
||||
dspacesolr:
|
||||
container_name: dspacesolr
|
||||
image: dspace/dspace-solr
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
- published: 8983
|
||||
target: 8983
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- solr_authority:/opt/solr/server/solr/authority/data
|
||||
- solr_oai:/opt/solr/server/solr/oai/data
|
||||
- solr_search:/opt/solr/server/solr/search/data
|
||||
- solr_statistics:/opt/solr/server/solr/statistics/data
|
||||
version: '3.7'
|
||||
volumes:
|
||||
assetstore:
|
||||
pgdata:
|
||||
solr_authority:
|
||||
solr_oai:
|
||||
solr_search:
|
||||
solr_statistics:
|
53
docker/docker-compose-travis.yml
Normal file
53
docker/docker-compose-travis.yml
Normal file
@@ -0,0 +1,53 @@
|
||||
networks:
|
||||
dspacenet:
|
||||
services:
|
||||
dspace:
|
||||
container_name: dspace
|
||||
depends_on:
|
||||
- dspacedb
|
||||
image: dspace/dspace:dspace-7_x-test
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
- published: 8080
|
||||
target: 8080
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- assetstore:/dspace/assetstore
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
dspacedb:
|
||||
container_name: dspacedb
|
||||
environment:
|
||||
LOADSQL: https://www.dropbox.com/s/xh3ack0vg0922p2/configurable-entities-2019-05-08.sql?dl=1
|
||||
PGDATA: /pgdata
|
||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||
networks:
|
||||
dspacenet:
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- pgdata:/pgdata
|
||||
dspacesolr:
|
||||
container_name: dspacesolr
|
||||
image: dspace/dspace-solr
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
- published: 8983
|
||||
target: 8983
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- solr_authority:/opt/solr/server/solr/authority/data
|
||||
- solr_oai:/opt/solr/server/solr/oai/data
|
||||
- solr_search:/opt/solr/server/solr/search/data
|
||||
- solr_statistics:/opt/solr/server/solr/statistics/data
|
||||
version: '3.7'
|
||||
volumes:
|
||||
assetstore:
|
||||
pgdata:
|
||||
solr_authority:
|
||||
solr_oai:
|
||||
solr_search:
|
||||
solr_statistics:
|
26
docker/docker-compose.yml
Normal file
26
docker/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
||||
version: '3.7'
|
||||
networks:
|
||||
dspacenet:
|
||||
services:
|
||||
dspace-angular:
|
||||
container_name: dspace-angular
|
||||
environment:
|
||||
DSPACE_HOST: dspace-angular
|
||||
DSPACE_NAMESPACE: /
|
||||
DSPACE_PORT: '3000'
|
||||
DSPACE_SSL: "false"
|
||||
image: dspace/dspace-angular:latest
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
- published: 3000
|
||||
target: 3000
|
||||
- published: 9876
|
||||
target: 9876
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- ./environment.dev.js:/app/config/environment.dev.js
|
16
docker/environment.dev.js
Normal file
16
docker/environment.dev.js
Normal file
@@ -0,0 +1,16 @@
|
||||
/*
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
module.exports = {
|
||||
rest: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
port: 8080,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: '/server/api'
|
||||
}
|
||||
};
|
5
docker/local.cfg
Normal file
5
docker/local.cfg
Normal file
@@ -0,0 +1,5 @@
|
||||
dspace.dir=/dspace
|
||||
db.url=jdbc:postgresql://dspacedb:5432/dspace
|
||||
dspace.server.url=http://localhost:8080/server
|
||||
dspace.name=DSpace Started with Docker Compose
|
||||
solr.server=http://dspacesolr:8983/solr
|
@@ -13,7 +13,7 @@ describe('protractor App', () => {
|
||||
});
|
||||
|
||||
it('should contain a news section', () => {
|
||||
page.navigateTo();
|
||||
expect<any>(page.getHomePageNewsText()).toBeDefined();
|
||||
page.navigateTo()
|
||||
.then(() => expect<any>(page.getHomePageNewsText()).toBeDefined());
|
||||
});
|
||||
});
|
||||
|
@@ -2,7 +2,8 @@ import { browser, element, by } from 'protractor';
|
||||
|
||||
export class ProtractorPage {
|
||||
navigateTo() {
|
||||
return browser.get('/');
|
||||
return browser.get('/')
|
||||
.then(() => browser.waitForAngular());
|
||||
}
|
||||
|
||||
getPageTitleText() {
|
||||
@@ -10,6 +11,6 @@ export class ProtractorPage {
|
||||
}
|
||||
|
||||
getHomePageNewsText() {
|
||||
return element(by.xpath('//ds-home-news')).getText();
|
||||
return element(by.css('ds-home-news')).getText();
|
||||
}
|
||||
}
|
||||
|
46
e2e/search-navbar/search-navbar.e2e-spec.ts
Normal file
46
e2e/search-navbar/search-navbar.e2e-spec.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { ProtractorPage } from './search-navbar.po';
|
||||
import { browser } from 'protractor';
|
||||
|
||||
describe('protractor SearchNavbar', () => {
|
||||
let page: ProtractorPage;
|
||||
let queryString: string;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new ProtractorPage();
|
||||
queryString = 'the test query';
|
||||
});
|
||||
|
||||
it('should go to search page with correct query if submitted (from home)', () => {
|
||||
page.navigateToHome();
|
||||
return checkIfSearchWorks();
|
||||
});
|
||||
|
||||
it('should go to search page with correct query if submitted (from search)', () => {
|
||||
page.navigateToSearch();
|
||||
return checkIfSearchWorks();
|
||||
});
|
||||
|
||||
it('check if can submit search box with pressing button', () => {
|
||||
page.navigateToHome();
|
||||
page.expandAndFocusSearchBox();
|
||||
page.setCurrentQuery(queryString);
|
||||
page.submitNavbarSearchForm();
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkIfSearchWorks(): boolean {
|
||||
page.setCurrentQuery(queryString);
|
||||
page.submitByPressingEnter();
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
35
e2e/search-navbar/search-navbar.po.ts
Normal file
35
e2e/search-navbar/search-navbar.po.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { browser, by, element, protractor } from 'protractor';
|
||||
import { promise } from 'selenium-webdriver';
|
||||
|
||||
export class ProtractorPage {
|
||||
HOME = '/home';
|
||||
SEARCH = '/search';
|
||||
|
||||
navigateToHome() {
|
||||
return browser.get(this.HOME);
|
||||
}
|
||||
|
||||
navigateToSearch() {
|
||||
return browser.get(this.SEARCH);
|
||||
}
|
||||
|
||||
getCurrentQuery(): promise.Promise<string> {
|
||||
return element(by.css('#search-navbar-container form input')).getAttribute('value');
|
||||
}
|
||||
|
||||
expandAndFocusSearchBox() {
|
||||
element(by.css('#search-navbar-container form a')).click();
|
||||
}
|
||||
|
||||
setCurrentQuery(query: string) {
|
||||
element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(query);
|
||||
}
|
||||
|
||||
submitNavbarSearchForm() {
|
||||
element(by.css('#search-navbar-container form .submit-icon')).click();
|
||||
}
|
||||
|
||||
submitByPressingEnter() {
|
||||
element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER);
|
||||
}
|
||||
}
|
@@ -11,33 +11,36 @@ describe('protractor SearchPage', () => {
|
||||
|
||||
it('should contain query value when navigating to page with query parameter', () => {
|
||||
const queryString = 'Interesting query string';
|
||||
page.navigateToSearchWithQueryParameter(queryString);
|
||||
page.getCurrentQuery().then((query: string) => {
|
||||
expect<string>(query).toEqual(queryString);
|
||||
});
|
||||
page.navigateToSearchWithQueryParameter(queryString)
|
||||
.then(() => page.getCurrentQuery())
|
||||
.then((query: string) => {
|
||||
expect<string>(query).toEqual(queryString);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have right scope selected when navigating to page with scope parameter', () => {
|
||||
const scope: promise.Promise<string> = page.getRandomScopeOption();
|
||||
scope.then((scopeString: string) => {
|
||||
page.navigateToSearchWithScopeParameter(scopeString);
|
||||
page.getCurrentScope().then((s: string) => {
|
||||
expect<string>(s).toEqual(scopeString);
|
||||
page.navigateToSearch()
|
||||
.then(() => page.getRandomScopeOption())
|
||||
.then((scopeString: string) => {
|
||||
page.navigateToSearchWithScopeParameter(scopeString);
|
||||
page.getCurrentScope().then((s: string) => {
|
||||
expect<string>(s).toEqual(scopeString);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to the correct url when scope was set and submit button was triggered', () => {
|
||||
const scope: promise.Promise<string> = page.getRandomScopeOption();
|
||||
scope.then((scopeString: string) => {
|
||||
page.setCurrentScope(scopeString);
|
||||
page.submitSearchForm();
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('scope=' + encodeURI(scopeString)) !== -1;
|
||||
page.navigateToSearch()
|
||||
.then(() => page.getRandomScopeOption())
|
||||
.then((scopeString: string) => {
|
||||
page.setCurrentScope(scopeString);
|
||||
page.submitSearchForm();
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('scope=' + encodeURI(scopeString)) !== -1;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { browser, element, by, protractor } from 'protractor';
|
||||
import { browser, by, element, protractor } from 'protractor';
|
||||
import { promise } from 'selenium-webdriver';
|
||||
|
||||
export class ProtractorPage {
|
||||
@@ -27,15 +27,15 @@ export class ProtractorPage {
|
||||
}
|
||||
|
||||
setCurrentScope(scope: string) {
|
||||
element(by.css('option[value="' + scope + '"]')).click();
|
||||
element(by.css('#search-form option[value="' + scope + '"]')).click();
|
||||
}
|
||||
|
||||
setCurrentQuery(query: string) {
|
||||
element(by.css('input[name="query"]')).sendKeys(query);
|
||||
element(by.css('#search-form input[name="query"]')).sendKeys(query);
|
||||
}
|
||||
|
||||
submitSearchForm() {
|
||||
element(by.css('button.search-button')).click();
|
||||
element(by.css('#search-form button.search-button')).click();
|
||||
}
|
||||
|
||||
getRandomScopeOption(): promise.Promise<string> {
|
||||
|
@@ -15,7 +15,11 @@ module.exports = function (config) {
|
||||
};
|
||||
|
||||
var configuration = {
|
||||
|
||||
client: {
|
||||
jasmine: {
|
||||
random: false
|
||||
}
|
||||
},
|
||||
// base path that will be used to resolve all patterns (e.g. files, exclude)
|
||||
basePath: '',
|
||||
|
||||
|
168
package.json
168
package.json
@@ -8,7 +8,11 @@
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=8.0.0"
|
||||
"node": "10.* || >= 12.*"
|
||||
},
|
||||
"resolutions": {
|
||||
"serialize-javascript": ">= 2.1.2",
|
||||
"set-value": ">= 2.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"global": "npm install -g @angular/cli marked node-gyp nodemon node-nightly npm-check-updates npm-run-all rimraf typescript ts-node typedoc webpack webpack-bundle-analyzer pm2 rollup",
|
||||
@@ -17,15 +21,16 @@
|
||||
"clean:doc": "rimraf doc",
|
||||
"clean:log": "rimraf *.log*",
|
||||
"clean:json": "rimraf *.records.json",
|
||||
"clean:bld": "rimraf build",
|
||||
"clean:node": "rimraf node_modules",
|
||||
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json",
|
||||
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld",
|
||||
"clean": "yarn run clean:prod && yarn run clean:node",
|
||||
"prebuild": "yarn run clean:dist",
|
||||
"prebuild:aot": "yarn run prebuild",
|
||||
"prebuild": "yarn run clean:bld && yarn run clean:dist",
|
||||
"prebuild:ci": "yarn run prebuild",
|
||||
"prebuild:prod": "yarn run prebuild",
|
||||
"build": "node ./webpack/run-webpack.js --progress --mode development",
|
||||
"build:aot": "node ./webpack/run-webpack.js --env.aot --env.server --mode development && node ./webpack/run-webpack.js --env.aot --env.client --mode development",
|
||||
"build:prod": "node ./webpack/run-webpack.js --env.aot --env.server --mode production && node ./webpack/run-webpack.js --env.aot --env.client --mode production",
|
||||
"build": "node ./scripts/webpack.js --progress --mode development",
|
||||
"build:ci": "yarn run syncbuilddir && node ./scripts/webpack.js --env.aot --env.server --mode development && node ./scripts/webpack.js --env.aot --env.client --mode development",
|
||||
"build:prod": "yarn run syncbuilddir && node ./scripts/webpack.js --env.aot --env.server --mode production && node ./scripts/webpack.js --env.aot --env.client --mode production",
|
||||
"postbuild:prod": "yarn run rollup",
|
||||
"rollup": "rollup -c rollup.config.js",
|
||||
"prestart": "yarn run build:prod",
|
||||
@@ -40,7 +45,8 @@
|
||||
"server": "node dist/server.js",
|
||||
"server:watch": "nodemon dist/server.js",
|
||||
"server:watch:debug": "nodemon --debug dist/server.js",
|
||||
"webpack:watch": "node ./webpack/run-webpack.js -w --mode development",
|
||||
"syncbuilddir": "node ./scripts/sync-build-dir.js",
|
||||
"webpack:watch": "node ./scripts/webpack.js -w --mode development",
|
||||
"watch": "yarn run build && npm-run-all -p webpack:watch server:watch",
|
||||
"watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug",
|
||||
"predebug": "yarn run build",
|
||||
@@ -49,10 +55,13 @@
|
||||
"debug:server": "node-nightly --inspect --debug-brk dist/server.js",
|
||||
"debug:build": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --mode development",
|
||||
"debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --env.aot --env.client --env.server --mode production",
|
||||
"ci": "yarn run lint && yarn run build:aot && yarn run test:headless && npm-run-all -p -r server e2e",
|
||||
"ci": "yarn run lint && yarn run build:ci && yarn run test:headless && npm-run-all -p -r server e2e",
|
||||
"protractor": "node node_modules/protractor/bin/protractor",
|
||||
"pree2e": "yarn run webdriver:update",
|
||||
"e2e": "yarn run protractor",
|
||||
"pretest": "yarn run clean:bld",
|
||||
"pretest:headless": "yarn run pretest",
|
||||
"pretest:watch": "yarn run pretest",
|
||||
"test": "karma start --single-run",
|
||||
"test:headless": "karma start --single-run --browsers ChromeHeadless",
|
||||
"test:watch": "karma start --no-single-run --auto-watch",
|
||||
@@ -60,84 +69,94 @@
|
||||
"webdriver:update": "node node_modules/protractor/bin/webdriver-manager update --standalone --gecko false",
|
||||
"lint": "tslint \"src/**/*.ts\" && tslint \"e2e/**/*.ts\"",
|
||||
"docs": "typedoc --options typedoc.json ./src/",
|
||||
"coverage": "http-server -c-1 -o -p 9875 ./coverage"
|
||||
"coverage": "http-server -c-1 -o -p 9875 ./coverage",
|
||||
"postinstall": "yarn run patch-protractor",
|
||||
"patch-protractor": "ncp node_modules/webdriver-manager node_modules/protractor/node_modules/webdriver-manager",
|
||||
"sync-i18n": "node ./scripts/sync-i18n-files.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^6.1.4",
|
||||
"@angular/cli": "^6.1.5",
|
||||
"@angular/common": "^6.1.4",
|
||||
"@angular/core": "^6.1.4",
|
||||
"@angular/forms": "^6.1.4",
|
||||
"@angular/http": "^6.1.4",
|
||||
"@angular/platform-browser": "^6.1.4",
|
||||
"@angular/platform-browser-dynamic": "^6.1.4",
|
||||
"@angular/platform-server": "^6.1.4",
|
||||
"@angular/router": "^6.1.4",
|
||||
"@angular/animations": "^8.2.14",
|
||||
"@angular/cdk": "8.2.3",
|
||||
"@angular/cli": "^8.3.25",
|
||||
"@angular/common": "^8.2.14",
|
||||
"@angular/core": "^8.2.14",
|
||||
"@angular/forms": "^8.2.14",
|
||||
"@angular/platform-browser": "^8.2.14",
|
||||
"@angular/platform-browser-dynamic": "^8.2.14",
|
||||
"@angular/platform-server": "^8.2.14",
|
||||
"@angular/router": "^8.2.14",
|
||||
"@angularclass/bootloader": "1.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^2.0.0",
|
||||
"@ng-dynamic-forms/core": "6.2.0",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "6.2.0",
|
||||
"@ngrx/effects": "^6.1.0",
|
||||
"@ngrx/router-store": "^6.1.0",
|
||||
"@ngrx/store": "^6.1.0",
|
||||
"@nguniversal/express-engine": "6.1.0",
|
||||
"@ngx-translate/core": "10.0.2",
|
||||
"@ngx-translate/http-loader": "3.0.1",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^1.0.0",
|
||||
"@ng-bootstrap/ng-bootstrap": "^5.2.1",
|
||||
"@ng-dynamic-forms/core": "8.1.1",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "8.1.1",
|
||||
"@ngrx/effects": "^8.6.0",
|
||||
"@ngrx/router-store": "^8.6.0",
|
||||
"@ngrx/store": "^8.6.0",
|
||||
"@nguniversal/express-engine": "^8.2.6",
|
||||
"@ngx-translate/core": "11.0.1",
|
||||
"@ngx-translate/http-loader": "4.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^3.0.1",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
"angular-sortablejs": "^2.5.0",
|
||||
"angular2-text-mask": "9.0.0",
|
||||
"angulartics2": "^6.2.0",
|
||||
"angulartics2": "7.5.2",
|
||||
"body-parser": "1.18.2",
|
||||
"bootstrap": "4.3.1",
|
||||
"cerialize": "0.1.18",
|
||||
"compression": "1.7.1",
|
||||
"cookie-parser": "1.4.3",
|
||||
"core-js": "^2.5.7",
|
||||
"core-js": "^3.6.4",
|
||||
"debug-loader": "^0.0.1",
|
||||
"express": "4.16.2",
|
||||
"express-session": "1.15.6",
|
||||
"fast-json-patch": "^2.0.7",
|
||||
"file-saver": "^1.3.8",
|
||||
"font-awesome": "4.7.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
||||
"hammerjs": "^2.0.8",
|
||||
"http-server": "0.11.1",
|
||||
"https": "1.0.0",
|
||||
"js-cookie": "2.2.0",
|
||||
"js.clone": "0.0.3",
|
||||
"json5": "^2.1.0",
|
||||
"jsonschema": "1.2.2",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"methods": "1.1.2",
|
||||
"moment": "^2.22.1",
|
||||
"moment-range": "^4.0.2",
|
||||
"morgan": "^1.9.1",
|
||||
"ng-mocks": "^6.2.1",
|
||||
"ng-mocks": "^8.1.0",
|
||||
"ng2-file-upload": "1.2.1",
|
||||
"ng2-nouislider": "^1.7.11",
|
||||
"ngx-bootstrap": "^3.2.0",
|
||||
"ng2-nouislider": "^1.8.2",
|
||||
"ngx-bootstrap": "^5.3.2",
|
||||
"ngx-infinite-scroll": "6.0.1",
|
||||
"ngx-moment": "^3.1.0",
|
||||
"ngx-moment": "^3.4.0",
|
||||
"ngx-pagination": "3.0.3",
|
||||
"ngx-sortablejs": "^3.1.4",
|
||||
"nouislider": "^11.0.0",
|
||||
"pem": "1.12.3",
|
||||
"reflect-metadata": "0.1.12",
|
||||
"rxjs": "6.2.2",
|
||||
"pem": "1.13.2",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "6.5.4",
|
||||
"rxjs-spy": "^7.5.1",
|
||||
"sass-resources-loader": "^2.0.0",
|
||||
"sortablejs": "1.7.0",
|
||||
"text-mask-core": "5.0.1",
|
||||
"ts-loader": "^5.2.1",
|
||||
"ts-md5": "^1.2.4",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^3.2.1",
|
||||
"webfontloader": "1.6.28",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"zone.js": "^0.8.26"
|
||||
"webpack-cli": "^3.2.0",
|
||||
"zone.js": "^0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular/compiler": "^6.1.4",
|
||||
"@angular/compiler-cli": "^6.1.4",
|
||||
"@angular-devkit/build-angular": "^0.803.25",
|
||||
"@angular/compiler": "^8.2.14",
|
||||
"@angular/compiler-cli": "^8.2.14",
|
||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||
"@ngrx/entity": "^6.1.0",
|
||||
"@ngrx/schematics": "^6.1.0",
|
||||
"@ngrx/store-devtools": "^6.1.0",
|
||||
"@ngtools/webpack": "^6.1.5",
|
||||
"@ngrx/entity": "^8.6.0",
|
||||
"@ngrx/schematics": "^8.6.0",
|
||||
"@ngrx/store-devtools": "^8.6.0",
|
||||
"@ngtools/webpack": "^8.3.25",
|
||||
"@schematics/angular": "^0.7.5",
|
||||
"@types/acorn": "^4.0.3",
|
||||
"@types/cookie-parser": "1.4.1",
|
||||
@@ -146,49 +165,55 @@
|
||||
"@types/express-serve-static-core": "4.16.0",
|
||||
"@types/file-saver": "^1.3.0",
|
||||
"@types/hammerjs": "2.0.35",
|
||||
"@types/jasmine": "^2.8.6",
|
||||
"@types/jasmine": "^3.3.9",
|
||||
"@types/js-cookie": "2.1.0",
|
||||
"@types/json5": "^0.0.30",
|
||||
"@types/lodash": "^4.14.110",
|
||||
"@types/memory-cache": "0.2.0",
|
||||
"@types/mime": "2.0.0",
|
||||
"@types/node": "^10.9.4",
|
||||
"@types/node": "^11.11.2",
|
||||
"@types/serve-static": "1.13.2",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@types/webfontloader": "1.6.29",
|
||||
"@typescript-eslint/eslint-plugin": "^2.12.0",
|
||||
"@typescript-eslint/parser": "^2.12.0",
|
||||
"ajv": "^6.1.1",
|
||||
"ajv-keywords": "^3.1.0",
|
||||
"angular2-template-loader": "0.6.2",
|
||||
"autoprefixer": "^9.1.3",
|
||||
"caniuse-lite": "^1.0.30000697",
|
||||
"codelyzer": "^4.4.4",
|
||||
"compression-webpack-plugin": "^1.1.6",
|
||||
"copy-webpack-plugin": "^4.4.1",
|
||||
"cli-progress": "^3.3.1",
|
||||
"codelyzer": "^5.1.0",
|
||||
"commander": "^3.0.2",
|
||||
"compression-webpack-plugin": "^3.0.1",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"copyfiles": "^2.1.1",
|
||||
"coveralls": "3.0.0",
|
||||
"css-loader": "1.0.0",
|
||||
"css-loader": "3.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"deep-freeze": "0.0.1",
|
||||
"eslint": "^6.7.2",
|
||||
"exports-loader": "^0.7.0",
|
||||
"html-webpack-plugin": "^4.0.0-alpha",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"imports-loader": "0.8.0",
|
||||
"istanbul-instrumenter-loader": "3.0.1",
|
||||
"jasmine-core": "^3.2.1",
|
||||
"jasmine-core": "^3.3.0",
|
||||
"jasmine-marbles": "0.3.1",
|
||||
"jasmine-spec-reporter": "4.2.1",
|
||||
"karma": "3.0.0",
|
||||
"karma": "4.0.1",
|
||||
"karma-chrome-launcher": "2.2.0",
|
||||
"karma-cli": "1.0.1",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "1.1.2",
|
||||
"karma-istanbul-preprocessor": "0.0.2",
|
||||
"karma-jasmine": "1.1.2",
|
||||
"karma-jasmine": "2.0.1",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-phantomjs-launcher": "1.0.4",
|
||||
"karma-remap-coverage": "^0.1.5",
|
||||
"karma-remap-istanbul": "0.6.0",
|
||||
"karma-sourcemap-loader": "0.3.7",
|
||||
"karma-webdriver-launcher": "1.0.5",
|
||||
"karma-webdriver-launcher": "^1.0.7",
|
||||
"karma-webpack": "3.0.0",
|
||||
"ngrx-store-freeze": "^0.2.4",
|
||||
"node-sass": "^4.11.0",
|
||||
"ncp": "^2.0.0",
|
||||
"nodemon": "^1.15.0",
|
||||
"npm-run-all": "4.1.3",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
@@ -199,31 +224,32 @@
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-responsive-type": "1.0.0",
|
||||
"postcss-smart-import": "0.7.6",
|
||||
"protractor": "^5.3.0",
|
||||
"protractor": "^5.4.2",
|
||||
"protractor-istanbul-plugin": "2.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"resolve-url-loader": "^2.3.0",
|
||||
"rimraf": "2.6.2",
|
||||
"rollup": "^0.65.0",
|
||||
"rollup-plugin-commonjs": "^9.1.6",
|
||||
"rollup-plugin-node-globals": "1.2.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.3",
|
||||
"rollup-plugin-terser": "^2.0.2",
|
||||
"sass-loader": "7.1.0",
|
||||
"script-ext-html-webpack-plugin": "2.0.1",
|
||||
"sass-loader": "7.3.1",
|
||||
"script-ext-html-webpack-plugin": "2.1.4",
|
||||
"source-map": "0.7.3",
|
||||
"source-map-loader": "0.2.4",
|
||||
"string-replace-loader": "2.1.1",
|
||||
"string-replace-loader": "^2.1.1",
|
||||
"terser-webpack-plugin": "^2.3.1",
|
||||
"to-string-loader": "1.1.5",
|
||||
"ts-helpers": "1.1.2",
|
||||
"ts-node": "4.1.0",
|
||||
"tslint": "5.11.0",
|
||||
"typedoc": "^0.9.0",
|
||||
"typescript": "^2.9.1",
|
||||
"webpack": "^4.17.1",
|
||||
"webpack-bundle-analyzer": "^2.13.1",
|
||||
"typescript": "3.5.3",
|
||||
"webdriver-manager": "^12.1.7",
|
||||
"webpack": "^4.29.6",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-dev-middleware": "3.2.0",
|
||||
"webpack-dev-server": "^3.1.5",
|
||||
"webpack-dev-server": "^3.1.11",
|
||||
"webpack-import-glob-loader": "^1.6.3",
|
||||
"webpack-merge": "4.1.4",
|
||||
"webpack-node-externals": "1.7.2"
|
||||
|
@@ -5,7 +5,7 @@
|
||||
var SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 11000,
|
||||
allScriptsTimeout: 600000,
|
||||
// -----------------------------------------------------------------
|
||||
// Uncomment to run tests using a remote Selenium server
|
||||
//seleniumAddress: 'http://selenium.address:4444/wd/hub',
|
||||
@@ -73,7 +73,7 @@ exports.config = {
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 30000,
|
||||
defaultTimeoutInterval: 600000,
|
||||
print: function () {}
|
||||
},
|
||||
useAllAngular2AppRoots: true,
|
||||
|
3
resources/fonts/README.md
Normal file
3
resources/fonts/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Supported font formats
|
||||
|
||||
DSpace supports EOT, TTF, OTF, SVG, WOFF and WOFF2 fonts.
|
4778
resources/i18n/ar.json5
Normal file
4778
resources/i18n/ar.json5
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,277 +0,0 @@
|
||||
{
|
||||
"footer": {
|
||||
"copyright": "copyright © 2002-{{ year }}",
|
||||
"link.dspace": "software DSpace",
|
||||
"link.duraspace": "DuraSpace"
|
||||
},
|
||||
"collection": {
|
||||
"page": {
|
||||
"news": "Novinky",
|
||||
"license": "Licence",
|
||||
"browse": {
|
||||
"recent": {
|
||||
"head": "Poslední příspěvky"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"community": {
|
||||
"page": {
|
||||
"news": "Novinky",
|
||||
"license": "Licence"
|
||||
},
|
||||
"sub-collection-list": {
|
||||
"head": "Kolekce v této komunitě"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"page": {
|
||||
"author": "Autor",
|
||||
"abstract": "Abstract",
|
||||
"date": "Datum",
|
||||
"uri": "URI",
|
||||
"files": "Soubory",
|
||||
"collections": "Kolekce",
|
||||
"filesection": {
|
||||
"download": "Stáhnout",
|
||||
"name": "Název:",
|
||||
"format": "Formát:",
|
||||
"size": "Velikost:",
|
||||
"description": "Popis:"
|
||||
},
|
||||
"link": {
|
||||
"simple": "Minimální záznam",
|
||||
"full": "Úplný záznam"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"home": "Domů",
|
||||
"login": "Přihlásit se",
|
||||
"logout": "Odhlásit se"
|
||||
},
|
||||
"pagination": {
|
||||
"results-per-page": "Výsledků na stránku",
|
||||
"sort-direction": "Seřazení",
|
||||
"showing": {
|
||||
"label": "Zobrazují se záznamy ",
|
||||
"detail": "{{ range }} z {{ total }}"
|
||||
}
|
||||
},
|
||||
"sorting": {
|
||||
"score": {
|
||||
"DESC": "Relevance"
|
||||
},
|
||||
"dc.title": {
|
||||
"ASC": "Název vzestupně",
|
||||
"DESC": "Název sestupně"
|
||||
}
|
||||
},
|
||||
"title": "DSpace",
|
||||
"404": {
|
||||
"help": "Nepodařilo se najít stránku, kterou hledáte. Je možné, že stránka byla přesunuta nebo smazána. Pomocí tlačítka níže můžete přejít na domovskou stránku. ",
|
||||
"page-not-found": "stránka nenalezena",
|
||||
"link": {
|
||||
"home-page": "Přejít na domovskou stránku"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "DSpace Angular :: Domů",
|
||||
"description": "",
|
||||
"top-level-communities": {
|
||||
"head": "Komunity v DSpace",
|
||||
"help": "Vybráním komunity můžete prohlížet její kolekce."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "DSpace Angular :: Hledat",
|
||||
"description": "",
|
||||
"form": {
|
||||
"search": "Hledat",
|
||||
"search_dspace": "Hledat v DSpace"
|
||||
},
|
||||
"results": {
|
||||
"head": "Výsledky hledání",
|
||||
"no-results": "Nebyli nalezeny žádné výsledky"
|
||||
},
|
||||
"sidebar": {
|
||||
"close": "Zpět na výsledky",
|
||||
"open": "Vyhledávací nástroje",
|
||||
"results": "výsledky",
|
||||
"filters": {
|
||||
"title": "Filtry"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Nastavení",
|
||||
"sort-by": "Řadit dle",
|
||||
"rpp": "Výsledků na stránku"
|
||||
}
|
||||
},
|
||||
"view-switch": {
|
||||
"show-list": "Zobrazit seznam",
|
||||
"show-grid": "Zobrazit mřížku"
|
||||
},
|
||||
"filters": {
|
||||
"head": "Filtry",
|
||||
"reset": "Obnovit filtry",
|
||||
"applied": {
|
||||
"f.author": "Autor",
|
||||
"f.dateIssued.min": "Od data",
|
||||
"f.dateIssued.max": "Do data",
|
||||
"f.subject": "Předmět",
|
||||
"f.has_content_in_original_bundle": "Má soubory"
|
||||
},
|
||||
"filter": {
|
||||
"show-more": "Zobrazit více",
|
||||
"show-less": "Sbalit",
|
||||
"author": {
|
||||
"placeholder": "Jméno autora",
|
||||
"head": "Autor"
|
||||
},
|
||||
"scope": {
|
||||
"placeholder": "Filtr rozsahu",
|
||||
"head": "Rozsah"
|
||||
},
|
||||
"subject": {
|
||||
"placeholder": "Předmět",
|
||||
"head": "Předmět"
|
||||
},
|
||||
"dateIssued": {
|
||||
"max": {
|
||||
"placeholder": "Datum od"
|
||||
},
|
||||
"min": {
|
||||
"placeholder": "Datum do"
|
||||
},
|
||||
"head": "Datum"
|
||||
},
|
||||
"has_content_in_original_bundle": {
|
||||
"head": "Má soubory"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browse": {
|
||||
"title": "Prohlížíte {{ collection }} dle {{ field }} {{ value }}"
|
||||
},
|
||||
"admin": {
|
||||
"registries": {
|
||||
"metadata": {
|
||||
"title": "DSpace Angular :: Registr metadat",
|
||||
"head": "Registr metadat",
|
||||
"description": "Registr metadat je seznam všech metadatových polí dostupných v repozitáři. Tyto pole mohou být rozdělena do více schémat. DSpace však vyžaduje použití schématu kvalifikový Dublin Core.",
|
||||
"schemas": {
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"namespace": "Jmenný prostor",
|
||||
"name": "Název"
|
||||
},
|
||||
"no-items": "Žádná schémata metadat."
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"title": "DSpace Angular :: Registr schémat metadat",
|
||||
"head": "Metadata Schema",
|
||||
"description": "Toto je schéma metadat pro „{{namespace}}“.",
|
||||
"fields": {
|
||||
"head": "Pole schématu metadat",
|
||||
"table": {
|
||||
"field": "Pole",
|
||||
"scopenote": "Poznámka o rozsahu"
|
||||
},
|
||||
"no-items": "Žádná metadatová pole."
|
||||
}
|
||||
},
|
||||
"bitstream-formats": {
|
||||
"title": "DSpace Angular :: Registr formátů souborů",
|
||||
"head": "Registr formátů souborů",
|
||||
"description": "Tento seznam formátů souborů poskytuje informace o známých formátech a o úrovni jejich podpory.",
|
||||
"formats": {
|
||||
"table": {
|
||||
"name": "Název",
|
||||
"mimetype": "Typ MIME",
|
||||
"supportLevel": {
|
||||
"head": "Úroveň podpory",
|
||||
"0": "Neznámá",
|
||||
"1": "Známá",
|
||||
"2": "Podpora"
|
||||
},
|
||||
"internal": "interní"
|
||||
},
|
||||
"no-items": "Žádné formáty souborů."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"default": "Načítá se...",
|
||||
"top-level-communities": "Načítají se komunity nejvyšší úrovně...",
|
||||
"community": "Načítá se komunita...",
|
||||
"collection": "Načítá se kolekce...",
|
||||
"sub-collections": "Načítají se subkolekce...",
|
||||
"recent-submissions": "Načítají se poslední příspěvky...",
|
||||
"item": "Načítá se záznam...",
|
||||
"objects": "Načítá se...",
|
||||
"search-results": "Načítají se výsledky hledání...",
|
||||
"browse-by": "Načítají se záznamy..."
|
||||
},
|
||||
"error": {
|
||||
"default": "Chyba",
|
||||
"top-level-communities": "Chyba během stahování komunit nejvyšší úrovně",
|
||||
"community": "Chyba během stahování komunity",
|
||||
"collection": "Chyba během stahování kolekce",
|
||||
"sub-collections": "Chyba během stahování subkolekcí",
|
||||
"recent-submissions": "Chyba během stahování posledních příspěvků",
|
||||
"item": "Chyba během stahování záznamu",
|
||||
"objects": "Chyba během stahování objektů",
|
||||
"search-results": "Chyba během stahování výsledků hledání",
|
||||
"browse-by": "Chyba během stahování záznamů",
|
||||
"validation": {
|
||||
"pattern": "Tento vstup je omezen dle vzoru: {{ pattern }}.",
|
||||
"license": {
|
||||
"notgranted": "Pro dokončení zaslání Musíte udělit licenci. Pokud v tuto chvíli tuto licenci nemůžete udělit, můžete svou práci uložit a později se k svému příspěveku vrátit nebo jej smazat."
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"submit": "Odeslat",
|
||||
"cancel": "Zrušit",
|
||||
"search": "Hledat",
|
||||
"remove": "Smazat",
|
||||
"first-name": "Křestní jméno",
|
||||
"last-name": "Příjmení",
|
||||
"loading": "Načítá se...",
|
||||
"no-results": "Nebyli nalezeny žádné výsledky",
|
||||
"no-value": "Nebyla zadána hodnota",
|
||||
"group-collapse": "Sbalit",
|
||||
"group-expand": "Rozbalit",
|
||||
"group-collapse-help": "Kliknutím sem sbalíte",
|
||||
"group-expand-help": "Kliknutím sem rozbalíte a přidáte další prvky"
|
||||
},
|
||||
"login": {
|
||||
"title": "Přihlásit se",
|
||||
"form": {
|
||||
"header": "Prosím, přihlaste se do DSpace",
|
||||
"email": "E-mailová adresa",
|
||||
"forgot-password": "Zapomněli jste své heslo?",
|
||||
"new-user": "Nový uživatel? Zaregistrujte se kliknutím sem.",
|
||||
"password": "Heslo",
|
||||
"submit": "Přihlásit se"
|
||||
}
|
||||
},
|
||||
"logout": {
|
||||
"title": "Odhlásit se",
|
||||
"form": {
|
||||
"header": "Odhlásit se z DSpace",
|
||||
"submit": "Odhlásit se"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"messages": {
|
||||
"expired": "Vaše relace vypršela. Prosím, znova se přihlaste."
|
||||
},
|
||||
"errors": {
|
||||
"invalid-user": "Neplatná e-mailová adresa nebo heslo."
|
||||
}
|
||||
}
|
||||
}
|
4645
resources/i18n/cs.json5
Normal file
4645
resources/i18n/cs.json5
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,277 +0,0 @@
|
||||
{
|
||||
"footer": {
|
||||
"copyright": "Copyright © 2002-{{ year }}",
|
||||
"link.dspace": "DSpace Software",
|
||||
"link.duraspace": "DuraSpace"
|
||||
},
|
||||
"collection": {
|
||||
"page": {
|
||||
"news": "Neuigkeiten",
|
||||
"license": "Lizenz",
|
||||
"browse": {
|
||||
"recent": {
|
||||
"head": "Aktuellste Veröffentlichungen"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"community": {
|
||||
"page": {
|
||||
"news": "Neuigkeiten",
|
||||
"license": "Lizenz"
|
||||
},
|
||||
"sub-collection-list": {
|
||||
"head": "Sammlungen in diesem Bereich"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"page": {
|
||||
"author": "Autor",
|
||||
"abstract": "Kurzfassung",
|
||||
"date": "Datum",
|
||||
"uri": "URI",
|
||||
"files": "Dateien",
|
||||
"collections": "Sammlungen",
|
||||
"filesection": {
|
||||
"download": "Herunterladen",
|
||||
"name": "Name:",
|
||||
"format": "Format:",
|
||||
"size": "Größe:",
|
||||
"description": "Beschreibung:"
|
||||
},
|
||||
"link": {
|
||||
"simple": "Kurzanzeige",
|
||||
"full": "Vollanzeige"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"home": "Zur Startseite",
|
||||
"login": "Anmelden",
|
||||
"logout": "Abmelden"
|
||||
},
|
||||
"pagination": {
|
||||
"results-per-page": "Ergebnisse pro Seite",
|
||||
"sort-direction": "Sortiermöglichkeiten",
|
||||
"showing": {
|
||||
"label": "Anzeige der Treffer ",
|
||||
"detail": "{{ range }} bis {{ total }}"
|
||||
}
|
||||
},
|
||||
"sorting": {
|
||||
"score": {
|
||||
"DESC": "Relevanz"
|
||||
},
|
||||
"dc.title": {
|
||||
"ASC": "Titel aufsteigend",
|
||||
"DESC": "Titel absteigend"
|
||||
}
|
||||
},
|
||||
"title": "DSpace",
|
||||
"404": {
|
||||
"help": "Die Seite, die Sie aufrufen wollten, konnte nicht gefunden werden. Sie könnte verschoben oder gelöscht worden sein. Mit dem Link unten kommen Sie zurück zur Startseite. ",
|
||||
"page-not-found": "Seite nicht gefunden",
|
||||
"link": {
|
||||
"home-page": "Zurück zur Startseite"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "DSpace Angular :: Startseite",
|
||||
"description": "",
|
||||
"top-level-communities": {
|
||||
"head": "Bereiche in DSpace",
|
||||
"help": "Wählen Sie einen Bereich, um seine Sammlungen einzusehen."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "DSpace Angular :: Suche",
|
||||
"description": "",
|
||||
"form": {
|
||||
"search": "Suche",
|
||||
"search_dspace": "DSpace durchsuchen"
|
||||
},
|
||||
"results": {
|
||||
"head": "Suchergebnisse",
|
||||
"no-results": "Zu dieser Suche gibt es keine Treffer."
|
||||
},
|
||||
"sidebar": {
|
||||
"close": "Zurück zu den Ergebnissen",
|
||||
"open": "Suchwerkzeuge",
|
||||
"results": "Ergebnisse",
|
||||
"filters": {
|
||||
"title": "Filter"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Einstellungen",
|
||||
"sort-by": "Sortiere nach",
|
||||
"rpp": "Treffer pro Seite"
|
||||
}
|
||||
},
|
||||
"view-switch": {
|
||||
"show-list": "Zeige als Liste",
|
||||
"show-grid": "Zeige als Raster"
|
||||
},
|
||||
"filters": {
|
||||
"head": "Filter",
|
||||
"reset": "Filter zurücksetzen",
|
||||
"applied": {
|
||||
"f.author": "Autor",
|
||||
"f.dateIssued.min": "Anfangsdatum",
|
||||
"f.dateIssued.max": "Enddatum",
|
||||
"f.subject": "Thema",
|
||||
"f.has_content_in_original_bundle": "Besitzt Dateien"
|
||||
},
|
||||
"filter": {
|
||||
"show-more": "Zeige mehr",
|
||||
"show-less": "Zeige weniger",
|
||||
"author": {
|
||||
"placeholder": "Autor",
|
||||
"head": "Autor"
|
||||
},
|
||||
"scope": {
|
||||
"placeholder": "Bereichsfilter",
|
||||
"head": "Bereich"
|
||||
},
|
||||
"subject": {
|
||||
"placeholder": "Schlagwort",
|
||||
"head": "Schlagwort"
|
||||
},
|
||||
"dateIssued": {
|
||||
"max": {
|
||||
"placeholder": "Frühestes Datum"
|
||||
},
|
||||
"min": {
|
||||
"placeholder": "Ältestes Datum"
|
||||
},
|
||||
"head": "Datum"
|
||||
},
|
||||
"has_content_in_original_bundle": {
|
||||
"head": "Besitzt Dateien"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browse": {
|
||||
"title": "Anzeige {{ collection }} nach {{ field }} {{ value }}"
|
||||
},
|
||||
"admin": {
|
||||
"registries": {
|
||||
"metadata": {
|
||||
"title": "DSpace Angular :: Metadatenreferenzliste",
|
||||
"head": "Metadatenreferenzliste",
|
||||
"description": "Die Metadatenreferenzliste beinhaltet alle Metadatenfelder, die zur Verfügung stehen. Die Felder können in unterschiedlichen Schemata enthalten sein. Nichtsdestotrotz benötigt DSpace mindestens qualifiziertes Dublin Core.",
|
||||
"schemas": {
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"namespace": "Namensraum",
|
||||
"name": "Name"
|
||||
},
|
||||
"no-items": "Es gbit keine Metadatenschemata."
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"title": "DSpace Angular :: Referenzliste der Metadatenschemata",
|
||||
"head": "Metadatenschemata",
|
||||
"description": "Dies ist das Metadatenschema für \"{{namespace}}\".",
|
||||
"fields": {
|
||||
"head": "Felder in diesem Schema",
|
||||
"table": {
|
||||
"field": "Feld",
|
||||
"scopenote": "Gültigkeitsbereich"
|
||||
},
|
||||
"no-items": "Es gibt keine Felder in diesem Schema."
|
||||
}
|
||||
},
|
||||
"bitstream-formats": {
|
||||
"title": "DSpace Angular :: Referenzliste der Dateiformate",
|
||||
"head": "Referenzliste der Dateiformate",
|
||||
"description": "Diese Liste enhtält die in diesem Repositorium zulässigen Dateiformate und den jeweiligen Unterstützungsgrad.",
|
||||
"formats": {
|
||||
"table": {
|
||||
"name": "Name",
|
||||
"mimetype": "MIME Type",
|
||||
"supportLevel": {
|
||||
"head": "Unterstützungsgrad",
|
||||
"0": "Unbekannt",
|
||||
"1": "Bekannt",
|
||||
"2": "Unterstützt"
|
||||
},
|
||||
"internal": "intern"
|
||||
},
|
||||
"no-items": "Es gibt keine Formate in dieser Referenzliste."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"default": "Am Laden ...",
|
||||
"top-level-communities": "Die Hauptbereiche werden geladen ...",
|
||||
"community": "Der Bereich wird geladen ...",
|
||||
"collection": "Die Sammlung wird geladen ...",
|
||||
"sub-collections": "Die untergeordneten Sammlungen werden geladen ...",
|
||||
"recent-submissions": "Die aktuellsten Veröffentlichungen werden geladen ...",
|
||||
"item": "Die Ressource wird geladen ...",
|
||||
"objects": "Am Laden ...",
|
||||
"search-results": "Die Suchergebnisse werden geladen ...",
|
||||
"browse-by": "Die Ressourcen werden geladen ..."
|
||||
},
|
||||
"error": {
|
||||
"default": "Fehler",
|
||||
"top-level-communities": "Fehler beim Laden der Hauptbereiche.",
|
||||
"community": "Fehler beim Laden des Bereiches.",
|
||||
"collection": "Fehler beim Laden der Sammlung.",
|
||||
"sub-collections": "Fehler beim Laden der untergeordneten Sammlungen.",
|
||||
"recent-submissions": "Fehler beim Laden der aktuellsten Veröffentlichungen.",
|
||||
"item": "Fehler beim Laden der Ressource.",
|
||||
"objects": "Fehler beim Laden der Objekte.",
|
||||
"search-results": "Fehler beim Laden der Suchergebnisse.",
|
||||
"browse-by": "Fehler beim Laden der Ressourcen",
|
||||
"validation": {
|
||||
"pattern": "Die Eingabe kann nur folgendes Muster haben: {{ pattern }}.",
|
||||
"license": {
|
||||
"notgranted": "Sie müssen der Lizenz zustimmen, um die Ressource einzureichen. Wenn dies zur Zeit nicht geht, können Sie die Einreichung speichern und später wiederaufnehmen oder löschen."
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"submit": "Los",
|
||||
"cancel": "Abbrechen",
|
||||
"search": "Suchen",
|
||||
"remove": "Löschen",
|
||||
"first-name": "Vorname",
|
||||
"last-name": "Nachname",
|
||||
"loading": "Am Laden ...",
|
||||
"no-results": "Keine Ergebnisse gefunden",
|
||||
"no-value": "Kein Wert eingegeben",
|
||||
"group-collapse": "Weniger",
|
||||
"group-expand": "Mehr",
|
||||
"group-collapse-help": "Hier klicken, um die Anzeige zu reduzieren",
|
||||
"group-expand-help": "Hier klicken, um mehr Elemente anzuzeigen"
|
||||
},
|
||||
"login": {
|
||||
"title": "Einloggen",
|
||||
"form": {
|
||||
"header": "Bitte Loggen Sie sich ein.",
|
||||
"email": "E-Mail-Adresse",
|
||||
"forgot-password": "Haben Sie Ihr Passwort vergessen?",
|
||||
"new-user": "Sind Sie neu hier? Klicken Sie hier, um sich zu registrieren.",
|
||||
"password": "Passwort",
|
||||
"submit": "Einloggen"
|
||||
}
|
||||
},
|
||||
"logout": {
|
||||
"title": "Ausloggen",
|
||||
"form": {
|
||||
"header": "Ausloggen aus DSpace",
|
||||
"submit": "Ausloggen"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"messages": {
|
||||
"expired": "Ihre Sitzung ist abgelaufen, bitte melden Sie sich erneut an."
|
||||
},
|
||||
"errors": {
|
||||
"invalid-user": "Ungültige E-Mail-Adresse oder Passwort."
|
||||
}
|
||||
}
|
||||
}
|
3871
resources/i18n/de.json5
Normal file
3871
resources/i18n/de.json5
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
2540
resources/i18n/en.json5
Normal file
2540
resources/i18n/en.json5
Normal file
File diff suppressed because it is too large
Load Diff
4029
resources/i18n/es.json5
Normal file
4029
resources/i18n/es.json5
Normal file
File diff suppressed because it is too large
Load Diff
4778
resources/i18n/fi.json5
Normal file
4778
resources/i18n/fi.json5
Normal file
File diff suppressed because it is too large
Load Diff
4032
resources/i18n/fr.json5
Normal file
4032
resources/i18n/fr.json5
Normal file
File diff suppressed because it is too large
Load Diff
4778
resources/i18n/ja.json5
Normal file
4778
resources/i18n/ja.json5
Normal file
File diff suppressed because it is too large
Load Diff
3772
resources/i18n/lv.json5
Normal file
3772
resources/i18n/lv.json5
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,277 +0,0 @@
|
||||
{
|
||||
"footer": {
|
||||
"copyright": "copyright © 2002-{{ year }}",
|
||||
"link.dspace": "DSpace software",
|
||||
"link.duraspace": "DuraSpace"
|
||||
},
|
||||
"collection": {
|
||||
"page": {
|
||||
"news": "Nieuws",
|
||||
"license": "Licentie",
|
||||
"browse": {
|
||||
"recent": {
|
||||
"head": "Recent toegevoegd"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"community": {
|
||||
"page": {
|
||||
"news": "Nieuws",
|
||||
"license": "Licentie"
|
||||
},
|
||||
"sub-collection-list": {
|
||||
"head": "Collecties in deze Community"
|
||||
}
|
||||
},
|
||||
"item": {
|
||||
"page": {
|
||||
"author": "Auteur",
|
||||
"abstract": "Abstract",
|
||||
"date": "Datum",
|
||||
"uri": "URI",
|
||||
"files": "Bestanden",
|
||||
"collections": "Collecties",
|
||||
"filesection": {
|
||||
"download": "Download",
|
||||
"name": "Naam:",
|
||||
"format": "Formaat:",
|
||||
"size": "Grootte:",
|
||||
"description": "Beschrijving:"
|
||||
},
|
||||
"link": {
|
||||
"simple": "Eenvoudige itemweergave",
|
||||
"full": "Volledige itemweergave"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nav": {
|
||||
"home": "Home",
|
||||
"login": "Log In",
|
||||
"logout": "Log Uit"
|
||||
},
|
||||
"pagination": {
|
||||
"results-per-page": "Resultaten per pagina",
|
||||
"sort-direction": "Sorteermogelijkheden",
|
||||
"showing": {
|
||||
"label": "Resultaten ",
|
||||
"detail": "{{ range }} van {{ total }}"
|
||||
}
|
||||
},
|
||||
"sorting": {
|
||||
"score": {
|
||||
"DESC": "Relevantie"
|
||||
},
|
||||
"dc.title": {
|
||||
"ASC": "Oplopend op titel",
|
||||
"DESC": "Aflopend op titel"
|
||||
}
|
||||
},
|
||||
"title": "DSpace",
|
||||
"404": {
|
||||
"help": "De pagina die u zoekt kan niet gevonden worden. De pagina werd mogelijk verplaatst of verwijderd. U kan onderstaande knop gebruiken om terug naar de homepagina te gaan. ",
|
||||
"page-not-found": "Pagina niet gevonden",
|
||||
"link": {
|
||||
"home-page": "Terug naar de homepagina"
|
||||
}
|
||||
},
|
||||
"home": {
|
||||
"title": "DSpace Angular :: Home",
|
||||
"description": "",
|
||||
"top-level-communities": {
|
||||
"head": "Communities in DSpace",
|
||||
"help": "Selecteer een community om diens collecties te verkennen."
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
"title": "DSpace Angular :: Zoek",
|
||||
"description": "",
|
||||
"form": {
|
||||
"search": "Zoek",
|
||||
"search_dspace": "Zoek in DSpace"
|
||||
},
|
||||
"results": {
|
||||
"head": "Zoekresultaten",
|
||||
"no-results": "Er waren geen resultaten voor deze zoekopdracht"
|
||||
},
|
||||
"sidebar": {
|
||||
"close": "Terug naar de resultaten",
|
||||
"open": "Zoek Tools",
|
||||
"results": "resultaten",
|
||||
"filters": {
|
||||
"title": "Filters"
|
||||
},
|
||||
"settings": {
|
||||
"title": "Instellingen",
|
||||
"sort-by": "Sorteer volgens",
|
||||
"rpp": "Resultaten per pagina"
|
||||
}
|
||||
},
|
||||
"view-switch": {
|
||||
"show-list": "Toon als lijst",
|
||||
"show-grid": "Toon in raster"
|
||||
},
|
||||
"filters": {
|
||||
"head": "Filters",
|
||||
"reset": "Filters verwijderen",
|
||||
"applied": {
|
||||
"f.author": "Auteur",
|
||||
"f.dateIssued.min": "Startdatum",
|
||||
"f.dateIssued.max": "Einddatum",
|
||||
"f.subject": "Sleutelwoord",
|
||||
"f.has_content_in_original_bundle": "Heeft bestanden"
|
||||
},
|
||||
"filter": {
|
||||
"show-more": "Toon meer",
|
||||
"show-less": "Inklappen",
|
||||
"author": {
|
||||
"placeholder": "Auteursnaam",
|
||||
"head": "Auteur"
|
||||
},
|
||||
"scope": {
|
||||
"placeholder": "Bereikfilter",
|
||||
"head": "Bereik"
|
||||
},
|
||||
"subject": {
|
||||
"placeholder": "Onderwerp",
|
||||
"head": "Onderwerp"
|
||||
},
|
||||
"dateIssued": {
|
||||
"max": {
|
||||
"placeholder": "Vroegste Datum"
|
||||
},
|
||||
"min": {
|
||||
"placeholder": "Laatste Datum"
|
||||
},
|
||||
"head": "Datum"
|
||||
},
|
||||
"has_content_in_original_bundle": {
|
||||
"head": "Heeft bestanden"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"browse": {
|
||||
"title": "Verken {{ collection }} volgens {{ field }} {{ value }}"
|
||||
},
|
||||
"admin": {
|
||||
"registries": {
|
||||
"metadata": {
|
||||
"title": "DSpace Angular :: Metadata Register",
|
||||
"head": "Metadata Register",
|
||||
"description": "Het metadataregister omvat de lijst van alle metadatavelden die beschikbaar zijn in het systeem. Deze velden kunnen verspreid zijn over verschillende metadataschema's. Het qualified Dublin Core schema (dc) is een verplicht schema en kan niet worden verwijderd.",
|
||||
"schemas": {
|
||||
"table": {
|
||||
"id": "ID",
|
||||
"namespace": "Naamruimte",
|
||||
"name": "Naam"
|
||||
},
|
||||
"no-items": "Er kunnen geen metadataschema's getoond worden."
|
||||
}
|
||||
},
|
||||
"schema": {
|
||||
"title": "DSpace Angular :: Metadata Schema Register",
|
||||
"head": "Metadata Schema",
|
||||
"description": "Dit is het metadataschema voor \"{{namespace}}\".",
|
||||
"fields": {
|
||||
"head": "Schema metadatavelden",
|
||||
"table": {
|
||||
"field": "Veld",
|
||||
"scopenote": "Opmerking over bereik"
|
||||
},
|
||||
"no-items": "Er kunnen geen metadatavelden getoond worden."
|
||||
}
|
||||
},
|
||||
"bitstream-formats": {
|
||||
"title": "DSpace Angular :: Bitstream Formaat Register",
|
||||
"head": "Bitstream Formaat Register",
|
||||
"description": "Deze lijst van Bitstream formaten biedt informatie over de formaten die in deze repository zijn toegelaten en op welke manier ze ondersteund worden. De term Bitstream wordt in DSpace gebruikt om een bestand aan te duiden dat samen met metadata onderdeel uitmaakt van een item. De naam bitstream duidt op het feit dat het bestand achterliggend wordt opgeslaan zonder bestandsextensie.",
|
||||
"formats": {
|
||||
"table": {
|
||||
"name": "Naam",
|
||||
"mimetype": "MIME Type",
|
||||
"supportLevel": {
|
||||
"head": "Ondersteuning",
|
||||
"0": "Onbekend",
|
||||
"1": "Gekend",
|
||||
"2": "Ondersteund"
|
||||
},
|
||||
"internal": "intern"
|
||||
},
|
||||
"no-items": "Er kunnen geen bitstreamformaten getoond worden."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"loading": {
|
||||
"default": "Laden...",
|
||||
"top-level-communities": "Inladen van de Communities op het hoogste niveau...",
|
||||
"community": "Community wordt ingeladen...",
|
||||
"collection": "Collectie wordt ingeladen...",
|
||||
"sub-collections": "De sub-collecties worden ingeladen...",
|
||||
"recent-submissions": "Recent toegevoegde items worden ingeladen...",
|
||||
"item": "Item wordt ingeladen...",
|
||||
"objects": "Laden...",
|
||||
"search-results": "Zoekresultaten worden ingeladen...",
|
||||
"browse-by": "Items worden ingeladen..."
|
||||
},
|
||||
"error": {
|
||||
"default": "Fout",
|
||||
"top-level-communities": "Fout bij het inladen van communities op het hoogste niveau",
|
||||
"community": "Fout bij het ophalen van een community",
|
||||
"collection": "Fout bij het ophalen van een collectie",
|
||||
"sub-collections": "Fout bij het ophalen van sub-collecties",
|
||||
"recent-submissions": "Fout bij het ophalen van recent toegevoegde items",
|
||||
"item": "Fout bij het ophalen van items",
|
||||
"objects": "Fout bij het ophalen van objecten",
|
||||
"search-results": "Fout bij het ophalen van zoekresultaten",
|
||||
"browse-by": "Fout bij het ophalen van items",
|
||||
"validation": {
|
||||
"pattern": "Deze invoer is niet toegelaten volgens dit patroon: {{ pattern }}.",
|
||||
"license": {
|
||||
"notgranted": "U moet de invoerlicentie goedkeuren om de invoer af te werken. Indien u deze licentie momenteel niet kan of mag goedkeuren, kan u uw werk opslaan en de invoer later afwerken. U kunt dit nieuwe item ook verwijderen indien u niet voldoet aan de vereisten van de invoerlicentie."
|
||||
}
|
||||
}
|
||||
},
|
||||
"form": {
|
||||
"submit": "Verstuur",
|
||||
"cancel": "Annuleer",
|
||||
"search": "Zoek",
|
||||
"remove": "Verwijder",
|
||||
"first-name": "Voornaam",
|
||||
"last-name": "Achternaam",
|
||||
"loading": "Inladen...",
|
||||
"no-results": "Geen resultaten gevonden",
|
||||
"no-value": "Geen waarde ingevoerd",
|
||||
"group-collapse": "Inklappen",
|
||||
"group-expand": "Uitklappen",
|
||||
"group-collapse-help": "Klik hier op in te klappen",
|
||||
"group-expand-help": "Klik hier om uit te klappen en om meer onderdelen toe te voegen"
|
||||
},
|
||||
"login": {
|
||||
"title": "Aanmelden",
|
||||
"form": {
|
||||
"header": "Gelieve in te loggen in DSpace",
|
||||
"email": "Email adres",
|
||||
"forgot-password": "Bent u uw wachtwoord vergeten?",
|
||||
"new-user": "Nieuwe gebruiker? Gelieve u hier te registreren",
|
||||
"password": "Wachtwoord",
|
||||
"submit": "Aanmelden"
|
||||
}
|
||||
},
|
||||
"logout": {
|
||||
"title": "Afmelden",
|
||||
"form": {
|
||||
"header": "Afmelden in DSpace",
|
||||
"submit": "Afmelden"
|
||||
}
|
||||
},
|
||||
"auth": {
|
||||
"messages": {
|
||||
"expired": "Uw sessie is vervallen. Gelieve opnieuw aan te melden."
|
||||
},
|
||||
"errors": {
|
||||
"invalid-user": "Ongeldig e-mailadres of wachtwoord."
|
||||
}
|
||||
}
|
||||
}
|
4026
resources/i18n/nl.json5
Normal file
4026
resources/i18n/nl.json5
Normal file
File diff suppressed because it is too large
Load Diff
4778
resources/i18n/pl.json5
Normal file
4778
resources/i18n/pl.json5
Normal file
File diff suppressed because it is too large
Load Diff
4026
resources/i18n/pt.json5
Normal file
4026
resources/i18n/pt.json5
Normal file
File diff suppressed because it is too large
Load Diff
4778
resources/i18n/sw.json5
Normal file
4778
resources/i18n/sw.json5
Normal file
File diff suppressed because it is too large
Load Diff
4778
resources/i18n/tr.json5
Normal file
4778
resources/i18n/tr.json5
Normal file
File diff suppressed because it is too large
Load Diff
22
scripts/sync-build-dir.js
Normal file
22
scripts/sync-build-dir.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const syncBuildDir = require('copyfiles');
|
||||
const path = require('path');
|
||||
const {
|
||||
projectRoot,
|
||||
theme,
|
||||
themePath,
|
||||
} = require('../webpack/helpers');
|
||||
|
||||
const projectDepth = projectRoot('./').split(path.sep).length;
|
||||
|
||||
let callback;
|
||||
|
||||
if (theme !== null && theme !== undefined) {
|
||||
callback = () => {
|
||||
syncBuildDir([path.join(themePath, '**/*'), 'build'], { up: projectDepth + 2 }, () => {})
|
||||
}
|
||||
}
|
||||
else {
|
||||
callback = () => {};
|
||||
}
|
||||
|
||||
syncBuildDir([projectRoot('src/**/*'), 'build'], { up: projectDepth + 1 }, callback);
|
342
scripts/sync-i18n-files.js
Executable file
342
scripts/sync-i18n-files.js
Executable file
@@ -0,0 +1,342 @@
|
||||
#!/usr/bin/env node
|
||||
const commander = require('commander');
|
||||
const fs = require('fs');
|
||||
const JSON5 = require('json5');
|
||||
const _cliProgress = require('cli-progress');
|
||||
const _ = require('lodash');
|
||||
const {projectRoot} = require('../webpack/helpers');
|
||||
|
||||
const program = new commander.Command();
|
||||
program.version('1.0.0', '-v, --version');
|
||||
|
||||
const NEW_MESSAGE_TODO = '// TODO New key - Add a translation';
|
||||
const MESSAGE_CHANGED_TODO = '// TODO Source message changed - Revise the translation';
|
||||
const COMMENTS_CHANGED_TODO = '// TODO Source comments changed - Revise the translation';
|
||||
|
||||
const DEFAULT_SOURCE_FILE_LOCATION = 'resources/i18n/en.json5';
|
||||
const LANGUAGE_FILES_LOCATION = 'resources/i18n';
|
||||
|
||||
parseCliInput();
|
||||
|
||||
/**
|
||||
* Parses the CLI input given by the user
|
||||
* If no parameters are set (standard usage) -> source file is default (set to DEFAULT_SOURCE_FILE_LOCATION) and all
|
||||
* other language files in the LANGUAGE_FILES_LOCATION are synced with this one in-place
|
||||
* (replaced with newly synced file)
|
||||
* If only target-file -t is set -> either -i in-place or -o output-file must be set
|
||||
* Source file can be set with -s if it should be something else than DEFAULT_SOURCE_FILE_LOCATION
|
||||
*
|
||||
* If any of the paths to files/dirs given by user are not valid, an error message is printed and script gets aborted
|
||||
*/
|
||||
function parseCliInput() {
|
||||
program
|
||||
.option('-d, --output-dir <output-dir>', 'output dir when running script on all language files; mutually exclusive with -o')
|
||||
.option('-t, --target-file <target>', 'target file we compare with and where completed output ends up if -o is not configured and -i is')
|
||||
.option('-i, --edit-in-place', 'edit-in-place; store output straight in target file; mutually exclusive with -o')
|
||||
.option('-s, --source-file <source>', 'source file to be parsed for translation', projectRoot(DEFAULT_SOURCE_FILE_LOCATION))
|
||||
.option('-o, --output-file <output>', 'where output of script ends up; mutually exclusive with -i')
|
||||
.usage('([-d <output-dir>] [-s <source-file>]) || (-t <target-file> (-i | -o <output>) [-s <source-file>])')
|
||||
.parse(process.argv);
|
||||
|
||||
if (!program.targetFile) {
|
||||
fs.readdirSync(projectRoot(LANGUAGE_FILES_LOCATION)).forEach(file => {
|
||||
if (!program.sourceFile.toString().endsWith(file)) {
|
||||
const targetFileLocation = projectRoot(LANGUAGE_FILES_LOCATION + "/" + file);
|
||||
console.log('Syncing file at: ' + targetFileLocation + ' with source file at: ' + program.sourceFile);
|
||||
if (program.outputDir) {
|
||||
if (!fs.existsSync(program.outputDir)) {
|
||||
fs.mkdirSync(program.outputDir);
|
||||
}
|
||||
const outputFileLocation = program.outputDir + "/" + file;
|
||||
console.log('Output location: ' + outputFileLocation);
|
||||
syncFileWithSource(targetFileLocation, outputFileLocation);
|
||||
} else {
|
||||
console.log('Replacing in target location');
|
||||
syncFileWithSource(targetFileLocation, targetFileLocation);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
if (program.targetFile && !checkIfPathToFileIsValid(program.targetFile)) {
|
||||
console.error('Directory path of target file is not valid.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
if (program.targetFile && checkIfFileExists(program.targetFile) && !(program.editInPlace || program.outputFile)) {
|
||||
console.error('This target file already exists, if you want to overwrite this add option -i, or add an -o output location');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
if (!checkIfFileExists(program.sourceFile)) {
|
||||
console.error('Path of source file is not valid.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
if (program.outputFile && !checkIfPathToFileIsValid(program.outputFile)) {
|
||||
console.error('Directory path of output file is not valid.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
syncFileWithSource(program.targetFile, getOutputFileLocationIfExistsElseTargetFileLocation(program.targetFile));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates chunk lists for both the source and the target files (for example en.json5 and nl.json5 respectively)
|
||||
* > Creates output chunks by comparing the source chunk with corresponding target chunk (based on key of translation)
|
||||
* > Writes the output chunks to a new valid lang.json5 file, either replacing the target file (-i in-place)
|
||||
* or sending it to an output file specified by the user
|
||||
* @param pathToTargetFile Valid path to target file to generate target chunks from
|
||||
* @param pathToOutputFile Valid path to output file to write output chunks to
|
||||
*/
|
||||
function syncFileWithSource(pathToTargetFile, pathToOutputFile) {
|
||||
const progressBar = new _cliProgress.SingleBar({}, _cliProgress.Presets.shades_classic);
|
||||
progressBar.start(100, 0);
|
||||
|
||||
const sourceLines = [];
|
||||
const targetLines = [];
|
||||
const existingTargetFile = readFileIfExists(pathToTargetFile);
|
||||
existingTargetFile.toString().split("\n").forEach((function (line) {
|
||||
targetLines.push(line.trim());
|
||||
}));
|
||||
progressBar.update(10);
|
||||
const sourceFile = readFileIfExists(program.sourceFile);
|
||||
sourceFile.toString().split("\n").forEach((function (line) {
|
||||
sourceLines.push(line.trim());
|
||||
}));
|
||||
progressBar.update(20);
|
||||
const sourceChunks = createChunks(sourceLines, progressBar, false);
|
||||
const targetChunks = createChunks(targetLines, progressBar, true);
|
||||
|
||||
const outputChunks = compareChunksAndCreateOutput(sourceChunks, targetChunks, progressBar);
|
||||
|
||||
const file = fs.createWriteStream(pathToOutputFile);
|
||||
file.on('error', function (err) {
|
||||
console.error('Something went wrong writing to output file at: ' + pathToOutputFile + err)
|
||||
});
|
||||
file.on('open', function() {
|
||||
file.write("{\n");
|
||||
outputChunks.forEach(function (chunk) {
|
||||
progressBar.increment();
|
||||
chunk.split("\n").forEach(function (line) {
|
||||
file.write(" " + line + "\n");
|
||||
});
|
||||
});
|
||||
file.write("\n}");
|
||||
file.end();
|
||||
});
|
||||
file.on('finish', function() {
|
||||
const osName = process.platform;
|
||||
if (osName.startsWith("win")) {
|
||||
replaceLineEndingsToCRLF(pathToOutputFile);
|
||||
}
|
||||
});
|
||||
|
||||
progressBar.update(100);
|
||||
progressBar.stop();
|
||||
}
|
||||
|
||||
/**
|
||||
* For each of the source chunks:
|
||||
* - Determine if it's a new key-value => Add it to output, with source comments, source key-value commented, a message indicating it's new and the source-key value uncommented
|
||||
* - If it's not new, compare it with the corresponding target chunk and log the differences, see createNewChunkComparingSourceAndTarget
|
||||
* @param sourceChunks All the source chunks, split per key-value pair group
|
||||
* @param targetChunks All the target chunks, split per key-value pair group
|
||||
* @param progressBar The progressbar for the CLI
|
||||
* @return {Array} All the output chunks, split per key-value pair group
|
||||
*/
|
||||
function compareChunksAndCreateOutput(sourceChunks, targetChunks, progressBar) {
|
||||
const outputChunks = [];
|
||||
sourceChunks.map((sourceChunk) => {
|
||||
progressBar.increment();
|
||||
if (sourceChunk.trim().length !== 0) {
|
||||
let newChunk = [];
|
||||
const sourceList = sourceChunk.split("\n");
|
||||
const keyValueSource = sourceList[sourceList.length - 1];
|
||||
const keySource = getSubStringBeforeLastString(keyValueSource, ":");
|
||||
const commentSource = getSubStringBeforeLastString(sourceChunk, keyValueSource);
|
||||
|
||||
const correspondingTargetChunk = targetChunks.find((targetChunk) => {
|
||||
return targetChunk.includes(keySource);
|
||||
});
|
||||
|
||||
// Create new chunk with: the source comments, the commented source key-value, the todos and either the old target key-value pair or if it's a new pair, the source key-value pair
|
||||
newChunk.push(removeWhiteLines(commentSource));
|
||||
newChunk.push("// " + keyValueSource);
|
||||
if (correspondingTargetChunk === undefined) {
|
||||
newChunk.push(NEW_MESSAGE_TODO);
|
||||
newChunk.push(keyValueSource);
|
||||
} else {
|
||||
createNewChunkComparingSourceAndTarget(correspondingTargetChunk, sourceChunk, commentSource, keyValueSource, newChunk);
|
||||
}
|
||||
|
||||
outputChunks.push(newChunk.filter(Boolean).join("\n"));
|
||||
} else {
|
||||
outputChunks.push(sourceChunk);
|
||||
}
|
||||
});
|
||||
return outputChunks;
|
||||
}
|
||||
|
||||
/**
|
||||
* If a corresponding target chunk is found:
|
||||
* - If old key value is not found in comments > Assumed it is new key
|
||||
* - If the target comments do not contain the source comments (because they have changed since last time) => Add comments changed message
|
||||
* - If the key-value in the target comments is not the same as the source key-value (because it changes since last time) => Add message changed message
|
||||
* - Add the old todos if they haven't been added already
|
||||
* - End with the original target key-value
|
||||
*/
|
||||
function createNewChunkComparingSourceAndTarget(correspondingTargetChunk, sourceChunk, commentSource, keyValueSource, newChunk) {
|
||||
let commentsOfSourceHaveChanged = false;
|
||||
let messageOfSourceHasChanged = false;
|
||||
|
||||
const targetList = correspondingTargetChunk.split("\n");
|
||||
const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*");
|
||||
const keyValueTarget = targetList[targetList.length - 1];
|
||||
|
||||
if (oldKeyValueInTargetComments != null) {
|
||||
const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0];
|
||||
|
||||
if (!(_.isEmpty(correspondingTargetChunk) && _.isEmpty(commentSource)) && !removeWhiteLines(correspondingTargetChunk).includes(removeWhiteLines(commentSource.trim()))) {
|
||||
commentsOfSourceHaveChanged = true;
|
||||
newChunk.push(COMMENTS_CHANGED_TODO);
|
||||
}
|
||||
const parsedOldKey = JSON5.stringify("{" + oldKeyValueUncommented + "}");
|
||||
const parsedSourceKey = JSON5.stringify("{" + keyValueSource + "}");
|
||||
if (!_.isEqual(parsedOldKey, parsedSourceKey)) {
|
||||
messageOfSourceHasChanged = true;
|
||||
newChunk.push(MESSAGE_CHANGED_TODO);
|
||||
}
|
||||
addOldTodosIfNeeded(targetList, newChunk, commentsOfSourceHaveChanged, messageOfSourceHasChanged);
|
||||
}
|
||||
newChunk.push(keyValueTarget);
|
||||
}
|
||||
|
||||
// Adds old todos found in target comments if they've not been added already
|
||||
function addOldTodosIfNeeded(targetList, newChunk, commentsOfSourceHaveChanged, messageOfSourceHasChanged) {
|
||||
targetList.map((targetLine) => {
|
||||
const foundTODO = getSubStringWithRegex(targetLine, "\\s*//\\s*TODO.*");
|
||||
if (foundTODO != null) {
|
||||
const todo = foundTODO[0];
|
||||
if (!((todo.includes(COMMENTS_CHANGED_TODO) && commentsOfSourceHaveChanged)
|
||||
|| (todo.includes(MESSAGE_CHANGED_TODO) && messageOfSourceHasChanged))) {
|
||||
newChunk.push(todo);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates chunks from an array of lines, each chunk contains either an empty line or a grouping of comments with their corresponding key-value pair
|
||||
* @param lines Array of lines, to be grouped into chunks
|
||||
* @param progressBar Progressbar of the CLI
|
||||
* @return {Array} Array of chunks, grouped by key-value and their corresponding comments or an empty line
|
||||
*/
|
||||
function createChunks(lines, progressBar, creatingTarget) {
|
||||
const chunks = [];
|
||||
let nextChunk = [];
|
||||
let onMultiLineComment = false;
|
||||
lines.map((line) => {
|
||||
progressBar.increment();
|
||||
if (line.length === 0) {
|
||||
chunks.push(line);
|
||||
}
|
||||
if (isOneLineCommentLine(line)) {
|
||||
nextChunk.push(line);
|
||||
}
|
||||
if (onMultiLineComment) {
|
||||
nextChunk.push(line);
|
||||
if (isEndOfMultiLineComment(line)) {
|
||||
onMultiLineComment = false;
|
||||
}
|
||||
}
|
||||
if (isStartOfMultiLineComment(line)) {
|
||||
nextChunk.push(line);
|
||||
onMultiLineComment = true;
|
||||
}
|
||||
if (isKeyValuePair(line)) {
|
||||
nextChunk.push(line);
|
||||
const newMessageLineIfExists = nextChunk.find((lineInChunk) => lineInChunk.trim().startsWith(NEW_MESSAGE_TODO));
|
||||
if (newMessageLineIfExists === undefined || !creatingTarget) {
|
||||
chunks.push(nextChunk.join("\n"));
|
||||
}
|
||||
nextChunk = [];
|
||||
}
|
||||
});
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function readFileIfExists(pathToFile) {
|
||||
if (checkIfFileExists(pathToFile)) {
|
||||
try {
|
||||
return fs.readFileSync(pathToFile, 'utf8');
|
||||
} catch (e) {
|
||||
console.error('Error:', e.stack);
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isOneLineCommentLine(line) {
|
||||
return (line.startsWith("//"));
|
||||
}
|
||||
|
||||
function isStartOfMultiLineComment(line) {
|
||||
return (line.startsWith("/*"));
|
||||
}
|
||||
|
||||
function isEndOfMultiLineComment(line) {
|
||||
return (line.endsWith("*/"));
|
||||
}
|
||||
|
||||
function isKeyValuePair(line) {
|
||||
return (line.startsWith("\""));
|
||||
}
|
||||
|
||||
|
||||
function getSubStringWithRegex(string, regex) {
|
||||
return string.match(regex);
|
||||
}
|
||||
|
||||
function getSubStringBeforeLastString(string, char) {
|
||||
const lastCharIndex = string.lastIndexOf(char);
|
||||
return string.substr(0, lastCharIndex);
|
||||
}
|
||||
|
||||
|
||||
function getOutputFileLocationIfExistsElseTargetFileLocation(targetLocation) {
|
||||
if (program.outputFile) {
|
||||
return program.outputFile;
|
||||
}
|
||||
return targetLocation;
|
||||
}
|
||||
|
||||
function checkIfPathToFileIsValid(pathToCheck) {
|
||||
if (!pathToCheck.includes("/")) {
|
||||
return true;
|
||||
}
|
||||
return checkIfFileExists(getPathOfDirectory(pathToCheck));
|
||||
}
|
||||
|
||||
function checkIfFileExists(pathToCheck) {
|
||||
return fs.existsSync(pathToCheck);
|
||||
}
|
||||
|
||||
function getPathOfDirectory(pathToCheck) {
|
||||
return getSubStringBeforeLastString(pathToCheck, "/");
|
||||
}
|
||||
|
||||
function removeWhiteLines(string) {
|
||||
return string.replace(/^(?=\n)$|^\s*|\s*$|\n\n+/gm, "")
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces UNIX \n LF line endings to windows \r\n CRLF line endings.
|
||||
* @param filePath Path to file whose line endings are being converted
|
||||
*/
|
||||
function replaceLineEndingsToCRLF(filePath) {
|
||||
const data = readFileIfExists(filePath);
|
||||
const result = data.replace(/\n/g,"\r\n");
|
||||
fs.writeFileSync(filePath, result, 'utf8');
|
||||
}
|
@@ -13,8 +13,8 @@
|
||||
*/
|
||||
Error.stackTraceLimit = Infinity;
|
||||
|
||||
require('core-js/es6');
|
||||
require('core-js/es7/reflect');
|
||||
require('core-js/es');
|
||||
require('core-js/features/reflect');
|
||||
|
||||
// Typescript emit helpers polyfill
|
||||
require('ts-helpers');
|
||||
|
@@ -0,0 +1,38 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getAccessControlModulePath } from '../admin-routing.module';
|
||||
|
||||
const GROUP_EDIT_PATH = 'groups';
|
||||
|
||||
export function getGroupEditPath(id: string) {
|
||||
return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: 'epeople', component: EPeopleRegistryComponent, data: { title: 'admin.access-control.epeople.title' } },
|
||||
{ path: GROUP_EDIT_PATH, component: GroupsRegistryComponent, data: { title: 'admin.access-control.groups.title' } },
|
||||
{
|
||||
path: `${GROUP_EDIT_PATH}/:groupId`,
|
||||
component: GroupFormComponent,
|
||||
data: {title: 'admin.registries.schema.title'}
|
||||
},
|
||||
{
|
||||
path: `${GROUP_EDIT_PATH}/newGroup`,
|
||||
component: GroupFormComponent,
|
||||
data: {title: 'admin.registries.schema.title'}
|
||||
},
|
||||
])
|
||||
]
|
||||
})
|
||||
/**
|
||||
* Routing module for the AccessControl section of the admin sidebar
|
||||
*/
|
||||
export class AdminAccessControlRoutingModule {
|
||||
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { AdminAccessControlRoutingModule } from './admin-access-control-routing.module';
|
||||
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||
import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component';
|
||||
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
|
||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
RouterModule,
|
||||
TranslateModule,
|
||||
AdminAccessControlRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
EPeopleRegistryComponent,
|
||||
EPersonFormComponent,
|
||||
GroupsRegistryComponent,
|
||||
GroupFormComponent,
|
||||
SubgroupsListComponent,
|
||||
MembersListComponent
|
||||
],
|
||||
entryComponents: []
|
||||
})
|
||||
/**
|
||||
* This module handles all components related to the access control pages
|
||||
*/
|
||||
export class AdminAccessControlModule {
|
||||
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const EPeopleRegistryActionTypes = {
|
||||
|
||||
EDIT_EPERSON: type('dspace/epeople-registry/EDIT_EPERSON'),
|
||||
CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_EDIT_EPERSON'),
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to edit an EPerson in the EPeople registry
|
||||
*/
|
||||
export class EPeopleRegistryEditEPersonAction implements Action {
|
||||
type = EPeopleRegistryActionTypes.EDIT_EPERSON;
|
||||
|
||||
eperson: EPerson;
|
||||
|
||||
constructor(eperson: EPerson) {
|
||||
this.eperson = eperson;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to cancel the editing of an EPerson in the EPeople registry
|
||||
*/
|
||||
export class EPeopleRegistryCancelEPersonAction implements Action {
|
||||
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
* so that reducers can easily compose action types
|
||||
* These are all the actions to perform on the EPeople registry state
|
||||
*/
|
||||
export type EPeopleRegistryAction
|
||||
= EPeopleRegistryEditEPersonAction
|
||||
| EPeopleRegistryCancelEPersonAction
|
@@ -0,0 +1,93 @@
|
||||
<div class="container">
|
||||
<div class="epeople-registry row">
|
||||
<div class="col-12">
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2">{{labelPrefix + 'head' | translate}}</h2>
|
||||
|
||||
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="forceUpdateEPeople()"
|
||||
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
|
||||
|
||||
<div *ngIf="!isEPersonFormShown" class="button-row top d-flex pb-2">
|
||||
<button class="mr-auto btn btn-success addEPerson-button"
|
||||
(click)="isEPersonFormShown = true">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-primary float-right">{{labelPrefix + 'button.see-all' | translate}}</button>
|
||||
</h3>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
|
||||
<div class="col-12 col-sm-3">
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
||||
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-9 col-12">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit"
|
||||
class="search-button btn btn-secondary">{{ labelPrefix + 'search.button' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(ePeople | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(ePeople | async)?.payload"
|
||||
[collectionSize]="(ePeople | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let eperson of (ePeople | async)?.payload?.page"
|
||||
[ngClass]="{'table-primary' : isActive(eperson) | async}">
|
||||
<td>{{eperson.id}}</td>
|
||||
<td>{{eperson.name}}</td>
|
||||
<td>{{eperson.email}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="toggleEditEPerson(eperson)"
|
||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: eperson.name} }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button (click)="deleteEPerson(eperson)"
|
||||
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: eperson.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeople | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{labelPrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,214 @@
|
||||
import { Router } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
|
||||
import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||
import { EPeopleRegistryComponent } from './epeople-registry.component';
|
||||
|
||||
describe('EPeopleRegistryComponent', () => {
|
||||
let component: EPeopleRegistryComponent;
|
||||
let fixture: ComponentFixture<EPeopleRegistryComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
|
||||
let mockEPeople;
|
||||
let ePersonDataServiceStub: any;
|
||||
|
||||
beforeEach(async(() => {
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
ePersonDataServiceStub = {
|
||||
activeEPerson: null,
|
||||
allEpeople: mockEPeople,
|
||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
},
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
},
|
||||
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
if (scope === 'email') {
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return ePerson.email === query
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
if (scope === 'metadata') {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
}
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return (ePerson.name.includes(query) || ePerson.email.includes(query))
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
},
|
||||
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||
return (ePerson2.uuid !== ePerson.uuid);
|
||||
});
|
||||
return observableOf(true);
|
||||
},
|
||||
editEPerson(ePerson: EPerson) {
|
||||
this.activeEPerson = ePerson;
|
||||
},
|
||||
cancelEditEPerson() {
|
||||
this.activeEPerson = null;
|
||||
},
|
||||
clearEPersonRequests(): void {
|
||||
// empty
|
||||
},
|
||||
getEPeoplePageRouterLink(): string {
|
||||
return '/admin/access-control/epeople';
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [EPeopleRegistryComponent],
|
||||
providers: [EPeopleRegistryComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EPeopleRegistryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create EPeopleRegistryComponent', inject([EPeopleRegistryComponent], (comp: EPeopleRegistryComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should display list of ePeople', () => {
|
||||
const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
expect(ePeopleIdsFound.length).toEqual(2);
|
||||
mockEPeople.map((ePerson: EPerson) => {
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === ePerson.uuid);
|
||||
})).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
describe('when searching with scope/query (scope metadata)', () => {
|
||||
let ePeopleIdsFound;
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ scope: 'metadata', query: EPersonMock2.name });
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
}));
|
||||
|
||||
it('should display search result', () => {
|
||||
expect(ePeopleIdsFound.length).toEqual(1);
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === EPersonMock2.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when searching with scope/query (scope email)', () => {
|
||||
let ePeopleIdsFound;
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ scope: 'email', query: EPersonMock.email });
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
}));
|
||||
|
||||
it('should display search result', () => {
|
||||
expect(ePeopleIdsFound.length).toEqual(1);
|
||||
expect(ePeopleIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === EPersonMock.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleEditEPerson', () => {
|
||||
describe('when you click on first edit eperson button', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const editButtons = fixture.debugElement.queryAll(By.css('.access-control-editEPersonButton'));
|
||||
editButtons[0].triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('editEPerson form is toggled', () => {
|
||||
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||
if (activeEPerson === ePeopleIds[0].nativeElement.textContent) {
|
||||
expect(component.isEPersonFormShown).toEqual(false);
|
||||
} else {
|
||||
expect(component.isEPersonFormShown).toEqual(true);
|
||||
}
|
||||
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteEPerson', () => {
|
||||
describe('when you click on first delete eperson button', () => {
|
||||
let ePeopleIdsFoundBeforeDelete;
|
||||
let ePeopleIdsFoundAfterDelete;
|
||||
beforeEach(fakeAsync(() => {
|
||||
ePeopleIdsFoundBeforeDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
const deleteButtons = fixture.debugElement.queryAll(By.css('.access-control-deleteEPersonButton'));
|
||||
deleteButtons[0].triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
ePeopleIdsFoundAfterDelete = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
}));
|
||||
|
||||
it('first ePerson is deleted', () => {
|
||||
expect(ePeopleIdsFoundBeforeDelete.length === ePeopleIdsFoundAfterDelete + 1);
|
||||
ePeopleIdsFoundAfterDelete.forEach((epersonElement) => {
|
||||
expect(epersonElement !== ePeopleIdsFoundBeforeDelete[0].nativeElement.textContent).toBeTrue();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,202 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-epeople-registry',
|
||||
templateUrl: './epeople-registry.component.html',
|
||||
})
|
||||
/**
|
||||
* A component used for managing all existing epeople within the repository.
|
||||
* The admin can create, edit or delete epeople here.
|
||||
*/
|
||||
export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
|
||||
labelPrefix = 'admin.access-control.epeople.';
|
||||
|
||||
/**
|
||||
* A list of all the current EPeople within the repository or the result of the search
|
||||
*/
|
||||
ePeople: Observable<RemoteData<PaginatedList<EPerson>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of epeople
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'epeople-list-pagination',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether or not to show the EPerson form
|
||||
*/
|
||||
isEPersonFormShown: boolean;
|
||||
|
||||
// The search form
|
||||
searchForm;
|
||||
|
||||
// Current search in epersons registry
|
||||
currentSearchQuery: string;
|
||||
currentSearchScope: string;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
constructor(private epersonService: EPersonDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
scope: 'metadata',
|
||||
query: '',
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.isEPersonFormShown = false;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
if (eperson != null && eperson.id) {
|
||||
this.isEPersonFormShown = true;
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery })
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of EPeople by first clearing the cache related to EPeople, then performing
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateEPeople() {
|
||||
this.epersonService.clearEPersonRequests();
|
||||
this.isEPersonFormShown = false;
|
||||
this.search({ query: '', scope: 'metadata' })
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the EPeople by metadata (default) or email
|
||||
* @param data Contains scope and query param
|
||||
*/
|
||||
search(data: any) {
|
||||
const query: string = data.query;
|
||||
const scope: string = data.scope;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.router.navigateByUrl(this.epersonService.getEPeoplePageRouterLink());
|
||||
this.currentSearchQuery = query;
|
||||
this.config.currentPage = 1;
|
||||
}
|
||||
if (scope != null && this.currentSearchScope !== scope) {
|
||||
this.router.navigateByUrl(this.epersonService.getEPeoplePageRouterLink());
|
||||
this.currentSearchScope = scope;
|
||||
this.config.currentPage = 1;
|
||||
}
|
||||
this.ePeople = this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the selected EPerson
|
||||
* @param ePerson
|
||||
*/
|
||||
toggleEditEPerson(ePerson: EPerson) {
|
||||
this.getActiveEPerson().pipe(take(1)).subscribe((activeEPerson: EPerson) => {
|
||||
if (ePerson === activeEPerson) {
|
||||
this.epersonService.cancelEditEPerson();
|
||||
this.isEPersonFormShown = false;
|
||||
} else {
|
||||
this.epersonService.editEPerson(ePerson);
|
||||
this.isEPersonFormShown = true;
|
||||
}
|
||||
});
|
||||
this.scrollToTop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes EPerson, show notification on success/failure & updates EPeople list
|
||||
*/
|
||||
deleteEPerson(ePerson: EPerson) {
|
||||
if (hasValue(ePerson.id)) {
|
||||
this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name }));
|
||||
this.forceUpdateEPeople();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', { name: ePerson.name }));
|
||||
}
|
||||
this.epersonService.cancelEditEPerson();
|
||||
this.isEPersonFormShown = false;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsub all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
scrollToTop() {
|
||||
(function smoothscroll() {
|
||||
const currentScroll = document.documentElement.scrollTop || document.body.scrollTop;
|
||||
if (currentScroll > 0) {
|
||||
window.requestAnimationFrame(smoothscroll);
|
||||
window.scrollTo(0, currentScroll - (currentScroll / 8));
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty and search all search
|
||||
*/
|
||||
clearFormAndResetResult() {
|
||||
this.searchForm.patchValue({
|
||||
query: '',
|
||||
});
|
||||
this.search({ query: '' });
|
||||
}
|
||||
}
|
@@ -0,0 +1,54 @@
|
||||
import { EPersonMock } from '../../../shared/testing/eperson-mock';
|
||||
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
|
||||
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
|
||||
|
||||
const initialState: EPeopleRegistryState = {
|
||||
editEPerson: null,
|
||||
};
|
||||
|
||||
const editState: EPeopleRegistryState = {
|
||||
editEPerson: EPersonMock,
|
||||
};
|
||||
|
||||
class NullAction extends EPeopleRegistryEditEPersonAction {
|
||||
type = null;
|
||||
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
describe('epeopleRegistryReducer', () => {
|
||||
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
const newState = ePeopleRegistryReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should start with an initial state', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
const initState = ePeopleRegistryReducer(undefined, action);
|
||||
|
||||
expect(initState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should update the current state to change the editEPerson to a new eperson when EPeopleRegistryEditEPersonAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new EPeopleRegistryEditEPersonAction(EPersonMock);
|
||||
const newState = ePeopleRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editEPerson).toEqual(EPersonMock);
|
||||
});
|
||||
|
||||
it('should update the current state to remove the editEPerson from the state when EPeopleRegistryCancelEPersonAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new EPeopleRegistryCancelEPersonAction();
|
||||
const newState = ePeopleRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editEPerson).toEqual(null);
|
||||
});
|
||||
});
|
@@ -0,0 +1,46 @@
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import {
|
||||
EPeopleRegistryAction,
|
||||
EPeopleRegistryActionTypes,
|
||||
EPeopleRegistryEditEPersonAction
|
||||
} from './epeople-registry.actions';
|
||||
|
||||
/**
|
||||
* The EPeople registry state.
|
||||
* @interface EPeopleRegistryState
|
||||
*/
|
||||
export interface EPeopleRegistryState {
|
||||
editEPerson: EPerson;
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial state.
|
||||
*/
|
||||
const initialState: EPeopleRegistryState = {
|
||||
editEPerson: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer that handles EPeopleRegistryActions to modify EPeople
|
||||
* @param state The current EPeopleRegistryState
|
||||
* @param action The EPeopleRegistryAction to perform on the state
|
||||
*/
|
||||
export function ePeopleRegistryReducer(state = initialState, action: EPeopleRegistryAction): EPeopleRegistryState {
|
||||
switch (action.type) {
|
||||
|
||||
case EPeopleRegistryActionTypes.EDIT_EPERSON: {
|
||||
return Object.assign({}, state, {
|
||||
editEPerson: (action as EPeopleRegistryEditEPersonAction).eperson
|
||||
});
|
||||
}
|
||||
|
||||
case EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON: {
|
||||
return Object.assign({}, state, {
|
||||
editEPerson: null
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@@ -0,0 +1,58 @@
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async; then editheader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h4>{{messagePrefix + '.create' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h4>{{messagePrefix + '.edit' | translate}}</h4>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
</ds-form>
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(groups | async)?.payload"
|
||||
[collectionSize]="(groups | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||
<td>{{group.id}}</td>
|
||||
<td><a (click)="groupsDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupsDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
<div>{{messagePrefix + '.memberOfNoGroups' | translate}}</div>
|
||||
<div>
|
||||
<button [routerLink]="[groupsDataService.getGroupRegistryRouterLink()]"
|
||||
class="btn btn-primary">{{messagePrefix + '.goToGroups' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,231 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { FindListOptions } from '../../../../core/data/request.models';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../../shared/mocks/mock-form-builder-service';
|
||||
import { getMockTranslateService } from '../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson-mock';
|
||||
import { MockTranslateLoader } from '../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||
import { EPeopleRegistryComponent } from '../epeople-registry.component';
|
||||
import { EPersonFormComponent } from './eperson-form.component';
|
||||
|
||||
describe('EPersonFormComponent', () => {
|
||||
let component: EPersonFormComponent;
|
||||
let fixture: ComponentFixture<EPersonFormComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
|
||||
let mockEPeople;
|
||||
let ePersonDataServiceStub: any;
|
||||
|
||||
beforeEach(async(() => {
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
ePersonDataServiceStub = {
|
||||
activeEPerson: null,
|
||||
allEpeople: mockEPeople,
|
||||
getEPeople(): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
},
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
},
|
||||
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
if (scope === 'email') {
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return ePerson.email === query
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
if (scope === 'metadata') {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
}
|
||||
const result = this.allEpeople.find((ePerson: EPerson) => {
|
||||
return (ePerson.name.includes(query) || ePerson.email.includes(query))
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allEpeople));
|
||||
},
|
||||
deleteEPerson(ePerson: EPerson): Observable<boolean> {
|
||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||
return (ePerson2.uuid !== ePerson.uuid);
|
||||
});
|
||||
return observableOf(true);
|
||||
},
|
||||
create(ePerson: EPerson) {
|
||||
this.allEpeople = [...this.allEpeople, ePerson]
|
||||
},
|
||||
editEPerson(ePerson: EPerson) {
|
||||
this.activeEPerson = ePerson;
|
||||
},
|
||||
cancelEditEPerson() {
|
||||
this.activeEPerson = null;
|
||||
},
|
||||
clearEPersonRequests(): void {
|
||||
// empty
|
||||
},
|
||||
tryToCreate(ePerson: EPerson): Observable<RestResponse> {
|
||||
this.allEpeople = [...this.allEpeople, ePerson]
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
updateEPerson(ePerson: EPerson): Observable<RestResponse> {
|
||||
this.allEpeople.forEach((ePersonInList: EPerson, i: number) => {
|
||||
if (ePersonInList.id === ePerson.id) {
|
||||
this.allEpeople[i] = ePerson;
|
||||
}
|
||||
});
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [EPeopleRegistryComponent, EPersonFormComponent],
|
||||
providers: [EPersonFormComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: HttpClient, useValue: {} },
|
||||
{ provide: ObjectCacheService, useValue: {} },
|
||||
{ provide: UUIDService, useValue: {} },
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: {} },
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
EPeopleRegistryComponent
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EPersonFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create EPersonFormComponent', inject([EPersonFormComponent], (comp: EPersonFormComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
let firstName;
|
||||
let lastName;
|
||||
let email;
|
||||
let canLogIn;
|
||||
let requireCertificate;
|
||||
|
||||
let expected;
|
||||
beforeEach(() => {
|
||||
firstName = 'testName';
|
||||
lastName = 'testLastName';
|
||||
email = 'testEmail@test.com';
|
||||
canLogIn = 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');
|
||||
component.firstName.value = firstName;
|
||||
component.lastName.value = lastName;
|
||||
component.email.value = email;
|
||||
component.canLogIn.value = canLogIn;
|
||||
component.requireCertificate.value = requireCertificate;
|
||||
});
|
||||
describe('without active EPerson', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(undefined));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new eperson using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('with an active eperson', () => {
|
||||
let expectedWithId;
|
||||
|
||||
beforeEach(() => {
|
||||
expectedWithId = Object.assign(new EPerson(), {
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: firstName
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: lastName
|
||||
},
|
||||
],
|
||||
},
|
||||
email: email,
|
||||
canLogIn: canLogIn,
|
||||
requireCertificate: requireCertificate,
|
||||
});
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit the existing eperson using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expectedWithId);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,374 @@
|
||||
import { Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import {
|
||||
DynamicCheckboxModel,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription, combineLatest } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-eperson-form',
|
||||
templateUrl: './eperson-form.component.html'
|
||||
})
|
||||
/**
|
||||
* A form used for creating and editing EPeople
|
||||
*/
|
||||
export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
labelPrefix = 'admin.access-control.epeople.form.';
|
||||
|
||||
/**
|
||||
* A unique id used for ds-form
|
||||
*/
|
||||
formId = 'eperson-form';
|
||||
|
||||
/**
|
||||
* The labelPrefix for all messages related to this form
|
||||
*/
|
||||
messagePrefix = 'admin.access-control.epeople.form';
|
||||
|
||||
/**
|
||||
* Dynamic input models for the inputs of form
|
||||
*/
|
||||
firstName: DynamicInputModel;
|
||||
lastName: DynamicInputModel;
|
||||
email: DynamicInputModel;
|
||||
// booleans
|
||||
canLogIn: DynamicCheckboxModel;
|
||||
requireCertificate: DynamicCheckboxModel;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
*/
|
||||
formModel: DynamicFormControlModel[];
|
||||
|
||||
/**
|
||||
* Layout used for structuring the form inputs
|
||||
*/
|
||||
formLayout: DynamicFormLayout = {
|
||||
firstName: {
|
||||
grid: {
|
||||
host: 'row'
|
||||
}
|
||||
},
|
||||
lastName: {
|
||||
grid: {
|
||||
host: 'row'
|
||||
}
|
||||
},
|
||||
email: {
|
||||
grid: {
|
||||
host: 'row'
|
||||
}
|
||||
},
|
||||
canLogIn: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
},
|
||||
requireCertificate: {
|
||||
grid: {
|
||||
host: 'col col-sm-6 d-inline-block'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
*/
|
||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is cancelled
|
||||
*/
|
||||
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* A list of all the groups this EPerson is a member of
|
||||
*/
|
||||
groups: Observable<RemoteData<PaginatedList<Group>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of groups
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'groups-ePersonMemberOf-list-pagination',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
|
||||
/**
|
||||
* Try to retrieve initial active eperson, to fill in checkboxes at component creation
|
||||
*/
|
||||
epersonInitial: EPerson;
|
||||
|
||||
constructor(public epersonService: EPersonDataService,
|
||||
public groupsDataService: GroupDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.epersonInitial = eperson;
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
combineLatest(
|
||||
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({
|
||||
id: 'firstName',
|
||||
label: firstName,
|
||||
name: 'firstName',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.lastName = new DynamicInputModel({
|
||||
id: 'lastName',
|
||||
label: lastName,
|
||||
name: 'lastName',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.email = new DynamicInputModel({
|
||||
id: 'email',
|
||||
label: email,
|
||||
name: 'email',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[a-z0-9._%+-]+@[a-z0-9.-]+\\.[a-z]{2,4}$'
|
||||
},
|
||||
required: true,
|
||||
hint: emailHint
|
||||
});
|
||||
this.canLogIn = new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'canLogIn',
|
||||
label: canLogIn,
|
||||
name: 'canLogIn',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
|
||||
});
|
||||
this.requireCertificate = new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'requireCertificate',
|
||||
label: requireCertificate,
|
||||
name: 'requireCertificate',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
|
||||
});
|
||||
this.formModel = [
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
this.email,
|
||||
this.canLogIn,
|
||||
this.requireCertificate,
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
if (eperson != null) {
|
||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}
|
||||
this.formGroup.patchValue({
|
||||
firstName: eperson != null ? eperson.firstMetadataValue('eperson.firstname') : '',
|
||||
lastName: eperson != null ? eperson.firstMetadataValue('eperson.lastname') : '',
|
||||
email: eperson != null ? eperson.email : '',
|
||||
canLogIn: eperson != null ? eperson.canLogIn : true,
|
||||
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop editing the currently selected eperson
|
||||
*/
|
||||
onCancel() {
|
||||
this.epersonService.cancelEditEPerson();
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form
|
||||
* When the eperson has an id attached -> Edit the eperson
|
||||
* When the eperson has no id attached -> Create new eperson
|
||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe(
|
||||
(ePerson: EPerson) => {
|
||||
const values = {
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: this.firstName.value
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: this.lastName.value
|
||||
},
|
||||
],
|
||||
},
|
||||
email: this.email.value,
|
||||
canLogIn: this.canLogIn.value,
|
||||
requireCertificate: this.requireCertificate.value,
|
||||
};
|
||||
if (ePerson == null) {
|
||||
this.createNewEPerson(values);
|
||||
} else {
|
||||
this.editEPerson(ePerson, values);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new EPerson based on given values from form
|
||||
* @param values
|
||||
*/
|
||||
createNewEPerson(values) {
|
||||
const ePersonToCreate = Object.assign(new EPerson(), values);
|
||||
|
||||
const response = this.epersonService.tryToCreate(ePersonToCreate);
|
||||
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||
if (restResponse.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.created.success', { name: ePersonToCreate.name }));
|
||||
this.submitForm.emit(ePersonToCreate);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.created.failure', { name: ePersonToCreate.name }));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
this.showNotificationIfEmailInUse(ePersonToCreate, 'created');
|
||||
}
|
||||
|
||||
/**
|
||||
* Edits existing EPerson based on given values from form and old EPerson
|
||||
* @param ePerson ePerson to edit
|
||||
* @param values new ePerson values (of form)
|
||||
*/
|
||||
editEPerson(ePerson: EPerson, values) {
|
||||
const editedEperson = Object.assign(new EPerson(), {
|
||||
id: ePerson.id,
|
||||
metadata: {
|
||||
'eperson.firstname': [
|
||||
{
|
||||
value: (this.firstName.value ? this.firstName.value : ePerson.firstMetadataValue('eperson.firstname'))
|
||||
}
|
||||
],
|
||||
'eperson.lastname': [
|
||||
{
|
||||
value: (this.lastName.value ? this.lastName.value : ePerson.firstMetadataValue('eperson.lastname'))
|
||||
},
|
||||
],
|
||||
},
|
||||
email: (hasValue(values.email) ? values.email : ePerson.email),
|
||||
canLogIn: (hasValue(values.canLogIn) ? values.canLogIn : ePerson.canLogIn),
|
||||
requireCertificate: (hasValue(values.requireCertificate) ? values.requireCertificate : ePerson.requireCertificate),
|
||||
_links: ePerson._links,
|
||||
});
|
||||
|
||||
const response = this.epersonService.updateEPerson(editedEperson);
|
||||
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||
if (restResponse.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.edited.success', { name: editedEperson.name }));
|
||||
this.submitForm.emit(editedEperson);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.edited.failure', { name: editedEperson.name }));
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
|
||||
if (values.email != null && values.email !== ePerson.email) {
|
||||
this.showNotificationIfEmailInUse(editedEperson, 'edited');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the given ePerson if there is already an ePerson in the system with that email
|
||||
* and shows notification if this is the case
|
||||
* @param ePerson ePerson values to check
|
||||
* @param notificationSection whether in create or edit
|
||||
*/
|
||||
private showNotificationIfEmailInUse(ePerson: EPerson, notificationSection: string) {
|
||||
// Relevant message for email in use
|
||||
this.subs.push(this.epersonService.searchByScope('email', ePerson.email, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 0
|
||||
}).pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
||||
.subscribe((list: PaginatedList<EPerson>) => {
|
||||
if (list.totalElements > 0) {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.' + notificationSection + '.failure.emailInUse', {
|
||||
name: ePerson.name,
|
||||
email: ePerson.email
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.updateGroups({
|
||||
currentPage: event,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of groups by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateGroups(options) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
<div class="container">
|
||||
<div class="group-form row">
|
||||
<div class="col-12">
|
||||
|
||||
<div *ngIf="groupDataService.getActiveGroup() | async; then editheader; else createHeader"></div>
|
||||
|
||||
<ng-template #createHeader>
|
||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.create' | translate}}</h2>
|
||||
</ng-template>
|
||||
|
||||
<ng-template #editheader>
|
||||
<h2 class="border-bottom pb-2">{{messagePrefix + '.head.edit' | translate}}</h2>
|
||||
</ng-template>
|
||||
|
||||
<ds-form [formId]="formId"
|
||||
[formModel]="formModel"
|
||||
[formGroup]="formGroup"
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
</ds-form>
|
||||
|
||||
<ds-members-list *ngIf="groupBeingEdited != null" [messagePrefix]="messagePrefix + '.members-list'"></ds-members-list>
|
||||
<ds-subgroups-list *ngIf="groupBeingEdited != null" [messagePrefix]="messagePrefix + '.subgroups-list'"></ds-subgroups-list>
|
||||
|
||||
<div>
|
||||
<button [routerLink]="[this.groupDataService.getGroupRegistryRouterLink()]"
|
||||
class="btn btn-primary">{{messagePrefix + '.return' | translate}}</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,153 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockRouter } from '../../../../shared/mocks/mock-router';
|
||||
import { getMockTranslateService } from '../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
|
||||
import { MockTranslateLoader } from '../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||
import { GroupFormComponent } from './group-form.component';
|
||||
|
||||
describe('GroupFormComponent', () => {
|
||||
let component: GroupFormComponent;
|
||||
let fixture: ComponentFixture<GroupFormComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let router;
|
||||
|
||||
let groups;
|
||||
let groupName;
|
||||
let groupDescription;
|
||||
let expected;
|
||||
|
||||
beforeEach(async(() => {
|
||||
groups = [GroupMock, GroupMock2]
|
||||
groupName = 'testGroupName';
|
||||
groupDescription = 'testDescription';
|
||||
expected = Object.assign(new Group(), {
|
||||
name: groupName,
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
value: groupDescription
|
||||
}
|
||||
],
|
||||
},
|
||||
});
|
||||
ePersonDataServiceStub = {};
|
||||
groupsDataServiceStub = {
|
||||
allGroups: groups,
|
||||
activeGroup: null,
|
||||
getActiveGroup(): Observable<Group> {
|
||||
return observableOf(this.activeGroup);
|
||||
},
|
||||
getGroupRegistryRouterLink(): string {
|
||||
return '/admin/access-control/groups';
|
||||
},
|
||||
editGroup(group: Group) {
|
||||
this.activeGroup = group
|
||||
},
|
||||
cancelEditGroup(): void {
|
||||
this.activeGroup = null;
|
||||
},
|
||||
findById(id: string) {
|
||||
return observableOf({ payload: null, hasSucceeded: true });
|
||||
},
|
||||
tryToCreate(group: Group): Observable<RestResponse> {
|
||||
this.allGroups = [...this.allGroups, group]
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
router = new MockRouter();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [GroupFormComponent],
|
||||
providers: [GroupFormComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: HttpClient, useValue: {} },
|
||||
{ provide: ObjectCacheService, useValue: {} },
|
||||
{ provide: UUIDService, useValue: {} },
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: {} },
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }), params: observableOf({}) } },
|
||||
{ provide: Router, useValue: router },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GroupFormComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create GroupFormComponent', inject([GroupFormComponent], (comp: GroupFormComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component.submitForm, 'emit');
|
||||
component.groupName.value = groupName;
|
||||
component.groupDescription.value = groupDescription;
|
||||
});
|
||||
describe('without active Group', () => {
|
||||
beforeEach(() => {
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should emit a new group using the correct values', async(() => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(component.submitForm.emit).toHaveBeenCalledWith(expected);
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,280 @@
|
||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
import { FormGroup } from '@angular/forms';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormLayout,
|
||||
DynamicInputModel,
|
||||
DynamicTextAreaModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../../core/eperson/models/group.model';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-group-form',
|
||||
templateUrl: './group-form.component.html'
|
||||
})
|
||||
/**
|
||||
* A form used for creating and editing groups
|
||||
*/
|
||||
export class GroupFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
messagePrefix = 'admin.access-control.groups.form';
|
||||
|
||||
/**
|
||||
* A unique id used for ds-form
|
||||
*/
|
||||
formId = 'group-form';
|
||||
|
||||
/**
|
||||
* Dynamic models for the inputs of form
|
||||
*/
|
||||
groupName: DynamicInputModel;
|
||||
groupDescription: DynamicTextAreaModel;
|
||||
|
||||
/**
|
||||
* A list of all dynamic input models
|
||||
*/
|
||||
formModel: DynamicFormControlModel[];
|
||||
|
||||
/**
|
||||
* Layout used for structuring the form inputs
|
||||
*/
|
||||
formLayout: DynamicFormLayout = {
|
||||
groupName: {
|
||||
grid: {
|
||||
host: 'row'
|
||||
}
|
||||
},
|
||||
groupDescription: {
|
||||
grid: {
|
||||
host: 'row'
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A FormGroup that combines all inputs
|
||||
*/
|
||||
formGroup: FormGroup;
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is being submitted
|
||||
*/
|
||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* An EventEmitter that's fired whenever the form is cancelled
|
||||
*/
|
||||
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Group currently being edited
|
||||
*/
|
||||
groupBeingEdited: Group;
|
||||
|
||||
constructor(public groupDataService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private route: ActivatedRoute,
|
||||
protected router: Router) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.subs.push(this.route.params.subscribe((params) => {
|
||||
this.setActiveGroup(params.groupId)
|
||||
}));
|
||||
combineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.groupName`),
|
||||
this.translateService.get(`${this.messagePrefix}.groupDescription`),
|
||||
).subscribe(([groupName, groupDescription]) => {
|
||||
this.groupName = new DynamicInputModel({
|
||||
id: 'groupName',
|
||||
label: groupName,
|
||||
name: 'groupName',
|
||||
validators: {
|
||||
required: null,
|
||||
},
|
||||
required: true,
|
||||
});
|
||||
this.groupDescription = new DynamicTextAreaModel({
|
||||
id: 'groupDescription',
|
||||
label: groupDescription,
|
||||
name: 'groupDescription',
|
||||
required: false,
|
||||
});
|
||||
this.formModel = [
|
||||
this.groupName,
|
||||
this.groupDescription
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.formGroup.patchValue({
|
||||
groupName: activeGroup != null ? activeGroup.name : '',
|
||||
groupDescription: activeGroup != null ? activeGroup.firstMetadataValue('dc.description') : '',
|
||||
});
|
||||
if (activeGroup.permanent) {
|
||||
this.formGroup.get('groupName').disable();
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop editing the currently selected group
|
||||
*/
|
||||
onCancel() {
|
||||
this.groupDataService.cancelEditGroup();
|
||||
this.cancelForm.emit();
|
||||
this.router.navigate([this.groupDataService.getGroupRegistryRouterLink()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Submit the form
|
||||
* When the eperson has an id attached -> Edit the eperson
|
||||
* When the eperson has no id attached -> Create new eperson
|
||||
* Emit the updated/created eperson using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe(
|
||||
(group: Group) => {
|
||||
const values = {
|
||||
name: this.groupName.value,
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
value: this.groupDescription.value
|
||||
}
|
||||
],
|
||||
},
|
||||
};
|
||||
if (group === null) {
|
||||
this.createNewGroup(values);
|
||||
} else {
|
||||
this.editGroup(group, values);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates new Group based on given values from form
|
||||
* @param values
|
||||
*/
|
||||
createNewGroup(values) {
|
||||
const groupToCreate = Object.assign(new Group(), values);
|
||||
const response = this.groupDataService.tryToCreate(groupToCreate);
|
||||
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||
if (restResponse.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.created.success', { name: groupToCreate.name }));
|
||||
this.submitForm.emit(groupToCreate);
|
||||
const resp: any = restResponse;
|
||||
if (isNotEmpty(resp.resourceSelfLinks)) {
|
||||
const groupSelfLink = resp.resourceSelfLinks[0];
|
||||
this.setActiveGroupWithLink(groupSelfLink);
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLinkWithID(this.groupDataService.getUUIDFromString(groupSelfLink)));
|
||||
}
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.created.failure', { name: groupToCreate.name }));
|
||||
this.showNotificationIfNameInUse(groupToCreate, 'created');
|
||||
this.cancelForm.emit();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks for the given group if there is already a group in the system with that group name and shows error if that
|
||||
* is the case
|
||||
* @param group group to check
|
||||
* @param notificationSection whether in create or edit
|
||||
*/
|
||||
private showNotificationIfNameInUse(group: Group, notificationSection: string) {
|
||||
// Relevant message for group name in use
|
||||
this.subs.push(this.groupDataService.searchGroups(group.name, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: 0
|
||||
}).pipe(getSucceededRemoteData(), getRemoteDataPayload())
|
||||
.subscribe((list: PaginatedList<Group>) => {
|
||||
if (list.totalElements > 0) {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.' + notificationSection + '.failure.groupNameInUse', {
|
||||
name: group.name
|
||||
}));
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* // TODO
|
||||
* @param group
|
||||
* @param values
|
||||
*/
|
||||
editGroup(group: Group, values) {
|
||||
// TODO (backend)
|
||||
console.log('TODO implement editGroup', values);
|
||||
this.notificationsService.error('TODO implement editGroup (not yet implemented in backend) ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the selected group
|
||||
* @param groupId ID of group to set as active
|
||||
*/
|
||||
setActiveGroup(groupId: string) {
|
||||
this.groupDataService.cancelEditGroup();
|
||||
this.groupDataService.findById(groupId)
|
||||
.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload())
|
||||
.subscribe((group: Group) => {
|
||||
this.groupDataService.editGroup(group);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Start editing the selected group
|
||||
* @param groupSelfLink SelfLink of group to set as active
|
||||
*/
|
||||
setActiveGroupWithLink(groupSelfLink: string) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup === null) {
|
||||
this.groupDataService.cancelEditGroup();
|
||||
this.groupDataService.findByHref(groupSelfLink)
|
||||
.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload())
|
||||
.subscribe((group: Group) => {
|
||||
this.groupDataService.editGroup(group);
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
@HostListener('window:beforeunload')
|
||||
ngOnDestroy(): void {
|
||||
this.onCancel();
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
}
|
@@ -0,0 +1,124 @@
|
||||
<ng-container>
|
||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||
|
||||
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-primary float-right">{{messagePrefix + '.button.see-all' | translate}}</button>
|
||||
</h4>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
|
||||
<div class="col-12 col-sm-3">
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
<option value="metadata">{{messagePrefix + '.search.scope.metadata' | translate}}</option>
|
||||
<option value="email">{{messagePrefix + '.search.scope.email' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-9 col-12">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit"
|
||||
class="search-button btn btn-secondary">{{ messagePrefix + '.search.button' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination *ngIf="(ePeopleSearch | async)?.payload.totalElements > 0"
|
||||
[paginationOptions]="configSearch"
|
||||
[pageInfoState]="(ePeopleSearch | async)?.payload"
|
||||
[collectionSize]="(ePeopleSearch | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChangeSearch($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epersonsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let ePerson of (ePeopleSearch | async)?.payload?.page">
|
||||
<td>{{ePerson.id}}</td>
|
||||
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="(isMemberOfGroup(ePerson) | async)"
|
||||
(click)="deleteMemberFromGroup(ePerson)"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
|
||||
<button *ngIf="!(isMemberOfGroup(ePerson) | async)"
|
||||
(click)="addMemberToGroup(ePerson)"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: ePerson.name} }}">
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeopleSearch | async)?.payload.totalElements == 0 && searchDone"
|
||||
class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<h4>{{messagePrefix + '.headMembers' | translate}}</h4>
|
||||
|
||||
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.payload.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(ePeopleMembersOfGroup | async)?.payload"
|
||||
[collectionSize]="(ePeopleMembersOfGroup | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="ePeopleMembersOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let ePerson of (ePeopleMembersOfGroup | async)?.payload?.page">
|
||||
<td>{{ePerson.id}}</td>
|
||||
<td><a (click)="ePersonDataService.startEditingNewEPerson(ePerson)"
|
||||
[routerLink]="[ePersonDataService.getEPeoplePageRouterLink()]">{{ePerson.name}}</a></td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteMemberFromGroup(ePerson)"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: ePerson.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeopleMembersOfGroup | async)?.payload.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-members-yet' | translate}}
|
||||
</div>
|
||||
|
||||
</ng-container>
|
@@ -0,0 +1,241 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RestResponse } from '../../../../../core/cache/response.models';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../../../core/eperson/models/group.model';
|
||||
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockRouter } from '../../../../../shared/mocks/mock-router';
|
||||
import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson-mock';
|
||||
import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock';
|
||||
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils';
|
||||
import { MembersListComponent } from './members-list.component';
|
||||
|
||||
describe('MembersListComponent', () => {
|
||||
let component: MembersListComponent;
|
||||
let fixture: ComponentFixture<MembersListComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let activeGroup;
|
||||
let allEPersons;
|
||||
let allGroups;
|
||||
let epersonMembers;
|
||||
let subgroupMembers;
|
||||
|
||||
beforeEach(async(() => {
|
||||
activeGroup = GroupMock;
|
||||
epersonMembers = [EPersonMock2];
|
||||
subgroupMembers = [GroupMock2];
|
||||
allEPersons = [EPersonMock, EPersonMock2];
|
||||
allGroups = [GroupMock, GroupMock2];
|
||||
ePersonDataServiceStub = {
|
||||
activeGroup: activeGroup,
|
||||
epersonMembers: epersonMembers,
|
||||
subgroupMembers: subgroupMembers,
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()))
|
||||
},
|
||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allEPersons))
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
|
||||
},
|
||||
clearEPersonRequests() {
|
||||
// empty
|
||||
},
|
||||
clearLinkRequests() {
|
||||
// empty
|
||||
},
|
||||
getEPeoplePageRouterLink(): string {
|
||||
return '/admin/access-control/epeople';
|
||||
}
|
||||
};
|
||||
groupsDataServiceStub = {
|
||||
activeGroup: activeGroup,
|
||||
epersonMembers: epersonMembers,
|
||||
subgroupMembers: subgroupMembers,
|
||||
allGroups: allGroups,
|
||||
getActiveGroup(): Observable<Group> {
|
||||
return observableOf(activeGroup);
|
||||
},
|
||||
getEPersonMembers() {
|
||||
return this.epersonMembers;
|
||||
},
|
||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), this.allGroups))
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
|
||||
},
|
||||
addMemberToGroup(parentGroup, eperson: EPerson): Observable<RestResponse> {
|
||||
this.epersonMembers = [...this.epersonMembers, eperson];
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
clearGroupsRequests() {
|
||||
// empty
|
||||
},
|
||||
clearGroupLinkRequests() {
|
||||
// empty
|
||||
},
|
||||
getGroupEditPageRouterLink(group: Group): string {
|
||||
return '/admin/access-control/groups/' + group.id;
|
||||
},
|
||||
deleteMemberFromGroup(parentGroup, epersonToDelete: EPerson): Observable<RestResponse> {
|
||||
this.epersonMembers = this.epersonMembers.find((eperson: EPerson) => {
|
||||
if (eperson.id !== epersonToDelete.id) {
|
||||
return eperson;
|
||||
}
|
||||
});
|
||||
if (this.epersonMembers === undefined) {
|
||||
this.epersonMembers = []
|
||||
}
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [MembersListComponent],
|
||||
providers: [MembersListComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MembersListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
afterEach(fakeAsync(() => {
|
||||
fixture.destroy();
|
||||
flush();
|
||||
component = null;
|
||||
}));
|
||||
|
||||
it('should create MembersListComponent', inject([MembersListComponent], (comp: MembersListComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should show list of eperson members of current active group', () => {
|
||||
const epersonIdsFound = fixture.debugElement.queryAll(By.css('#ePeopleMembersOfGroup tr td:first-child'));
|
||||
expect(epersonIdsFound.length).toEqual(1);
|
||||
epersonMembers.map((eperson: EPerson) => {
|
||||
expect(epersonIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === eperson.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
describe('when searching without query', () => {
|
||||
let epersonsFound;
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ scope: 'metadata', query: '' });
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||
}));
|
||||
|
||||
it('should display all epersons', () => {
|
||||
expect(epersonsFound.length).toEqual(2);
|
||||
});
|
||||
|
||||
describe('if eperson is already a eperson', () => {
|
||||
it('should have delete button, else it should have add button', () => {
|
||||
activeGroup.epersons.map((eperson: EPerson) => {
|
||||
epersonsFound.map((foundEPersonRowElement) => {
|
||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
||||
const epersonId = foundEPersonRowElement.debugElement.query(By.css('td:first-child'));
|
||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
if (epersonId.nativeElement.textContent === eperson.id) {
|
||||
expect(addButton).toBeUndefined();
|
||||
expect(deleteButton).toBeDefined();
|
||||
} else {
|
||||
expect(deleteButton).toBeUndefined();
|
||||
expect(addButton).toBeDefined();
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('if first add button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-plus'));
|
||||
addButton.nativeElement.click();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('all groups in search member of selected group', () => {
|
||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||
expect(epersonsFound.length).toEqual(2);
|
||||
epersonsFound.map((foundEPersonRowElement) => {
|
||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(addButton).toBeUndefined();
|
||||
expect(deleteButton).toBeDefined();
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
describe('if first delete button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const addButton = fixture.debugElement.query(By.css('#epersonsSearch tbody .fa-trash-alt'));
|
||||
addButton.nativeElement.click();
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('first eperson in search delete button, because now member', () => {
|
||||
epersonsFound = fixture.debugElement.queryAll(By.css('#epersonsSearch tbody tr'));
|
||||
epersonsFound.map((foundEPersonRowElement) => {
|
||||
if (foundEPersonRowElement.debugElement !== undefined) {
|
||||
const addButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton = foundEPersonRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(deleteButton).toBeUndefined();
|
||||
expect(addButton).toBeDefined();
|
||||
}
|
||||
})
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,247 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { map, mergeMap, take } from 'rxjs/operators';
|
||||
import { RestResponse } from '../../../../../core/cache/response.models';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../../../core/eperson/models/group.model';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators';
|
||||
import { hasValue } from '../../../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-members-list',
|
||||
templateUrl: './members-list.component.html'
|
||||
})
|
||||
/**
|
||||
* The list of members in the edit group page
|
||||
*/
|
||||
export class MembersListComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input()
|
||||
messagePrefix: string;
|
||||
|
||||
/**
|
||||
* EPeople being displayed in search result, initially all members, after search result of search
|
||||
*/
|
||||
ePeopleSearch: Observable<RemoteData<PaginatedList<EPerson>>>;
|
||||
/**
|
||||
* List of EPeople members of currently active group being edited
|
||||
*/
|
||||
ePeopleMembersOfGroup: Observable<RemoteData<PaginatedList<EPerson>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of EPeople that are result of EPeople search
|
||||
*/
|
||||
configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'search-members-list-pagination',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
/**
|
||||
* Pagination config used to display the list of EPerson Membes of active group being edited
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'members-list-pagination',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
// The search form
|
||||
searchForm;
|
||||
|
||||
// Current search in edit group - epeople search form
|
||||
currentSearchQuery: string;
|
||||
currentSearchScope: string;
|
||||
|
||||
// Whether or not user has done a EPeople search yet
|
||||
searchDone: boolean;
|
||||
|
||||
// current active group being edited
|
||||
groupBeingEdited: Group;
|
||||
|
||||
constructor(private groupDataService: GroupDataService,
|
||||
public ePersonDataService: EPersonDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
scope: 'metadata',
|
||||
query: '',
|
||||
}));
|
||||
this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.forceUpdateEPeople(activeGroup);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page on search result
|
||||
* @param event
|
||||
*/
|
||||
onPageChangeSearch(event) {
|
||||
this.configSearch.currentPage = event;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page on EPerson embers of active group
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
|
||||
currentPage: event,
|
||||
elementsPerPage: this.config.pageSize
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a given EPerson from the members list of the group currently being edited
|
||||
* @param ePerson EPerson we want to delete as member from group that is currently being edited
|
||||
*/
|
||||
deleteMemberFromGroup(ePerson: EPerson) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson);
|
||||
this.showNotifications('deleteMember', response, ePerson.name, activeGroup);
|
||||
this.forceUpdateEPeople(activeGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a given EPerson to the members list of the group currently being edited
|
||||
* @param ePerson EPerson we want to add as member to group that is currently being edited
|
||||
*/
|
||||
addMemberToGroup(ePerson: EPerson) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.addMemberToGroup(activeGroup, ePerson);
|
||||
this.showNotifications('addMember', response, ePerson.name, activeGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
});
|
||||
this.forceUpdateEPeople(this.groupBeingEdited, ePerson);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the given ePerson is a member of the group currently being edited
|
||||
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
||||
*/
|
||||
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||
mergeMap((group: Group) => {
|
||||
if (group != null) {
|
||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
|
||||
currentPage: 0,
|
||||
elementsPerPage: Number.MAX_SAFE_INTEGER
|
||||
})
|
||||
.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((listEPeopleInGroup: PaginatedList<EPerson>) => listEPeopleInGroup.page.filter((ePersonInList: EPerson) => ePersonInList.id === possibleMember.id)),
|
||||
map((epeople: EPerson[]) => epeople.length > 0))
|
||||
} else {
|
||||
return observableOf(false);
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the EPeople by name, email or metadata
|
||||
* @param data Contains scope and query param
|
||||
*/
|
||||
search(data: any) {
|
||||
const query: string = data.query;
|
||||
const scope: string = data.scope;
|
||||
if (query != null && this.currentSearchQuery !== query && this.groupBeingEdited) {
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
|
||||
this.currentSearchQuery = query;
|
||||
this.configSearch.currentPage = 1;
|
||||
}
|
||||
if (scope != null && this.currentSearchScope !== scope && this.groupBeingEdited) {
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
|
||||
this.currentSearchScope = scope;
|
||||
this.configSearch.currentPage = 1;
|
||||
}
|
||||
this.searchDone = true;
|
||||
this.ePeopleSearch = this.ePersonDataService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
currentPage: this.configSearch.currentPage,
|
||||
elementsPerPage: this.configSearch.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of EPeople by first clearing the cache related to EPeople, then performing
|
||||
* a new REST call
|
||||
* @param activeGroup Group currently being edited
|
||||
*/
|
||||
public forceUpdateEPeople(activeGroup: Group, ePersonToUpdate?: EPerson) {
|
||||
if (ePersonToUpdate != null) {
|
||||
this.ePersonDataService.clearLinkRequests(ePersonToUpdate._links.groups.href);
|
||||
}
|
||||
this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href);
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup));
|
||||
this.ePeopleMembersOfGroup = this.ePersonDataService.findAllByHref(activeGroup._links.epersons.href, {
|
||||
currentPage: this.configSearch.currentPage,
|
||||
elementsPerPage: this.configSearch.pageSize
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* unsub all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a notification based on the success/failure of the request
|
||||
* @param messageSuffix Suffix for message
|
||||
* @param response RestResponse observable containing success/failure request
|
||||
* @param nameObject Object request was about
|
||||
* @param activeGroup Group currently being edited
|
||||
*/
|
||||
showNotifications(messageSuffix: string, response: Observable<RestResponse>, nameObject: string, activeGroup: Group) {
|
||||
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||
if (restResponse.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty and search all search
|
||||
*/
|
||||
clearFormAndResetResult() {
|
||||
this.searchForm.patchValue({
|
||||
query: '',
|
||||
});
|
||||
this.search({ query: '' });
|
||||
}
|
||||
}
|
@@ -0,0 +1,117 @@
|
||||
<ng-container>
|
||||
<h3 class="border-bottom pb-2">{{messagePrefix + '.head' | translate}}</h3>
|
||||
|
||||
<h4 id="search" class="border-bottom pb-2">{{messagePrefix + '.search.head' | translate}}
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-primary float-right">{{messagePrefix + '.button.see-all' | translate}}</button>
|
||||
</h4>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit"
|
||||
class="search-button btn btn-secondary">{{ messagePrefix + '.search.button' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination *ngIf="(groupsSearch | async)?.payload.totalElements > 0"
|
||||
[paginationOptions]="configSearch"
|
||||
[pageInfoState]="(groupsSearch | async)?.payload"
|
||||
[collectionSize]="(groupsSearch | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChangeSearch($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groupsSearch" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groupsSearch | async)?.payload?.page">
|
||||
<td>{{group.id}}</td>
|
||||
<td><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button *ngIf="(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||
(click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
|
||||
<p *ngIf="(isActiveGroup(group) | async)">{{ messagePrefix + '.table.edit.currentGroup' | translate }}</p>
|
||||
|
||||
<button *ngIf="!(isSubgroupOfGroup(group) | async) && !(isActiveGroup(group) | async)"
|
||||
(click)="addSubgroupToGroup(group)"
|
||||
class="btn btn-outline-primary btn-sm addButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.add' | translate: {name: group.name} }}">
|
||||
<i class="fas fa-plus fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(groupsSearch | async)?.payload.totalElements == 0 && searchDone" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<h4>{{messagePrefix + '.headSubgroups' | translate}}</h4>
|
||||
|
||||
<ds-pagination *ngIf="(subgroupsOfGroup | async)?.payload.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(subgroupsOfGroup | async)?.payload"
|
||||
[collectionSize]="(subgroupsOfGroup | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="subgroupsOfGroup" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + '.table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + '.table.name' | translate}}</th>
|
||||
<th>{{messagePrefix + '.table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (subgroupsOfGroup | async)?.payload?.page">
|
||||
<td>{{group.id}}</td>
|
||||
<td><a (click)="groupDataService.startEditingNewGroup(group)"
|
||||
[routerLink]="[groupDataService.getGroupEditPageRouterLink(group)]">{{group.name}}</a></td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="deleteSubgroupFromGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm deleteButton"
|
||||
title="{{messagePrefix + '.table.edit.buttons.remove' | translate: {name: group.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(subgroupsOfGroup | async)?.payload.totalElements == 0" class="alert alert-info w-100 mb-2"
|
||||
role="alert">
|
||||
{{messagePrefix + '.no-subgroups-yet' | translate}}
|
||||
</div>
|
||||
|
||||
</ng-container>
|
@@ -0,0 +1,208 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RestResponse } from '../../../../../core/cache/response.models';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { GroupDataService } from '../../../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../../../core/eperson/models/group.model';
|
||||
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockRouter } from '../../../../../shared/mocks/mock-router';
|
||||
import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||
import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock';
|
||||
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils';
|
||||
import { SubgroupsListComponent } from './subgroups-list.component';
|
||||
|
||||
describe('SubgroupsListComponent', () => {
|
||||
let component: SubgroupsListComponent;
|
||||
let fixture: ComponentFixture<SubgroupsListComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
let activeGroup;
|
||||
let subgroups;
|
||||
let allGroups;
|
||||
let routerStub;
|
||||
|
||||
beforeEach(async(() => {
|
||||
activeGroup = GroupMock;
|
||||
subgroups = [GroupMock2];
|
||||
allGroups = [GroupMock, GroupMock2];
|
||||
ePersonDataServiceStub = {};
|
||||
groupsDataServiceStub = {
|
||||
activeGroup: activeGroup,
|
||||
subgroups: subgroups,
|
||||
getActiveGroup(): Observable<Group> {
|
||||
return observableOf(this.activeGroup);
|
||||
},
|
||||
getSubgroups(): Group {
|
||||
return this.activeGroup;
|
||||
},
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList<Group>(new PageInfo(), this.subgroups))
|
||||
},
|
||||
getGroupEditPageRouterLink(group: Group): string {
|
||||
return '/admin/access-control/groups/' + group.id;
|
||||
},
|
||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), allGroups))
|
||||
}
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
|
||||
},
|
||||
addSubGroupToGroup(parentGroup, subgroup: Group): Observable<RestResponse> {
|
||||
this.subgroups = [...this.subgroups, subgroup];
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
clearGroupsRequests() {
|
||||
// empty
|
||||
},
|
||||
clearGroupLinkRequests() {
|
||||
// empty
|
||||
},
|
||||
deleteSubGroupFromGroup(parentGroup, subgroup: Group): Observable<RestResponse> {
|
||||
this.subgroups = this.subgroups.find((group: Group) => {
|
||||
if (group.id !== subgroup.id) {
|
||||
return group;
|
||||
}
|
||||
});
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
}
|
||||
};
|
||||
routerStub = new MockRouter();
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [SubgroupsListComponent],
|
||||
providers: [SubgroupsListComponent,
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(SubgroupsListComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
afterEach(fakeAsync(() => {
|
||||
fixture.destroy();
|
||||
flush();
|
||||
component = null;
|
||||
}));
|
||||
|
||||
it('should create SubgroupsListComponent', inject([SubgroupsListComponent], (comp: SubgroupsListComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should show list of subgroups of current active group', () => {
|
||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tr td:first-child'));
|
||||
expect(groupIdsFound.length).toEqual(1);
|
||||
activeGroup.subgroups.map((group: Group) => {
|
||||
expect(groupIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||
})).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
describe('if first group delete button is pressed', () => {
|
||||
let groupsFound;
|
||||
beforeEach(fakeAsync(() => {
|
||||
const addButton = fixture.debugElement.query(By.css('#subgroupsOfGroup tbody .deleteButton'));
|
||||
addButton.triggerEventHandler('click', {
|
||||
preventDefault: () => {/**/
|
||||
}
|
||||
});
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('one less subgroup in list from 1 to 0 (of 2 total groups)', () => {
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#subgroupsOfGroup tbody tr'));
|
||||
expect(groupsFound.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
describe('when searching with empty query', () => {
|
||||
let groupsFound;
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ query: '' });
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||
}));
|
||||
|
||||
it('should display all groups', () => {
|
||||
fixture.detectChanges();
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||
expect(groupsFound.length).toEqual(2);
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr td:first-child'));
|
||||
allGroups.map((group: Group) => {
|
||||
expect(groupIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||
})).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
describe('if group is already a subgroup', () => {
|
||||
it('should have delete button, else it should have add button', () => {
|
||||
fixture.detectChanges();
|
||||
groupsFound = fixture.debugElement.queryAll(By.css('#groupsSearch tbody tr'));
|
||||
const getSubgroups = groupsDataServiceStub.getSubgroups().subgroups;
|
||||
if (getSubgroups !== undefined && getSubgroups.length > 0) {
|
||||
groupsFound.map((foundGroupRowElement) => {
|
||||
if (foundGroupRowElement.debugElement !== undefined) {
|
||||
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
expect(addButton).toBeUndefined();
|
||||
expect(deleteButton).toBeDefined();
|
||||
}
|
||||
})
|
||||
} else {
|
||||
getSubgroups.map((group: Group) => {
|
||||
groupsFound.map((foundGroupRowElement) => {
|
||||
if (foundGroupRowElement.debugElement !== undefined) {
|
||||
const groupId = foundGroupRowElement.debugElement.query(By.css('td:first-child'));
|
||||
const addButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-plus'));
|
||||
const deleteButton = foundGroupRowElement.debugElement.query(By.css('td:last-child .fa-trash-alt'));
|
||||
if (groupId.nativeElement.textContent === group.id) {
|
||||
expect(addButton).toBeUndefined();
|
||||
expect(deleteButton).toBeDefined();
|
||||
} else {
|
||||
expect(deleteButton).toBeUndefined();
|
||||
expect(addButton).toBeDefined();
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
@@ -0,0 +1,253 @@
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { map, mergeMap, take } from 'rxjs/operators';
|
||||
import { RestResponse } from '../../../../../core/cache/response.models';
|
||||
import { PaginatedList } from '../../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../../core/data/remote-data';
|
||||
import { GroupDataService } from '../../../../../core/eperson/group-data.service';
|
||||
import { Group } from '../../../../../core/eperson/models/group.model';
|
||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators';
|
||||
import { hasValue } from '../../../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-subgroups-list',
|
||||
templateUrl: './subgroups-list.component.html'
|
||||
})
|
||||
/**
|
||||
* The list of subgroups in the edit group page
|
||||
*/
|
||||
export class SubgroupsListComponent implements OnInit, OnDestroy {
|
||||
|
||||
@Input()
|
||||
messagePrefix: string;
|
||||
|
||||
/**
|
||||
* Result of search groups, initially all groups
|
||||
*/
|
||||
groupsSearch: Observable<RemoteData<PaginatedList<Group>>>;
|
||||
/**
|
||||
* List of all subgroups of group being edited
|
||||
*/
|
||||
subgroupsOfGroup: Observable<RemoteData<PaginatedList<Group>>>;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of groups that are result of groups search
|
||||
*/
|
||||
configSearch: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'search-subgroups-list-pagination',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
/**
|
||||
* Pagination config used to display the list of subgroups of currently active group being edited
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'subgroups-list-pagination',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
|
||||
// The search form
|
||||
searchForm;
|
||||
|
||||
// Current search in edit group - groups search form
|
||||
currentSearchQuery: string;
|
||||
|
||||
// Whether or not user has done a Groups search yet
|
||||
searchDone: boolean;
|
||||
|
||||
// current active group being edited
|
||||
groupBeingEdited: Group;
|
||||
|
||||
constructor(public groupDataService: GroupDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router) {
|
||||
this.currentSearchQuery = '';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
query: '',
|
||||
}));
|
||||
this.subs.push(this.groupDataService.getActiveGroup().subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
this.groupBeingEdited = activeGroup;
|
||||
this.forceUpdateGroups(activeGroup);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page on search result
|
||||
* @param event
|
||||
*/
|
||||
onPageChangeSearch(event) {
|
||||
this.configSearch.currentPage = event;
|
||||
this.search({ query: this.currentSearchQuery });
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page on subgroups of active group
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.subgroupsOfGroup = this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||
currentPage: event,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the given group is a subgroup of the group currently being edited
|
||||
* @param possibleSubgroup Group that is a possible subgroup (being tested) of the group currently being edited
|
||||
*/
|
||||
isSubgroupOfGroup(possibleSubgroup: Group): Observable<boolean> {
|
||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||
mergeMap((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
if (activeGroup.uuid === possibleSubgroup.uuid) {
|
||||
return observableOf(false);
|
||||
} else {
|
||||
return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
|
||||
currentPage: 0,
|
||||
elementsPerPage: Number.MAX_SAFE_INTEGER
|
||||
})
|
||||
.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((listTotalGroups: PaginatedList<Group>) => listTotalGroups.page.filter((groupInList: Group) => groupInList.id === possibleSubgroup.id)),
|
||||
map((groups: Group[]) => groups.length > 0))
|
||||
}
|
||||
} else {
|
||||
return observableOf(false);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether or not the given group is the current group being edited
|
||||
* @param group Group that is possibly the current group being edited
|
||||
*/
|
||||
isActiveGroup(group: Group): Observable<boolean> {
|
||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||
mergeMap((activeGroup: Group) => {
|
||||
if (activeGroup != null && activeGroup.uuid === group.uuid) {
|
||||
return observableOf(true);
|
||||
}
|
||||
return observableOf(false);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes given subgroup from the group currently being edited
|
||||
* @param subgroup Group we want to delete from the subgroups of the group currently being edited
|
||||
*/
|
||||
deleteSubgroupFromGroup(subgroup: Group) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
const response = this.groupDataService.deleteSubGroupFromGroup(activeGroup, subgroup);
|
||||
this.showNotifications('deleteSubgroup', response, subgroup.name, activeGroup);
|
||||
this.forceUpdateGroups(activeGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds given subgroup to the group currently being edited
|
||||
* @param subgroup Subgroup to add to group currently being edited
|
||||
*/
|
||||
addSubgroupToGroup(subgroup: Group) {
|
||||
this.groupDataService.getActiveGroup().pipe(take(1)).subscribe((activeGroup: Group) => {
|
||||
if (activeGroup != null) {
|
||||
if (activeGroup.uuid !== subgroup.uuid) {
|
||||
const response = this.groupDataService.addSubGroupToGroup(activeGroup, subgroup);
|
||||
this.showNotifications('addSubgroup', response, subgroup.name, activeGroup);
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.subgroupToAddIsActiveGroup'));
|
||||
}
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||
}
|
||||
});
|
||||
this.forceUpdateGroups(this.groupBeingEdited);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the groups (searches by group name and by uuid exact match)
|
||||
* @param data Contains query param
|
||||
*/
|
||||
search(data: any) {
|
||||
const query: string = data.query;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(this.groupBeingEdited));
|
||||
this.currentSearchQuery = query;
|
||||
this.configSearch.currentPage = 1;
|
||||
}
|
||||
this.searchDone = true;
|
||||
this.groupsSearch = this.groupDataService.searchGroups(this.currentSearchQuery, {
|
||||
currentPage: this.configSearch.currentPage,
|
||||
elementsPerPage: this.configSearch.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of groups by first clearing the cache of results of this active groups' subgroups, then performing a new REST call
|
||||
* @param activeGroup Group currently being edited
|
||||
*/
|
||||
public forceUpdateGroups(activeGroup: Group) {
|
||||
this.groupDataService.clearGroupLinkRequests(activeGroup._links.subgroups.href);
|
||||
this.router.navigateByUrl(this.groupDataService.getGroupEditPageRouterLink(activeGroup));
|
||||
this.subgroupsOfGroup = this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* unsub all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a notification based on the success/failure of the request
|
||||
* @param messageSuffix Suffix for message
|
||||
* @param response RestResponse observable containing success/failure request
|
||||
* @param nameObject Object request was about
|
||||
* @param activeGroup Group currently being edited
|
||||
*/
|
||||
showNotifications(messageSuffix: string, response: Observable<RestResponse>, nameObject: string, activeGroup: Group) {
|
||||
response.pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||
if (restResponse.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty and search all search
|
||||
*/
|
||||
clearFormAndResetResult() {
|
||||
this.searchForm.patchValue({
|
||||
query: '',
|
||||
});
|
||||
this.search({ query: '' });
|
||||
}
|
||||
}
|
@@ -0,0 +1,49 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const GroupRegistryActionTypes = {
|
||||
|
||||
EDIT_GROUP: type('dspace/epeople-registry/EDIT_GROUP'),
|
||||
CANCEL_EDIT_GROUP: type('dspace/epeople-registry/CANCEL_EDIT_GROUP'),
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to edit a Group in the Group registry
|
||||
*/
|
||||
export class GroupRegistryEditGroupAction implements Action {
|
||||
type = GroupRegistryActionTypes.EDIT_GROUP;
|
||||
|
||||
group: Group;
|
||||
|
||||
constructor(group: Group) {
|
||||
this.group = group;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to cancel the editing of a Group in the Group registry
|
||||
*/
|
||||
export class GroupRegistryCancelGroupAction implements Action {
|
||||
type = GroupRegistryActionTypes.CANCEL_EDIT_GROUP;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
* so that reducers can easily compose action types
|
||||
* These are all the actions to perform on the EPeople registry state
|
||||
*/
|
||||
export type GroupRegistryAction
|
||||
= GroupRegistryEditGroupAction
|
||||
| GroupRegistryCancelGroupAction
|
@@ -0,0 +1,54 @@
|
||||
import { GroupMock } from '../../../shared/testing/group-mock';
|
||||
import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction } from './group-registry.actions';
|
||||
import { groupRegistryReducer, GroupRegistryState } from './group-registry.reducers';
|
||||
|
||||
const initialState: GroupRegistryState = {
|
||||
editGroup: null,
|
||||
};
|
||||
|
||||
const editState: GroupRegistryState = {
|
||||
editGroup: GroupMock,
|
||||
};
|
||||
|
||||
class NullAction extends GroupRegistryEditGroupAction {
|
||||
type = null;
|
||||
|
||||
constructor() {
|
||||
super(undefined);
|
||||
}
|
||||
}
|
||||
|
||||
describe('groupRegistryReducer', () => {
|
||||
|
||||
it('should return the current state when no valid actions have been made', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
const newState = groupRegistryReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should start with an initial state', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
const initState = groupRegistryReducer(undefined, action);
|
||||
|
||||
expect(initState).toEqual(state);
|
||||
});
|
||||
|
||||
it('should update the current state to change the editGroup to a new group when GroupRegistryEditGroupAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new GroupRegistryEditGroupAction(GroupMock);
|
||||
const newState = groupRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editGroup).toEqual(GroupMock);
|
||||
});
|
||||
|
||||
it('should update the current state to remove the editGroup from the state when GroupRegistryCancelGroupAction is dispatched', () => {
|
||||
const state = editState;
|
||||
const action = new GroupRegistryCancelGroupAction();
|
||||
const newState = groupRegistryReducer(state, action);
|
||||
|
||||
expect(newState.editGroup).toEqual(null);
|
||||
});
|
||||
});
|
@@ -0,0 +1,43 @@
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { GroupRegistryAction, GroupRegistryActionTypes, GroupRegistryEditGroupAction } from './group-registry.actions';
|
||||
|
||||
/**
|
||||
* The metadata registry state.
|
||||
* @interface GroupRegistryState
|
||||
*/
|
||||
export interface GroupRegistryState {
|
||||
editGroup: Group;
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial state.
|
||||
*/
|
||||
const initialState: GroupRegistryState = {
|
||||
editGroup: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer that handles GroupRegistryActions to modify Groups
|
||||
* @param state The current GroupRegistryState
|
||||
* @param action The GroupRegistryAction to perform on the state
|
||||
*/
|
||||
export function groupRegistryReducer(state = initialState, action: GroupRegistryAction): GroupRegistryState {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case GroupRegistryActionTypes.EDIT_GROUP: {
|
||||
return Object.assign({}, state, {
|
||||
editGroup: (action as GroupRegistryEditGroupAction).group
|
||||
});
|
||||
}
|
||||
|
||||
case GroupRegistryActionTypes.CANCEL_EDIT_GROUP: {
|
||||
return Object.assign({}, state, {
|
||||
editGroup: null
|
||||
});
|
||||
}
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@@ -0,0 +1,84 @@
|
||||
<div class="container">
|
||||
<div class="groups-registry row">
|
||||
<div class="col-12">
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2">{{messagePrefix + 'head' | translate}}</h2>
|
||||
|
||||
<div class="button-row top d-flex pb-2">
|
||||
<button class="mr-auto btn btn-success"
|
||||
[routerLink]="['newGroup']">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline"> {{messagePrefix + 'button.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 id="search" class="border-bottom pb-2">{{messagePrefix + 'search.head' | translate}}
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-primary float-right">{{messagePrefix + 'button.see-all' | translate}}</button>
|
||||
</h3>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
|
||||
<div class="col-12">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit"
|
||||
class="search-button btn btn-secondary">{{ messagePrefix + 'search.button' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(groups | async)?.payload"
|
||||
[collectionSize]="(groups | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="groups" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{messagePrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{messagePrefix + 'table.members' | translate}}</th>
|
||||
<!-- <th scope="col">{{messagePrefix + 'table.comcol' | translate}}</th>-->
|
||||
<th>{{messagePrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let group of (groups | async)?.payload?.page">
|
||||
<td>{{group.id}}</td>
|
||||
<td>{{group.name}}</td>
|
||||
<td>{{(getMembers(group) | async)?.payload?.totalElements + (getSubgroups(group) | async)?.payload?.totalElements}}</td>
|
||||
<!-- <td>{{getOptionalComColFromName(group.name)}}</td>-->
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button [routerLink]="groupService.getGroupEditPageRouterLink(group)"
|
||||
class="btn btn-outline-primary btn-sm"
|
||||
title="{{messagePrefix + 'table.edit.buttons.edit' | translate: {name: group.name} }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button *ngIf="!group?.permanent" (click)="deleteGroup(group)"
|
||||
class="btn btn-outline-danger btn-sm"
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: group.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(groups | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{messagePrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,139 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { MockRouter } from '../../../shared/mocks/mock-router';
|
||||
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
|
||||
import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { routeServiceStub } from '../../../shared/testing/route-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||
import { GroupsRegistryComponent } from './groups-registry.component';
|
||||
|
||||
describe('GroupRegistryComponent', () => {
|
||||
let component: GroupsRegistryComponent;
|
||||
let fixture: ComponentFixture<GroupsRegistryComponent>;
|
||||
let ePersonDataServiceStub: any;
|
||||
let groupsDataServiceStub: any;
|
||||
|
||||
let mockGroups;
|
||||
let mockEPeople;
|
||||
|
||||
beforeEach(async(() => {
|
||||
mockGroups = [GroupMock, GroupMock2];
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
ePersonDataServiceStub = {
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
switch (href) {
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/epersons':
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, [EPersonMock]));
|
||||
default:
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
||||
}
|
||||
}
|
||||
};
|
||||
groupsDataServiceStub = {
|
||||
allGroups: mockGroups,
|
||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
switch (href) {
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid/groups':
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, [GroupMock2]));
|
||||
default:
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, []));
|
||||
}
|
||||
},
|
||||
getGroupEditPageRouterLink(group: Group): string {
|
||||
return '/admin/access-control/groups/' + group.id;
|
||||
},
|
||||
getGroupRegistryRouterLink(): string {
|
||||
return '/admin/access-control/groups';
|
||||
},
|
||||
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
if (query === '') {
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(null, this.allGroups));
|
||||
}
|
||||
const result = this.allGroups.find((group: Group) => {
|
||||
return (group.id.includes(query))
|
||||
});
|
||||
return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [result]));
|
||||
}
|
||||
};
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [GroupsRegistryComponent],
|
||||
providers: [GroupsRegistryComponent,
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(GroupsRegistryComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create GroupRegistryComponent', inject([GroupsRegistryComponent], (comp: GroupsRegistryComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
|
||||
it('should display list of groups', () => {
|
||||
const groupIdsFound = fixture.debugElement.queryAll(By.css('#groups tr td:first-child'));
|
||||
expect(groupIdsFound.length).toEqual(2);
|
||||
mockGroups.map((group: Group) => {
|
||||
expect(groupIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === group.uuid);
|
||||
})).toBeTruthy();
|
||||
})
|
||||
});
|
||||
|
||||
describe('search', () => {
|
||||
describe('when searching with query', () => {
|
||||
let groupIdsFound;
|
||||
beforeEach(fakeAsync(() => {
|
||||
component.search({ query: GroupMock2.id });
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
groupIdsFound = fixture.debugElement.queryAll(By.css('#groups tr td:first-child'));
|
||||
}));
|
||||
|
||||
it('should display search result', () => {
|
||||
expect(groupIdsFound.length).toEqual(1);
|
||||
expect(groupIdsFound.find((foundEl) => {
|
||||
return (foundEl.nativeElement.textContent.trim() === GroupMock2.uuid);
|
||||
})).toBeTruthy();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,154 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { GroupDataService } from '../../../core/eperson/group-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-groups-registry',
|
||||
templateUrl: './groups-registry.component.html',
|
||||
})
|
||||
/**
|
||||
* A component used for managing all existing groups within the repository.
|
||||
* The admin can create, edit or delete groups here.
|
||||
*/
|
||||
export class GroupsRegistryComponent implements OnInit {
|
||||
|
||||
messagePrefix = 'admin.access-control.groups.';
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of groups
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'groups-list-pagination',
|
||||
pageSize: 5,
|
||||
currentPage: 1
|
||||
});
|
||||
|
||||
/**
|
||||
* A list of all the current groups within the repository or the result of the search
|
||||
*/
|
||||
groups: Observable<RemoteData<PaginatedList<Group>>>;
|
||||
|
||||
// The search form
|
||||
searchForm;
|
||||
|
||||
// Current search in groups registry
|
||||
currentSearchQuery: string;
|
||||
|
||||
constructor(private groupService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private formBuilder: FormBuilder,
|
||||
protected routeService: RouteService,
|
||||
private router: Router) {
|
||||
this.currentSearchQuery = '';
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
query: this.currentSearchQuery,
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.search({ query: this.currentSearchQuery });
|
||||
}
|
||||
|
||||
/**
|
||||
* Event triggered when the user changes page
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.search({ query: this.currentSearchQuery })
|
||||
}
|
||||
|
||||
/**
|
||||
* Search in the groups (searches by group name and by uuid exact match)
|
||||
* @param data Contains query param
|
||||
*/
|
||||
search(data: any) {
|
||||
const query: string = data.query;
|
||||
if (query != null && this.currentSearchQuery !== query) {
|
||||
this.router.navigateByUrl(this.groupService.getGroupRegistryRouterLink());
|
||||
this.currentSearchQuery = query;
|
||||
this.config.currentPage = 1;
|
||||
}
|
||||
this.groups = this.groupService.searchGroups(this.currentSearchQuery.trim(), {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Group
|
||||
*/
|
||||
deleteGroup(group: Group) {
|
||||
// TODO (backend)
|
||||
console.log('TODO implement editGroup', group);
|
||||
this.notificationsService.error('TODO implement deleteGroup (not yet implemented in backend)');
|
||||
if (hasValue(group.id)) {
|
||||
this.groupService.deleteGroup(group).pipe(take(1)).subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.name }));
|
||||
this.forceUpdateGroup();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + 'notification.deleted.failure', { name: group.name }));
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of groups by first clearing the cache related to groups, then performing a new REST call
|
||||
*/
|
||||
public forceUpdateGroup() {
|
||||
this.groupService.clearGroupsRequests();
|
||||
this.search({ query: this.currentSearchQuery })
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the members (epersons embedded value of a group)
|
||||
* @param group
|
||||
*/
|
||||
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the subgroups (groups embedded value of a group)
|
||||
* @param group
|
||||
*/
|
||||
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
||||
return this.groupService.findAllByHref(group._links.subgroups.href);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset all input-fields to be empty and search all search
|
||||
*/
|
||||
clearFormAndResetResult() {
|
||||
this.searchForm.patchValue({
|
||||
query: '',
|
||||
});
|
||||
this.search({ query: '' });
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract optional UUID from a group name => To be resolved to community or collection with link
|
||||
* (Or will be resolved in backend and added to group object, tbd) //TODO
|
||||
* @param groupName
|
||||
*/
|
||||
getOptionalComColFromName(groupName: string): string {
|
||||
return this.groupService.getUUIDFromString(groupName);
|
||||
}
|
||||
}
|
@@ -2,14 +2,29 @@ import { MetadataRegistryComponent } from './metadata-registry/metadata-registry
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getRegistriesModulePath } from '../admin-routing.module';
|
||||
|
||||
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
||||
|
||||
export function getBitstreamFormatsModulePath() {
|
||||
return new URLCombiner(getRegistriesModulePath(), BITSTREAMFORMATS_MODULE_PATH).toString();
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{ path: 'metadata', component: MetadataRegistryComponent, data: { title: 'admin.registries.metadata.title' } },
|
||||
{ path: 'metadata/:schemaName', component: MetadataSchemaComponent, data: { title: 'admin.registries.schema.title' } },
|
||||
{ path: 'bitstream-formats', component: BitstreamFormatsComponent, data: { title: 'admin.registries.bitstream-formats.title' } },
|
||||
{path: 'metadata', component: MetadataRegistryComponent, data: {title: 'admin.registries.metadata.title'}},
|
||||
{
|
||||
path: 'metadata/:schemaName',
|
||||
component: MetadataSchemaComponent,
|
||||
data: {title: 'admin.registries.schema.title'}
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMATS_MODULE_PATH,
|
||||
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
|
||||
data: {title: 'admin.registries.bitstream-formats.title'}
|
||||
},
|
||||
])
|
||||
]
|
||||
})
|
||||
|
@@ -5,10 +5,10 @@ import { CommonModule } from '@angular/common';
|
||||
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats/bitstream-formats.component';
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component';
|
||||
import {MetadataFieldFormComponent} from './metadata-schema/metadata-field-form/metadata-field-form.component';
|
||||
import { MetadataFieldFormComponent } from './metadata-schema/metadata-field-form/metadata-field-form.component';
|
||||
import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.module';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -16,12 +16,12 @@ import {MetadataFieldFormComponent} from './metadata-schema/metadata-field-form/
|
||||
SharedModule,
|
||||
RouterModule,
|
||||
TranslateModule,
|
||||
BitstreamFormatsModule,
|
||||
AdminRegistriesRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
MetadataRegistryComponent,
|
||||
MetadataSchemaComponent,
|
||||
BitstreamFormatsComponent,
|
||||
MetadataSchemaFormComponent,
|
||||
MetadataFieldFormComponent
|
||||
],
|
||||
|
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<h2 id="sub-header"
|
||||
class="border-bottom mb-2">{{ 'admin.registries.bitstream-formats.create.new' | translate }}</h2>
|
||||
|
||||
<ds-bitstream-format-form (updatedFormat)="createBitstreamFormat($event)"></ds-bitstream-format-form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,105 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||
import { AddBitstreamFormatComponent } from './add-bitstream-format.component';
|
||||
|
||||
describe('AddBitstreamFormatComponent', () => {
|
||||
let comp: AddBitstreamFormatComponent;
|
||||
let fixture: ComponentFixture<AddBitstreamFormatComponent>;
|
||||
|
||||
const bitstreamFormat = new BitstreamFormat();
|
||||
bitstreamFormat.uuid = 'test-uuid-1';
|
||||
bitstreamFormat.id = 'test-uuid-1';
|
||||
bitstreamFormat.shortDescription = 'Unknown';
|
||||
bitstreamFormat.description = 'Unknown data format';
|
||||
bitstreamFormat.mimetype = 'application/octet-stream';
|
||||
bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||
bitstreamFormat.internal = false;
|
||||
bitstreamFormat.extensions = null;
|
||||
|
||||
let router;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let bitstreamFormatDataService: BitstreamFormatDataService;
|
||||
|
||||
const initAsync = () => {
|
||||
router = new RouterStub();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||
createBitstreamFormat: observableOf(new RestResponse(true, 200, 'Success')),
|
||||
clearBitStreamFormatRequests: observableOf(null)
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [AddBitstreamFormatComponent],
|
||||
providers: [
|
||||
{provide: Router, useValue: router},
|
||||
{provide: NotificationsService, useValue: notificationService},
|
||||
{provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
};
|
||||
|
||||
const initBeforeEach = () => {
|
||||
fixture = TestBed.createComponent(AddBitstreamFormatComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
describe('createBitstreamFormat success', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should send the updated form to the service, show a notification and navigate to ', () => {
|
||||
comp.createBitstreamFormat(bitstreamFormat);
|
||||
|
||||
expect(bitstreamFormatDataService.createBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat);
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']);
|
||||
|
||||
});
|
||||
});
|
||||
describe('createBitstreamFormat error', () => {
|
||||
beforeEach(async(() => {
|
||||
router = new RouterStub();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||
createBitstreamFormat: observableOf(new RestResponse(false, 400, 'Bad Request')),
|
||||
clearBitStreamFormatRequests: observableOf(null)
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [AddBitstreamFormatComponent],
|
||||
providers: [
|
||||
{provide: Router, useValue: router},
|
||||
{provide: NotificationsService, useValue: notificationService},
|
||||
{provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should send the updated form to the service, show a notification and navigate to ', () => {
|
||||
comp.createBitstreamFormat(bitstreamFormat);
|
||||
|
||||
expect(bitstreamFormatDataService.createBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat);
|
||||
expect(notificationService.error).toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,49 @@
|
||||
import { take } from 'rxjs/operators';
|
||||
import { Router } from '@angular/router';
|
||||
import { Component } from '@angular/core';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* This component renders the page to create a new bitstream format.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-add-bitstream-format',
|
||||
templateUrl: './add-bitstream-format.component.html',
|
||||
})
|
||||
export class AddBitstreamFormatComponent {
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
private notificationService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new bitstream format based on the provided bitstream format emitted by the form.
|
||||
* When successful, a success notification will be shown and the user will be navigated back to the overview page.
|
||||
* When failed, an error notification will be shown.
|
||||
* @param bitstreamFormat
|
||||
*/
|
||||
createBitstreamFormat(bitstreamFormat: BitstreamFormat) {
|
||||
this.bitstreamFormatDataService.createBitstreamFormat(bitstreamFormat).pipe(take(1)
|
||||
).subscribe((response: RestResponse) => {
|
||||
if (response.isSuccessful) {
|
||||
this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.create.success.head'),
|
||||
this.translateService.get('admin.registries.bitstream-formats.create.success.content'));
|
||||
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||
this.bitstreamFormatDataService.clearBitStreamFormatRequests().subscribe();
|
||||
} else {
|
||||
this.notificationService.error(this.translateService.get('admin.registries.bitstream-formats.create.failure.head'),
|
||||
this.translateService.get('admin.registries.bitstream-formats.create.failure.content'));
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,64 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
* enum object for all of this group's action types.
|
||||
*
|
||||
* The 'type' utility function coerces strings into string
|
||||
* literal types and runs a simple check to guarantee all
|
||||
* action types in the application are unique.
|
||||
*/
|
||||
export const BitstreamFormatsRegistryActionTypes = {
|
||||
|
||||
SELECT_FORMAT: type('dspace/bitstream-formats-registry/SELECT_FORMAT'),
|
||||
DESELECT_FORMAT: type('dspace/bitstream-formats-registry/DESELECT_FORMAT'),
|
||||
DESELECT_ALL_FORMAT: type('dspace/bitstream-formats-registry/DESELECT_ALL_FORMAT')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
* Used to select a single bitstream format in the bitstream format registry
|
||||
*/
|
||||
export class BitstreamFormatsRegistrySelectAction implements Action {
|
||||
type = BitstreamFormatsRegistryActionTypes.SELECT_FORMAT;
|
||||
|
||||
bitstreamFormat: BitstreamFormat;
|
||||
|
||||
constructor(bitstreamFormat: BitstreamFormat) {
|
||||
this.bitstreamFormat = bitstreamFormat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to deselect a single bitstream format in the bitstream format registry
|
||||
*/
|
||||
export class BitstreamFormatsRegistryDeselectAction implements Action {
|
||||
type = BitstreamFormatsRegistryActionTypes.DESELECT_FORMAT;
|
||||
|
||||
bitstreamFormat: BitstreamFormat;
|
||||
|
||||
constructor(bitstreamFormat: BitstreamFormat) {
|
||||
this.bitstreamFormat = bitstreamFormat;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to deselect all bitstream formats in the bitstream format registry
|
||||
*/
|
||||
export class BitstreamFormatsRegistryDeselectAllAction implements Action {
|
||||
type = BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT;
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
* Export a type alias of all actions in this action group
|
||||
* so that reducers can easily compose action types
|
||||
* These are all the actions to perform on the bitstream format registry state
|
||||
*/
|
||||
export type BitstreamFormatsRegistryAction
|
||||
= BitstreamFormatsRegistrySelectAction
|
||||
| BitstreamFormatsRegistryDeselectAction
|
||||
| BitstreamFormatsRegistryDeselectAllAction
|
@@ -0,0 +1,83 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { bitstreamFormatReducer, BitstreamFormatRegistryState } from './bitstream-format.reducers';
|
||||
import {
|
||||
BitstreamFormatsRegistryDeselectAction,
|
||||
BitstreamFormatsRegistryDeselectAllAction,
|
||||
BitstreamFormatsRegistrySelectAction
|
||||
} from './bitstream-format.actions';
|
||||
|
||||
const bitstreamFormat1: BitstreamFormat = new BitstreamFormat();
|
||||
bitstreamFormat1.id = 'test-uuid-1';
|
||||
bitstreamFormat1.shortDescription = 'test-short-1';
|
||||
|
||||
const bitstreamFormat2: BitstreamFormat = new BitstreamFormat();
|
||||
bitstreamFormat2.id = 'test-uuid-2';
|
||||
bitstreamFormat2.shortDescription = 'test-short-2';
|
||||
|
||||
const initialState: BitstreamFormatRegistryState = {
|
||||
selectedBitstreamFormats: []
|
||||
};
|
||||
|
||||
const bitstream1SelectedState: BitstreamFormatRegistryState = {
|
||||
selectedBitstreamFormats: [bitstreamFormat1]
|
||||
};
|
||||
|
||||
const bitstream1and2SelectedState: BitstreamFormatRegistryState = {
|
||||
selectedBitstreamFormats: [bitstreamFormat1, bitstreamFormat2]
|
||||
};
|
||||
|
||||
describe('BitstreamFormatReducer', () => {
|
||||
describe('BitstreamFormatsRegistryActionTypes.SELECT_FORMAT', () => {
|
||||
it('should add the format to the list of selected formats when initial list is empty', () => {
|
||||
const state = initialState;
|
||||
const action = new BitstreamFormatsRegistrySelectAction(bitstreamFormat1);
|
||||
const newState = bitstreamFormatReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(bitstream1SelectedState);
|
||||
});
|
||||
it('should add the format to the list of selected formats when formats are already present', () => {
|
||||
const state = bitstream1SelectedState;
|
||||
const action = new BitstreamFormatsRegistrySelectAction(bitstreamFormat2);
|
||||
const newState = bitstreamFormatReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(bitstream1and2SelectedState);
|
||||
});
|
||||
});
|
||||
describe('BitstreamFormatsRegistryActionTypes.DESELECT_FORMAT', () => {
|
||||
it('should deselect a format', () => {
|
||||
const state = bitstream1and2SelectedState;
|
||||
const action = new BitstreamFormatsRegistryDeselectAction(bitstreamFormat2);
|
||||
const newState = bitstreamFormatReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(bitstream1SelectedState);
|
||||
});
|
||||
});
|
||||
describe('BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT', () => {
|
||||
it('should deselect all formats', () => {
|
||||
const state = bitstream1and2SelectedState;
|
||||
const action = new BitstreamFormatsRegistryDeselectAllAction();
|
||||
const newState = bitstreamFormatReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(initialState);
|
||||
});
|
||||
});
|
||||
describe('Invalid action', () => {
|
||||
it('should return the current state', () => {
|
||||
const state = initialState;
|
||||
const action = new NullAction();
|
||||
|
||||
const newState = bitstreamFormatReducer(state, action);
|
||||
|
||||
expect(newState).toEqual(state);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class NullAction implements Action {
|
||||
type = null;
|
||||
|
||||
constructor() {
|
||||
// empty constructor
|
||||
}
|
||||
}
|
@@ -0,0 +1,55 @@
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import {
|
||||
BitstreamFormatsRegistryAction,
|
||||
BitstreamFormatsRegistryActionTypes,
|
||||
BitstreamFormatsRegistryDeselectAction,
|
||||
BitstreamFormatsRegistrySelectAction
|
||||
} from './bitstream-format.actions';
|
||||
|
||||
/**
|
||||
* The bitstream format registry state.
|
||||
* @interface BitstreamFormatRegistryState
|
||||
*/
|
||||
export interface BitstreamFormatRegistryState {
|
||||
selectedBitstreamFormats: BitstreamFormat[];
|
||||
}
|
||||
|
||||
/**
|
||||
* The initial state.
|
||||
*/
|
||||
const initialState: BitstreamFormatRegistryState = {
|
||||
selectedBitstreamFormats: [],
|
||||
};
|
||||
|
||||
/**
|
||||
* Reducer that handles BitstreamFormatsRegistryActions to modify the bitstream format registry state
|
||||
* @param state The current BitstreamFormatRegistryState
|
||||
* @param action The BitstreamFormatsRegistryAction to perform on the state
|
||||
*/
|
||||
export function bitstreamFormatReducer(state = initialState, action: BitstreamFormatsRegistryAction): BitstreamFormatRegistryState {
|
||||
|
||||
switch (action.type) {
|
||||
|
||||
case BitstreamFormatsRegistryActionTypes.SELECT_FORMAT: {
|
||||
return Object.assign({}, state, {
|
||||
selectedBitstreamFormats: [...state.selectedBitstreamFormats, (action as BitstreamFormatsRegistrySelectAction).bitstreamFormat]
|
||||
});
|
||||
}
|
||||
|
||||
case BitstreamFormatsRegistryActionTypes.DESELECT_FORMAT: {
|
||||
return Object.assign({}, state, {
|
||||
selectedBitstreamFormats: state.selectedBitstreamFormats.filter(
|
||||
(selectedBitstreamFormats) => selectedBitstreamFormats !== (action as BitstreamFormatsRegistryDeselectAction).bitstreamFormat
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
case BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT: {
|
||||
return Object.assign({}, state, {
|
||||
selectedBitstreamFormats: []
|
||||
});
|
||||
}
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
@@ -0,0 +1,37 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { BitstreamFormatsResolver } from './bitstream-formats.resolver';
|
||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||
|
||||
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
|
||||
const BITSTREAMFORMAT_ADD_PATH = 'add';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path: '',
|
||||
component: BitstreamFormatsComponent
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMAT_ADD_PATH,
|
||||
component: AddBitstreamFormatComponent,
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMAT_EDIT_PATH,
|
||||
component: EditBitstreamFormatComponent,
|
||||
resolve: {
|
||||
bitstreamFormat: BitstreamFormatsResolver
|
||||
}
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
BitstreamFormatsResolver,
|
||||
]
|
||||
})
|
||||
export class BitstreamFormatsRoutingModule {
|
||||
|
||||
}
|
@@ -2,13 +2,15 @@
|
||||
<div class="bitstream-formats row">
|
||||
<div class="col-12">
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.bitstream-formats.head' | translate}}</h2>
|
||||
<h2 id="header" class="border-bottom pb-2 ">{{'admin.registries.bitstream-formats.head' | translate}}</h2>
|
||||
|
||||
<p id="description">{{'admin.registries.bitstream-formats.description' | translate}}</p>
|
||||
<p id="create-new" class="mb-2"><a [routerLink]="'add'" class="btn btn-success">{{'admin.registries.bitstream-formats.create.new' | translate}}</a></p>
|
||||
|
||||
<p id="description" class="pb-2">{{'admin.registries.bitstream-formats.description' | translate}}</p>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[paginationOptions]="pageConfig"
|
||||
[pageInfoState]="(bitstreamFormats | async)?.payload"
|
||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
@@ -18,25 +20,38 @@
|
||||
<table id="formats" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.formats.table.name' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.formats.table.mimetype' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.formats.table.supportLevel.head' | translate}}</th>
|
||||
<th scope="col"></th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.name' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.mimetype' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.bitstream-formats.table.supportLevel.head' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let bitstreamFormat of (bitstreamFormats | async)?.payload?.page">
|
||||
<td>{{bitstreamFormat.shortDescription}}</td>
|
||||
<td>{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.formats.table.internal' | translate}})</span></td>
|
||||
<td>{{'admin.registries.bitstream-formats.formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</td>
|
||||
<td>
|
||||
<label>
|
||||
<input type="checkbox"
|
||||
[checked]="isSelected(bitstreamFormat) | async"
|
||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||
>
|
||||
</label>
|
||||
</td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.shortDescription}}</a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.mimetype}} <span *ngIf="bitstreamFormat.internal">({{'admin.registries.bitstream-formats.table.internal' | translate}})</span></a></td>
|
||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{'admin.registries.bitstream-formats.table.supportLevel.'+bitstreamFormat.supportLevel | translate}}</a></td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
<div *ngIf="(bitstreamFormats | async)?.payload?.totalElements == 0" class="alert alert-info" role="alert">
|
||||
{{'admin.registries.bitstream-formats.formats.no-items' | translate}}
|
||||
{{'admin.registries.bitstream-formats.no-items' | translate}}
|
||||
</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" type="submit" class="btn btn-danger float-right" (click)="deleteFormats()">{{'admin.registries.bitstream-formats.table.delete' | translate}}</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
@@ -13,85 +12,278 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
describe('BitstreamFormatsComponent', () => {
|
||||
let comp: BitstreamFormatsComponent;
|
||||
let fixture: ComponentFixture<BitstreamFormatsComponent>;
|
||||
let registryService: RegistryService;
|
||||
const mockFormatsList = [
|
||||
{
|
||||
shortDescription: 'Unknown',
|
||||
description: 'Unknown data format',
|
||||
mimetype: 'application/octet-stream',
|
||||
supportLevel: 0,
|
||||
internal: false,
|
||||
extensions: null
|
||||
},
|
||||
{
|
||||
shortDescription: 'License',
|
||||
description: 'Item-specific license agreed upon to submission',
|
||||
mimetype: 'text/plain; charset=utf-8',
|
||||
supportLevel: 1,
|
||||
internal: true,
|
||||
extensions: null
|
||||
},
|
||||
{
|
||||
shortDescription: 'CC License',
|
||||
description: 'Item-specific Creative Commons license agreed upon to submission',
|
||||
mimetype: 'text/html; charset=utf-8',
|
||||
supportLevel: 2,
|
||||
internal: true,
|
||||
extensions: null
|
||||
},
|
||||
{
|
||||
shortDescription: 'Adobe PDF',
|
||||
description: 'Adobe Portable Document Format',
|
||||
mimetype: 'application/pdf',
|
||||
supportLevel: 0,
|
||||
internal: false,
|
||||
extensions: null
|
||||
}
|
||||
];
|
||||
const mockFormats = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFormatsList)));
|
||||
const registryServiceStub = {
|
||||
getBitstreamFormats: () => mockFormats
|
||||
};
|
||||
let bitstreamFormatService;
|
||||
let scheduler: TestScheduler;
|
||||
let notificationsServiceStub;
|
||||
|
||||
const bitstreamFormat1 = new BitstreamFormat();
|
||||
bitstreamFormat1.uuid = 'test-uuid-1';
|
||||
bitstreamFormat1.id = 'test-uuid-1';
|
||||
bitstreamFormat1.shortDescription = 'Unknown';
|
||||
bitstreamFormat1.description = 'Unknown data format';
|
||||
bitstreamFormat1.mimetype = 'application/octet-stream';
|
||||
bitstreamFormat1.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||
bitstreamFormat1.internal = false;
|
||||
bitstreamFormat1.extensions = null;
|
||||
|
||||
const bitstreamFormat2 = new BitstreamFormat();
|
||||
bitstreamFormat2.uuid = 'test-uuid-2';
|
||||
bitstreamFormat2.id = 'test-uuid-2';
|
||||
bitstreamFormat2.shortDescription = 'License';
|
||||
bitstreamFormat2.description = 'Item-specific license agreed upon to submission';
|
||||
bitstreamFormat2.mimetype = 'text/plain; charset=utf-8';
|
||||
bitstreamFormat2.supportLevel = BitstreamFormatSupportLevel.Known;
|
||||
bitstreamFormat2.internal = true;
|
||||
bitstreamFormat2.extensions = null;
|
||||
|
||||
const bitstreamFormat3 = new BitstreamFormat();
|
||||
bitstreamFormat3.uuid = 'test-uuid-3';
|
||||
bitstreamFormat3.id = 'test-uuid-3';
|
||||
bitstreamFormat3.shortDescription = 'CC License';
|
||||
bitstreamFormat3.description = 'Item-specific Creative Commons license agreed upon to submission';
|
||||
bitstreamFormat3.mimetype = 'text/html; charset=utf-8';
|
||||
bitstreamFormat3.supportLevel = BitstreamFormatSupportLevel.Supported;
|
||||
bitstreamFormat3.internal = true;
|
||||
bitstreamFormat3.extensions = null;
|
||||
|
||||
const bitstreamFormat4 = new BitstreamFormat();
|
||||
bitstreamFormat4.uuid = 'test-uuid-4';
|
||||
bitstreamFormat4.id = 'test-uuid-4';
|
||||
bitstreamFormat4.shortDescription = 'Adobe PDF';
|
||||
bitstreamFormat4.description = 'Adobe Portable Document Format';
|
||||
bitstreamFormat4.mimetype = 'application/pdf';
|
||||
bitstreamFormat4.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||
bitstreamFormat4.internal = false;
|
||||
bitstreamFormat4.extensions = null;
|
||||
|
||||
const mockFormatsList: BitstreamFormat[] = [
|
||||
bitstreamFormat1,
|
||||
bitstreamFormat2,
|
||||
bitstreamFormat3,
|
||||
bitstreamFormat4
|
||||
];
|
||||
const mockFormatsRD = new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFormatsList));
|
||||
|
||||
const initAsync = () => {
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||
findAll: observableOf(mockFormatsRD),
|
||||
find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])),
|
||||
getSelectedBitstreamFormats: hot('a', {a: mockFormatsList}),
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: observableOf(true),
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
|
||||
{provide: HostWindowService, useValue: new HostWindowServiceStub(0)},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub}
|
||||
]
|
||||
}).compileComponents();
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
const initBeforeEach = () => {
|
||||
fixture = TestBed.createComponent(BitstreamFormatsComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
registryService = (comp as any).service;
|
||||
};
|
||||
|
||||
describe('Bitstream format page content', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
|
||||
it('should contain four formats', () => {
|
||||
const tbody: HTMLElement = fixture.debugElement.query(By.css('#formats>tbody')).nativeElement;
|
||||
expect(tbody.children.length).toBe(4);
|
||||
});
|
||||
|
||||
it('should contain the correct formats', () => {
|
||||
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(2)')).nativeElement;
|
||||
expect(unknownName.textContent).toBe('Unknown');
|
||||
|
||||
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(2)')).nativeElement;
|
||||
expect(licenseName.textContent).toBe('License');
|
||||
|
||||
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(2)')).nativeElement;
|
||||
expect(ccLicenseName.textContent).toBe('CC License');
|
||||
|
||||
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(2)')).nativeElement;
|
||||
expect(adobeName.textContent).toBe('Adobe PDF');
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain four formats', () => {
|
||||
const tbody: HTMLElement = fixture.debugElement.query(By.css('#formats>tbody')).nativeElement;
|
||||
expect(tbody.children.length).toBe(4);
|
||||
describe('selectBitStreamFormat', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should select a bitstreamFormat if it was selected in the event', () => {
|
||||
const event = {target: {checked: true}};
|
||||
|
||||
comp.selectBitStreamFormat(bitstreamFormat1, event);
|
||||
|
||||
expect(bitstreamFormatService.selectBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat1);
|
||||
});
|
||||
it('should deselect a bitstreamFormat if it is deselected in the event', () => {
|
||||
const event = {target: {checked: false}};
|
||||
|
||||
comp.selectBitStreamFormat(bitstreamFormat1, event);
|
||||
|
||||
expect(bitstreamFormatService.deselectBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat1);
|
||||
});
|
||||
it('should be called when a user clicks a checkbox', () => {
|
||||
spyOn(comp, 'selectBitStreamFormat');
|
||||
const unknownFormat = fixture.debugElement.query(By.css('#formats tr:nth-child(1) input'));
|
||||
|
||||
const event = {target: {checked: true}};
|
||||
unknownFormat.triggerEventHandler('change', event);
|
||||
|
||||
expect(comp.selectBitStreamFormat).toHaveBeenCalledWith(bitstreamFormat1, event);
|
||||
});
|
||||
});
|
||||
|
||||
it('should contain the correct formats', () => {
|
||||
const unknownName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(1) td:nth-child(1)')).nativeElement;
|
||||
expect(unknownName.textContent).toBe('Unknown');
|
||||
describe('isSelected', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should return an observable of true if the provided bistream is in the list returned by the service', () => {
|
||||
const result = comp.isSelected(bitstreamFormat1);
|
||||
|
||||
const licenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(2) td:nth-child(1)')).nativeElement;
|
||||
expect(licenseName.textContent).toBe('License');
|
||||
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', () => {
|
||||
const format = new BitstreamFormat();
|
||||
format.uuid = 'new';
|
||||
|
||||
const ccLicenseName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(3) td:nth-child(1)')).nativeElement;
|
||||
expect(ccLicenseName.textContent).toBe('CC License');
|
||||
const result = comp.isSelected(format);
|
||||
|
||||
const adobeName: HTMLElement = fixture.debugElement.query(By.css('#formats tr:nth-child(4) td:nth-child(1)')).nativeElement;
|
||||
expect(adobeName.textContent).toBe('Adobe PDF');
|
||||
expect(result).toBeObservable(cold('b', {b: false}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('deselectAll', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should deselect all bitstreamFormats', () => {
|
||||
comp.deselectAll();
|
||||
expect(bitstreamFormatService.deselectAllBitstreamFormats).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should be called when the deselect all button is clicked', () => {
|
||||
spyOn(comp, 'deselectAll');
|
||||
const deselectAllButton = fixture.debugElement.query(By.css('button.deselect'));
|
||||
deselectAllButton.triggerEventHandler('click', null);
|
||||
|
||||
expect(comp.deselectAll).toHaveBeenCalled();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFormats success', () => {
|
||||
beforeEach(async(() => {
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||
findAll: observableOf(mockFormatsRD),
|
||||
find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])),
|
||||
getSelectedBitstreamFormats: observableOf(mockFormatsList),
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: observableOf(true),
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
|
||||
{provide: HostWindowService, useValue: new HostWindowServiceStub(0)},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub}
|
||||
]
|
||||
}).compileComponents();
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(initBeforeEach);
|
||||
it('should clear bitstream formats ', () => {
|
||||
comp.deleteFormats();
|
||||
|
||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1.id);
|
||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2.id);
|
||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3.id);
|
||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4.id);
|
||||
|
||||
expect(notificationsServiceStub.success).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.success.head',
|
||||
'admin.registries.bitstream-formats.delete.success.amount');
|
||||
expect(notificationsServiceStub.error).not.toHaveBeenCalled();
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteFormats error', () => {
|
||||
beforeEach(async(() => {
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
scheduler = getTestScheduler();
|
||||
|
||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||
findAll: observableOf(mockFormatsRD),
|
||||
find: observableOf(new RemoteData(false, false, true, undefined, mockFormatsList[0])),
|
||||
getSelectedBitstreamFormats: observableOf(mockFormatsList),
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: observableOf(false),
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [BitstreamFormatsComponent, PaginationComponent, EnumKeysPipe],
|
||||
providers: [
|
||||
{provide: BitstreamFormatDataService, useValue: bitstreamFormatService},
|
||||
{provide: HostWindowService, useValue: new HostWindowServiceStub(0)},
|
||||
{provide: NotificationsService, useValue: notificationsServiceStub}
|
||||
]
|
||||
}).compileComponents();
|
||||
}
|
||||
));
|
||||
|
||||
beforeEach(initBeforeEach);
|
||||
it('should clear bitstream formats ', () => {
|
||||
comp.deleteFormats();
|
||||
|
||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat1.id);
|
||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat2.id);
|
||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat3.id);
|
||||
expect(bitstreamFormatService.delete).toHaveBeenCalledWith(bitstreamFormat4.id);
|
||||
|
||||
expect(notificationsServiceStub.error).toHaveBeenCalledWith('admin.registries.bitstream-formats.delete.failure.head',
|
||||
'admin.registries.bitstream-formats.delete.failure.amount');
|
||||
expect(notificationsServiceStub.success).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,10 +1,16 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, zip } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { BitstreamFormat } from '../../../core/registry/mock-bitstream-format.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* This component renders a list of bitstream formats
|
||||
@@ -13,24 +19,125 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
||||
selector: 'ds-bitstream-formats',
|
||||
templateUrl: './bitstream-formats.component.html'
|
||||
})
|
||||
export class BitstreamFormatsComponent {
|
||||
export class BitstreamFormatsComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* A paginated list of bitstream formats to be shown on the page
|
||||
*/
|
||||
bitstreamFormats: Observable<RemoteData<PaginatedList<BitstreamFormat>>>;
|
||||
|
||||
/**
|
||||
* A BehaviourSubject that keeps track of the pageState used to update the currently displayed bitstreamFormats
|
||||
*/
|
||||
pageState: BehaviorSubject<string>;
|
||||
|
||||
/**
|
||||
* The current pagination configuration for the page used by the FindAll method
|
||||
* Currently simply renders all bitstream formats
|
||||
*/
|
||||
config: FindListOptions = Object.assign(new FindListOptions(), {
|
||||
elementsPerPage: 20
|
||||
});
|
||||
|
||||
/**
|
||||
* The current pagination configuration for the page
|
||||
* Currently simply renders all bitstream formats
|
||||
*/
|
||||
config: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
pageConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
|
||||
id: 'registry-bitstreamformats-pagination',
|
||||
pageSize: 10000
|
||||
pageSize: 20
|
||||
});
|
||||
|
||||
constructor(private registryService: RegistryService) {
|
||||
this.updateFormats();
|
||||
constructor(private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
private translateService: TranslateService,
|
||||
private bitstreamFormatService: BitstreamFormatDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the currently selected formats from the registry and updates the presented list
|
||||
*/
|
||||
deleteFormats() {
|
||||
this.bitstreamFormatService.clearBitStreamFormatRequests().subscribe();
|
||||
this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(take(1)).subscribe(
|
||||
(formats) => {
|
||||
const tasks$ = [];
|
||||
for (const format of formats) {
|
||||
if (hasValue(format.id)) {
|
||||
tasks$.push(this.bitstreamFormatService.delete(format.id));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
||||
const successResponses = results.filter((result: boolean) => result);
|
||||
const failedResponses = results.filter((result: boolean) => !result);
|
||||
if (successResponses.length > 0) {
|
||||
this.showNotification(true, successResponses.length);
|
||||
}
|
||||
if (failedResponses.length > 0) {
|
||||
this.showNotification(false, failedResponses.length);
|
||||
}
|
||||
|
||||
this.deselectAll();
|
||||
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({}, { page: 1 }),
|
||||
queryParamsHandling: 'merge'
|
||||
}); });
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Deselects all selecetd bitstream formats
|
||||
*/
|
||||
deselectAll() {
|
||||
this.bitstreamFormatService.deselectAllBitstreamFormats();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether a given bitstream format is selected in the list (checkbox)
|
||||
* @param bitstreamFormat
|
||||
*/
|
||||
isSelected(bitstreamFormat: BitstreamFormat): Observable<boolean> {
|
||||
return this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||
map((bitstreamFormats: BitstreamFormat[]) => {
|
||||
return bitstreamFormats.find((selectedFormat) => selectedFormat.id === bitstreamFormat.id) != null;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Selects or deselects a bitstream format based on the checkbox state
|
||||
* @param bitstreamFormat
|
||||
* @param event
|
||||
*/
|
||||
selectBitStreamFormat(bitstreamFormat: BitstreamFormat, event) {
|
||||
event.target.checked ?
|
||||
this.bitstreamFormatService.selectBitstreamFormat(bitstreamFormat) :
|
||||
this.bitstreamFormatService.deselectBitstreamFormat(bitstreamFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show notifications for an amount of deleted bitstream formats
|
||||
* @param success Whether or not the notification should be a success message (error message when false)
|
||||
* @param amount The amount of deleted bitstream formats
|
||||
*/
|
||||
private showNotification(success: boolean, amount: number) {
|
||||
const prefix = 'admin.registries.bitstream-formats.delete';
|
||||
const suffix = success ? 'success' : 'failure';
|
||||
|
||||
const messages = observableCombineLatest(
|
||||
this.translateService.get(`${prefix}.${suffix}.head`),
|
||||
this.translateService.get(`${prefix}.${suffix}.amount`, {amount: amount})
|
||||
);
|
||||
messages.subscribe(([head, content]) => {
|
||||
|
||||
if (success) {
|
||||
this.notificationsService.success(head, content);
|
||||
} else {
|
||||
this.notificationsService.error(head, content);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,14 +145,26 @@ export class BitstreamFormatsComponent {
|
||||
* @param event The page change event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateFormats();
|
||||
this.config = Object.assign(new FindListOptions(), this.config, {
|
||||
currentPage: event,
|
||||
});
|
||||
this.pageConfig.currentPage = event;
|
||||
this.pageState.next('pageChange');
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pageState = new BehaviorSubject('init');
|
||||
this.bitstreamFormats = this.pageState.pipe(
|
||||
switchMap(() => {
|
||||
return this.updateFormats()
|
||||
;
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to update the bitstream formats that are shown
|
||||
* Finds all formats based on the current config
|
||||
*/
|
||||
private updateFormats() {
|
||||
this.bitstreamFormats = this.registryService.getBitstreamFormats(this.config);
|
||||
return this.bitstreamFormatService.findAll(this.config);
|
||||
}
|
||||
}
|
||||
|
@@ -0,0 +1,30 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||
import { SharedModule } from '../../../shared/shared.module';
|
||||
import { FormatFormComponent } from './format-form/format-form.component';
|
||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||
import { BitstreamFormatsRoutingModule } from './bitstream-formats-routing.module';
|
||||
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
SharedModule,
|
||||
RouterModule,
|
||||
TranslateModule,
|
||||
BitstreamFormatsRoutingModule
|
||||
],
|
||||
declarations: [
|
||||
BitstreamFormatsComponent,
|
||||
EditBitstreamFormatComponent,
|
||||
AddBitstreamFormatComponent,
|
||||
FormatFormComponent
|
||||
],
|
||||
entryComponents: []
|
||||
})
|
||||
export class BitstreamFormatsModule {
|
||||
|
||||
}
|
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { find } from 'rxjs/operators';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* This class represents a resolver that requests a specific bitstreamFormat before the route is activated
|
||||
*/
|
||||
@Injectable()
|
||||
export class BitstreamFormatsResolver implements Resolve<RemoteData<BitstreamFormat>> {
|
||||
constructor(private bitstreamFormatDataService: BitstreamFormatDataService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method for resolving an bitstreamFormat based on the parameters in the current route
|
||||
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||
* @returns Observable<<RemoteData<BitstreamFormat>> Emits the found bitstreamFormat based on the parameters in the current route,
|
||||
* or an error if something went wrong
|
||||
*/
|
||||
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<BitstreamFormat>> {
|
||||
return this.bitstreamFormatDataService.findById(route.params.id)
|
||||
.pipe(
|
||||
find((RD) => hasValue(RD.error) || RD.hasSucceeded),
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,11 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12 mb-4">
|
||||
<h2 id="sub-header"
|
||||
class="border-bottom mb-2">{{'admin.registries.bitstream-formats.edit.head' | translate:{format: (bitstreamFormatRD$ | async)?.payload.shortDescription} }}</h2>
|
||||
|
||||
<ds-bitstream-format-form [bitstreamFormat]="(bitstreamFormatRD$ | async)?.payload" (updatedFormat)="updateFormat($event)"></ds-bitstream-format-form>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -0,0 +1,122 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format.component';
|
||||
|
||||
describe('EditBitstreamFormatComponent', () => {
|
||||
let comp: EditBitstreamFormatComponent;
|
||||
let fixture: ComponentFixture<EditBitstreamFormatComponent>;
|
||||
|
||||
const bitstreamFormat = new BitstreamFormat();
|
||||
bitstreamFormat.uuid = 'test-uuid-1';
|
||||
bitstreamFormat.id = 'test-uuid-1';
|
||||
bitstreamFormat.shortDescription = 'Unknown';
|
||||
bitstreamFormat.description = 'Unknown data format';
|
||||
bitstreamFormat.mimetype = 'application/octet-stream';
|
||||
bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||
bitstreamFormat.internal = false;
|
||||
bitstreamFormat.extensions = null;
|
||||
|
||||
const routeStub = {
|
||||
data: observableOf({
|
||||
bitstreamFormat: new RemoteData(false, false, true, null, bitstreamFormat)
|
||||
})
|
||||
};
|
||||
|
||||
let router;
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let bitstreamFormatDataService: BitstreamFormatDataService;
|
||||
|
||||
const initAsync = () => {
|
||||
router = new RouterStub();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||
updateBitstreamFormat: observableOf(new RestResponse(true, 200, 'Success'))
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [EditBitstreamFormatComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Router, useValue: router},
|
||||
{provide: NotificationsService, useValue: notificationService},
|
||||
{provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
};
|
||||
|
||||
const initBeforeEach = () => {
|
||||
fixture = TestBed.createComponent(EditBitstreamFormatComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
describe('init', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should initialise the bitstreamFormat based on the route', () => {
|
||||
|
||||
comp.bitstreamFormatRD$.subscribe((format: RemoteData<BitstreamFormat>) => {
|
||||
expect(format).toEqual(new RemoteData(false, false, true, null, bitstreamFormat));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('updateFormat success', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should send the updated form to the service, show a notification and navigate to ', () => {
|
||||
comp.updateFormat(bitstreamFormat);
|
||||
|
||||
expect(bitstreamFormatDataService.updateBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat);
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']);
|
||||
|
||||
});
|
||||
});
|
||||
describe('updateFormat error', () => {
|
||||
beforeEach(async( () => {
|
||||
router = new RouterStub();
|
||||
notificationService = new NotificationsServiceStub();
|
||||
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||
updateBitstreamFormat: observableOf(new RestResponse(false, 400, 'Bad Request'))
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [EditBitstreamFormatComponent],
|
||||
providers: [
|
||||
{provide: ActivatedRoute, useValue: routeStub},
|
||||
{provide: Router, useValue: router},
|
||||
{provide: NotificationsService, useValue: notificationService},
|
||||
{provide: BitstreamFormatDataService, useValue: bitstreamFormatDataService},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should send the updated form to the service, show a notification and navigate to ', () => {
|
||||
comp.updateFormat(bitstreamFormat);
|
||||
|
||||
expect(bitstreamFormatDataService.updateBitstreamFormat).toHaveBeenCalledWith(bitstreamFormat);
|
||||
expect(notificationService.error).toHaveBeenCalled();
|
||||
expect(router.navigate).not.toHaveBeenCalled();
|
||||
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,62 @@
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* This component renders the edit page of a bitstream format.
|
||||
* The route parameter 'id' is used to request the bitstream format.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-edit-bitstream-format',
|
||||
templateUrl: './edit-bitstream-format.component.html',
|
||||
})
|
||||
export class EditBitstreamFormatComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The bitstream format wrapped in a remote-data object
|
||||
*/
|
||||
bitstreamFormatRD$: Observable<RemoteData<BitstreamFormat>>;
|
||||
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private notificationService: NotificationsService,
|
||||
private translateService: TranslateService,
|
||||
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||
) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.bitstreamFormatRD$ = this.route.data.pipe(
|
||||
map((data) => data.bitstreamFormat as RemoteData<BitstreamFormat>)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the bitstream format based on the provided bitstream format emitted by the form.
|
||||
* When successful, a success notification will be shown and the user will be navigated back to the overview page.
|
||||
* When failed, an error notification will be shown.
|
||||
*/
|
||||
updateFormat(bitstreamFormat: BitstreamFormat) {
|
||||
this.bitstreamFormatDataService.updateBitstreamFormat(bitstreamFormat).pipe(take(1)
|
||||
).subscribe((response: RestResponse) => {
|
||||
if (response.isSuccessful) {
|
||||
this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.edit.success.head'),
|
||||
this.translateService.get('admin.registries.bitstream-formats.edit.success.content'));
|
||||
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||
} else {
|
||||
this.notificationService.error('admin.registries.bitstream-formats.edit.failure.head',
|
||||
'admin.registries.bitstream-formats.create.edit.content');
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
@@ -0,0 +1,3 @@
|
||||
<ds-form *ngIf="formModel"
|
||||
[formId]="'comcol-form-id'"
|
||||
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>
|
@@ -0,0 +1,104 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormatFormComponent } from './format-form.component';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||
import { DynamicCheckboxModel, DynamicFormArrayModel, DynamicInputModel } from '@ng-dynamic-forms/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { isEmpty } from '../../../../shared/empty.util';
|
||||
|
||||
describe('FormatFormComponent', () => {
|
||||
let comp: FormatFormComponent;
|
||||
let fixture: ComponentFixture<FormatFormComponent>;
|
||||
|
||||
const router = new RouterStub();
|
||||
|
||||
const bitstreamFormat = new BitstreamFormat();
|
||||
bitstreamFormat.uuid = 'test-uuid-1';
|
||||
bitstreamFormat.id = 'test-uuid-1';
|
||||
bitstreamFormat.shortDescription = 'Unknown';
|
||||
bitstreamFormat.description = 'Unknown data format';
|
||||
bitstreamFormat.mimetype = 'application/octet-stream';
|
||||
bitstreamFormat.supportLevel = BitstreamFormatSupportLevel.Unknown;
|
||||
bitstreamFormat.internal = false;
|
||||
bitstreamFormat.extensions = [];
|
||||
|
||||
const submittedBitstreamFormat = new BitstreamFormat();
|
||||
submittedBitstreamFormat.id = bitstreamFormat.id;
|
||||
submittedBitstreamFormat.shortDescription = bitstreamFormat.shortDescription;
|
||||
submittedBitstreamFormat.mimetype = bitstreamFormat.mimetype;
|
||||
submittedBitstreamFormat.description = bitstreamFormat.description;
|
||||
submittedBitstreamFormat.supportLevel = bitstreamFormat.supportLevel;
|
||||
submittedBitstreamFormat.internal = bitstreamFormat.internal;
|
||||
submittedBitstreamFormat.extensions = bitstreamFormat.extensions;
|
||||
|
||||
const initAsync = () => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), ReactiveFormsModule, FormsModule, TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [FormatFormComponent],
|
||||
providers: [
|
||||
{provide: Router, useValue: router},
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
};
|
||||
|
||||
const initBeforeEach = () => {
|
||||
fixture = TestBed.createComponent(FormatFormComponent);
|
||||
comp = fixture.componentInstance;
|
||||
|
||||
comp.bitstreamFormat = bitstreamFormat;
|
||||
fixture.detectChanges();
|
||||
};
|
||||
|
||||
describe('initialise', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
it('should initialises the values in the form', () => {
|
||||
|
||||
expect((comp.formModel[0] as DynamicInputModel).value).toBe(bitstreamFormat.shortDescription);
|
||||
expect((comp.formModel[1] as DynamicInputModel).value).toBe(bitstreamFormat.mimetype);
|
||||
expect((comp.formModel[2] as DynamicInputModel).value).toBe(bitstreamFormat.description);
|
||||
expect((comp.formModel[3] as DynamicInputModel).value).toBe(bitstreamFormat.supportLevel);
|
||||
expect((comp.formModel[4] as DynamicCheckboxModel).value).toBe(bitstreamFormat.internal);
|
||||
|
||||
const formArray = (comp.formModel[5] as DynamicFormArrayModel);
|
||||
const extensions = [];
|
||||
for (let i = 0; i < formArray.groups.length; i++) {
|
||||
const value = (formArray.get(i).get(0) as DynamicInputModel).value;
|
||||
if (!isEmpty(value)) {
|
||||
extensions.push((formArray.get(i).get(0) as DynamicInputModel).value);
|
||||
}
|
||||
}
|
||||
|
||||
expect(extensions).toEqual(bitstreamFormat.extensions);
|
||||
|
||||
});
|
||||
});
|
||||
describe('onSubmit', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
|
||||
it('should emit the bitstreamFormat currently present in the form', () => {
|
||||
spyOn(comp.updatedFormat, 'emit');
|
||||
comp.onSubmit();
|
||||
|
||||
expect(comp.updatedFormat.emit).toHaveBeenCalledWith(submittedBitstreamFormat);
|
||||
});
|
||||
});
|
||||
describe('onCancel', () => {
|
||||
beforeEach(async(initAsync));
|
||||
beforeEach(initBeforeEach);
|
||||
|
||||
it('should navigate back to the bitstream overview', () => {
|
||||
comp.onCancel();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['/admin/registries/bitstream-formats']);
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,194 @@
|
||||
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||
import {
|
||||
DynamicCheckboxModel,
|
||||
DynamicFormArrayModel,
|
||||
DynamicFormControlLayout, DynamicFormControlLayoutConfig,
|
||||
DynamicFormControlModel,
|
||||
DynamicFormService,
|
||||
DynamicInputModel,
|
||||
DynamicSelectModel,
|
||||
DynamicTextAreaModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||
import { hasValue, isEmpty } from '../../../../shared/empty.util';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
/**
|
||||
* The component responsible for rendering the form to create/edit a bitstream format
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-bitstream-format-form',
|
||||
templateUrl: './format-form.component.html'
|
||||
})
|
||||
export class FormatFormComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The current bitstream format
|
||||
* This can either be and existing one or a new one
|
||||
*/
|
||||
@Input() bitstreamFormat: BitstreamFormat = new BitstreamFormat();
|
||||
|
||||
/**
|
||||
* EventEmitter that will emit the updated bitstream format
|
||||
*/
|
||||
@Output() updatedFormat: EventEmitter<BitstreamFormat> = new EventEmitter<BitstreamFormat>();
|
||||
|
||||
/**
|
||||
* The different supported support level of the bitstream format
|
||||
*/
|
||||
supportLevelOptions = [{label: BitstreamFormatSupportLevel.Known, value: BitstreamFormatSupportLevel.Known},
|
||||
{label: BitstreamFormatSupportLevel.Unknown, value: BitstreamFormatSupportLevel.Unknown},
|
||||
{label: BitstreamFormatSupportLevel.Supported, value: BitstreamFormatSupportLevel.Supported}];
|
||||
|
||||
/**
|
||||
* Styling element for repeatable field
|
||||
*/
|
||||
arrayElementLayout: DynamicFormControlLayout = {
|
||||
grid: {
|
||||
group: 'form-row',
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Styling element for element of repeatable field
|
||||
*/
|
||||
arrayInputElementLayout: DynamicFormControlLayout = {
|
||||
grid: {
|
||||
host: 'col'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* The form model representing the bitstream format
|
||||
*/
|
||||
formModel: DynamicFormControlModel[] = [
|
||||
new DynamicInputModel({
|
||||
id: 'shortDescription',
|
||||
name: 'shortDescription',
|
||||
label: 'admin.registries.bitstream-formats.edit.shortDescription.label',
|
||||
hint: 'admin.registries.bitstream-formats.edit.shortDescription.hint',
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'Please enter a name for this bitstream format'
|
||||
},
|
||||
}),
|
||||
new DynamicInputModel({
|
||||
id: 'mimetype',
|
||||
name: 'mimetype',
|
||||
label: 'admin.registries.bitstream-formats.edit.mimetype.label',
|
||||
hint: 'admin.registries.bitstream-formats.edit.mimetype.hint',
|
||||
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'description',
|
||||
label: 'admin.registries.bitstream-formats.edit.description.label',
|
||||
hint: 'admin.registries.bitstream-formats.edit.description.hint',
|
||||
|
||||
}),
|
||||
new DynamicSelectModel({
|
||||
id: 'supportLevel',
|
||||
name: 'supportLevel',
|
||||
options: this.supportLevelOptions,
|
||||
label: 'admin.registries.bitstream-formats.edit.supportLevel.label',
|
||||
hint: 'admin.registries.bitstream-formats.edit.supportLevel.hint',
|
||||
value: this.supportLevelOptions[0].value
|
||||
|
||||
}),
|
||||
new DynamicCheckboxModel({
|
||||
id: 'internal',
|
||||
name: 'internal',
|
||||
label: 'Internal',
|
||||
hint: 'admin.registries.bitstream-formats.edit.internal.hint',
|
||||
}),
|
||||
new DynamicFormArrayModel({
|
||||
id: 'extensions',
|
||||
name: 'extensions',
|
||||
label: 'admin.registries.bitstream-formats.edit.extensions.label',
|
||||
groupFactory: () => [
|
||||
new DynamicInputModel({
|
||||
id: 'extension',
|
||||
placeholder: 'admin.registries.bitstream-formats.edit.extensions.placeholder',
|
||||
}, this.arrayInputElementLayout)
|
||||
]
|
||||
}, this.arrayElementLayout),
|
||||
];
|
||||
|
||||
constructor(private dynamicFormService: DynamicFormService,
|
||||
private translateService: TranslateService,
|
||||
private router: Router) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.initValues();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the form based on the provided bitstream format
|
||||
*/
|
||||
initValues() {
|
||||
this.formModel.forEach(
|
||||
(fieldModel: DynamicFormControlModel) => {
|
||||
if (fieldModel.name === 'extensions') {
|
||||
if (hasValue(this.bitstreamFormat.extensions)) {
|
||||
const extenstions = this.bitstreamFormat.extensions;
|
||||
const formArray = (fieldModel as DynamicFormArrayModel);
|
||||
for (let i = 0; i < extenstions.length; i++) {
|
||||
formArray.insertGroup(i).group[0] = new DynamicInputModel({
|
||||
id: `extension-${i}`,
|
||||
value: extenstions[i]
|
||||
}, this.arrayInputElementLayout);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (hasValue(this.bitstreamFormat[fieldModel.name])) {
|
||||
(fieldModel as DynamicInputModel).value = this.bitstreamFormat[fieldModel.name];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an updated bistream format based on the current values in the form
|
||||
* Emits the updated bitstream format trouhg the updatedFormat emitter
|
||||
*/
|
||||
onSubmit() {
|
||||
const updatedBitstreamFormat = Object.assign(new BitstreamFormat(),
|
||||
{
|
||||
id: this.bitstreamFormat.id
|
||||
});
|
||||
|
||||
this.formModel.forEach(
|
||||
(fieldModel: DynamicFormControlModel) => {
|
||||
if (fieldModel.name === 'extensions') {
|
||||
const formArray = (fieldModel as DynamicFormArrayModel);
|
||||
const extensions = [];
|
||||
for (let i = 0; i < formArray.groups.length; i++) {
|
||||
const value = (formArray.get(i).get(0) as DynamicInputModel).value;
|
||||
if (!isEmpty(value)) {
|
||||
extensions.push((formArray.get(i).get(0) as DynamicInputModel).value);
|
||||
}
|
||||
}
|
||||
updatedBitstreamFormat.extensions = extensions;
|
||||
} else {
|
||||
updatedBitstreamFormat[fieldModel.name] = (fieldModel as DynamicInputModel).value;
|
||||
}
|
||||
});
|
||||
this.updatedFormat.emit(updatedBitstreamFormat);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancels the edit/create action of the bitstream format and navigates back to the bitstream format registry
|
||||
*/
|
||||
onCancel() {
|
||||
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||
}
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
import { Action } from '@ngrx/store';
|
||||
import { type } from '../../../shared/ngrx/type';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||
import { MetadataField } from '../../../core/metadata/metadatafield.model';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
|
||||
/**
|
||||
* For each action type in an action group, make a simple
|
||||
@@ -148,4 +148,6 @@ export type MetadataRegistryAction
|
||||
| MetadataRegistryEditFieldAction
|
||||
| MetadataRegistryCancelFieldAction
|
||||
| MetadataRegistrySelectFieldAction
|
||||
| MetadataRegistryDeselectFieldAction;
|
||||
| MetadataRegistryDeselectFieldAction
|
||||
| MetadataRegistryDeselectAllSchemaAction
|
||||
| MetadataRegistryDeselectAllFieldAction;
|
||||
|
@@ -1,5 +1,3 @@
|
||||
@import '../../../../styles/variables.scss';
|
||||
|
||||
.selectable-row:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user