mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-10 11:33:04 +00:00
Compare commits
1647 Commits
dspace-7.1
...
dspace-7.4
Author | SHA1 | Date | |
---|---|---|---|
![]() |
ca864379c8 | ||
![]() |
a2cb60adeb | ||
![]() |
cebf2df4d2 | ||
![]() |
5728f35f46 | ||
![]() |
1d4f2edfdb | ||
![]() |
e572e4f41e | ||
![]() |
f5deb88c5f | ||
![]() |
8d30788f05 | ||
![]() |
acc32bf77d | ||
![]() |
8ffc32540a | ||
![]() |
4c20ab8de3 | ||
![]() |
8cc144cb73 | ||
![]() |
c55c15b6f6 | ||
![]() |
417c4dee12 | ||
![]() |
ba75051117 | ||
![]() |
c47405cfe1 | ||
![]() |
8e5ef54f97 | ||
![]() |
3b7a830ffe | ||
![]() |
d8c8a43da4 | ||
![]() |
f72f6a3166 | ||
![]() |
a94877ad5a | ||
![]() |
9305705ab6 | ||
![]() |
7b8371dc87 | ||
![]() |
c2376b6fc7 | ||
![]() |
60c317c020 | ||
![]() |
2e4b96b2dd | ||
![]() |
ec375fb910 | ||
![]() |
8f66f1fe8f | ||
![]() |
89cdad42d2 | ||
![]() |
076fa3e1af | ||
![]() |
458df454eb | ||
![]() |
967f531eb5 | ||
![]() |
445255e92d | ||
![]() |
789a1471b0 | ||
![]() |
d064c26eec | ||
![]() |
738aa9f098 | ||
![]() |
f16dcc7942 | ||
![]() |
84bdfb8306 | ||
![]() |
ac36cc20dc | ||
![]() |
bbdaa253aa | ||
![]() |
3b0577be2f | ||
![]() |
b96502583a | ||
![]() |
2bb4c71ddd | ||
![]() |
f3cf13a040 | ||
![]() |
b26da8991a | ||
![]() |
0ef8dccac8 | ||
![]() |
e1b21e2dc6 | ||
![]() |
1779b1f9f3 | ||
![]() |
fb9ed87f95 | ||
![]() |
e24f0e3fa5 | ||
![]() |
3b653880b9 | ||
![]() |
e2e122b4ee | ||
![]() |
8a6ff75560 | ||
![]() |
bf08244e93 | ||
![]() |
c20853d2fe | ||
![]() |
80500e716d | ||
![]() |
fad23560b0 | ||
![]() |
50cb219045 | ||
![]() |
379d78eaa2 | ||
![]() |
c352ac440c | ||
![]() |
7a52b69573 | ||
![]() |
a384a462eb | ||
![]() |
88d5f02df2 | ||
![]() |
c69934aab6 | ||
![]() |
6f9d31034c | ||
![]() |
262fac3dd0 | ||
![]() |
a370f42996 | ||
![]() |
d12aa01eec | ||
![]() |
1370363a54 | ||
![]() |
6fa9eb8d0a | ||
![]() |
d73c163443 | ||
![]() |
0025567daf | ||
![]() |
fadcd2e0c6 | ||
![]() |
9f1749e1ee | ||
![]() |
4371698a6e | ||
![]() |
7b42446c11 | ||
![]() |
6972a384a2 | ||
![]() |
7acc8c888e | ||
![]() |
a6d29f5a23 | ||
![]() |
7f30e92782 | ||
![]() |
e3351eb739 | ||
![]() |
2af9984ace | ||
![]() |
81830bb797 | ||
![]() |
5e54422daf | ||
![]() |
133f35cb42 | ||
![]() |
8805ac33fc | ||
![]() |
7c194ff700 | ||
![]() |
3799ed28c1 | ||
![]() |
f5316b5805 | ||
![]() |
48c3fd24ed | ||
![]() |
24943b9b09 | ||
![]() |
b0d1a74128 | ||
![]() |
c7029ed8f6 | ||
![]() |
fbe35e14dd | ||
![]() |
0235cc2fe8 | ||
![]() |
30f0622810 | ||
![]() |
42bb39d097 | ||
![]() |
60bcfe8cf5 | ||
![]() |
8bcbf02ed1 | ||
![]() |
63cedc0386 | ||
![]() |
695ce3ab9e | ||
![]() |
77c38ad573 | ||
![]() |
7064014623 | ||
![]() |
389ae85c55 | ||
![]() |
d98a261985 | ||
![]() |
171ac62beb | ||
![]() |
941e71a75b | ||
![]() |
241816e836 | ||
![]() |
750c6032bd | ||
![]() |
aa53e2fff6 | ||
![]() |
27af7fad94 | ||
![]() |
1d13fd3f35 | ||
![]() |
ad13a396e6 | ||
![]() |
f3f89b3dc1 | ||
![]() |
f1888f718c | ||
![]() |
0e23e850b7 | ||
![]() |
9140fb6106 | ||
![]() |
6cd01ec471 | ||
![]() |
e9a8526b1b | ||
![]() |
7771cff5c8 | ||
![]() |
42608c6b09 | ||
![]() |
8800b1e0e8 | ||
![]() |
98ee0751ec | ||
![]() |
e9a87a607a | ||
![]() |
73c81ea924 | ||
![]() |
fc624a406f | ||
![]() |
1355fb1d35 | ||
![]() |
d1ca092897 | ||
![]() |
396bbd4f31 | ||
![]() |
05d1554740 | ||
![]() |
6b0513aba1 | ||
![]() |
a011c300aa | ||
![]() |
b89640e4d2 | ||
![]() |
e9b83f3b0d | ||
![]() |
0824fff6ef | ||
![]() |
614ce4aa83 | ||
![]() |
2f13beac7c | ||
![]() |
ab3b05b950 | ||
![]() |
7ad68530ea | ||
![]() |
9f43c1d0b9 | ||
![]() |
5e5e04cb1b | ||
![]() |
064dae2581 | ||
![]() |
a6ee2b8aa3 | ||
![]() |
994c9b3985 | ||
![]() |
dc54990977 | ||
![]() |
d46ab3b932 | ||
![]() |
7a89f416f9 | ||
![]() |
9c93a8ad86 | ||
![]() |
3834d170e9 | ||
![]() |
37ac449cb3 | ||
![]() |
f5ff72372c | ||
![]() |
f57fb3923a | ||
![]() |
b345b27c0f | ||
![]() |
2f71dc358b | ||
![]() |
5b1e0d3e0d | ||
![]() |
11eacc10f7 | ||
![]() |
9de6f38ea6 | ||
![]() |
0222fb0061 | ||
![]() |
711982d423 | ||
![]() |
b022a93128 | ||
![]() |
5d37efdfc0 | ||
![]() |
a269817fe3 | ||
![]() |
001d6dc20a | ||
![]() |
edd193a51e | ||
![]() |
f0fd196ba9 | ||
![]() |
e1b9368166 | ||
![]() |
3717c661ca | ||
![]() |
f01db3e984 | ||
![]() |
cc3f570da7 | ||
![]() |
d4107e3f9a | ||
![]() |
68def61f20 | ||
![]() |
551365a0a8 | ||
![]() |
fbcaaf5a10 | ||
![]() |
c76bae78d4 | ||
![]() |
891c9165d7 | ||
![]() |
8d6222807e | ||
![]() |
2642ed35b7 | ||
![]() |
f599e80190 | ||
![]() |
ab77e66be7 | ||
![]() |
7f696b24d6 | ||
![]() |
dda15e998a | ||
![]() |
53b5d27eea | ||
![]() |
a1bab693e5 | ||
![]() |
9e779cf235 | ||
![]() |
85194c1487 | ||
![]() |
f2a6b8208f | ||
![]() |
e072cdf75b | ||
![]() |
6d3f3cad2f | ||
![]() |
5a815da5f2 | ||
![]() |
4d43a34cb3 | ||
![]() |
1ebaa65161 | ||
![]() |
e752ef74fb | ||
![]() |
4be09885a2 | ||
![]() |
76416fffa3 | ||
![]() |
0f1b9bb817 | ||
![]() |
b9ac08eba3 | ||
![]() |
594b6d61c3 | ||
![]() |
c844423fc3 | ||
![]() |
bf5a8daa78 | ||
![]() |
9371c0b3b7 | ||
![]() |
81d3b1708f | ||
![]() |
5099d0b18a | ||
![]() |
f787491af4 | ||
![]() |
87d9c28acc | ||
![]() |
86364ad375 | ||
![]() |
086bd4723b | ||
![]() |
3e803e8411 | ||
![]() |
043b92fbc7 | ||
![]() |
20ff386c6e | ||
![]() |
ed611c5716 | ||
![]() |
5bdf911fc7 | ||
![]() |
a4b9a6da1d | ||
![]() |
80a1eed63c | ||
![]() |
4ca5571a2b | ||
![]() |
bc1e49c783 | ||
![]() |
e4229e3993 | ||
![]() |
31167a3ce4 | ||
![]() |
ee46594b5b | ||
![]() |
4484c3ebbc | ||
![]() |
ab745983aa | ||
![]() |
79f65f1d99 | ||
![]() |
34beb7d8da | ||
![]() |
cd83ba4275 | ||
![]() |
4a30295df8 | ||
![]() |
f84e4d6146 | ||
![]() |
36b8b8b61b | ||
![]() |
a17ee028df | ||
![]() |
898f23a997 | ||
![]() |
dd2317699a | ||
![]() |
f87b83ad82 | ||
![]() |
d2d6554e26 | ||
![]() |
eb48b1b204 | ||
![]() |
41d1f5383f | ||
![]() |
5e9bbbe06a | ||
![]() |
927dfdad1c | ||
![]() |
e1d3832d64 | ||
![]() |
44c4f993f7 | ||
![]() |
f3d43b5745 | ||
![]() |
cfff05b1da | ||
![]() |
2a35329efa | ||
![]() |
231b7e2c2e | ||
![]() |
8d36de27d1 | ||
![]() |
faaaad6bfb | ||
![]() |
4b62fb2db4 | ||
![]() |
ccfa1410c2 | ||
![]() |
06f3f8d31c | ||
![]() |
7b47c22531 | ||
![]() |
7de56a4f14 | ||
![]() |
87a7baa2e8 | ||
![]() |
16523c3c90 | ||
![]() |
3cc4b96ff4 | ||
![]() |
a2f8a5ccfa | ||
![]() |
7c5887654a | ||
![]() |
85d44ad3d4 | ||
![]() |
4a573eb981 | ||
![]() |
900dd31351 | ||
![]() |
16b3cb8008 | ||
![]() |
75da8ca10d | ||
![]() |
7047f9ccef | ||
![]() |
c2da87e617 | ||
![]() |
3d5951eeb9 | ||
![]() |
516300f3e5 | ||
![]() |
fcae738db6 | ||
![]() |
a13f4ee3b9 | ||
![]() |
46ecf287a4 | ||
![]() |
3665071dea | ||
![]() |
bb31e1d25c | ||
![]() |
04c0690e8d | ||
![]() |
b2d34f0dad | ||
![]() |
1df41deb8f | ||
![]() |
70c6eac88d | ||
![]() |
1677349fbb | ||
![]() |
70f2625d15 | ||
![]() |
7cbac04072 | ||
![]() |
aae3c217c5 | ||
![]() |
6e5fe96696 | ||
![]() |
0f6fdceb45 | ||
![]() |
183653112e | ||
![]() |
abe19b12bb | ||
![]() |
6360f89aac | ||
![]() |
84ad250de6 | ||
![]() |
c49d2f17a8 | ||
![]() |
8953a3c50b | ||
![]() |
59ce1a9950 | ||
![]() |
a46098112e | ||
![]() |
e7447071b1 | ||
![]() |
cad4d1f101 | ||
![]() |
c517ed0dfa | ||
![]() |
7ca434b7bb | ||
![]() |
919b406260 | ||
![]() |
ac2922e59d | ||
![]() |
4fff311fe4 | ||
![]() |
78358a4067 | ||
![]() |
6d361beb88 | ||
![]() |
c94e5d0709 | ||
![]() |
59f9534418 | ||
![]() |
44489fa4e0 | ||
![]() |
916d7fc13d | ||
![]() |
cc697041eb | ||
![]() |
c1498424f3 | ||
![]() |
db79d46a76 | ||
![]() |
335d613df7 | ||
![]() |
147c7180d0 | ||
![]() |
8b4dbbad55 | ||
![]() |
6907e6a517 | ||
![]() |
c32e4ad7c7 | ||
![]() |
a29a7f5883 | ||
![]() |
abc9bbeae3 | ||
![]() |
7f9c34f08e | ||
![]() |
989d243c8e | ||
![]() |
b692fa5924 | ||
![]() |
06a68ece9d | ||
![]() |
8ff9f81247 | ||
![]() |
aad0085d61 | ||
![]() |
9218c0545f | ||
![]() |
ccb4c0794c | ||
![]() |
580986adae | ||
![]() |
b02826d24d | ||
![]() |
ac34a7893b | ||
![]() |
e57970349d | ||
![]() |
89a7320823 | ||
![]() |
6dfc5ef2f5 | ||
![]() |
13d33d802f | ||
![]() |
8af725e76f | ||
![]() |
06e34355c3 | ||
![]() |
895e44a25f | ||
![]() |
97a12cae47 | ||
![]() |
293ba8408e | ||
![]() |
7c13db2f89 | ||
![]() |
d40f163c49 | ||
![]() |
acfd01e774 | ||
![]() |
2ac0bcdc98 | ||
![]() |
18dda8c44e | ||
![]() |
606663dd76 | ||
![]() |
4f2f70f851 | ||
![]() |
05784bfec6 | ||
![]() |
0b7cf23e3f | ||
![]() |
032a276494 | ||
![]() |
4ddc4b7541 | ||
![]() |
e7dc5f8d14 | ||
![]() |
24189c4ce0 | ||
![]() |
8622e4c059 | ||
![]() |
877659a7c8 | ||
![]() |
f8404c540e | ||
![]() |
9e14985fc5 | ||
![]() |
2c63245050 | ||
![]() |
37e79ddc69 | ||
![]() |
d01bfe2cb1 | ||
![]() |
56c8c8c370 | ||
![]() |
aafe3543d8 | ||
![]() |
db6c8f00a8 | ||
![]() |
cd1d409577 | ||
![]() |
744bcae3f2 | ||
![]() |
5a0ad41bfd | ||
![]() |
d88352f513 | ||
![]() |
05ec096ec9 | ||
![]() |
b6d6091c87 | ||
![]() |
e523583f1f | ||
![]() |
e5165c151c | ||
![]() |
28fff891d2 | ||
![]() |
527fb134b9 | ||
![]() |
ba2f173199 | ||
![]() |
8ca4da47da | ||
![]() |
d89020a712 | ||
![]() |
f15440ca5a | ||
![]() |
d463ea5893 | ||
![]() |
66380857c9 | ||
![]() |
77d0600453 | ||
![]() |
66b5398d91 | ||
![]() |
3c5fe8e098 | ||
![]() |
a6fb4a6303 | ||
![]() |
38203490c7 | ||
![]() |
236216b556 | ||
![]() |
e6f363a0f6 | ||
![]() |
1f1b04e88f | ||
![]() |
9f609a2966 | ||
![]() |
458a721c08 | ||
![]() |
030b1c33db | ||
![]() |
47b9b09139 | ||
![]() |
a8cf6df03f | ||
![]() |
b72b37a647 | ||
![]() |
6d1d7c3611 | ||
![]() |
bcc747dc3e | ||
![]() |
e295dccc8a | ||
![]() |
fcad492a25 | ||
![]() |
2ef701a231 | ||
![]() |
0953806865 | ||
![]() |
4906516359 | ||
![]() |
0783cd5cb6 | ||
![]() |
a3eb544422 | ||
![]() |
2532e37010 | ||
![]() |
50e849dd44 | ||
![]() |
77eb6c1807 | ||
![]() |
38c1214117 | ||
![]() |
342a712513 | ||
![]() |
1c3c275209 | ||
![]() |
9ba2807b43 | ||
![]() |
72e5909b71 | ||
![]() |
95e8346228 | ||
![]() |
ac8688ea79 | ||
![]() |
eee0d72345 | ||
![]() |
18c208f6a4 | ||
![]() |
0840bfd8ce | ||
![]() |
bcc71ce2e1 | ||
![]() |
01daf93d01 | ||
![]() |
310237d30f | ||
![]() |
e9894c91aa | ||
![]() |
c4a163e169 | ||
![]() |
fa20e9e5c0 | ||
![]() |
b891ae0237 | ||
![]() |
795870d712 | ||
![]() |
154d66f1e8 | ||
![]() |
ed9570e7cc | ||
![]() |
f154318855 | ||
![]() |
c1f6993144 | ||
![]() |
c5d3776df7 | ||
![]() |
317c615b30 | ||
![]() |
72852dd031 | ||
![]() |
ee26084d6a | ||
![]() |
a77b1da804 | ||
![]() |
d5605e43d4 | ||
![]() |
ca341e53b4 | ||
![]() |
db169251df | ||
![]() |
e464c0f8c7 | ||
![]() |
528c4c31ea | ||
![]() |
fbaab69121 | ||
![]() |
05b131edb9 | ||
![]() |
ad316f7316 | ||
![]() |
a76555c518 | ||
![]() |
cd4ed018dd | ||
![]() |
42a2c3c7e2 | ||
![]() |
8f4b3b58fb | ||
![]() |
9a5a7c1306 | ||
![]() |
cb4620e536 | ||
![]() |
bd4190d4fc | ||
![]() |
282d0a770d | ||
![]() |
50828e9c06 | ||
![]() |
4b20b0cb81 | ||
![]() |
cacce82b0a | ||
![]() |
53dcd48e98 | ||
![]() |
b2feadc290 | ||
![]() |
5ed369d097 | ||
![]() |
ca4f2cc5c0 | ||
![]() |
78598dc1b5 | ||
![]() |
606995881f | ||
![]() |
f8e1db4987 | ||
![]() |
8c40ed42da | ||
![]() |
d9facf5fb7 | ||
![]() |
35614d1473 | ||
![]() |
d7fc14aba3 | ||
![]() |
762f8930b7 | ||
![]() |
904c6168a8 | ||
![]() |
7a3284f6bc | ||
![]() |
14dfba8118 | ||
![]() |
c602a22f6c | ||
![]() |
27bb45b1bd | ||
![]() |
092b3d862a | ||
![]() |
17ab269e25 | ||
![]() |
a2e8b2a61c | ||
![]() |
967f4a99b9 | ||
![]() |
f6d2014bf4 | ||
![]() |
5342d0921d | ||
![]() |
ba7ecf432b | ||
![]() |
4870d818f6 | ||
![]() |
8dc356a658 | ||
![]() |
bf48971047 | ||
![]() |
9e67b7f7a5 | ||
![]() |
2d7b5768bf | ||
![]() |
2e59e1e775 | ||
![]() |
ed0204ab9c | ||
![]() |
0bc31fc48e | ||
![]() |
f203f27e53 | ||
![]() |
4dcf6a345a | ||
![]() |
f4c0f79288 | ||
![]() |
2ac8d4140f | ||
![]() |
7afa4dcd8d | ||
![]() |
f2e977c402 | ||
![]() |
3d5f7bb061 | ||
![]() |
250043fde8 | ||
![]() |
4403555c29 | ||
![]() |
ca87f09625 | ||
![]() |
bfbe38974b | ||
![]() |
f80e72df9a | ||
![]() |
6b36412f6c | ||
![]() |
67b4cce25d | ||
![]() |
c7a88f99d6 | ||
![]() |
4da017a0ee | ||
![]() |
5ff80a8a02 | ||
![]() |
bdc004f64d | ||
![]() |
5cb737c7f2 | ||
![]() |
372cddfd5e | ||
![]() |
7b48e7c91f | ||
![]() |
577a92bc6b | ||
![]() |
39c2aa85ec | ||
![]() |
0ef2be6693 | ||
![]() |
e9372395f4 | ||
![]() |
ea67a15784 | ||
![]() |
13266ecf42 | ||
![]() |
0d582e5160 | ||
![]() |
517aee0e8c | ||
![]() |
5013890b35 | ||
![]() |
2d9e5b43ac | ||
![]() |
612f8f25da | ||
![]() |
02f309756d | ||
![]() |
e0e9450f93 | ||
![]() |
4a4578474a | ||
![]() |
0c1015dd58 | ||
![]() |
fae355a713 | ||
![]() |
7ab5354ab8 | ||
![]() |
df00137c55 | ||
![]() |
72832e5e12 | ||
![]() |
977dbb38ef | ||
![]() |
4290e54b02 | ||
![]() |
10bbb01a44 | ||
![]() |
993bb54cfd | ||
![]() |
fd356d4ca7 | ||
![]() |
222a12bbba | ||
![]() |
7b4716c439 | ||
![]() |
36560e0e65 | ||
![]() |
1b5c801d06 | ||
![]() |
900354112a | ||
![]() |
4de5ab0f56 | ||
![]() |
5ee4c6b61b | ||
![]() |
bde52ba4b4 | ||
![]() |
b9d3855c74 | ||
![]() |
9bf0320b16 | ||
![]() |
b55e4327a8 | ||
![]() |
9e2a682a51 | ||
![]() |
377ac3ab84 | ||
![]() |
e0109b947c | ||
![]() |
f4c6284c23 | ||
![]() |
79c97c94a2 | ||
![]() |
cfca5c8ca1 | ||
![]() |
5a7ba93e50 | ||
![]() |
78f4096ab4 | ||
![]() |
f2b1d11fb2 | ||
![]() |
0aa14bcddd | ||
![]() |
2a20b0c989 | ||
![]() |
1168616f80 | ||
![]() |
f9c6f5f5b6 | ||
![]() |
146bbd8042 | ||
![]() |
395b2762b5 | ||
![]() |
5e15eb07f7 | ||
![]() |
9b93e9c2a4 | ||
![]() |
f589b2f8de | ||
![]() |
f37f32b4ab | ||
![]() |
f649a82c3c | ||
![]() |
815788bc4c | ||
![]() |
f2f8f0929c | ||
![]() |
8c14822eef | ||
![]() |
5d130156e8 | ||
![]() |
12000d7b97 | ||
![]() |
0475a9e22a | ||
![]() |
17a89387f2 | ||
![]() |
20975a2274 | ||
![]() |
f3e4fb9901 | ||
![]() |
f942eced3a | ||
![]() |
eb662fc156 | ||
![]() |
0a73875d17 | ||
![]() |
20314d4620 | ||
![]() |
7724db530b | ||
![]() |
555d98f700 | ||
![]() |
ac41e7b3d6 | ||
![]() |
6abef91094 | ||
![]() |
ac677d52d7 | ||
![]() |
c56cded549 | ||
![]() |
922e57c90f | ||
![]() |
e15da9b76b | ||
![]() |
c62f72789d | ||
![]() |
26ed23d1b6 | ||
![]() |
84f362fe21 | ||
![]() |
f13401266e | ||
![]() |
fe85a596ff | ||
![]() |
b1683e1424 | ||
![]() |
144451ea26 | ||
![]() |
2bf5e70b87 | ||
![]() |
baa55b2595 | ||
![]() |
0e9dff41ad | ||
![]() |
edcbdabd52 | ||
![]() |
5eb6355858 | ||
![]() |
63356aa127 | ||
![]() |
aa7da8608a | ||
![]() |
83ded46e83 | ||
![]() |
4755b15db1 | ||
![]() |
9a9010f146 | ||
![]() |
793169dd31 | ||
![]() |
d1c012407a | ||
![]() |
ab6e2b87f8 | ||
![]() |
da45ff2d83 | ||
![]() |
9bcd92efae | ||
![]() |
d3ba1d2926 | ||
![]() |
2c82893e72 | ||
![]() |
82748c5ac9 | ||
![]() |
3979c51e61 | ||
![]() |
8eca0d93e4 | ||
![]() |
781fa08d1e | ||
![]() |
3adf846786 | ||
![]() |
94281fd60e | ||
![]() |
adcef89c29 | ||
![]() |
09be33582b | ||
![]() |
eda4797034 | ||
![]() |
a0ea69aafa | ||
![]() |
851855fc2a | ||
![]() |
d779521dc9 | ||
![]() |
383703a4d6 | ||
![]() |
9850174fa2 | ||
![]() |
f39283cae5 | ||
![]() |
d063c42f6d | ||
![]() |
2750931e92 | ||
![]() |
3a4d7708a9 | ||
![]() |
03af265ac4 | ||
![]() |
d94e6ad8d7 | ||
![]() |
5aa569c5f2 | ||
![]() |
50f96884de | ||
![]() |
0f94bbb874 | ||
![]() |
a43137e847 | ||
![]() |
fd4d541de3 | ||
![]() |
35708ae3a8 | ||
![]() |
9db9441afc | ||
![]() |
06d07dc1df | ||
![]() |
e04404bdd6 | ||
![]() |
41bdd625dc | ||
![]() |
529384ab7f | ||
![]() |
02b124f7dd | ||
![]() |
9b6aa9f324 | ||
![]() |
a915659cc9 | ||
![]() |
98f1baea2f | ||
![]() |
0b0fae45fa | ||
![]() |
fe4568a573 | ||
![]() |
10f4f80f0d | ||
![]() |
03369b8e10 | ||
![]() |
68d162432b | ||
![]() |
afe798942b | ||
![]() |
5bf34b868c | ||
![]() |
b372a27513 | ||
![]() |
3cb5e0c226 | ||
![]() |
859172717a | ||
![]() |
fa5dc5ccf0 | ||
![]() |
a4699d0faf | ||
![]() |
eca0526cce | ||
![]() |
17cc3078df | ||
![]() |
c3ececdde7 | ||
![]() |
0fbe9731cb | ||
![]() |
4c19d3a027 | ||
![]() |
fca8c223e7 | ||
![]() |
3c3a679ef7 | ||
![]() |
df1324af90 | ||
![]() |
b03c73e0c5 | ||
![]() |
19bc9df5a1 | ||
![]() |
a1570128ea | ||
![]() |
4d73a3f2bf | ||
![]() |
ce9e811526 | ||
![]() |
bd02278f80 | ||
![]() |
cf6e58d194 | ||
![]() |
9db342606b | ||
![]() |
a1598f9f8a | ||
![]() |
485cc2bd31 | ||
![]() |
8bdb4b7be3 | ||
![]() |
e2abea7373 | ||
![]() |
e68bfd2eb6 | ||
![]() |
12f073bdbe | ||
![]() |
7ea208e314 | ||
![]() |
b0625342b7 | ||
![]() |
1e9e4d5b12 | ||
![]() |
a9fcdce960 | ||
![]() |
b6f83461ab | ||
![]() |
9ff075b23e | ||
![]() |
9d5aba7499 | ||
![]() |
83fce87792 | ||
![]() |
43f4ff7cde | ||
![]() |
7320d1cc66 | ||
![]() |
24b8c6b606 | ||
![]() |
73c60e3eef | ||
![]() |
1d33d1537b | ||
![]() |
245b86c68a | ||
![]() |
318d0ead61 | ||
![]() |
8ac9425db3 | ||
![]() |
09b7d2e52f | ||
![]() |
39cba0414a | ||
![]() |
eca9f79924 | ||
![]() |
dd049270a7 | ||
![]() |
37492f2f82 | ||
![]() |
692ab040dc | ||
![]() |
377214c996 | ||
![]() |
02e089a51b | ||
![]() |
68a36c4635 | ||
![]() |
c6e591402f | ||
![]() |
e2488b9848 | ||
![]() |
f1191c5428 | ||
![]() |
6db5cff426 | ||
![]() |
3c6a41f82a | ||
![]() |
fc1e6b6b85 | ||
![]() |
c4f2e8ce11 | ||
![]() |
cfb4a627be | ||
![]() |
541968fdd9 | ||
![]() |
dc5d46993e | ||
![]() |
bca332a23a | ||
![]() |
2264067240 | ||
![]() |
2441db16e9 | ||
![]() |
d75136408b | ||
![]() |
9e8071727d | ||
![]() |
533e4c982f | ||
![]() |
f40907ea9f | ||
![]() |
03e1e468bd | ||
![]() |
3a483c393d | ||
![]() |
5996efec1a | ||
![]() |
3a9783d031 | ||
![]() |
ea7a40a75a | ||
![]() |
d719a02597 | ||
![]() |
d6e181d0b2 | ||
![]() |
358e7c6d54 | ||
![]() |
091494750b | ||
![]() |
dfd759e2fb | ||
![]() |
a830c99f90 | ||
![]() |
2622f374b0 | ||
![]() |
2c5da265e1 | ||
![]() |
87064761b5 | ||
![]() |
99b12cdd0f | ||
![]() |
2c53b4ae2e | ||
![]() |
c834f8a075 | ||
![]() |
bc37372f05 | ||
![]() |
e6f6bc96f3 | ||
![]() |
0b7782d230 | ||
![]() |
21a6540a36 | ||
![]() |
d2ee2cd29e | ||
![]() |
be3fceb185 | ||
![]() |
76046ded84 | ||
![]() |
5810309ff8 | ||
![]() |
ebbae79854 | ||
![]() |
d44253ea86 | ||
![]() |
7a84ea483d | ||
![]() |
5153308c46 | ||
![]() |
d297eb708d | ||
![]() |
cfeef5f2d9 | ||
![]() |
7f6c17df75 | ||
![]() |
d54f959613 | ||
![]() |
fc83facf1e | ||
![]() |
ab9df8ac98 | ||
![]() |
98b1667ca7 | ||
![]() |
a38172c035 | ||
![]() |
d1f8bb6d7b | ||
![]() |
bb898022e3 | ||
![]() |
5842d83e91 | ||
![]() |
ec6bc20010 | ||
![]() |
53851ba18e | ||
![]() |
24e641e024 | ||
![]() |
87d015371c | ||
![]() |
268ee04d1e | ||
![]() |
5ec460d236 | ||
![]() |
166f4c3b25 | ||
![]() |
3e2cd387df | ||
![]() |
436c6dd084 | ||
![]() |
c206005d42 | ||
![]() |
9d577c5317 | ||
![]() |
857a3c56b7 | ||
![]() |
d2e881eba3 | ||
![]() |
bdc8c5d41e | ||
![]() |
06091e39ca | ||
![]() |
51bbbb697e | ||
![]() |
383485b16d | ||
![]() |
9889a9d1a6 | ||
![]() |
305203dba7 | ||
![]() |
9124f06e4c | ||
![]() |
7b6ccb780f | ||
![]() |
d751de0aa6 | ||
![]() |
2db80022b5 | ||
![]() |
bb5cdfc1a3 | ||
![]() |
cba7a60476 | ||
![]() |
de2202799d | ||
![]() |
50184c8784 | ||
![]() |
a03557fcad | ||
![]() |
9aff792cac | ||
![]() |
bc63e14512 | ||
![]() |
c43e25296d | ||
![]() |
b05125dd16 | ||
![]() |
ef332af17e | ||
![]() |
733688bd7e | ||
![]() |
983db9c050 | ||
![]() |
d4dc176870 | ||
![]() |
b6277f1e96 | ||
![]() |
a732f1534d | ||
![]() |
618ff0ce19 | ||
![]() |
881af64495 | ||
![]() |
1f9b8ba92a | ||
![]() |
ba6011bd89 | ||
![]() |
74b68a5e15 | ||
![]() |
b906a65385 | ||
![]() |
73573793a0 | ||
![]() |
c508e0035e | ||
![]() |
4f7e37d348 | ||
![]() |
80ff8a517c | ||
![]() |
0de3c2ed48 | ||
![]() |
4c5c99d05d | ||
![]() |
c9885c13ec | ||
![]() |
771f92eaab | ||
![]() |
dd6ff7f48f | ||
![]() |
913d128e16 | ||
![]() |
20850033df | ||
![]() |
743513cf84 | ||
![]() |
3a6112532e | ||
![]() |
460efa42c7 | ||
![]() |
e741573348 | ||
![]() |
e197e496b5 | ||
![]() |
8980ae4a50 | ||
![]() |
5682c81217 | ||
![]() |
7904cead76 | ||
![]() |
21579d1525 | ||
![]() |
5c8ef44070 | ||
![]() |
c1f64ff1ef | ||
![]() |
b788579457 | ||
![]() |
8288559c63 | ||
![]() |
095ddd2ebd | ||
![]() |
42c459a51d | ||
![]() |
0791287cf9 | ||
![]() |
79bf27c659 | ||
![]() |
cb1b7ceb0a | ||
![]() |
eacbcfd15d | ||
![]() |
6cd22fa004 | ||
![]() |
8785270363 | ||
![]() |
90f1fc186c | ||
![]() |
be8a8f5f6b | ||
![]() |
0bf0e1f274 | ||
![]() |
2b9383d2d5 | ||
![]() |
ffb34da3e7 | ||
![]() |
efb76ea883 | ||
![]() |
b5943b48b4 | ||
![]() |
b8a96e48a7 | ||
![]() |
99e5b9c898 | ||
![]() |
f0d98c9b6f | ||
![]() |
db375b8f47 | ||
![]() |
924f53204f | ||
![]() |
c705fc42d6 | ||
![]() |
3abf83a5e9 | ||
![]() |
659a9a69ca | ||
![]() |
c79e83fdd3 | ||
![]() |
f8ad0306b0 | ||
![]() |
a89375f67b | ||
![]() |
5b4ecb9fc2 | ||
![]() |
67ffa89c47 | ||
![]() |
166a163c24 | ||
![]() |
98d75798a4 | ||
![]() |
aec7d9f25b | ||
![]() |
36208d1303 | ||
![]() |
0ce709ee68 | ||
![]() |
1e9d393edf | ||
![]() |
cbf0db5342 | ||
![]() |
c1ef672943 | ||
![]() |
55005d3fa4 | ||
![]() |
5c9510993c | ||
![]() |
b8109edd62 | ||
![]() |
853dcecfb8 | ||
![]() |
0b664431af | ||
![]() |
f2e4a005a2 | ||
![]() |
5282377350 | ||
![]() |
42ef35b2bc | ||
![]() |
aac41a76cf | ||
![]() |
d8b6e65f6f | ||
![]() |
c63a25b18e | ||
![]() |
d6be2e8651 | ||
![]() |
870a36180c | ||
![]() |
51058daf27 | ||
![]() |
32d7fca27a | ||
![]() |
6cd41be3b8 | ||
![]() |
e79c045ac0 | ||
![]() |
7fcbeb20e8 | ||
![]() |
724fc3c6f0 | ||
![]() |
b6cdc90d57 | ||
![]() |
d15277849b | ||
![]() |
9a433b50ff | ||
![]() |
da1fb5506b | ||
![]() |
bdad05b36f | ||
![]() |
d51dcca87c | ||
![]() |
7f1d1ed7de | ||
![]() |
eb2d9b2fde | ||
![]() |
e77821eef0 | ||
![]() |
8c937a55c0 | ||
![]() |
c0f8dd078b | ||
![]() |
2ff79d93b9 | ||
![]() |
8e03b28151 | ||
![]() |
6c8d12394c | ||
![]() |
2b77e4a90d | ||
![]() |
d69a02e6cc | ||
![]() |
b9f239e3a9 | ||
![]() |
a24587d101 | ||
![]() |
39c11c6986 | ||
![]() |
9173b9db60 | ||
![]() |
ba3069215a | ||
![]() |
bf9208169e | ||
![]() |
2fbf33b641 | ||
![]() |
5363ae1ac1 | ||
![]() |
49e2a5b1a8 | ||
![]() |
3b550b591b | ||
![]() |
804930fbe2 | ||
![]() |
675f3910cc | ||
![]() |
3c2f26f6c1 | ||
![]() |
32a91f64d9 | ||
![]() |
22d5643d8b | ||
![]() |
b9b5b50999 | ||
![]() |
fd0c8f409e | ||
![]() |
833637c215 | ||
![]() |
bb3cc1c619 | ||
![]() |
26d496d55c | ||
![]() |
f3a40a5ef9 | ||
![]() |
c8c8d1e1ff | ||
![]() |
917e5fe5bc | ||
![]() |
5fd692ce1b | ||
![]() |
37ebe259f3 | ||
![]() |
1e341aa789 | ||
![]() |
c0ed90368c | ||
![]() |
aa7ceec15a | ||
![]() |
662f119434 | ||
![]() |
8498504b93 | ||
![]() |
788a326592 | ||
![]() |
23e8e03185 | ||
![]() |
6b3c6c2dda | ||
![]() |
d5e5a4a550 | ||
![]() |
0f80b6533b | ||
![]() |
342a62081c | ||
![]() |
e94454be97 | ||
![]() |
2e979afd22 | ||
![]() |
2f84d0294b | ||
![]() |
1e651c2d47 | ||
![]() |
34ca590fa5 | ||
![]() |
bfb84c86df | ||
![]() |
55e77d1edb | ||
![]() |
d3ef3d3079 | ||
![]() |
9e2928f653 | ||
![]() |
2b7ed5c258 | ||
![]() |
58b8ba3aa7 | ||
![]() |
618a7f4dcd | ||
![]() |
de1cd8619f | ||
![]() |
7bbce89b40 | ||
![]() |
d32566beda | ||
![]() |
d435d8eeb1 | ||
![]() |
b023a5a03c | ||
![]() |
64f3af7a1b | ||
![]() |
a754a20ec6 | ||
![]() |
3d206165b2 | ||
![]() |
edb77b4a09 | ||
![]() |
393a4a3d40 | ||
![]() |
a75387602b | ||
![]() |
34be1179ab | ||
![]() |
63de6139a3 | ||
![]() |
7480a19a65 | ||
![]() |
1522c36ed0 | ||
![]() |
95b704f889 | ||
![]() |
b8904f5fe1 | ||
![]() |
bcbd8dbd93 | ||
![]() |
572a10db6b | ||
![]() |
7e391f0411 | ||
![]() |
e65b5e2f1c | ||
![]() |
8a4f811575 | ||
![]() |
cc745b4225 | ||
![]() |
44f393d65a | ||
![]() |
b83e87dd4e | ||
![]() |
7278b1515c | ||
![]() |
5448f2c2af | ||
![]() |
400ef9a433 | ||
![]() |
83471ecd50 | ||
![]() |
aa78a2991c | ||
![]() |
aaa166593e | ||
![]() |
9bb8fc1d1c | ||
![]() |
e307c5de9f | ||
![]() |
f9d55dc3e8 | ||
![]() |
1f4248ccd5 | ||
![]() |
63366e6ca2 | ||
![]() |
a34e7428c3 | ||
![]() |
da5d540668 | ||
![]() |
2ad87c50d1 | ||
![]() |
ad39ad323e | ||
![]() |
107199eb8e | ||
![]() |
fd00b55465 | ||
![]() |
0d09dbd498 | ||
![]() |
3bc5ee0253 | ||
![]() |
7a193cc9a2 | ||
![]() |
5c0222747e | ||
![]() |
21241571f7 | ||
![]() |
0812377b58 | ||
![]() |
8cd07de4fc | ||
![]() |
c2f57b448d | ||
![]() |
13dac1af0e | ||
![]() |
533b1f5e40 | ||
![]() |
afbd31a138 | ||
![]() |
9cc20c7417 | ||
![]() |
e7e67c779e | ||
![]() |
cb84bc758e | ||
![]() |
1507bbf733 | ||
![]() |
21c26897bc | ||
![]() |
4c68280108 | ||
![]() |
56d0522f89 | ||
![]() |
abe1d5c6c7 | ||
![]() |
78a3a93f24 | ||
![]() |
307d3f2e53 | ||
![]() |
ccac67d238 | ||
![]() |
291f2997ed | ||
![]() |
91dc50a0a3 | ||
![]() |
809072e86a | ||
![]() |
eb57b28b52 | ||
![]() |
15e4200f7e | ||
![]() |
6f9e5b8c50 | ||
![]() |
c0907c1987 | ||
![]() |
f9e402b69f | ||
![]() |
2ca7b72ac2 | ||
![]() |
8d6f156db1 | ||
![]() |
2c0afaabec | ||
![]() |
5b607ccb86 | ||
![]() |
0146a11953 | ||
![]() |
3757a414bc | ||
![]() |
6a91a9c3e1 | ||
![]() |
be6ea812bb | ||
![]() |
e182378572 | ||
![]() |
d3a78b8ad5 | ||
![]() |
7fd7eb31cb | ||
![]() |
c538bbbe46 | ||
![]() |
d390920cca | ||
![]() |
fbcb6f7d78 | ||
![]() |
bc9f02f49e | ||
![]() |
aefc4e1310 | ||
![]() |
9509d6d901 | ||
![]() |
f3e2a7a6f1 | ||
![]() |
192b9aa29f | ||
![]() |
e7c56dbb12 | ||
![]() |
a041368019 | ||
![]() |
be488c04bb | ||
![]() |
c628d4320b | ||
![]() |
4e38d2b145 | ||
![]() |
9699491269 | ||
![]() |
c19d12c5c0 | ||
![]() |
02ee1eca87 | ||
![]() |
bc62764345 | ||
![]() |
d7afaf9cb8 | ||
![]() |
fc037d79ca | ||
![]() |
c414b8cebc | ||
![]() |
617717069b | ||
![]() |
9ceb358080 | ||
![]() |
ce0194108f | ||
![]() |
96774971de | ||
![]() |
93d2f5d8ac | ||
![]() |
f97a87702c | ||
![]() |
61d64d5e5e | ||
![]() |
be7f21eb32 | ||
![]() |
361bb7f7dc | ||
![]() |
19fa36f243 | ||
![]() |
9f50b4997c | ||
![]() |
a2a241b906 | ||
![]() |
550eb6c7ab | ||
![]() |
459da211be | ||
![]() |
da2cba5827 | ||
![]() |
9b56911c43 | ||
![]() |
f228a93889 | ||
![]() |
5add939a1d | ||
![]() |
fa1b7d11ad | ||
![]() |
0506c5596a | ||
![]() |
d806f4da95 | ||
![]() |
ffe5922990 | ||
![]() |
e314a70957 | ||
![]() |
f49793ec04 | ||
![]() |
c250408382 | ||
![]() |
2671a4cb83 | ||
![]() |
d364804c42 | ||
![]() |
d51af2739e | ||
![]() |
014fe1b733 | ||
![]() |
6784ac39f6 | ||
![]() |
01f3cbcaea | ||
![]() |
7a14488a42 | ||
![]() |
0fa44f4b74 | ||
![]() |
beae47ef19 | ||
![]() |
3a9d4fad95 | ||
![]() |
743bbb6afe | ||
![]() |
ffa61438f9 | ||
![]() |
404886e247 | ||
![]() |
99aee55efa | ||
![]() |
728bce56a4 | ||
![]() |
5dd7efcbab | ||
![]() |
004f95b6dc | ||
![]() |
88ffc37894 | ||
![]() |
f828215f48 | ||
![]() |
6480b75aed | ||
![]() |
4fe82112d6 | ||
![]() |
71ffeac80e | ||
![]() |
7830d1c632 | ||
![]() |
ef7f6d1306 | ||
![]() |
8e5bfc023e | ||
![]() |
b80334f3f1 | ||
![]() |
32676fc500 | ||
![]() |
a9162eb920 | ||
![]() |
dd4ff5e40c | ||
![]() |
cc08a2829e | ||
![]() |
e3172fd6e8 | ||
![]() |
8924b2c92c | ||
![]() |
b3f010974d | ||
![]() |
5379b14c2c | ||
![]() |
88c324cb5a | ||
![]() |
155db64c63 | ||
![]() |
2ef9c1db18 | ||
![]() |
61650df492 | ||
![]() |
1e47da0a51 | ||
![]() |
ef7bd1df57 | ||
![]() |
f8297218ad | ||
![]() |
3f340c18f2 | ||
![]() |
042a0dd389 | ||
![]() |
c736ebaebf | ||
![]() |
708375593d | ||
![]() |
0115c5217b | ||
![]() |
a14cc6fde2 | ||
![]() |
22ac66787a | ||
![]() |
99c41b9e80 | ||
![]() |
bb51609af9 | ||
![]() |
b5911b8536 | ||
![]() |
245977a432 | ||
![]() |
ec6327edc2 | ||
![]() |
cad76ff378 | ||
![]() |
59d46ffbdf | ||
![]() |
5488d0f83a | ||
![]() |
48efccb53b | ||
![]() |
bfdda43a48 | ||
![]() |
d78019cd39 | ||
![]() |
563956c5df | ||
![]() |
8e4f1993bf | ||
![]() |
8f7389c83a | ||
![]() |
49f470c07b | ||
![]() |
c1d870c32d | ||
![]() |
1b460fe1d3 | ||
![]() |
47c8ca7342 | ||
![]() |
ae89571519 | ||
![]() |
26d45fd5e7 | ||
![]() |
3eb3afcc96 | ||
![]() |
5476062648 | ||
![]() |
0e178ce1bc | ||
![]() |
808d4e925a | ||
![]() |
a47d7dd846 | ||
![]() |
621a874b29 | ||
![]() |
9093ef4ae9 | ||
![]() |
29d8dd68f4 | ||
![]() |
22c5976095 | ||
![]() |
e68e605211 | ||
![]() |
0ed7f2e0aa | ||
![]() |
712aac911c | ||
![]() |
bf6c4f401e | ||
![]() |
6740e379f1 | ||
![]() |
ddca6701ac | ||
![]() |
06f1cc2d82 | ||
![]() |
2b17bb8f1e | ||
![]() |
45887154b6 | ||
![]() |
8a6fd925b4 | ||
![]() |
6d3ef58ecc | ||
![]() |
bcf2560ee7 | ||
![]() |
53c67ac878 | ||
![]() |
30b7e8eb39 | ||
![]() |
8fad82c549 | ||
![]() |
27986f8c24 | ||
![]() |
c8235ddb6b | ||
![]() |
d7c3a20f2a | ||
![]() |
392d0e366d | ||
![]() |
249fa8e7f4 | ||
![]() |
28622a82dd | ||
![]() |
41eebbe661 | ||
![]() |
9c0fce3a7a | ||
![]() |
c1e6976502 | ||
![]() |
dac852481e | ||
![]() |
2ce744280b | ||
![]() |
2830f208ce | ||
![]() |
eb7713a702 | ||
![]() |
72885d896f | ||
![]() |
e75cb5f64a | ||
![]() |
c4a0bbdf1f | ||
![]() |
bb57992d63 | ||
![]() |
50accfffb0 | ||
![]() |
75bdf3de79 | ||
![]() |
adbad6907d | ||
![]() |
108f6e60f9 | ||
![]() |
46a0ea10f4 | ||
![]() |
25133935ef | ||
![]() |
ceec5661fe | ||
![]() |
c1f109b5ce | ||
![]() |
521e4ddb8d | ||
![]() |
c5110f89bc | ||
![]() |
59104c4fbe | ||
![]() |
41ad172796 | ||
![]() |
716cea376d | ||
![]() |
9600bacf46 | ||
![]() |
304b6226ab | ||
![]() |
26f821e865 | ||
![]() |
1697d1396e | ||
![]() |
55431f1e06 | ||
![]() |
075cd578a7 | ||
![]() |
8d7d65958d | ||
![]() |
15acb47afa | ||
![]() |
e84589a5bc | ||
![]() |
e19d0a0334 | ||
![]() |
f00664b281 | ||
![]() |
4a8f6c357f | ||
![]() |
a79b4d1bdb | ||
![]() |
c50e6c224d | ||
![]() |
f4364c2d04 | ||
![]() |
97dc241c52 | ||
![]() |
90e469ee16 | ||
![]() |
0016348c47 | ||
![]() |
b21f76456d | ||
![]() |
3ecb3c2209 | ||
![]() |
577a83d2c4 | ||
![]() |
3a4a10e453 | ||
![]() |
d4ed4ca883 | ||
![]() |
4affbd8d57 | ||
![]() |
29a870b13f | ||
![]() |
d8b498bd02 | ||
![]() |
b439c3bfcc | ||
![]() |
0768bfbac1 | ||
![]() |
9335c4ebea | ||
![]() |
8cc7cd1e99 | ||
![]() |
027b281d7a | ||
![]() |
2ffb723202 | ||
![]() |
1f26ca9566 | ||
![]() |
7046e0ec48 | ||
![]() |
0700029718 | ||
![]() |
3bbd2d9d64 | ||
![]() |
8b0c0348b2 | ||
![]() |
59f03817b3 | ||
![]() |
503cfd92f6 | ||
![]() |
bc705df144 | ||
![]() |
a0e2ac3d12 | ||
![]() |
852da27c2c | ||
![]() |
ccf6c8f36e | ||
![]() |
8c865b758e | ||
![]() |
53042179f0 | ||
![]() |
fe0e414343 | ||
![]() |
22d2c9ed66 | ||
![]() |
1f808f0096 | ||
![]() |
fb2d22d7a2 | ||
![]() |
3cbebdb785 | ||
![]() |
b1bd5c7c85 | ||
![]() |
fffc81389b | ||
![]() |
9906d5fec0 | ||
![]() |
8663ded836 | ||
![]() |
106a73f0d3 | ||
![]() |
ba40cfe75a | ||
![]() |
ad32dbb864 | ||
![]() |
d1399f106e | ||
![]() |
6b8eaa4b75 | ||
![]() |
794aa33c8f | ||
![]() |
cb91ccbc33 | ||
![]() |
1f1c55d9dc | ||
![]() |
34c33d7cf9 | ||
![]() |
0b543ac4de | ||
![]() |
aeba6cb222 | ||
![]() |
e583ce2a91 | ||
![]() |
7924db512b | ||
![]() |
b149cf2d77 | ||
![]() |
8d953f8488 | ||
![]() |
40881cf550 | ||
![]() |
bcbd64ad77 | ||
![]() |
395c4aa69d | ||
![]() |
3cfde7bf66 | ||
![]() |
00d77175c8 | ||
![]() |
951e1d98ae | ||
![]() |
2a74326f80 | ||
![]() |
0aa83f7644 | ||
![]() |
52ab145f46 | ||
![]() |
23573fb2e8 | ||
![]() |
3a3ebd4c4c | ||
![]() |
cba0ad6403 | ||
![]() |
27c247d46c | ||
![]() |
d7c155508f | ||
![]() |
3f4d67e932 | ||
![]() |
0b13ea1a72 | ||
![]() |
0bbd505083 | ||
![]() |
e22d06376f | ||
![]() |
b308ee1b56 | ||
![]() |
b66c5030f4 | ||
![]() |
79a69e6b31 | ||
![]() |
dc00fd4c75 | ||
![]() |
7fa87c3418 | ||
![]() |
4a6c896eea | ||
![]() |
63d98e5f9f | ||
![]() |
2830da9557 | ||
![]() |
fba1dfb689 | ||
![]() |
736802cdbd | ||
![]() |
b2586d31ec | ||
![]() |
2e7fb7dda6 | ||
![]() |
143b7c3e0d | ||
![]() |
a698610043 | ||
![]() |
f4b8d4fe41 | ||
![]() |
78c8c38ad4 | ||
![]() |
5bf1d870c2 | ||
![]() |
797e1882b8 | ||
![]() |
90fb99be9a | ||
![]() |
0abb53928d | ||
![]() |
079b2a772a | ||
![]() |
1919f251a1 | ||
![]() |
28cc54e3f8 | ||
![]() |
15ab8216ec | ||
![]() |
8c23c47645 | ||
![]() |
e4f483c308 | ||
![]() |
57ff37ec7f | ||
![]() |
e4d099df43 | ||
![]() |
d7f64d6139 | ||
![]() |
7777fa6229 | ||
![]() |
5290ca8756 | ||
![]() |
e6299030f0 | ||
![]() |
ffec5b7f74 | ||
![]() |
ac716c4b99 | ||
![]() |
0fbd48ede9 | ||
![]() |
94b2f53220 | ||
![]() |
3d7fcef079 | ||
![]() |
ccdba9b307 | ||
![]() |
c98ffd21ea | ||
![]() |
67d6c6fcbb | ||
![]() |
4e5bf6e73d | ||
![]() |
f991b4edb2 | ||
![]() |
6c720b8031 | ||
![]() |
232baf5973 | ||
![]() |
4ca2e9a4b1 | ||
![]() |
9019b80993 | ||
![]() |
8bfd6ca031 | ||
![]() |
7c5d31fbbd | ||
![]() |
47581db60a | ||
![]() |
d91d12ed0d | ||
![]() |
5f90b29d96 | ||
![]() |
8a668a5073 | ||
![]() |
16884682d8 | ||
![]() |
eca9088337 | ||
![]() |
2f4a667119 | ||
![]() |
74a6e3ce6a | ||
![]() |
6bbc7f96c2 | ||
![]() |
42cc68f8b7 | ||
![]() |
3fa72ebcaa | ||
![]() |
56e7d4b8c6 | ||
![]() |
d98d3fe0de | ||
![]() |
8c14193f32 | ||
![]() |
d156e01517 | ||
![]() |
ed6cb04de4 | ||
![]() |
2ac493cb19 | ||
![]() |
1de409d6a4 | ||
![]() |
acb842edf0 | ||
![]() |
2ceaba742f | ||
![]() |
239888dc9e | ||
![]() |
d9df7336df | ||
![]() |
db3d760f2b | ||
![]() |
04c8cb7590 | ||
![]() |
8a30748b41 | ||
![]() |
1ec61e4aec | ||
![]() |
944f605671 | ||
![]() |
4cfff33301 | ||
![]() |
6a78c1bcf8 | ||
![]() |
36e5a75325 | ||
![]() |
78ce20ab8f | ||
![]() |
89c9e9aba3 | ||
![]() |
973ceb3b4b | ||
![]() |
fedb2fce12 | ||
![]() |
4fdd3b84cb | ||
![]() |
f94820d70e | ||
![]() |
132e68a9f4 | ||
![]() |
c34f75b443 | ||
![]() |
9649b5fb68 | ||
![]() |
32b2e181bd | ||
![]() |
0e6dae2b61 | ||
![]() |
8d3b265038 | ||
![]() |
70b456dbfc | ||
![]() |
e61de0682f | ||
![]() |
0903ca5286 | ||
![]() |
001ba43404 | ||
![]() |
bcb96c522c | ||
![]() |
d75ab378ac | ||
![]() |
9310d6e649 | ||
![]() |
532d8fb9a3 | ||
![]() |
b81cc103ff | ||
![]() |
a95bf63ad0 | ||
![]() |
016bf4b368 | ||
![]() |
f65930276e | ||
![]() |
98bdc59c28 | ||
![]() |
fd610dbf4d | ||
![]() |
710d893187 | ||
![]() |
1abcc027a5 | ||
![]() |
746d0201e8 | ||
![]() |
b7c46ffe29 | ||
![]() |
d7388bcfd8 | ||
![]() |
1f01cbaa93 | ||
![]() |
0d73b6d164 | ||
![]() |
6a7691000b | ||
![]() |
aab544e723 | ||
![]() |
9196610170 | ||
![]() |
e321bbf9ab | ||
![]() |
6b2efd2b16 | ||
![]() |
539ae50214 | ||
![]() |
768c7f8b28 | ||
![]() |
8666ae74f6 | ||
![]() |
6eca73bdec | ||
![]() |
0c9dc4286c | ||
![]() |
6d1674cc8a | ||
![]() |
7e7ad9a4f3 | ||
![]() |
4f6b579204 | ||
![]() |
2835597073 | ||
![]() |
216eb40ab5 | ||
![]() |
3b618ff66f | ||
![]() |
c7663dc311 | ||
![]() |
e7f6321f01 | ||
![]() |
3b00d01466 | ||
![]() |
565c4106f9 | ||
![]() |
99aef98443 | ||
![]() |
f154fb60e0 | ||
![]() |
504604cfc7 | ||
![]() |
522816c29c | ||
![]() |
e76514ca39 | ||
![]() |
cff29539fb | ||
![]() |
6f86824f23 | ||
![]() |
9fd34034b9 | ||
![]() |
ffaebabd3b | ||
![]() |
d2a4a55507 | ||
![]() |
12ab877ae4 | ||
![]() |
0550cd55d1 | ||
![]() |
bb64058f63 | ||
![]() |
13db7c8c19 | ||
![]() |
fc059520a0 | ||
![]() |
c1e8bbbeae | ||
![]() |
a2515c11e1 | ||
![]() |
50d8719c41 | ||
![]() |
3c8c425843 | ||
![]() |
2fe5587e02 | ||
![]() |
db25e27bff | ||
![]() |
3452680b47 | ||
![]() |
ba268d4f28 | ||
![]() |
9646088931 | ||
![]() |
72aeeff5ca | ||
![]() |
e594cabe4a | ||
![]() |
f055d1676e | ||
![]() |
c43fdb9754 | ||
![]() |
3476fe3f0f | ||
![]() |
15fb55ccb5 | ||
![]() |
a34eb4682b | ||
![]() |
ebbae16fb3 | ||
![]() |
39e0f1a65b | ||
![]() |
f52dd92fbc | ||
![]() |
e2c1319011 | ||
![]() |
1c63fb2b49 | ||
![]() |
0e6c3a3a9d | ||
![]() |
15dfa3cd82 | ||
![]() |
8f2ef71e3c | ||
![]() |
f04f4b4f34 | ||
![]() |
f46767be89 | ||
![]() |
6a1bbc8afc | ||
![]() |
21e78e33e5 | ||
![]() |
d246965cfb | ||
![]() |
66555c9fc1 | ||
![]() |
f9328da826 | ||
![]() |
f75854b77b | ||
![]() |
d59e8becc3 | ||
![]() |
f652490c1c | ||
![]() |
0fe6d4536c | ||
![]() |
e86afacff1 | ||
![]() |
c43970ffd4 | ||
![]() |
1c2dcab82d | ||
![]() |
c9b48ccb65 | ||
![]() |
099582ee96 | ||
![]() |
f43317c2ee | ||
![]() |
9722164705 | ||
![]() |
b3808ba6a1 | ||
![]() |
a818727ed7 | ||
![]() |
ef18308893 | ||
![]() |
4fbf99a451 | ||
![]() |
7a5381aa0f | ||
![]() |
7d88897afb | ||
![]() |
4c46d9db2b | ||
![]() |
e02bb75075 | ||
![]() |
c1a0b21e2a | ||
![]() |
982f7bcd17 | ||
![]() |
9f44cecdad | ||
![]() |
0592e9a32d | ||
![]() |
ff6cde76df | ||
![]() |
c2b5ce191a | ||
![]() |
e7f0921e6e | ||
![]() |
9801ae3414 | ||
![]() |
b6ae15fbd2 | ||
![]() |
21fcc9dde8 | ||
![]() |
c112a036df | ||
![]() |
b4b17136a6 | ||
![]() |
fe5b7663e9 | ||
![]() |
e521f2d579 | ||
![]() |
9ae38f4a6c | ||
![]() |
5865b83697 | ||
![]() |
a4d91c37a7 | ||
![]() |
b8016d7fae | ||
![]() |
7abdceb095 | ||
![]() |
32ae686e36 | ||
![]() |
7529fcde35 | ||
![]() |
b7d01127a5 | ||
![]() |
dd69bc65ab | ||
![]() |
a1578303fa | ||
![]() |
6594a3877f | ||
![]() |
b4693b9bc4 | ||
![]() |
8f4379f3b4 | ||
![]() |
2f022f505d | ||
![]() |
71e40fdb6e | ||
![]() |
b820794790 | ||
![]() |
c7321f9a22 | ||
![]() |
0afa7c5bab | ||
![]() |
4ace07156f | ||
![]() |
d407397775 | ||
![]() |
df46bcd16f | ||
![]() |
bbb8d708b9 | ||
![]() |
f672e12433 | ||
![]() |
3e11162d0e | ||
![]() |
d426f5f179 | ||
![]() |
a952438247 | ||
![]() |
662f846caa | ||
![]() |
7d0b9ec8a2 | ||
![]() |
34b73cc4f7 | ||
![]() |
44fc86c9fe | ||
![]() |
ddcb1ecdf2 | ||
![]() |
b6904a4df9 | ||
![]() |
13cfd71686 | ||
![]() |
b1c3967a5b | ||
![]() |
5acc29e498 | ||
![]() |
e2614b9dad | ||
![]() |
4c27a11747 | ||
![]() |
64231683b3 | ||
![]() |
549529c889 | ||
![]() |
31fd89a9fc | ||
![]() |
d3b5e09e2a | ||
![]() |
9be1733bc8 | ||
![]() |
a3892dc7e7 | ||
![]() |
d8ec0fe9f7 | ||
![]() |
01b200279b | ||
![]() |
f74716a459 | ||
![]() |
b64b7c2607 | ||
![]() |
2bf880b216 | ||
![]() |
efe51aa340 | ||
![]() |
ed806dc3bf | ||
![]() |
5b1c9286ed | ||
![]() |
a6eeceeb67 | ||
![]() |
10622008c4 | ||
![]() |
d955fe7ca3 | ||
![]() |
35584d44aa | ||
![]() |
86c2f389d5 | ||
![]() |
bc999d0b5f | ||
![]() |
0dba48be13 | ||
![]() |
18dd2ad884 | ||
![]() |
46bb3e109c | ||
![]() |
33488ccf40 | ||
![]() |
fffc43f443 | ||
![]() |
eb9d72ad72 | ||
![]() |
71f5b46639 | ||
![]() |
c1555326fa | ||
![]() |
46d340a5ce | ||
![]() |
768de1a1e7 | ||
![]() |
77f9b27fcf | ||
![]() |
11ecfd370c | ||
![]() |
ad5a76aedc | ||
![]() |
03fd57e426 | ||
![]() |
47ed6bedb4 | ||
![]() |
ed2c774d86 | ||
![]() |
b7b949b415 | ||
![]() |
c8de6ccb4c | ||
![]() |
e99086c228 | ||
![]() |
ddccae60b5 | ||
![]() |
dcb80b7c9c | ||
![]() |
ab3d53c19f | ||
![]() |
8d8b24f00a | ||
![]() |
dbbb19e37f | ||
![]() |
99af22b621 | ||
![]() |
5e17a4e958 | ||
![]() |
714624147c | ||
![]() |
7a567a47b9 | ||
![]() |
6df1ee64f2 | ||
![]() |
7218c450e6 | ||
![]() |
27bce0e5bb | ||
![]() |
6752acbf12 | ||
![]() |
e6040303df | ||
![]() |
787358d1b0 | ||
![]() |
b6dc7af13e | ||
![]() |
7c23e2ef82 | ||
![]() |
5e8813f5b6 | ||
![]() |
893a306da0 | ||
![]() |
7990510099 | ||
![]() |
99a2cf926a | ||
![]() |
ef720170cd | ||
![]() |
75c0bf7b61 | ||
![]() |
55216ad5ed | ||
![]() |
a5a91d5139 | ||
![]() |
d982fe59b4 | ||
![]() |
ffff337aba | ||
![]() |
31442f36a3 | ||
![]() |
597e5396f9 | ||
![]() |
d2cc184763 | ||
![]() |
466aaaa0d5 | ||
![]() |
d926a24088 | ||
![]() |
10c342483a | ||
![]() |
ab3fa88913 | ||
![]() |
624f39df1e | ||
![]() |
be289617e1 | ||
![]() |
2d633d79dc | ||
![]() |
8fedb2c177 | ||
![]() |
0d031d0b25 | ||
![]() |
9f1a017b56 | ||
![]() |
890731e3e2 | ||
![]() |
5df2f6f8d5 | ||
![]() |
cefb73c11e | ||
![]() |
2579577225 | ||
![]() |
23586810b3 | ||
![]() |
9363b0fb35 | ||
![]() |
df957fc31b | ||
![]() |
49a3a9a0f2 | ||
![]() |
8ce6a043bb | ||
![]() |
3befdb9f48 | ||
![]() |
f7bea3eaa9 | ||
![]() |
d049caa8c0 | ||
![]() |
0b99ce3211 | ||
![]() |
b0ee227918 | ||
![]() |
8d66f68dfa | ||
![]() |
600fbc6df7 | ||
![]() |
ebf900686b | ||
![]() |
dc9e93a907 | ||
![]() |
4ed748381a | ||
![]() |
08b1025eb3 | ||
![]() |
92e9f79f09 | ||
![]() |
f9c8ac6568 | ||
![]() |
a323aefc22 | ||
![]() |
ce7a5b1499 | ||
![]() |
9aea3e0bbf | ||
![]() |
76ddce7239 | ||
![]() |
8e0fd14b4e | ||
![]() |
d3c3624816 | ||
![]() |
7d5493fcf4 | ||
![]() |
cb88885870 | ||
![]() |
81c4403ee6 | ||
![]() |
25a51c9764 | ||
![]() |
2151d1af58 | ||
![]() |
826875e207 | ||
![]() |
29f342380d | ||
![]() |
df87c892f0 | ||
![]() |
c332db4a32 | ||
![]() |
5dfa82322e | ||
![]() |
2503b39897 | ||
![]() |
d5c841253c | ||
![]() |
1e2f51ef3e | ||
![]() |
9b2e533038 | ||
![]() |
dd6ec04801 | ||
![]() |
9ea67b1b36 | ||
![]() |
04548237fd | ||
![]() |
fc6678515a | ||
![]() |
72a5e4b0c7 | ||
![]() |
816af086a4 | ||
![]() |
0ac2fc2168 | ||
![]() |
119af38d6a | ||
![]() |
859ff4a2f5 | ||
![]() |
931560ee26 | ||
![]() |
14b1fd463d | ||
![]() |
2bc1217dff |
@@ -2,10 +2,16 @@
|
|||||||
# For additional information regarding the format and rule options, please see:
|
# For additional information regarding the format and rule options, please see:
|
||||||
# https://github.com/browserslist/browserslist#queries
|
# https://github.com/browserslist/browserslist#queries
|
||||||
|
|
||||||
|
# For the full list of supported browsers by the Angular framework, please see:
|
||||||
|
# https://angular.io/guide/browser-support
|
||||||
|
|
||||||
# You can see what browsers were selected by your queries by running:
|
# You can see what browsers were selected by your queries by running:
|
||||||
# npx browserslist
|
# npx browserslist
|
||||||
|
|
||||||
> 0.5%
|
last 1 Chrome version
|
||||||
last 2 versions
|
last 1 Firefox version
|
||||||
|
last 2 Edge major versions
|
||||||
|
last 2 Safari major versions
|
||||||
|
last 2 iOS major versions
|
||||||
Firefox ESR
|
Firefox ESR
|
||||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
not IE 11 # Angular supports IE 11 only as an opt-in. To opt-in, remove the 'not' prefix on this line.
|
||||||
|
222
.eslintrc.json
Normal file
222
.eslintrc.json
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
{
|
||||||
|
"root": true,
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"@angular-eslint/eslint-plugin",
|
||||||
|
"eslint-plugin-import",
|
||||||
|
"eslint-plugin-jsdoc",
|
||||||
|
"eslint-plugin-deprecation",
|
||||||
|
"eslint-plugin-unused-imports"
|
||||||
|
],
|
||||||
|
"overrides": [
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.ts"
|
||||||
|
],
|
||||||
|
"parserOptions": {
|
||||||
|
"project": [
|
||||||
|
"./tsconfig.json",
|
||||||
|
"./cypress/tsconfig.json"
|
||||||
|
],
|
||||||
|
"createDefaultProgram": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
|
"plugin:@angular-eslint/recommended",
|
||||||
|
"plugin:@angular-eslint/template/process-inline-templates"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"max-classes-per-file": [
|
||||||
|
"error",
|
||||||
|
1
|
||||||
|
],
|
||||||
|
"comma-dangle": [
|
||||||
|
"off",
|
||||||
|
"always-multiline"
|
||||||
|
],
|
||||||
|
"eol-last": [
|
||||||
|
"error",
|
||||||
|
"always"
|
||||||
|
],
|
||||||
|
"no-console": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allow": [
|
||||||
|
"log",
|
||||||
|
"warn",
|
||||||
|
"dir",
|
||||||
|
"timeLog",
|
||||||
|
"assert",
|
||||||
|
"clear",
|
||||||
|
"count",
|
||||||
|
"countReset",
|
||||||
|
"group",
|
||||||
|
"groupEnd",
|
||||||
|
"table",
|
||||||
|
"debug",
|
||||||
|
"info",
|
||||||
|
"dirxml",
|
||||||
|
"error",
|
||||||
|
"groupCollapsed",
|
||||||
|
"Console",
|
||||||
|
"profile",
|
||||||
|
"profileEnd",
|
||||||
|
"timeStamp",
|
||||||
|
"context"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"curly": "error",
|
||||||
|
"brace-style": [
|
||||||
|
"error",
|
||||||
|
"1tbs",
|
||||||
|
{
|
||||||
|
"allowSingleLine": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"eqeqeq": [
|
||||||
|
"error",
|
||||||
|
"always",
|
||||||
|
{
|
||||||
|
"null": "ignore"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"radix": "error",
|
||||||
|
"guard-for-in": "error",
|
||||||
|
"no-bitwise": "error",
|
||||||
|
"no-restricted-imports": "error",
|
||||||
|
"no-caller": "error",
|
||||||
|
"no-debugger": "error",
|
||||||
|
"no-redeclare": "error",
|
||||||
|
"no-eval": "error",
|
||||||
|
"no-fallthrough": "error",
|
||||||
|
"no-trailing-spaces": "error",
|
||||||
|
"space-infix-ops": "error",
|
||||||
|
"keyword-spacing": "error",
|
||||||
|
"no-var": "error",
|
||||||
|
"no-unused-expressions": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"allowTernary": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint)
|
||||||
|
"prefer-spread": "off",
|
||||||
|
"no-underscore-dangle": "off",
|
||||||
|
|
||||||
|
// todo: disabled rules from eslint:recommended, consider re-enabling & fixing
|
||||||
|
"no-prototype-builtins": "off",
|
||||||
|
"no-useless-escape": "off",
|
||||||
|
"no-case-declarations": "off",
|
||||||
|
"no-extra-boolean-cast": "off",
|
||||||
|
|
||||||
|
"@angular-eslint/directive-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "attribute",
|
||||||
|
"prefix": "ds",
|
||||||
|
"style": "camelCase"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@angular-eslint/component-selector": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"type": "element",
|
||||||
|
"prefix": "ds",
|
||||||
|
"style": "kebab-case"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@angular-eslint/pipe-prefix": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"prefixes": [
|
||||||
|
"ds"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@angular-eslint/no-attribute-decorator": "error",
|
||||||
|
"@angular-eslint/no-forward-ref": "error",
|
||||||
|
"@angular-eslint/no-output-native": "warn",
|
||||||
|
"@angular-eslint/no-output-on-prefix": "warn",
|
||||||
|
"@angular-eslint/no-conflicting-lifecycle": "warn",
|
||||||
|
|
||||||
|
"@typescript-eslint/no-inferrable-types":[
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"ignoreParameters": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/quotes": [
|
||||||
|
"error",
|
||||||
|
"single",
|
||||||
|
{
|
||||||
|
"avoidEscape": true,
|
||||||
|
"allowTemplateLiterals": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/semi": "error",
|
||||||
|
"@typescript-eslint/no-shadow": "error",
|
||||||
|
"@typescript-eslint/dot-notation": "error",
|
||||||
|
"@typescript-eslint/consistent-type-definitions": "error",
|
||||||
|
"@typescript-eslint/prefer-function-type": "error",
|
||||||
|
"@typescript-eslint/naming-convention": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"selector": "property",
|
||||||
|
"format": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/member-ordering": [
|
||||||
|
"error",
|
||||||
|
{
|
||||||
|
"default": [
|
||||||
|
"static-field",
|
||||||
|
"instance-field",
|
||||||
|
"static-method",
|
||||||
|
"instance-method"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"@typescript-eslint/type-annotation-spacing": "error",
|
||||||
|
"@typescript-eslint/unified-signatures": "error",
|
||||||
|
"@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable
|
||||||
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
|
"@typescript-eslint/no-misused-promises": "warn",
|
||||||
|
"@typescript-eslint/restrict-plus-operands": "warn",
|
||||||
|
"@typescript-eslint/unbound-method": "off",
|
||||||
|
"@typescript-eslint/ban-ts-comment": "off",
|
||||||
|
"@typescript-eslint/no-var-requires": "off",
|
||||||
|
"@typescript-eslint/no-unused-vars": "off",
|
||||||
|
"@typescript-eslint/no-unnecessary-type-assertion": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-assignment": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-member-access": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-call": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-argument": "off",
|
||||||
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
|
"@typescript-eslint/require-await": "off",
|
||||||
|
|
||||||
|
"deprecation/deprecation": "warn",
|
||||||
|
|
||||||
|
"import/order": "off",
|
||||||
|
"import/no-deprecated": "warn"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"files": [
|
||||||
|
"*.html"
|
||||||
|
],
|
||||||
|
"extends": [
|
||||||
|
"plugin:@angular-eslint/template/recommended"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
// todo: re-enable & fix errors
|
||||||
|
"@angular-eslint/template/no-negated-async": "off",
|
||||||
|
"@angular-eslint/template/eqeqeq": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
16
.gitattributes
vendored
Normal file
16
.gitattributes
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# By default, auto detect text files and perform LF normalization
|
||||||
|
# This ensures code is always checked in with LF line endings
|
||||||
|
* text=auto
|
||||||
|
|
||||||
|
# JS and TS files must always use LF for Angular tools to work
|
||||||
|
# Some Angular tools expect LF line endings, even on Windows.
|
||||||
|
# This ensures Windows always checks out these files with LF line endings
|
||||||
|
# We've copied many of these rules from https://github.com/angular/angular-cli/
|
||||||
|
*.js eol=lf
|
||||||
|
*.ts eol=lf
|
||||||
|
*.json eol=lf
|
||||||
|
*.json5 eol=lf
|
||||||
|
*.css eol=lf
|
||||||
|
*.scss eol=lf
|
||||||
|
*.html eol=lf
|
||||||
|
*.svg eol=lf
|
25
.github/workflows/build.yml
vendored
25
.github/workflows/build.yml
vendored
@@ -22,18 +22,18 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
# Create a matrix of Node versions to test against (in parallel)
|
# Create a matrix of Node versions to test against (in parallel)
|
||||||
matrix:
|
matrix:
|
||||||
node-version: [12.x, 14.x]
|
node-version: [14.x, 16.x]
|
||||||
# Do NOT exit immediately if one matrix job fails
|
# Do NOT exit immediately if one matrix job fails
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
# These are the actual CI steps to perform per job
|
# These are the actual CI steps to perform per job
|
||||||
steps:
|
steps:
|
||||||
# https://github.com/actions/checkout
|
# https://github.com/actions/checkout
|
||||||
- name: Checkout codebase
|
- name: Checkout codebase
|
||||||
uses: actions/checkout@v1
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
# https://github.com/actions/setup-node
|
# https://github.com/actions/setup-node
|
||||||
- name: Install Node.js ${{ matrix.node-version }}
|
- name: Install Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v1
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
|
|
||||||
@@ -70,7 +70,10 @@ jobs:
|
|||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: yarn run lint
|
run: yarn run lint --quiet
|
||||||
|
|
||||||
|
- name: Check for circular dependencies
|
||||||
|
run: yarn run check-circ-deps
|
||||||
|
|
||||||
- name: Run build
|
- name: Run build
|
||||||
run: yarn run build:prod
|
run: yarn run build:prod
|
||||||
@@ -79,11 +82,11 @@ jobs:
|
|||||||
run: yarn run test:headless
|
run: yarn run test:headless
|
||||||
|
|
||||||
# NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286
|
# NOTE: Angular CLI only supports code coverage for specs. See https://github.com/angular/angular-cli/issues/6286
|
||||||
# Upload coverage reports to Codecov (for Node v12 only)
|
# Upload coverage reports to Codecov (for one version of Node only)
|
||||||
# https://github.com/codecov/codecov-action
|
# https://github.com/codecov/codecov-action
|
||||||
- name: Upload coverage to Codecov.io
|
- name: Upload coverage to Codecov.io
|
||||||
uses: codecov/codecov-action@v1
|
uses: codecov/codecov-action@v2
|
||||||
if: matrix.node-version == '12.x'
|
if: matrix.node-version == '16.x'
|
||||||
|
|
||||||
# Using docker-compose start backend using CI configuration
|
# Using docker-compose start backend using CI configuration
|
||||||
# and load assetstore from a cached copy
|
# and load assetstore from a cached copy
|
||||||
@@ -128,6 +131,14 @@ jobs:
|
|||||||
name: e2e-test-screenshots
|
name: e2e-test-screenshots
|
||||||
path: cypress/screenshots
|
path: cypress/screenshots
|
||||||
|
|
||||||
|
- name: Stop app (in case it stays up after e2e tests)
|
||||||
|
run: |
|
||||||
|
app_pid=$(lsof -t -i:4000)
|
||||||
|
if [[ ! -z $app_pid ]]; then
|
||||||
|
echo "App was still up! (PID: $app_pid)"
|
||||||
|
kill -9 $app_pid
|
||||||
|
fi
|
||||||
|
|
||||||
# Start up the app with SSR enabled (run in background)
|
# Start up the app with SSR enabled (run in background)
|
||||||
- name: Start app in SSR (server-side rendering) mode
|
- name: Start app in SSR (server-side rendering) mode
|
||||||
run: |
|
run: |
|
||||||
|
87
.github/workflows/docker.yml
vendored
Normal file
87
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
# DSpace Docker image build for hub.docker.com
|
||||||
|
name: Docker images
|
||||||
|
|
||||||
|
# Run this Build for all pushes to 'main' or maintenance branches, or tagged releases.
|
||||||
|
# Also run for PRs to ensure PR doesn't break Docker build process
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- 'dspace-**'
|
||||||
|
tags:
|
||||||
|
- 'dspace-**'
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
docker:
|
||||||
|
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
||||||
|
if: github.repository == 'dspace/dspace-angular'
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
# Define tags to use for Docker images based on Git tags/branches (for docker/metadata-action)
|
||||||
|
# For a new commit on default branch (main), use the literal tag 'dspace-7_x' on Docker image.
|
||||||
|
# For a new commit on other branches, use the branch name as the tag for Docker image.
|
||||||
|
# For a new tag, copy that tag name as the tag for Docker image.
|
||||||
|
IMAGE_TAGS: |
|
||||||
|
type=raw,value=dspace-7_x,enable=${{ endsWith(github.ref, github.event.repository.default_branch) }}
|
||||||
|
type=ref,event=branch,enable=${{ !endsWith(github.ref, github.event.repository.default_branch) }}
|
||||||
|
type=ref,event=tag
|
||||||
|
# Define default tag "flavor" for docker/metadata-action per
|
||||||
|
# https://github.com/docker/metadata-action#flavor-input
|
||||||
|
# We turn off 'latest' tag by default.
|
||||||
|
TAGS_FLAVOR: |
|
||||||
|
latest=false
|
||||||
|
# Architectures / Platforms for which we will build Docker images
|
||||||
|
# If this is a PR, we ONLY build for AMD64. For PRs we only do a sanity check test to ensure Docker builds work.
|
||||||
|
# If this is NOT a PR (e.g. a tag or merge commit), also build for ARM64.
|
||||||
|
PLATFORMS: linux/amd64${{ github.event_name != 'pull_request' && ', linux/arm64' || '' }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# https://github.com/actions/checkout
|
||||||
|
- name: Checkout codebase
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# https://github.com/docker/setup-buildx-action
|
||||||
|
- name: Setup Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v1
|
||||||
|
|
||||||
|
# https://github.com/docker/setup-qemu-action
|
||||||
|
- name: Set up QEMU emulation to build for multiple architectures
|
||||||
|
uses: docker/setup-qemu-action@v2
|
||||||
|
|
||||||
|
# https://github.com/docker/login-action
|
||||||
|
- name: Login to DockerHub
|
||||||
|
# Only login if not a PR, as PRs only trigger a Docker build and not a push
|
||||||
|
if: github.event_name != 'pull_request'
|
||||||
|
uses: docker/login-action@v1
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_ACCESS_TOKEN }}
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Build/Push the 'dspace/dspace-angular' image
|
||||||
|
###############################################
|
||||||
|
# https://github.com/docker/metadata-action
|
||||||
|
# Get Metadata for docker_build step below
|
||||||
|
- name: Sync metadata (tags, labels) from GitHub to Docker for 'dspace-angular' image
|
||||||
|
id: meta_build
|
||||||
|
uses: docker/metadata-action@v3
|
||||||
|
with:
|
||||||
|
images: dspace/dspace-angular
|
||||||
|
tags: ${{ env.IMAGE_TAGS }}
|
||||||
|
flavor: ${{ env.TAGS_FLAVOR }}
|
||||||
|
|
||||||
|
# https://github.com/docker/build-push-action
|
||||||
|
- name: Build and push 'dspace-angular' image
|
||||||
|
id: docker_build
|
||||||
|
uses: docker/build-push-action@v2
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./Dockerfile
|
||||||
|
platforms: ${{ env.PLATFORMS }}
|
||||||
|
# For pull requests, we run the Docker build (to ensure no PR changes break the build),
|
||||||
|
# but we ONLY do an image push to DockerHub if it's NOT a PR
|
||||||
|
push: ${{ github.event_name != 'pull_request' }}
|
||||||
|
# Use tags / labels provided by 'docker/metadata-action' above
|
||||||
|
tags: ${{ steps.meta_build.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta_build.outputs.labels }}
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
/.angular/cache
|
||||||
/__build__
|
/__build__
|
||||||
/__server_build__
|
/__server_build__
|
||||||
/node_modules
|
/node_modules
|
||||||
@@ -7,10 +8,6 @@ npm-debug.log
|
|||||||
|
|
||||||
/build/
|
/build/
|
||||||
|
|
||||||
/src/environments/environment.ts
|
|
||||||
/src/environments/environment.dev.ts
|
|
||||||
/src/environments/environment.prod.ts
|
|
||||||
|
|
||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
/dist/
|
/dist/
|
||||||
@@ -40,3 +37,5 @@ package-lock.json
|
|||||||
|
|
||||||
.env
|
.env
|
||||||
/nbproject/
|
/nbproject/
|
||||||
|
|
||||||
|
junit.xml
|
||||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -3,5 +3,6 @@
|
|||||||
"i18n-ally.localesPaths": [
|
"i18n-ally.localesPaths": [
|
||||||
"src/assets/i18n",
|
"src/assets/i18n",
|
||||||
"src/app/core/locale"
|
"src/app/core/locale"
|
||||||
]
|
],
|
||||||
|
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||||
}
|
}
|
11
Dockerfile
11
Dockerfile
@@ -1,7 +1,7 @@
|
|||||||
# This image will be published as dspace/dspace-angular
|
# This image will be published as dspace/dspace-angular
|
||||||
# See https://dspace-labs.github.io/DSpace-Docker-Images/ for usage details
|
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||||
|
|
||||||
FROM node:12-alpine
|
FROM node:14-alpine
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ADD . /app/
|
ADD . /app/
|
||||||
EXPOSE 4000
|
EXPOSE 4000
|
||||||
@@ -9,4 +9,9 @@ EXPOSE 4000
|
|||||||
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
# 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
|
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||||
RUN yarn install --network-timeout 300000
|
RUN yarn install --network-timeout 300000
|
||||||
CMD yarn run start:dev
|
|
||||||
|
# On startup, run in DEVELOPMENT mode (this defaults to live reloading enabled, etc).
|
||||||
|
# Listen / accept connections from all IP addresses.
|
||||||
|
# NOTE: At this time it is only possible to run Docker container in Production mode
|
||||||
|
# if you have a public IP. See https://github.com/DSpace/dspace-angular/issues/1485
|
||||||
|
CMD yarn serve --host 0.0.0.0
|
||||||
|
18
LICENSE
18
LICENSE
@@ -1,4 +1,4 @@
|
|||||||
DSpace source code BSD License:
|
BSD 3-Clause License
|
||||||
|
|
||||||
Copyright (c) 2002-2021, LYRASIS. All rights reserved.
|
Copyright (c) 2002-2021, LYRASIS. All rights reserved.
|
||||||
|
|
||||||
@@ -13,13 +13,12 @@ notice, this list of conditions and the following disclaimer.
|
|||||||
notice, this list of conditions and the following disclaimer in the
|
notice, this list of conditions and the following disclaimer in the
|
||||||
documentation and/or other materials provided with the distribution.
|
documentation and/or other materials provided with the distribution.
|
||||||
|
|
||||||
- Neither the name DuraSpace nor the name of the DSpace Foundation
|
- Neither the name of the copyright holder nor the names of its
|
||||||
nor the names of its contributors may be used to endorse or promote
|
contributors may be used to endorse or promote products derived from
|
||||||
products derived from this software without specific prior written
|
this software without specific prior written permission.
|
||||||
permission.
|
|
||||||
|
|
||||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||||
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||||
@@ -30,10 +29,3 @@ ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
|
|||||||
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE
|
||||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||||
DAMAGE.
|
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.
|
|
||||||
|
28
NOTICE
Normal file
28
NOTICE
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
Licenses of Third-Party Libraries
|
||||||
|
=================================
|
||||||
|
|
||||||
|
DSpace uses third-party libraries which may be distributed under
|
||||||
|
different licenses than specified in our LICENSE file. 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 DSpace source code license, in order to
|
||||||
|
use this software.
|
||||||
|
|
||||||
|
Licensing Notices
|
||||||
|
=================
|
||||||
|
|
||||||
|
[July 2019] DuraSpace joined with LYRASIS (another 501(c)3 organization) in July 2019.
|
||||||
|
LYRASIS holds the copyrights of DuraSpace.
|
||||||
|
|
||||||
|
[July 2009] Fedora Commons joined with the DSpace Foundation and began operating under
|
||||||
|
the new name DuraSpace in July 2009. DuraSpace holds the copyrights of
|
||||||
|
the DSpace Foundation, Inc.
|
||||||
|
|
||||||
|
[July 2007] The DSpace Foundation, Inc. is a 501(c)3 corporation established in July 2007
|
||||||
|
with a mission to promote and advance the dspace platform enabling management,
|
||||||
|
access and preservation of digital works. The Foundation was able to transfer
|
||||||
|
the legal copyright from Hewlett-Packard Company (HP) and Massachusetts
|
||||||
|
Institute of Technology (MIT) to the DSpace Foundation in October 2007. Many
|
||||||
|
of the files in the source code may contain a copyright statement stating HP
|
||||||
|
and MIT possess the copyright, in these instances please note that the copy
|
||||||
|
right has transferred to the DSpace foundation, and subsequently to DuraSpace.
|
236
README.md
236
README.md
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
|||||||
Quick start
|
Quick start
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**Ensure you're running [Node](https://nodejs.org) `v12.x` or `v14.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) >= `v1.x`**
|
**Ensure you're running [Node](https://nodejs.org) `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# clone the repo
|
# clone the repo
|
||||||
@@ -90,61 +90,114 @@ Requirements
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
||||||
- Ensure you're running node `v12.x` or `v14.x` and yarn >= `v1.x`
|
- Ensure you're running node `v14.x` or `v16.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.
|
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.
|
||||||
|
|
||||||
Installing
|
Installing
|
||||||
----------
|
----------
|
||||||
|
|
||||||
- `yarn run global` to install the required global dependencies
|
|
||||||
- `yarn install` to install the local dependencies
|
- `yarn install` to install the local dependencies
|
||||||
|
|
||||||
### Configuring
|
### Configuring
|
||||||
|
|
||||||
Default configuration file is located in `src/environments/` folder.
|
Default runtime configuration file is located in `config/` folder. These configurations can be changed without rebuilding the distribution.
|
||||||
|
|
||||||
To change the default configuration values, create local files that override the parameters you need to change. You can use `environment.template.ts` as a starting point.
|
To override the default configuration values, create local files that override the parameters you need to change. You can use `config.example.yml` as a starting point.
|
||||||
|
|
||||||
- Create a new `environment.dev.ts` file in `src/environments/` for a `development` environment;
|
- Create a new `config.(dev or development).yml` file in `config/` for a `development` environment;
|
||||||
- Create a new `environment.prod.ts` file in `src/environments/` for a `production` environment;
|
- Create a new `config.(prod or production).yml` file in `config/` for a `production` environment;
|
||||||
|
|
||||||
The server settings can also be overwritten using an environment file.
|
The settings can also be overwritten using an environment file or environment variables.
|
||||||
|
|
||||||
This file should be called `.env` and be placed in the project root.
|
This file should be called `.env` and be placed in the project root.
|
||||||
|
|
||||||
The following settings can be overwritten in this file:
|
The following non-convention settings:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
DSPACE_HOST # The host name of the angular application
|
DSPACE_HOST # The host name of the angular application
|
||||||
DSPACE_PORT # The port number of the angular application
|
DSPACE_PORT # The port number of the angular application
|
||||||
DSPACE_NAMESPACE # The namespace of the angular application
|
DSPACE_NAMESPACE # The namespace of the angular application
|
||||||
DSPACE_SSL # Whether the angular application uses SSL [true/false]
|
DSPACE_SSL # Whether the angular application uses SSL [true/false]
|
||||||
|
```
|
||||||
|
|
||||||
DSPACE_REST_HOST # The host name of the REST application
|
All other settings can be set using the following convention for naming the environment variables:
|
||||||
DSPACE_REST_PORT # The port number of the REST application
|
|
||||||
DSPACE_REST_NAMESPACE # The namespace of the REST application
|
1. replace all `.` with `_`
|
||||||
DSPACE_REST_SSL # Whether the angular REST uses SSL [true/false]
|
2. convert all characters to upper case
|
||||||
|
3. prefix with `DSPACE_`
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# The host name of the REST application
|
||||||
|
rest.host => DSPACE_REST_HOST
|
||||||
|
|
||||||
|
# The port number of the REST application
|
||||||
|
rest.port => DSPACE_REST_PORT
|
||||||
|
|
||||||
|
# The namespace of the REST application
|
||||||
|
rest.nameSpace => DSPACE_REST_NAMESPACE
|
||||||
|
|
||||||
|
# Whether the angular REST uses SSL [true/false]
|
||||||
|
rest.ssl => DSPACE_REST_SSL
|
||||||
|
|
||||||
|
cache.msToLive.default => DSPACE_CACHE_MSTOLIVE_DEFAULT
|
||||||
|
auth.ui.timeUntilIdle => DSPACE_AUTH_UI_TIMEUNTILIDLE
|
||||||
|
```
|
||||||
|
|
||||||
|
The equavelant to the non-conventional legacy settings:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
DSPACE_UI_HOST => DSPACE_HOST
|
||||||
|
DSPACE_UI_PORT => DSPACE_PORT
|
||||||
|
DSPACE_UI_NAMESPACE => DSPACE_NAMESPACE
|
||||||
|
DSPACE_UI_SSL => DSPACE_SSL
|
||||||
```
|
```
|
||||||
|
|
||||||
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
||||||
```bash
|
```bash
|
||||||
export DSPACE_HOST=api7.dspace.org
|
export DSPACE_HOST=api7.dspace.org
|
||||||
|
export DSPACE_UI_PORT=4200
|
||||||
```
|
```
|
||||||
|
|
||||||
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides **`environment.(prod, dev or test).ts`** overrides **`environment.common.ts`**
|
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides external config set by `DSPACE_APP_CONFIG_PATH` overrides **`config.(prod or dev).yml`**
|
||||||
|
|
||||||
These configuration sources are collected **at build time**, and written to `src/environments/environment.ts`. At runtime the configuration is fixed, and neither `.env` nor the process' environment will be consulted.
|
These configuration sources are collected **at run time**, and written to `dist/browser/assets/config.json` for production and `src/app/assets/config.json` for development.
|
||||||
|
|
||||||
|
The configuration file can be externalized by using environment variable `DSPACE_APP_CONFIG_PATH`.
|
||||||
|
|
||||||
|
#### Buildtime Configuring
|
||||||
|
|
||||||
|
Buildtime configuration must defined before build in order to include in transpiled JavaScript. This is primarily for the server. These settings can be found under `src/environment/` folder.
|
||||||
|
|
||||||
|
To override the default configuration values for development, create local file that override the build time parameters you need to change.
|
||||||
|
|
||||||
|
- Create a new `environment.(dev or development).ts` file in `src/environment/` for a `development` environment;
|
||||||
|
|
||||||
|
If needing to update default configurations values for production, update local file that override the build time parameters you need to change.
|
||||||
|
|
||||||
|
- Update `environment.production.ts` file in `src/environment/` for a `production` environment;
|
||||||
|
|
||||||
|
The environment object is provided for use as import in code and is extended with the runtime configuration on bootstrap of the application.
|
||||||
|
|
||||||
|
> Take caution moving runtime configs into the buildtime configuration. They will be overwritten by what is defined in the runtime config on bootstrap.
|
||||||
|
|
||||||
#### Using environment variables in code
|
#### Using environment variables in code
|
||||||
To use environment variables in a UI component, use:
|
To use environment variables in a UI component, use:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { environment } from '../environment.ts';
|
import { AppConfig, APP_CONFIG } from 'src/config/app-config.interface';
|
||||||
|
...
|
||||||
|
constructor(@Inject(APP_CONFIG) private appConfig: AppConfig) {}
|
||||||
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
This file is generated by the script located in `scripts/set-env.ts`. This script will run automatically before every build, or can be manually triggered using the appropriate `config` script in `package.json`
|
or
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { environment } from '../environment.ts';
|
||||||
|
```
|
||||||
|
|
||||||
Running the app
|
Running the app
|
||||||
---------------
|
---------------
|
||||||
@@ -155,7 +208,7 @@ After you have installed all dependencies you can now run the app. Run `yarn run
|
|||||||
|
|
||||||
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.
|
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.
|
||||||
|
|
||||||
To build the app for production and start the server run:
|
To build the app for production and start the server (in one command) run:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
yarn start
|
yarn start
|
||||||
@@ -169,6 +222,10 @@ yarn run build:prod
|
|||||||
```
|
```
|
||||||
This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`.
|
This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`.
|
||||||
|
|
||||||
|
After building the app for production, it can be started by running:
|
||||||
|
```bash
|
||||||
|
yarn run serve:ssr
|
||||||
|
```
|
||||||
|
|
||||||
### Running the application with Docker
|
### Running the application with Docker
|
||||||
NOTE: At this time, we do not have production-ready Docker images for DSpace.
|
NOTE: At this time, we do not have production-ready Docker images for DSpace.
|
||||||
@@ -230,9 +287,29 @@ E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Con
|
|||||||
|
|
||||||
The test files can be found in the `./cypress/integration/` folder.
|
The test files can be found in the `./cypress/integration/` folder.
|
||||||
|
|
||||||
Before you can run e2e tests, you MUST have a running backend (i.e. REST API). By default, the e2e tests look for this at http://localhost:8080/server/ or whatever `rest` backend is defined in your `environment.prod.ts` or `environment.common.ts`. You may override this using env variables, see [Configuring](#configuring).
|
Before you can run e2e tests, two things are REQUIRED:
|
||||||
|
1. You MUST be running the DSpace backend (i.e. REST API) locally. The e2e tests will *NOT* succeed if run against our demo REST API (https://api7.dspace.org/server/), as that server is uncontrolled and may have content added/removed at any time.
|
||||||
|
* After starting up your backend on localhost, make sure either your `config.prod.yml` or `config.dev.yml` has its `rest` settings defined to use that localhost backend.
|
||||||
|
* If you'd prefer, you may instead use environment variables as described at [Configuring](#configuring). For example:
|
||||||
|
```
|
||||||
|
DSPACE_REST_SSL = false
|
||||||
|
DSPACE_REST_HOST = localhost
|
||||||
|
DSPACE_REST_PORT = 8080
|
||||||
|
```
|
||||||
|
2. Your backend MUST include our [Entities Test Data set](https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data). Some tests run against a specific Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set.
|
||||||
|
* (Recommended) The Entities Test Data set may be installed easily via Docker, see https://github.com/DSpace/DSpace/tree/main/dspace/src/main/docker-compose#ingest-option-2-ingest-entities-test-data
|
||||||
|
* Alternatively, the Entities Test Data set may be installed via a simple SQL import (e. g. `psql -U dspace < dspace7-entities-data.sql`). See instructions in link above.
|
||||||
|
|
||||||
Run `ng e2e` to kick off the tests. This will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
|
After performing the above setup, you can run the e2e tests using
|
||||||
|
```
|
||||||
|
ng e2e
|
||||||
|
````
|
||||||
|
NOTE: By default these tests will run against the REST API backend configured via environment variables or in `config.prod.yml`. If you'd rather it use `config.dev.yml`, just set the NODE_ENV environment variable like this:
|
||||||
|
```
|
||||||
|
NODE_ENV=development ng e2e
|
||||||
|
```
|
||||||
|
|
||||||
|
The `ng e2e` command will start Cypress and allow you to select the browser you wish to use, as well as whether you wish to run all tests or an individual test file. Once you click run on test(s), this opens the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) to run your test(s) and show you the results.
|
||||||
|
|
||||||
#### Writing E2E Tests
|
#### Writing E2E Tests
|
||||||
|
|
||||||
@@ -253,7 +330,10 @@ All E2E tests must be created under the `./cypress/integration/` folder, and mus
|
|||||||
* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_.
|
* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_.
|
||||||
* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page.
|
* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page.
|
||||||
* Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector
|
* Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector
|
||||||
|
* It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test.
|
||||||
* Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc.
|
* Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc.
|
||||||
|
* When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail.
|
||||||
|
* To work around this issue, define the attributes you use for Cypress selectors as `[attr.data-test]="'button' | ngBrowserOnly"`. This will only show the attribute in CSR HTML, forcing Cypress to wait until CSR is complete before interacting with the element.
|
||||||
* Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
|
* Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
|
||||||
* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly.
|
* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly.
|
||||||
* Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests.
|
* Cypress also has a great guide on [writing your first test](https://on.cypress.io/writing-first-test) with much more info. Keep in mind, while the examples in the Cypress docs often involve Javascript files (.js), the same examples will work in our Typescript (.ts) e2e tests.
|
||||||
@@ -309,49 +389,85 @@ File Structure
|
|||||||
|
|
||||||
```
|
```
|
||||||
dspace-angular
|
dspace-angular
|
||||||
├── README.md * This document
|
├── config *
|
||||||
├── app.yaml * Application manifest file
|
│ └── config.yml * Default app config
|
||||||
├── config * Folder for configuration files
|
|
||||||
│ ├── environment.default.js * Default configuration files
|
|
||||||
│ └── environment.test.js * Test configuration files
|
|
||||||
├── docs * Folder for documentation
|
|
||||||
├── cypress * Folder for Cypress (https://cypress.io/) / e2e tests
|
├── cypress * Folder for Cypress (https://cypress.io/) / e2e tests
|
||||||
│ ├── integration * Folder for e2e/integration test files
|
│ ├── downloads *
|
||||||
│ ├── fixtures * Folder for any fixtures needed by e2e tests
|
│ ├── fixtures * Folder for e2e/integration test files
|
||||||
│ ├── plugins * Folder for Cypress plugins (if any)
|
│ ├── integration * Folder for any fixtures needed by e2e tests
|
||||||
│ ├── support * Folder for global e2e test actions/commands (run for all tests)
|
│ ├── plugins * Folder for Cypress plugins (if any)
|
||||||
│ └── tsconfig.json * TypeScript configuration file for e2e tests
|
│ ├── support * Folder for global e2e test actions/commands (run for all tests)
|
||||||
|
│ └── tsconfig.json * TypeScript configuration file for e2e tests
|
||||||
|
├── docker * See docker/README.md for details
|
||||||
|
│ ├── cli.assetstore.yml *
|
||||||
|
│ ├── cli.ingest.yml *
|
||||||
|
│ ├── cli.yml *
|
||||||
|
│ ├── db.entities.yml *
|
||||||
|
│ ├── docker-compose-ci.yml *
|
||||||
|
│ ├── docker-compose-rest.yml *
|
||||||
|
│ ├── docker-compose.yml *
|
||||||
|
│ └── README.md *
|
||||||
|
├── docs * Folder for documentation
|
||||||
|
│ └── Configuration.md * Configuration documentation
|
||||||
|
├── scripts *
|
||||||
|
│ ├── merge-i18n-files.ts *
|
||||||
|
│ ├── serve.ts *
|
||||||
|
│ ├── sync-i18n-files.ts *
|
||||||
|
│ ├── test-rest.ts *
|
||||||
|
│ └── webpack.js *
|
||||||
|
├── src * The source of the application
|
||||||
|
│ ├── app * The source code of the application, subdivided by module/page.
|
||||||
|
│ ├── assets * Folder for static resources
|
||||||
|
│ │ ├── fonts * Folder for fonts
|
||||||
|
│ │ ├── i18n * Folder for i18n translations
|
||||||
|
│ │ └── images * Folder for images
|
||||||
|
│ ├── backend * Folder containing a mock of the REST API, hosted by the express server
|
||||||
|
│ ├── config *
|
||||||
|
│ ├── environments *
|
||||||
|
│ │ ├── environment.production.ts * Production configuration files
|
||||||
|
│ │ ├── environment.test.ts * Test configuration files
|
||||||
|
│ │ └── environment.ts * Default (development) configuration files
|
||||||
|
│ ├── mirador-viewer *
|
||||||
|
│ ├── modules *
|
||||||
|
│ ├── ngx-translate-loaders *
|
||||||
|
│ ├── styles * Folder containing global styles
|
||||||
|
│ ├── themes * Folder containing available themes
|
||||||
|
│ │ ├── custom * Template folder for creating a custom theme
|
||||||
|
│ │ └── dspace * Default 'dspace' theme
|
||||||
|
│ ├── index.csr.html * The index file for client side rendering fallback
|
||||||
|
│ ├── index.html * The index file
|
||||||
|
│ ├── main.browser.ts * The bootstrap file for the client
|
||||||
|
│ ├── main.server.ts * The express (http://expressjs.com/) config and bootstrap file for the server
|
||||||
|
│ ├── polyfills.ts *
|
||||||
|
│ ├── robots.txt * The robots.txt file
|
||||||
|
│ ├── test.ts *
|
||||||
|
│ └── typings.d.ts *
|
||||||
|
├── webpack *
|
||||||
|
│ ├── helpers.ts * Webpack helpers
|
||||||
|
│ ├── webpack.browser.ts * Webpack (https://webpack.github.io/) config for browser build
|
||||||
|
│ ├── webpack.common.ts * Webpack (https://webpack.github.io/) common build config
|
||||||
|
│ ├── webpack.mirador.config.ts * Webpack (https://webpack.github.io/) config for mirador config build
|
||||||
|
│ ├── webpack.prod.ts * Webpack (https://webpack.github.io/) config for prod build
|
||||||
|
│ └── webpack.test.ts * Webpack (https://webpack.github.io/) config for test build
|
||||||
|
├── angular.json * Angular CLI (https://angular.io/cli) configuration
|
||||||
|
├── cypress.json * Cypress Test (https://www.cypress.io/) configuration
|
||||||
|
├── Dockerfile *
|
||||||
├── karma.conf.js * Karma configuration file for Unit Test
|
├── karma.conf.js * Karma configuration file for Unit Test
|
||||||
|
├── LICENSE *
|
||||||
|
├── LICENSES_THIRD_PARTY *
|
||||||
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
|
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
|
||||||
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
|
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
|
||||||
├── postcss.config.js * PostCSS (http://postcss.org/) configuration file
|
├── postcss.config.js * PostCSS (http://postcss.org/) configuration
|
||||||
├── src * The source of the application
|
├── README.md * This document
|
||||||
│ ├── app * The source code of the application, subdivided by module/page.
|
├── SECURITY.md *
|
||||||
│ ├── assets * Folder for static resources
|
├── server.ts * Angular Universal Node.js Express server
|
||||||
│ │ ├── fonts * Folder for fonts
|
├── tsconfig.app.json * TypeScript config for browser (app)
|
||||||
│ │ ├── i18n * Folder for i18n translations
|
├── tsconfig.json * TypeScript common config
|
||||||
│ | └── en.json5 * i18n translations for English
|
├── tsconfig.server.json * TypeScript config for server
|
||||||
│ │ └── images * Folder for images
|
├── tsconfig.spec.json * TypeScript config for tests
|
||||||
│ ├── backend * Folder containing a mock of the REST API, hosted by the express server
|
├── tsconfig.ts-node.json * TypeScript config for using ts-node directly
|
||||||
│ ├── config *
|
|
||||||
│ ├── index.csr.html * The index file for client side rendering fallback
|
|
||||||
│ ├── index.html * The index file
|
|
||||||
│ ├── main.browser.ts * The bootstrap file for the client
|
|
||||||
│ ├── main.server.ts * The express (http://expressjs.com/) config and bootstrap file for the server
|
|
||||||
│ ├── robots.txt * The robots.txt file
|
|
||||||
│ ├── modules *
|
|
||||||
│ ├── styles * Folder containing global styles
|
|
||||||
│ └── themes * Folder containing available themes
|
|
||||||
│ ├── custom * Template folder for creating a custom theme
|
|
||||||
│ └── dspace * Default 'dspace' theme
|
|
||||||
├── tsconfig.json * TypeScript config
|
|
||||||
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
||||||
├── typedoc.json * TYPEDOC configuration
|
├── typedoc.json * TYPEDOC configuration
|
||||||
├── webpack * Webpack (https://webpack.github.io/) config directory
|
|
||||||
│ ├── webpack.browser.ts * Webpack (https://webpack.github.io/) config for client build
|
|
||||||
│ ├── webpack.common.ts *
|
|
||||||
│ ├── webpack.prod.ts * Webpack (https://webpack.github.io/) config for production build
|
|
||||||
│ └── webpack.test.ts * Webpack (https://webpack.github.io/) config for test build
|
|
||||||
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
|
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -449,4 +565,8 @@ DSpace uses GitHub to track issues:
|
|||||||
|
|
||||||
License
|
License
|
||||||
-------
|
-------
|
||||||
This project's source code is made available under the DSpace BSD License: http://www.dspace.org/license
|
DSpace source code is freely available under a standard [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause).
|
||||||
|
The full license is available in the [LICENSE](LICENSE) file or online at http://www.dspace.org/license/
|
||||||
|
|
||||||
|
DSpace uses third-party libraries which may be distributed under different licenses. Those licenses are listed
|
||||||
|
in the [LICENSES_THIRD_PARTY](LICENSES_THIRD_PARTY) file.
|
||||||
|
79
angular.json
79
angular.json
@@ -17,7 +17,6 @@
|
|||||||
"build": {
|
"build": {
|
||||||
"builder": "@angular-builders/custom-webpack:browser",
|
"builder": "@angular-builders/custom-webpack:browser",
|
||||||
"options": {
|
"options": {
|
||||||
"extractCss": true,
|
|
||||||
"preserveSymlinks": true,
|
"preserveSymlinks": true,
|
||||||
"customWebpackConfig": {
|
"customWebpackConfig": {
|
||||||
"path": "./webpack/webpack.browser.ts",
|
"path": "./webpack/webpack.browser.ts",
|
||||||
@@ -64,13 +63,31 @@
|
|||||||
"bundleName": "dspace-theme"
|
"bundleName": "dspace-theme"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": [],
|
||||||
|
"baseHref": "/"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
"development": {
|
||||||
|
"buildOptimizer": false,
|
||||||
|
"optimization": false,
|
||||||
|
"vendorChunk": true,
|
||||||
|
"extractLicenses": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"namedChunks": true
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
|
"fileReplacements": [
|
||||||
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.production.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"replace": "src/config/store/devtools.ts",
|
||||||
|
"with": "src/config/store/devtools.prod.ts"
|
||||||
|
}
|
||||||
|
],
|
||||||
"optimization": true,
|
"optimization": true,
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"extractCss": true,
|
|
||||||
"namedChunks": false,
|
"namedChunks": false,
|
||||||
"aot": true,
|
"aot": true,
|
||||||
"extractLicenses": true,
|
"extractLicenses": true,
|
||||||
@@ -98,6 +115,9 @@
|
|||||||
"port": 4000
|
"port": 4000
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
"development": {
|
||||||
|
"browserTarget": "dspace-angular:build:development"
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "dspace-angular:build:production"
|
"browserTarget": "dspace-angular:build:production"
|
||||||
}
|
}
|
||||||
@@ -139,20 +159,17 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"scripts": []
|
"scripts": []
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"lint": {
|
"configurations": {
|
||||||
"builder": "@angular-devkit/build-angular:tslint",
|
"test": {
|
||||||
"options": {
|
"fileReplacements": [
|
||||||
"tsConfig": [
|
{
|
||||||
"tsconfig.app.json",
|
"replace": "src/environments/environment.ts",
|
||||||
"tsconfig.spec.json",
|
"with": "src/environments/environment.test.ts"
|
||||||
"cypress/tsconfig.json"
|
}
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"**/node_modules/**"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"e2e": {
|
"e2e": {
|
||||||
"builder": "@cypress/schematic:cypress",
|
"builder": "@cypress/schematic:cypress",
|
||||||
@@ -177,16 +194,27 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"outputPath": "dist/server",
|
"outputPath": "dist/server",
|
||||||
"main": "src/main.server.ts",
|
"main": "server.ts",
|
||||||
"tsConfig": "tsconfig.server.json"
|
"tsConfig": "tsconfig.server.json"
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
|
"development": {
|
||||||
|
"sourceMap": true,
|
||||||
|
"optimization": false
|
||||||
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"sourceMap": false,
|
"sourceMap": false,
|
||||||
"optimization": {
|
"optimization": true,
|
||||||
"scripts": false,
|
"fileReplacements": [
|
||||||
"styles": true
|
{
|
||||||
|
"replace": "src/environments/environment.ts",
|
||||||
|
"with": "src/environments/environment.production.ts"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"replace": "src/config/store/devtools.ts",
|
||||||
|
"with": "src/config/store/devtools.prod.ts"
|
||||||
}
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -234,9 +262,22 @@
|
|||||||
"watch": true,
|
"watch": true,
|
||||||
"headless": false
|
"headless": false
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"lint": {
|
||||||
|
"builder": "@angular-eslint/builder:lint",
|
||||||
|
"options": {
|
||||||
|
"lintFilePatterns": [
|
||||||
|
"src/**/*.ts",
|
||||||
|
"src/**/*.html"
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"defaultProject": "dspace-angular"
|
"defaultProject": "dspace-angular",
|
||||||
|
"cli": {
|
||||||
|
"analytics": false,
|
||||||
|
"defaultCollection": "@angular-eslint/schematics"
|
||||||
|
}
|
||||||
}
|
}
|
2
config/.gitignore
vendored
Normal file
2
config/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
config.*.yml
|
||||||
|
!config.example.yml
|
298
config/config.example.yml
Normal file
298
config/config.example.yml
Normal file
@@ -0,0 +1,298 @@
|
|||||||
|
# NOTE: will log all redux actions and transfers in console
|
||||||
|
debug: false
|
||||||
|
|
||||||
|
# Angular Universal server settings
|
||||||
|
# NOTE: these settings define where Node.js will start your UI application. Therefore, these
|
||||||
|
# "ui" settings usually specify a localhost port/URL which is later proxied to a public URL (using Apache or similar)
|
||||||
|
ui:
|
||||||
|
ssl: false
|
||||||
|
host: localhost
|
||||||
|
port: 4000
|
||||||
|
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
|
nameSpace: /
|
||||||
|
# The rateLimiter settings limit each IP to a 'max' of 500 requests per 'windowMs' (1 minute).
|
||||||
|
rateLimiter:
|
||||||
|
windowMs: 60000 # 1 minute
|
||||||
|
max: 500 # limit each IP to 500 requests per windowMs
|
||||||
|
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||||
|
useProxies: true
|
||||||
|
|
||||||
|
# The REST API server settings
|
||||||
|
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||||
|
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
|
||||||
|
rest:
|
||||||
|
ssl: true
|
||||||
|
host: api7.dspace.org
|
||||||
|
port: 443
|
||||||
|
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
|
nameSpace: /server
|
||||||
|
|
||||||
|
# Caching settings
|
||||||
|
cache:
|
||||||
|
# NOTE: how long should objects be cached for by default
|
||||||
|
msToLive:
|
||||||
|
default: 900000 # 15 minutes
|
||||||
|
control: max-age=60 # revalidate browser
|
||||||
|
autoSync:
|
||||||
|
defaultTime: 0
|
||||||
|
maxBufferSize: 100
|
||||||
|
timePerMethod:
|
||||||
|
PATCH: 3 # time in seconds
|
||||||
|
|
||||||
|
# Authentication settings
|
||||||
|
auth:
|
||||||
|
# Authentication UI settings
|
||||||
|
ui:
|
||||||
|
# the amount of time before the idle warning is shown
|
||||||
|
timeUntilIdle: 900000 # 15 minutes
|
||||||
|
# the amount of time the user has to react after the idle warning is shown before they are logged out.
|
||||||
|
idleGracePeriod: 300000 # 5 minutes
|
||||||
|
# Authentication REST settings
|
||||||
|
rest:
|
||||||
|
# If the rest token expires in less than this amount of time, it will be refreshed automatically.
|
||||||
|
# This is independent from the idle warning.
|
||||||
|
timeLeftBeforeTokenRefresh: 120000 # 2 minutes
|
||||||
|
|
||||||
|
# Form settings
|
||||||
|
form:
|
||||||
|
# NOTE: Map server-side validators to comparative Angular form validators
|
||||||
|
validatorMap:
|
||||||
|
required: required
|
||||||
|
regex: pattern
|
||||||
|
|
||||||
|
# Notification settings
|
||||||
|
notifications:
|
||||||
|
rtl: false
|
||||||
|
position:
|
||||||
|
- top
|
||||||
|
- right
|
||||||
|
maxStack: 8
|
||||||
|
# NOTE: after how many seconds notification is closed automatically. If set to zero notifications are not closed automatically
|
||||||
|
timeOut: 5000 # 5 second
|
||||||
|
clickToClose: true
|
||||||
|
# NOTE: 'fade' | 'fromTop' | 'fromRight' | 'fromBottom' | 'fromLeft' | 'rotate' | 'scale'
|
||||||
|
animate: scale
|
||||||
|
|
||||||
|
# Submission settings
|
||||||
|
submission:
|
||||||
|
autosave:
|
||||||
|
# NOTE: which metadata trigger an autosave
|
||||||
|
metadata: []
|
||||||
|
# NOTE: after how many time (milliseconds) submission is saved automatically
|
||||||
|
# eg. timer: 5 * (1000 * 60); // 5 minutes
|
||||||
|
timer: 0
|
||||||
|
icons:
|
||||||
|
metadata:
|
||||||
|
# NOTE: example of configuration
|
||||||
|
# # NOTE: metadata name
|
||||||
|
# - name: dc.author
|
||||||
|
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
||||||
|
# style: fas fa-user
|
||||||
|
- name: dc.author
|
||||||
|
style: fas fa-user
|
||||||
|
# default configuration
|
||||||
|
- name: default
|
||||||
|
style: ''
|
||||||
|
authority:
|
||||||
|
confidence:
|
||||||
|
# NOTE: example of configuration
|
||||||
|
# # NOTE: confidence value
|
||||||
|
# - name: dc.author
|
||||||
|
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
||||||
|
# style: fa-user
|
||||||
|
- value: 600
|
||||||
|
style: text-success
|
||||||
|
- value: 500
|
||||||
|
style: text-info
|
||||||
|
- value: 400
|
||||||
|
style: text-warning
|
||||||
|
# default configuration
|
||||||
|
- value: default
|
||||||
|
style: text-muted
|
||||||
|
|
||||||
|
# Default Language in which the UI will be rendered if the user's browser language is not an active language
|
||||||
|
defaultLanguage: en
|
||||||
|
|
||||||
|
# Languages. DSpace Angular holds a message catalog for each of the following languages.
|
||||||
|
# When set to active, users will be able to switch to the use of this language in the user interface.
|
||||||
|
languages:
|
||||||
|
- code: en
|
||||||
|
label: English
|
||||||
|
active: true
|
||||||
|
- code: cs
|
||||||
|
label: Čeština
|
||||||
|
active: true
|
||||||
|
- code: de
|
||||||
|
label: Deutsch
|
||||||
|
active: true
|
||||||
|
- code: es
|
||||||
|
label: Español
|
||||||
|
active: true
|
||||||
|
- code: fr
|
||||||
|
label: Français
|
||||||
|
active: true
|
||||||
|
- code: gd
|
||||||
|
label: Gàidhlig
|
||||||
|
active: true
|
||||||
|
- code: lv
|
||||||
|
label: Latviešu
|
||||||
|
active: true
|
||||||
|
- code: hu
|
||||||
|
label: Magyar
|
||||||
|
active: true
|
||||||
|
- code: nl
|
||||||
|
label: Nederlands
|
||||||
|
active: true
|
||||||
|
- code: pt-PT
|
||||||
|
label: Português
|
||||||
|
active: true
|
||||||
|
- code: pt-BR
|
||||||
|
label: Português do Brasil
|
||||||
|
active: true
|
||||||
|
- code: fi
|
||||||
|
label: Suomi
|
||||||
|
active: true
|
||||||
|
- code: sv
|
||||||
|
label: Svenska
|
||||||
|
active: true
|
||||||
|
- code: tr
|
||||||
|
label: Türkçe
|
||||||
|
active: true
|
||||||
|
- code: kk
|
||||||
|
label: Қазақ
|
||||||
|
active: true
|
||||||
|
- code: bn
|
||||||
|
label: বাংলা
|
||||||
|
active: true
|
||||||
|
- code: hi
|
||||||
|
label: हिंदी
|
||||||
|
active: true
|
||||||
|
- code: el
|
||||||
|
label: Ελληνικά
|
||||||
|
active: true
|
||||||
|
|
||||||
|
# Browse-By Pages
|
||||||
|
browseBy:
|
||||||
|
# Amount of years to display using jumps of one year (current year - oneYearLimit)
|
||||||
|
oneYearLimit: 10
|
||||||
|
# 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
|
||||||
|
# If true, thumbnail images for items will be added to BOTH search and browse result lists.
|
||||||
|
showThumbnails: true
|
||||||
|
# The number of entries in a paginated browse results list.
|
||||||
|
# Rounded to the nearest size in the list of selectable sizes on the
|
||||||
|
# settings menu.
|
||||||
|
pageSize: 20
|
||||||
|
|
||||||
|
communityList:
|
||||||
|
# No. of communities to list per expansion (show more)
|
||||||
|
pageSize: 20
|
||||||
|
|
||||||
|
homePage:
|
||||||
|
recentSubmissions:
|
||||||
|
# The number of item showing in recent submission components
|
||||||
|
pageSize: 5
|
||||||
|
# Sort record of recent submission
|
||||||
|
sortField: 'dc.date.accessioned'
|
||||||
|
topLevelCommunityList:
|
||||||
|
# No. of communities to list per page on the home page
|
||||||
|
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
|
||||||
|
pageSize: 5
|
||||||
|
|
||||||
|
# Item Config
|
||||||
|
item:
|
||||||
|
edit:
|
||||||
|
undoTimeout: 10000 # 10 seconds
|
||||||
|
# Show the item access status label in items lists
|
||||||
|
showAccessStatuses: false
|
||||||
|
|
||||||
|
# Collection Page Config
|
||||||
|
collection:
|
||||||
|
edit:
|
||||||
|
undoTimeout: 10000 # 10 seconds
|
||||||
|
|
||||||
|
# Theme Config
|
||||||
|
themes:
|
||||||
|
# Add additional themes here. In the case where multiple themes match a route, the first one
|
||||||
|
# in this list will get priority. It is advisable to always have a theme that matches
|
||||||
|
# every route as the last one
|
||||||
|
#
|
||||||
|
# # A theme with a handle property will match the community, collection or item with the given
|
||||||
|
# # handle, and all collections and/or items within it
|
||||||
|
# - name: 'custom',
|
||||||
|
# handle: '10673/1233'
|
||||||
|
#
|
||||||
|
# # A theme with a regex property will match the route using a regular expression. If it
|
||||||
|
# # matches the route for a community or collection it will also apply to all collections
|
||||||
|
# # and/or items within it
|
||||||
|
# - name: 'custom',
|
||||||
|
# regex: 'collections\/e8043bc2.*'
|
||||||
|
#
|
||||||
|
# # A theme with a uuid property will match the community, collection or item with the given
|
||||||
|
# # ID, and all collections and/or items within it
|
||||||
|
# - name: 'custom',
|
||||||
|
# uuid: '0958c910-2037-42a9-81c7-dca80e3892b4'
|
||||||
|
#
|
||||||
|
# # The extends property specifies an ancestor theme (by name). Whenever a themed component is not found
|
||||||
|
# # in the current theme, its ancestor theme(s) will be checked recursively before falling back to default.
|
||||||
|
# - name: 'custom-A',
|
||||||
|
# extends: 'custom-B',
|
||||||
|
# # Any of the matching properties above can be used
|
||||||
|
# handle: '10673/34'
|
||||||
|
#
|
||||||
|
# - name: 'custom-B',
|
||||||
|
# extends: 'custom',
|
||||||
|
# handle: '10673/12'
|
||||||
|
#
|
||||||
|
# # A theme with only a name will match every route
|
||||||
|
# name: 'custom'
|
||||||
|
#
|
||||||
|
# # This theme will use the default bootstrap styling for DSpace components
|
||||||
|
# - name: BASE_THEME_NAME
|
||||||
|
#
|
||||||
|
- name: dspace
|
||||||
|
headTags:
|
||||||
|
- tagName: link
|
||||||
|
attributes:
|
||||||
|
rel: icon
|
||||||
|
href: assets/dspace/images/favicons/favicon.ico
|
||||||
|
sizes: any
|
||||||
|
- tagName: link
|
||||||
|
attributes:
|
||||||
|
rel: icon
|
||||||
|
href: assets/dspace/images/favicons/favicon.svg
|
||||||
|
type: image/svg+xml
|
||||||
|
- tagName: link
|
||||||
|
attributes:
|
||||||
|
rel: apple-touch-icon
|
||||||
|
href: assets/dspace/images/favicons/apple-touch-icon.png
|
||||||
|
- tagName: link
|
||||||
|
attributes:
|
||||||
|
rel: manifest
|
||||||
|
href: assets/dspace/images/favicons/manifest.webmanifest
|
||||||
|
|
||||||
|
# The default bundles that should always be displayed as suggestions when you upload a new bundle
|
||||||
|
bundle:
|
||||||
|
standardBundles: [ ORIGINAL, THUMBNAIL, LICENSE ]
|
||||||
|
|
||||||
|
# Whether to enable media viewer for image and/or video Bitstreams (i.e. Bitstreams whose MIME type starts with 'image' or 'video').
|
||||||
|
# For images, this enables a gallery viewer where you can zoom or page through images.
|
||||||
|
# For videos, this enables embedded video streaming
|
||||||
|
mediaViewer:
|
||||||
|
image: false
|
||||||
|
video: false
|
||||||
|
|
||||||
|
# Whether the end user agreement is required before users use the repository.
|
||||||
|
# If enabled, the user will be required to accept the agreement before they can use the repository.
|
||||||
|
# And whether the privacy statement should exist or not.
|
||||||
|
info:
|
||||||
|
enableEndUserAgreement: true
|
||||||
|
enablePrivacyStatement: true
|
||||||
|
|
||||||
|
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||||
|
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
||||||
|
markdown:
|
||||||
|
enabled: false
|
||||||
|
mathjax: false
|
5
config/config.yml
Normal file
5
config/config.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
rest:
|
||||||
|
ssl: true
|
||||||
|
host: api7.dspace.org
|
||||||
|
port: 443
|
||||||
|
nameSpace: /server
|
18
cypress.json
18
cypress.json
@@ -5,5 +5,21 @@
|
|||||||
"screenshotsFolder": "cypress/screenshots",
|
"screenshotsFolder": "cypress/screenshots",
|
||||||
"pluginsFile": "cypress/plugins/index.ts",
|
"pluginsFile": "cypress/plugins/index.ts",
|
||||||
"fixturesFolder": "cypress/fixtures",
|
"fixturesFolder": "cypress/fixtures",
|
||||||
"baseUrl": "http://localhost:4000"
|
"baseUrl": "http://localhost:4000",
|
||||||
|
"retries": {
|
||||||
|
"runMode": 2,
|
||||||
|
"openMode": 0
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"DSPACE_TEST_ADMIN_USER": "dspacedemo+admin@gmail.com",
|
||||||
|
"DSPACE_TEST_ADMIN_PASSWORD": "dspace",
|
||||||
|
"DSPACE_TEST_COMMUNITY": "0958c910-2037-42a9-81c7-dca80e3892b4",
|
||||||
|
"DSPACE_TEST_COLLECTION": "282164f5-d325-4740-8dd1-fa4d6d3e7200",
|
||||||
|
"DSPACE_TEST_ENTITY_PUBLICATION": "e98b0f27-5c19-49a0-960d-eb6ad5287067",
|
||||||
|
"DSPACE_TEST_SEARCH_TERM": "test",
|
||||||
|
"DSPACE_TEST_SUBMIT_COLLECTION_NAME": "Sample Collection",
|
||||||
|
"DSPACE_TEST_SUBMIT_COLLECTION_UUID": "9d8334e9-25d3-4a67-9cea-3dffdef80144",
|
||||||
|
"DSPACE_TEST_SUBMIT_USER": "dspacedemo+submit@gmail.com",
|
||||||
|
"DSPACE_TEST_SUBMIT_USER_PASSWORD": "dspace"
|
||||||
|
}
|
||||||
}
|
}
|
3
cypress/.gitignore
vendored
Normal file
3
cypress/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
screenshots/
|
||||||
|
videos/
|
||||||
|
downloads/
|
@@ -16,8 +16,8 @@ describe('Homepage', () => {
|
|||||||
|
|
||||||
it('should have a working search box', () => {
|
it('should have a working search box', () => {
|
||||||
const queryString = 'test';
|
const queryString = 'test';
|
||||||
cy.get('ds-search-form input[name="query"]').type(queryString);
|
cy.get('[data-test="search-box"]').type(queryString);
|
||||||
cy.get('ds-search-form button.search-button').click();
|
cy.get('[data-test="search-button"]').click();
|
||||||
cy.url().should('include', '/search');
|
cy.url().should('include', '/search');
|
||||||
cy.url().should('include', 'query=' + encodeURI(queryString));
|
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||||
});
|
});
|
||||||
|
126
cypress/integration/login-modal.spec.ts
Normal file
126
cypress/integration/login-modal.spec.ts
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
import { TEST_ADMIN_PASSWORD, TEST_ADMIN_USER, TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||||
|
|
||||||
|
const page = {
|
||||||
|
openLoginMenu() {
|
||||||
|
// Click the "Log In" dropdown menu in header
|
||||||
|
cy.get('ds-themed-navbar [data-test="login-menu"]').click();
|
||||||
|
},
|
||||||
|
openUserMenu() {
|
||||||
|
// Once logged in, click the User menu in header
|
||||||
|
cy.get('ds-themed-navbar [data-test="user-menu"]').click();
|
||||||
|
},
|
||||||
|
submitLoginAndPasswordByPressingButton(email, password) {
|
||||||
|
// Enter email
|
||||||
|
cy.get('ds-themed-navbar [data-test="email"]').type(email);
|
||||||
|
// Enter password
|
||||||
|
cy.get('ds-themed-navbar [data-test="password"]').type(password);
|
||||||
|
// Click login button
|
||||||
|
cy.get('ds-themed-navbar [data-test="login-button"]').click();
|
||||||
|
},
|
||||||
|
submitLoginAndPasswordByPressingEnter(email, password) {
|
||||||
|
// In opened Login modal, fill out email & password, then click Enter
|
||||||
|
cy.get('ds-themed-navbar [data-test="email"]').type(email);
|
||||||
|
cy.get('ds-themed-navbar [data-test="password"]').type(password);
|
||||||
|
cy.get('ds-themed-navbar [data-test="password"]').type('{enter}');
|
||||||
|
},
|
||||||
|
submitLogoutByPressingButton() {
|
||||||
|
// This is the POST command that will actually log us out
|
||||||
|
cy.intercept('POST', '/server/api/authn/logout').as('logout');
|
||||||
|
// Click logout button
|
||||||
|
cy.get('ds-themed-navbar [data-test="logout-button"]').click();
|
||||||
|
// Wait until above POST command responds before continuing
|
||||||
|
// (This ensures next action waits until logout completes)
|
||||||
|
cy.wait('@logout');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('Login Modal', () => {
|
||||||
|
it('should login when clicking button & stay on same page', () => {
|
||||||
|
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
|
||||||
|
cy.visit(ENTITYPAGE);
|
||||||
|
|
||||||
|
// Login menu should exist
|
||||||
|
cy.get('ds-log-in').should('exist');
|
||||||
|
|
||||||
|
// Login, and the <ds-log-in> tag should no longer exist
|
||||||
|
page.openLoginMenu();
|
||||||
|
cy.get('.form-login').should('be.visible');
|
||||||
|
|
||||||
|
page.submitLoginAndPasswordByPressingButton(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
|
||||||
|
cy.get('ds-log-in').should('not.exist');
|
||||||
|
|
||||||
|
// Verify we are still on the same page
|
||||||
|
cy.url().should('include', ENTITYPAGE);
|
||||||
|
|
||||||
|
// Open user menu, verify user menu & logout button now available
|
||||||
|
page.openUserMenu();
|
||||||
|
cy.get('ds-user-menu').should('be.visible');
|
||||||
|
cy.get('ds-log-out').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should login when clicking enter key & stay on same page', () => {
|
||||||
|
cy.visit('/home');
|
||||||
|
|
||||||
|
// Open login menu in header & verify <ds-log-in> tag is visible
|
||||||
|
page.openLoginMenu();
|
||||||
|
cy.get('.form-login').should('be.visible');
|
||||||
|
|
||||||
|
// Login, and the <ds-log-in> tag should no longer exist
|
||||||
|
page.submitLoginAndPasswordByPressingEnter(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
|
||||||
|
cy.get('.form-login').should('not.exist');
|
||||||
|
|
||||||
|
// Verify we are still on homepage
|
||||||
|
cy.url().should('include', '/home');
|
||||||
|
|
||||||
|
// Open user menu, verify user menu & logout button now available
|
||||||
|
page.openUserMenu();
|
||||||
|
cy.get('ds-user-menu').should('be.visible');
|
||||||
|
cy.get('ds-log-out').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support logout', () => {
|
||||||
|
// First authenticate & access homepage
|
||||||
|
cy.login(TEST_ADMIN_USER, TEST_ADMIN_PASSWORD);
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
// Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist
|
||||||
|
cy.get('ds-log-in').should('not.exist');
|
||||||
|
cy.get('ds-log-out').should('exist');
|
||||||
|
|
||||||
|
// Click logout button
|
||||||
|
page.openUserMenu();
|
||||||
|
page.submitLogoutByPressingButton();
|
||||||
|
|
||||||
|
// Verify ds-log-in tag now exists
|
||||||
|
cy.get('ds-log-in').should('exist');
|
||||||
|
cy.get('ds-log-out').should('not.exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow new user registration', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
page.openLoginMenu();
|
||||||
|
|
||||||
|
// Registration link should be visible
|
||||||
|
cy.get('ds-themed-navbar [data-test="register"]').should('be.visible');
|
||||||
|
|
||||||
|
// Click registration link & you should go to registration page
|
||||||
|
cy.get('ds-themed-navbar [data-test="register"]').click();
|
||||||
|
cy.location('pathname').should('eq', '/register');
|
||||||
|
cy.get('ds-register-email').should('exist');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow forgot password', () => {
|
||||||
|
cy.visit('/');
|
||||||
|
|
||||||
|
page.openLoginMenu();
|
||||||
|
|
||||||
|
// Forgot password link should be visible
|
||||||
|
cy.get('ds-themed-navbar [data-test="forgot"]').should('be.visible');
|
||||||
|
|
||||||
|
// Click link & you should go to Forgot Password page
|
||||||
|
cy.get('ds-themed-navbar [data-test="forgot"]').click();
|
||||||
|
cy.location('pathname').should('eq', '/forgot');
|
||||||
|
cy.get('ds-forgot-email').should('exist');
|
||||||
|
});
|
||||||
|
});
|
149
cypress/integration/my-dspace.spec.ts
Normal file
149
cypress/integration/my-dspace.spec.ts
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME } from 'cypress/support';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('My DSpace page', () => {
|
||||||
|
it('should display recent submissions and pass accessibility tests', () => {
|
||||||
|
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
|
cy.get('ds-my-dspace-page').should('exist');
|
||||||
|
|
||||||
|
// At least one recent submission should be displayed
|
||||||
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
|
|
||||||
|
// Click each filter toggle to open *every* filter
|
||||||
|
// (As we want to scan filter section for accessibility issues as well)
|
||||||
|
cy.get('.filter-toggle').click({ multiple: true });
|
||||||
|
|
||||||
|
// Analyze <ds-my-dspace-page> for accessibility issues
|
||||||
|
testA11y(
|
||||||
|
{
|
||||||
|
include: ['ds-my-dspace-page'],
|
||||||
|
exclude: [
|
||||||
|
['nouislider'] // Date filter slider is missing ARIA labels. Will be fixed by #1175
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Search filters fail these two "moderate" impact rules
|
||||||
|
'heading-order': { enabled: false },
|
||||||
|
'landmark-unique': { enabled: false }
|
||||||
|
}
|
||||||
|
} as Options
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have a working detailed view that passes accessibility tests', () => {
|
||||||
|
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
|
cy.get('ds-my-dspace-page').should('exist');
|
||||||
|
|
||||||
|
// Click button in sidebar to display detailed view
|
||||||
|
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
|
||||||
|
|
||||||
|
cy.get('ds-object-detail').should('exist');
|
||||||
|
|
||||||
|
// Analyze <ds-search-page> for accessibility issues
|
||||||
|
testA11y('ds-my-dspace-page',
|
||||||
|
{
|
||||||
|
rules: {
|
||||||
|
// Search filters fail these two "moderate" impact rules
|
||||||
|
'heading-order': { enabled: false },
|
||||||
|
'landmark-unique': { enabled: false }
|
||||||
|
}
|
||||||
|
} as Options
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
|
||||||
|
it('should let you start a new submission & edit in-progress submissions', () => {
|
||||||
|
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
|
// Open the New Submission dropdown
|
||||||
|
cy.get('button[data-test="submission-dropdown"]').click();
|
||||||
|
// Click on the "Item" type in that dropdown
|
||||||
|
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
|
||||||
|
|
||||||
|
// This should display the <ds-create-item-parent-selector> (popup window)
|
||||||
|
cy.get('ds-create-item-parent-selector').should('be.visible');
|
||||||
|
|
||||||
|
// Type in a known Collection name in the search box
|
||||||
|
cy.get('ds-authorized-collection-selector input[type="search"]').type(TEST_SUBMIT_COLLECTION_NAME);
|
||||||
|
|
||||||
|
// Click on the button matching that known Collection name
|
||||||
|
cy.get('ds-authorized-collection-selector button[title="' + TEST_SUBMIT_COLLECTION_NAME + '"]').click();
|
||||||
|
|
||||||
|
// New URL should include /workspaceitems, as we've started a new submission
|
||||||
|
cy.url().should('include', '/workspaceitems');
|
||||||
|
|
||||||
|
// The Submission edit form tag should be visible
|
||||||
|
cy.get('ds-submission-edit').should('be.visible');
|
||||||
|
|
||||||
|
// A Collection menu button should exist & its value should be the selected collection
|
||||||
|
cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME);
|
||||||
|
|
||||||
|
// Now that we've created a submission, we'll test that we can go back and Edit it.
|
||||||
|
// Get our Submission URL, to parse out the ID of this new submission
|
||||||
|
cy.location().then(fullUrl => {
|
||||||
|
// This will be the full path (/workspaceitems/[id]/edit)
|
||||||
|
const path = fullUrl.pathname;
|
||||||
|
// Split on the slashes
|
||||||
|
const subpaths = path.split('/');
|
||||||
|
// Part 2 will be the [id] of the submission
|
||||||
|
const id = subpaths[2];
|
||||||
|
|
||||||
|
// Click the "Save for Later" button to save this submission
|
||||||
|
cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
|
||||||
|
|
||||||
|
// "Save for Later" should send us to MyDSpace
|
||||||
|
cy.url().should('include', '/mydspace');
|
||||||
|
|
||||||
|
// Close any open notifications, to make sure they don't get in the way of next steps
|
||||||
|
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||||
|
|
||||||
|
// This is the GET command that will actually run the search
|
||||||
|
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||||
|
// On MyDSpace, find the submission we just created via its ID
|
||||||
|
cy.get('[data-test="search-box"]').type(id);
|
||||||
|
cy.get('[data-test="search-button"]').click();
|
||||||
|
|
||||||
|
// Wait for search results to come back from the above GET command
|
||||||
|
cy.wait('@search-results');
|
||||||
|
|
||||||
|
// Click the Edit button for this in-progress submission
|
||||||
|
cy.get('#edit_' + id).click();
|
||||||
|
|
||||||
|
// Should send us back to the submission form
|
||||||
|
cy.url().should('include', '/workspaceitems/' + id + '/edit');
|
||||||
|
|
||||||
|
// Discard our new submission by clicking Discard in Submission form & confirming
|
||||||
|
cy.get('ds-submission-form-footer [data-test="discard"]').click();
|
||||||
|
cy.get('button#discard_submit').click();
|
||||||
|
|
||||||
|
// Discarding should send us back to MyDSpace
|
||||||
|
cy.url().should('include', '/mydspace');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should let you import from external sources', () => {
|
||||||
|
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
cy.visit('/mydspace');
|
||||||
|
|
||||||
|
// Open the New Import dropdown
|
||||||
|
cy.get('button[data-test="import-dropdown"]').click();
|
||||||
|
// Click on the "Item" type in that dropdown
|
||||||
|
cy.get('#importControlsDropdownMenu button[title="none"]').click();
|
||||||
|
|
||||||
|
// New URL should include /import-external, as we've moved to the import page
|
||||||
|
cy.url().should('include', '/import-external');
|
||||||
|
|
||||||
|
// The external import searchbox should be visible
|
||||||
|
cy.get('ds-submission-import-external-searchbar').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -1,49 +1,66 @@
|
|||||||
|
import { TEST_SEARCH_TERM } from 'cypress/support';
|
||||||
|
|
||||||
const page = {
|
const page = {
|
||||||
fillOutQueryInNavBar(query) {
|
fillOutQueryInNavBar(query) {
|
||||||
// Click the magnifying glass
|
// Click the magnifying glass
|
||||||
cy.get('.navbar-container #search-navbar-container form a').click();
|
cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
|
||||||
// Fill out a query in input that appears
|
// Fill out a query in input that appears
|
||||||
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type(query);
|
cy.get('ds-themed-navbar [data-test="header-search-box"]').type(query);
|
||||||
},
|
},
|
||||||
submitQueryByPressingEnter() {
|
submitQueryByPressingEnter() {
|
||||||
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type('{enter}');
|
cy.get('ds-themed-navbar [data-test="header-search-box"]').type('{enter}');
|
||||||
},
|
},
|
||||||
submitQueryByPressingIcon() {
|
submitQueryByPressingIcon() {
|
||||||
cy.get('.navbar-container #search-navbar-container form .submit-icon').click();
|
cy.get('ds-themed-navbar [data-test="header-search-icon"]').click();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Search from Navigation Bar', () => {
|
describe('Search from Navigation Bar', () => {
|
||||||
// NOTE: these tests currently assume this query will return results!
|
// NOTE: these tests currently assume this query will return results!
|
||||||
const query = 'test';
|
const query = TEST_SEARCH_TERM;
|
||||||
|
|
||||||
it('should go to search page with correct query if submitted (from home)', () => {
|
it('should go to search page with correct query if submitted (from home)', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
// This is the GET command that will actually run the search
|
||||||
|
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||||
|
// Run the search
|
||||||
page.fillOutQueryInNavBar(query);
|
page.fillOutQueryInNavBar(query);
|
||||||
page.submitQueryByPressingEnter();
|
page.submitQueryByPressingEnter();
|
||||||
// New URL should include query param
|
// New URL should include query param
|
||||||
cy.url().should('include', 'query=' + query);
|
cy.url().should('include', 'query=' + query);
|
||||||
|
// Wait for search results to come back from the above GET command
|
||||||
|
cy.wait('@search-results');
|
||||||
// At least one search result should be displayed
|
// At least one search result should be displayed
|
||||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should go to search page with correct query if submitted (from search)', () => {
|
it('should go to search page with correct query if submitted (from search)', () => {
|
||||||
cy.visit('/search');
|
cy.visit('/search');
|
||||||
|
// This is the GET command that will actually run the search
|
||||||
|
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||||
|
// Run the search
|
||||||
page.fillOutQueryInNavBar(query);
|
page.fillOutQueryInNavBar(query);
|
||||||
page.submitQueryByPressingEnter();
|
page.submitQueryByPressingEnter();
|
||||||
// New URL should include query param
|
// New URL should include query param
|
||||||
cy.url().should('include', 'query=' + query);
|
cy.url().should('include', 'query=' + query);
|
||||||
|
// Wait for search results to come back from the above GET command
|
||||||
|
cy.wait('@search-results');
|
||||||
// At least one search result should be displayed
|
// At least one search result should be displayed
|
||||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should allow user to also submit query by clicking icon', () => {
|
it('should allow user to also submit query by clicking icon', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
// This is the GET command that will actually run the search
|
||||||
|
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||||
|
// Run the search
|
||||||
page.fillOutQueryInNavBar(query);
|
page.fillOutQueryInNavBar(query);
|
||||||
page.submitQueryByPressingIcon();
|
page.submitQueryByPressingIcon();
|
||||||
// New URL should include query param
|
// New URL should include query param
|
||||||
cy.url().should('include', 'query=' + query);
|
cy.url().should('include', 'query=' + query);
|
||||||
|
// Wait for search results to come back from the above GET command
|
||||||
|
cy.wait('@search-results');
|
||||||
// At least one search result should be displayed
|
// At least one search result should be displayed
|
||||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,34 +1,30 @@
|
|||||||
import { Options } from 'cypress-axe';
|
import { Options } from 'cypress-axe';
|
||||||
|
import { TEST_SEARCH_TERM } from 'cypress/support';
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Search Page', () => {
|
describe('Search Page', () => {
|
||||||
// unique ID of the search form (for selecting specific elements below)
|
|
||||||
const SEARCHFORM_ID = '#search-form';
|
|
||||||
|
|
||||||
it('should contain query value when navigating to page with query parameter', () => {
|
|
||||||
const queryString = 'test query';
|
|
||||||
cy.visit('/search?query=' + queryString);
|
|
||||||
cy.get(SEARCHFORM_ID + ' input[name="query"]').should('have.value', queryString);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
||||||
const queryString = 'Another interesting query string';
|
const queryString = 'Another interesting query string';
|
||||||
cy.visit('/search');
|
cy.visit('/search');
|
||||||
// Type query in searchbox & click search button
|
// Type query in searchbox & click search button
|
||||||
cy.get(SEARCHFORM_ID + ' input[name="query"]').type(queryString);
|
cy.get('[data-test="search-box"]').type(queryString);
|
||||||
cy.get(SEARCHFORM_ID + ' button.search-button').click();
|
cy.get('[data-test="search-button"]').click();
|
||||||
cy.url().should('include', 'query=' + encodeURI(queryString));
|
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should load results and pass accessibility tests', () => {
|
||||||
cy.visit('/search');
|
cy.visit('/search?query=' + TEST_SEARCH_TERM);
|
||||||
|
cy.get('[data-test="search-box"]').should('have.value', TEST_SEARCH_TERM);
|
||||||
|
|
||||||
// <ds-search-page> tag must be loaded
|
// <ds-search-page> tag must be loaded
|
||||||
cy.get('ds-search-page').should('exist');
|
cy.get('ds-search-page').should('exist');
|
||||||
|
|
||||||
|
// At least one search result should be displayed
|
||||||
|
cy.get('[data-test="list-object"]').should('be.visible');
|
||||||
|
|
||||||
// Click each filter toggle to open *every* filter
|
// Click each filter toggle to open *every* filter
|
||||||
// (As we want to scan filter section for accessibility issues as well)
|
// (As we want to scan filter section for accessibility issues as well)
|
||||||
cy.get('.filter-toggle').click({ multiple: true });
|
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
|
||||||
|
|
||||||
// Analyze <ds-search-page> for accessibility issues
|
// Analyze <ds-search-page> for accessibility issues
|
||||||
testA11y(
|
testA11y(
|
||||||
@@ -48,16 +44,18 @@ describe('Search Page', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests in Grid view', () => {
|
it('should have a working grid view that passes accessibility tests', () => {
|
||||||
cy.visit('/search');
|
cy.visit('/search?query=' + TEST_SEARCH_TERM);
|
||||||
|
|
||||||
// Click to display grid view
|
// Click button in sidebar to display grid view
|
||||||
// TODO: These buttons should likely have an easier way to uniquely select
|
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
|
||||||
cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?spc.sf=score&spc.sd=DESC&view=grid"] > .fas').click();
|
|
||||||
|
|
||||||
// <ds-search-page> tag must be loaded
|
// <ds-search-page> tag must be loaded
|
||||||
cy.get('ds-search-page').should('exist');
|
cy.get('ds-search-page').should('exist');
|
||||||
|
|
||||||
|
// At least one grid object (card) should be displayed
|
||||||
|
cy.get('[data-test="grid-object"]').should('be.visible');
|
||||||
|
|
||||||
// Analyze <ds-search-page> for accessibility issues
|
// Analyze <ds-search-page> for accessibility issues
|
||||||
testA11y('ds-search-page',
|
testA11y('ds-search-page',
|
||||||
{
|
{
|
||||||
|
135
cypress/integration/submission.spec.ts
Normal file
135
cypress/integration/submission.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { Options } from 'cypress-axe';
|
||||||
|
import { TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD, TEST_SUBMIT_COLLECTION_NAME, TEST_SUBMIT_COLLECTION_UUID } from 'cypress/support';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
|
describe('New Submission page', () => {
|
||||||
|
// NOTE: We already test that new submissions can be started from MyDSpace in my-dspace.spec.ts
|
||||||
|
|
||||||
|
it('should create a new submission when using /submit path & pass accessibility', () => {
|
||||||
|
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
|
// Test that calling /submit with collection & entityType will create a new submission
|
||||||
|
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||||
|
|
||||||
|
// Should redirect to /workspaceitems, as we've started a new submission
|
||||||
|
cy.url().should('include', '/workspaceitems');
|
||||||
|
|
||||||
|
// The Submission edit form tag should be visible
|
||||||
|
cy.get('ds-submission-edit').should('be.visible');
|
||||||
|
|
||||||
|
// A Collection menu button should exist & it's value should be the selected collection
|
||||||
|
cy.get('#collectionControlsMenuButton span').should('have.text', TEST_SUBMIT_COLLECTION_NAME);
|
||||||
|
|
||||||
|
// 4 sections should be visible by default
|
||||||
|
cy.get('div#section_traditionalpageone').should('be.visible');
|
||||||
|
cy.get('div#section_traditionalpagetwo').should('be.visible');
|
||||||
|
cy.get('div#section_upload').should('be.visible');
|
||||||
|
cy.get('div#section_license').should('be.visible');
|
||||||
|
|
||||||
|
// Discard button should work
|
||||||
|
// Clicking it will display a confirmation, which we will confirm with another click
|
||||||
|
cy.get('button#discard').click();
|
||||||
|
cy.get('button#discard_submit').click();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should block submission & show errors if required fields are missing', () => {
|
||||||
|
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
|
// Create a new submission
|
||||||
|
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||||
|
|
||||||
|
// Attempt an immediate deposit without filling out any fields
|
||||||
|
cy.get('button#deposit').click();
|
||||||
|
|
||||||
|
// A warning alert should display.
|
||||||
|
cy.get('ds-notification div.alert-success').should('not.exist');
|
||||||
|
cy.get('ds-notification div.alert-warning').should('be.visible');
|
||||||
|
|
||||||
|
// First section should have an exclamation error in the header
|
||||||
|
// (as it has required fields)
|
||||||
|
cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible');
|
||||||
|
|
||||||
|
// Title field should have class "is-invalid" applied, as it's required
|
||||||
|
cy.get('input#dc_title').should('have.class', 'is-invalid');
|
||||||
|
|
||||||
|
// Date Year field should also have "is-valid" class
|
||||||
|
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
|
||||||
|
|
||||||
|
// FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button.
|
||||||
|
// Get our Submission URL, to parse out the ID of this submission
|
||||||
|
cy.location().then(fullUrl => {
|
||||||
|
// This will be the full path (/workspaceitems/[id]/edit)
|
||||||
|
const path = fullUrl.pathname;
|
||||||
|
// Split on the slashes
|
||||||
|
const subpaths = path.split('/');
|
||||||
|
// Part 2 will be the [id] of the submission
|
||||||
|
const id = subpaths[2];
|
||||||
|
|
||||||
|
// Even though form is incomplete, the "Save for Later" button should still work
|
||||||
|
cy.get('button#saveForLater').click();
|
||||||
|
|
||||||
|
// "Save for Later" should send us to MyDSpace
|
||||||
|
cy.url().should('include', '/mydspace');
|
||||||
|
|
||||||
|
// A success alert should be visible
|
||||||
|
cy.get('ds-notification div.alert-success').should('be.visible');
|
||||||
|
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
|
||||||
|
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||||
|
|
||||||
|
// This is the GET command that will actually run the search
|
||||||
|
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||||
|
// On MyDSpace, find the submission we just saved via its ID
|
||||||
|
cy.get('[data-test="search-box"]').type(id);
|
||||||
|
cy.get('[data-test="search-button"]').click();
|
||||||
|
|
||||||
|
// Wait for search results to come back from the above GET command
|
||||||
|
cy.wait('@search-results');
|
||||||
|
|
||||||
|
// Delete our created submission & confirm deletion
|
||||||
|
cy.get('button#delete_' + id).click();
|
||||||
|
cy.get('button#delete_confirm').click();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow for deposit if all required fields completed & file uploaded', () => {
|
||||||
|
cy.login(TEST_SUBMIT_USER, TEST_SUBMIT_USER_PASSWORD);
|
||||||
|
|
||||||
|
// Create a new submission
|
||||||
|
cy.visit('/submit?collection=' + TEST_SUBMIT_COLLECTION_UUID + '&entityType=none');
|
||||||
|
|
||||||
|
// Fill out all required fields (Title, Date)
|
||||||
|
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
|
||||||
|
cy.get('input#dc_date_issued_year').type('2022');
|
||||||
|
|
||||||
|
// Confirm the required license by checking checkbox
|
||||||
|
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
|
||||||
|
cy.get('input#granted').check( {force: true} );
|
||||||
|
|
||||||
|
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
|
||||||
|
// This ensures our UI displays the dropzone that covers the entire submission page.
|
||||||
|
// (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger)
|
||||||
|
cy.get('ds-uploader').trigger('dragover');
|
||||||
|
|
||||||
|
// This is the POST command that will upload the file
|
||||||
|
cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload');
|
||||||
|
|
||||||
|
// Upload our DSpace logo via drag & drop onto submission form
|
||||||
|
// cy.get('div#section_upload')
|
||||||
|
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.png', {
|
||||||
|
action: 'drag-drop'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Wait for upload to complete before proceeding
|
||||||
|
cy.wait('@upload');
|
||||||
|
// Close the upload success notice
|
||||||
|
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||||
|
|
||||||
|
// Wait for deposit button to not be disabled & click it.
|
||||||
|
cy.get('button#deposit').should('not.be.disabled').click();
|
||||||
|
|
||||||
|
// No warnings should exist. Instead, just successful deposit alert is displayed
|
||||||
|
cy.get('ds-notification div.alert-warning').should('not.exist');
|
||||||
|
cy.get('ds-notification div.alert-success').should('be.visible');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -1,15 +1,34 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
|
||||||
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
||||||
// For more info, visit https://on.cypress.io/plugins-api
|
// For more info, visit https://on.cypress.io/plugins-api
|
||||||
module.exports = (on, config) => {
|
module.exports = (on, config) => {
|
||||||
|
on('task', {
|
||||||
// Define "log" and "table" tasks, used for logging accessibility errors during CI
|
// Define "log" and "table" tasks, used for logging accessibility errors during CI
|
||||||
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
|
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
|
||||||
on('task', {
|
|
||||||
log(message: string) {
|
log(message: string) {
|
||||||
console.log(message);
|
console.log(message);
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
table(message: string) {
|
table(message: string) {
|
||||||
console.table(message);
|
console.table(message);
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
// Cypress doesn't have access to the running application in Node.js.
|
||||||
|
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
|
||||||
|
// Instead, we'll read our running application's config.json, which contains the configs &
|
||||||
|
// is regenerated at runtime each time the Angular UI application starts up.
|
||||||
|
readUIConfig() {
|
||||||
|
// Check if we have a config.json in the src/assets. If so, use that.
|
||||||
|
// This is where it's written when running "ng e2e" or "yarn serve"
|
||||||
|
if (fs.existsSync('./src/assets/config.json')) {
|
||||||
|
return fs.readFileSync('./src/assets/config.json', 'utf8');
|
||||||
|
// Otherwise, check the dist/browser/assets
|
||||||
|
// This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
|
||||||
|
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
|
||||||
|
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@@ -1,43 +1,83 @@
|
|||||||
// ***********************************************
|
// ***********************************************
|
||||||
// This example namespace declaration will help
|
// This File is for Custom Cypress commands.
|
||||||
// with Intellisense and code completion in your
|
// See docs at https://docs.cypress.io/api/cypress-api/custom-commands
|
||||||
// IDE or Text Editor.
|
|
||||||
// ***********************************************
|
// ***********************************************
|
||||||
// declare namespace Cypress {
|
|
||||||
// interface Chainable<Subject = any> {
|
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
||||||
// customCommand(param: any): typeof customCommand;
|
import { FALLBACK_TEST_REST_BASE_URL } from '.';
|
||||||
// }
|
|
||||||
// }
|
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
|
||||||
//
|
// ALL custom commands MUST be listed here for code completion to work
|
||||||
// function customCommand(param: any): void {
|
// tslint:disable-next-line:no-namespace
|
||||||
// console.warn(param);
|
declare global {
|
||||||
// }
|
namespace Cypress {
|
||||||
//
|
interface Chainable<Subject = any> {
|
||||||
// NOTE: You can use it like so:
|
/**
|
||||||
// Cypress.Commands.add('customCommand', customCommand);
|
* Login to backend before accessing the next page. Ensures that the next
|
||||||
//
|
* call to "cy.visit()" will be authenticated as this user.
|
||||||
// ***********************************************
|
* @param email email to login as
|
||||||
// This example commands.js shows you how to
|
* @param password password to login as
|
||||||
// create various custom commands and overwrite
|
*/
|
||||||
// existing commands.
|
login(email: string, password: string): typeof login;
|
||||||
//
|
}
|
||||||
// For more comprehensive examples of custom
|
}
|
||||||
// commands please read more here:
|
}
|
||||||
// https://on.cypress.io/custom-commands
|
|
||||||
// ***********************************************
|
/**
|
||||||
//
|
* Login user via REST API directly, and pass authentication token to UI via
|
||||||
//
|
* the UI's dsAuthInfo cookie.
|
||||||
// -- This is a parent command --
|
* @param email email to login as
|
||||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
* @param password password to login as
|
||||||
//
|
*/
|
||||||
//
|
function login(email: string, password: string): void {
|
||||||
// -- This is a child command --
|
// Cypress doesn't have access to the running application in Node.js.
|
||||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
|
||||||
//
|
// Instead, we'll read our running application's config.json, which contains the configs &
|
||||||
//
|
// is regenerated at runtime each time the Angular UI application starts up.
|
||||||
// -- This is a dual command --
|
cy.task('readUIConfig').then((str: string) => {
|
||||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
// Parse config into a JSON object
|
||||||
//
|
const config = JSON.parse(str);
|
||||||
//
|
|
||||||
// -- This will overwrite an existing command --
|
// Find the URL of our REST API. Have a fallback ready, just in case 'rest.baseUrl' cannot be found.
|
||||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
|
||||||
|
if (!config.rest.baseUrl) {
|
||||||
|
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
|
||||||
|
} else {
|
||||||
|
console.log("Found 'rest.baseUrl' in config.json. Using this REST API for login: " + config.rest.baseUrl);
|
||||||
|
baseRestUrl = config.rest.baseUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
// To login via REST, first we have to do a GET to obtain a valid CSRF token
|
||||||
|
cy.request( baseRestUrl + '/api/authn/status' )
|
||||||
|
.then((response) => {
|
||||||
|
// We should receive a CSRF token returned in a response header
|
||||||
|
expect(response.headers).to.have.property('dspace-xsrf-token');
|
||||||
|
const csrfToken = response.headers['dspace-xsrf-token'];
|
||||||
|
|
||||||
|
// Now, send login POST request including that CSRF token
|
||||||
|
cy.request({
|
||||||
|
method: 'POST',
|
||||||
|
url: baseRestUrl + '/api/authn/login',
|
||||||
|
headers: { 'X-XSRF-TOKEN' : csrfToken},
|
||||||
|
form: true, // indicates the body should be form urlencoded
|
||||||
|
body: { user: email, password: password }
|
||||||
|
}).then((resp) => {
|
||||||
|
// We expect a successful login
|
||||||
|
expect(resp.status).to.eq(200);
|
||||||
|
// We expect to have a valid authorization header returned (with our auth token)
|
||||||
|
expect(resp.headers).to.have.property('authorization');
|
||||||
|
|
||||||
|
// Initialize our AuthTokenInfo object from the authorization header.
|
||||||
|
const authheader = resp.headers.authorization as string;
|
||||||
|
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
|
||||||
|
|
||||||
|
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
|
||||||
|
// This ensures the UI will recognize we are logged in on next "visit()"
|
||||||
|
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Add as a Cypress command (i.e. assign to 'cy.login')
|
||||||
|
Cypress.Commands.add('login', login);
|
||||||
|
@@ -13,14 +13,51 @@
|
|||||||
// https://on.cypress.io/configuration
|
// https://on.cypress.io/configuration
|
||||||
// ***********************************************************
|
// ***********************************************************
|
||||||
|
|
||||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
// Import all custom Commands (from commands.ts) for all tests
|
||||||
// import './commands';
|
import './commands';
|
||||||
|
|
||||||
// Import Cypress Axe tools for all tests
|
// Import Cypress Axe tools for all tests
|
||||||
// https://github.com/component-driven/cypress-axe
|
// https://github.com/component-driven/cypress-axe
|
||||||
import 'cypress-axe';
|
import 'cypress-axe';
|
||||||
|
|
||||||
|
// Runs once before the first test in each "block"
|
||||||
|
beforeEach(() => {
|
||||||
|
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
|
||||||
|
// This just ensures it doesn't get in the way of matching other objects in the page.
|
||||||
|
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}');
|
||||||
|
});
|
||||||
|
|
||||||
|
// For better stability between tests, we visit "about:blank" (i.e. blank page) after each test.
|
||||||
|
// This ensures any remaining/outstanding XHR requests are killed, so they don't affect the next test.
|
||||||
|
// Borrowed from: https://glebbahmutov.com/blog/visit-blank-page-between-tests/
|
||||||
|
afterEach(() => {
|
||||||
|
cy.window().then((win) => {
|
||||||
|
win.location.href = 'about:blank';
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
// Global constants used in tests
|
// Global constants used in tests
|
||||||
export const TEST_COLLECTION = '282164f5-d325-4740-8dd1-fa4d6d3e7200';
|
// May be overridden in our cypress.json config file using specified environment variables.
|
||||||
export const TEST_COMMUNITY = '0958c910-2037-42a9-81c7-dca80e3892b4';
|
// Default values listed here are all valid for the Demo Entities Data set available at
|
||||||
export const TEST_ENTITY_PUBLICATION = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
// https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
|
// (This is the data set used in our CI environment)
|
||||||
|
|
||||||
|
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL
|
||||||
|
// from the Angular UI's config.json. See 'getBaseRESTUrl()' in commands.ts
|
||||||
|
export const FALLBACK_TEST_REST_BASE_URL = 'http://localhost:8080/server';
|
||||||
|
|
||||||
|
// Admin account used for administrative tests
|
||||||
|
export const TEST_ADMIN_USER = Cypress.env('DSPACE_TEST_ADMIN_USER') || 'dspacedemo+admin@gmail.com';
|
||||||
|
export const TEST_ADMIN_PASSWORD = Cypress.env('DSPACE_TEST_ADMIN_PASSWORD') || 'dspace';
|
||||||
|
// Community/collection/publication used for view/edit tests
|
||||||
|
export const TEST_COLLECTION = Cypress.env('DSPACE_TEST_COLLECTION') || '282164f5-d325-4740-8dd1-fa4d6d3e7200';
|
||||||
|
export const TEST_COMMUNITY = Cypress.env('DSPACE_TEST_COMMUNITY') || '0958c910-2037-42a9-81c7-dca80e3892b4';
|
||||||
|
export const TEST_ENTITY_PUBLICATION = Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION') || 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
||||||
|
// Search term (should return results) used in search tests
|
||||||
|
export const TEST_SEARCH_TERM = Cypress.env('DSPACE_TEST_SEARCH_TERM') || 'test';
|
||||||
|
// Collection used for submission tests
|
||||||
|
export const TEST_SUBMIT_COLLECTION_NAME = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME') || 'Sample Collection';
|
||||||
|
export const TEST_SUBMIT_COLLECTION_UUID = Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID') || '9d8334e9-25d3-4a67-9cea-3dffdef80144';
|
||||||
|
export const TEST_SUBMIT_USER = Cypress.env('DSPACE_TEST_SUBMIT_USER') || 'dspacedemo+submit@gmail.com';
|
||||||
|
export const TEST_SUBMIT_USER_PASSWORD = Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD') || 'dspace';
|
||||||
|
@@ -1,9 +1,25 @@
|
|||||||
# Docker Compose files
|
# Docker Compose files
|
||||||
|
|
||||||
***
|
***
|
||||||
:warning: **NOT PRODUCTION READY** The below Docker Compose resources are not guaranteed "production ready" at this time. They have been built for development/testing only. Therefore, DSpace Docker images may not be fully secured or up-to-date. While you are welcome to base your own images on these DSpace images/resources, these should not be used "as is" in any production scenario.
|
:warning: **THESE IMAGES ARE NOT PRODUCTION READY** The below Docker Compose images/resources were built for development/testing only. Therefore, they may not be fully secured or up-to-date, and should not be used in production.
|
||||||
|
|
||||||
|
If you wish to run DSpace on Docker in production, we recommend building your own Docker images. You are welcome to borrow ideas/concepts from the below images in doing so. But, the below images should not be used "as is" in any production scenario.
|
||||||
***
|
***
|
||||||
|
|
||||||
|
## 'Dockerfile' in root directory
|
||||||
|
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t dspace/dspace-angular:dspace-7_x .
|
||||||
|
```
|
||||||
|
|
||||||
|
This image is built *automatically* after each commit is made to the `main` branch.
|
||||||
|
|
||||||
|
Admins to our DockerHub repo can manually publish with the following command.
|
||||||
|
```
|
||||||
|
docker push dspace/dspace-angular:dspace-7_x
|
||||||
|
```
|
||||||
|
|
||||||
## docker directory
|
## docker directory
|
||||||
- docker-compose.yml
|
- 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.
|
- 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.
|
||||||
@@ -15,10 +31,6 @@
|
|||||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||||
- cli.assetstore.yml
|
- 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.
|
- 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.ts
|
|
||||||
- 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
|
## To refresh / pull DSpace images from Dockerhub
|
||||||
|
@@ -35,6 +35,6 @@ services:
|
|||||||
tar xvfz /tmp/assetstore.tar.gz
|
tar xvfz /tmp/assetstore.tar.gz
|
||||||
fi
|
fi
|
||||||
|
|
||||||
/dspace/bin/dspace index-discovery
|
/dspace/bin/dspace index-discovery -b
|
||||||
/dspace/bin/dspace oai import
|
/dspace/bin/dspace oai import
|
||||||
/dspace/bin/dspace oai clean-cache
|
/dspace/bin/dspace oai clean-cache
|
||||||
|
@@ -18,10 +18,19 @@ services:
|
|||||||
dspace-cli:
|
dspace-cli:
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
||||||
container_name: dspace-cli
|
container_name: dspace-cli
|
||||||
#environment:
|
environment:
|
||||||
|
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||||
|
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
|
||||||
|
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
|
||||||
|
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
||||||
|
# dspace.dir
|
||||||
|
dspace__P__dir: /dspace
|
||||||
|
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||||
|
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||||
|
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||||
|
solr__P__server: http://dspacesolr:8983/solr
|
||||||
volumes:
|
volumes:
|
||||||
- "assetstore:/dspace/assetstore"
|
- "assetstore:/dspace/assetstore"
|
||||||
- "./local.cfg:/dspace/config/local.cfg"
|
|
||||||
entrypoint: /dspace/bin/dspace
|
entrypoint: /dspace/bin/dspace
|
||||||
command: help
|
command: help
|
||||||
networks:
|
networks:
|
||||||
|
@@ -20,12 +20,12 @@ services:
|
|||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
||||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
|
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
||||||
dspace:
|
dspace:
|
||||||
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
# 2. Then, run database migration to init database tables
|
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||||
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
||||||
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
||||||
@@ -35,7 +35,7 @@ services:
|
|||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate
|
/dspace/bin/dspace database migrate ignored
|
||||||
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
||||||
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
||||||
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
||||||
|
@@ -17,6 +17,19 @@ services:
|
|||||||
# DSpace (backend) webapp container
|
# DSpace (backend) webapp container
|
||||||
dspace:
|
dspace:
|
||||||
container_name: dspace
|
container_name: dspace
|
||||||
|
environment:
|
||||||
|
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||||
|
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
|
||||||
|
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
|
||||||
|
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
||||||
|
# dspace.dir, dspace.server.url and dspace.ui.url
|
||||||
|
dspace__P__dir: /dspace
|
||||||
|
dspace__P__server__P__url: http://localhost:8080/server
|
||||||
|
dspace__P__ui__P__url: http://localhost:4000
|
||||||
|
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||||
|
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||||
|
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||||
|
solr__P__server: http://dspacesolr:8983/solr
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
image: dspace/dspace:dspace-7_x-test
|
image: dspace/dspace:dspace-7_x-test
|
||||||
@@ -29,19 +42,18 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
- assetstore:/dspace/assetstore
|
- assetstore:/dspace/assetstore
|
||||||
- "./local.cfg:/dspace/config/local.cfg"
|
|
||||||
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
||||||
- solr_configs:/dspace/solr
|
- solr_configs:/dspace/solr
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
# 2. Then, run database migration to init database tables
|
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||||
# 3. Finally, start Tomcat
|
# 3. Finally, start Tomcat
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate
|
/dspace/bin/dspace database migrate ignored
|
||||||
catalina.sh run
|
catalina.sh run
|
||||||
# DSpace database container
|
# DSpace database container
|
||||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||||
@@ -51,7 +63,7 @@ services:
|
|||||||
# This LOADSQL should be kept in sync with the LOADSQL in
|
# This LOADSQL should be kept in sync with the LOADSQL in
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
||||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-2021-04-14.sql
|
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
||||||
PGDATA: /pgdata
|
PGDATA: /pgdata
|
||||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||||
networks:
|
networks:
|
||||||
@@ -64,7 +76,7 @@ services:
|
|||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
# Uses official Solr image at https://hub.docker.com/_/solr/
|
# Uses official Solr image at https://hub.docker.com/_/solr/
|
||||||
image: solr:8.8
|
image: solr:8.11-slim
|
||||||
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspace
|
- dspace
|
||||||
|
@@ -13,10 +13,32 @@
|
|||||||
version: '3.7'
|
version: '3.7'
|
||||||
networks:
|
networks:
|
||||||
dspacenet:
|
dspacenet:
|
||||||
|
ipam:
|
||||||
|
config:
|
||||||
|
# Define a custom subnet for our DSpace network, so that we can easily trust requests from host to container.
|
||||||
|
# If you customize this value, be sure to customize the 'proxies.trusted.ipranges' env variable below.
|
||||||
|
- subnet: 172.23.0.0/16
|
||||||
services:
|
services:
|
||||||
# DSpace (backend) webapp container
|
# DSpace (backend) webapp container
|
||||||
dspace:
|
dspace:
|
||||||
container_name: dspace
|
container_name: dspace
|
||||||
|
environment:
|
||||||
|
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||||
|
# See https://github.com/DSpace/DSpace/blob/main/dspace/config/config-definition.xml
|
||||||
|
# __P__ => "." (e.g. dspace__P__dir => dspace.dir)
|
||||||
|
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
||||||
|
# dspace.dir, dspace.server.url, dspace.ui.url and dspace.name
|
||||||
|
dspace__P__dir: /dspace
|
||||||
|
dspace__P__server__P__url: http://localhost:8080/server
|
||||||
|
dspace__P__ui__P__url: http://localhost:4000
|
||||||
|
dspace__P__name: 'DSpace Started with Docker Compose'
|
||||||
|
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||||
|
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||||
|
# solr.server: Ensure we are using the 'dspacesolr' image for Solr
|
||||||
|
solr__P__server: http://dspacesolr:8983/solr
|
||||||
|
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
|
||||||
|
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||||
|
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||||
image: dspace/dspace:dspace-7_x-test
|
image: dspace/dspace:dspace-7_x-test
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
@@ -29,7 +51,6 @@ services:
|
|||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
- assetstore:/dspace/assetstore
|
- assetstore:/dspace/assetstore
|
||||||
- "./local.cfg:/dspace/config/local.cfg"
|
|
||||||
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
# Mount DSpace's solr configs to a volume, so that we can share to 'dspacesolr' container (see below)
|
||||||
- solr_configs:/dspace/solr
|
- solr_configs:/dspace/solr
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
@@ -62,7 +83,7 @@ services:
|
|||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
# Uses official Solr image at https://hub.docker.com/_/solr/
|
# Uses official Solr image at https://hub.docker.com/_/solr/
|
||||||
image: solr:8.8
|
image: solr:8.11-slim
|
||||||
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
# Needs main 'dspace' container to start first to guarantee access to solr_configs
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspace
|
- dspace
|
||||||
@@ -81,15 +102,22 @@ services:
|
|||||||
# Keep Solr data directory between reboots
|
# Keep Solr data directory between reboots
|
||||||
- solr_data:/var/solr/data
|
- solr_data:/var/solr/data
|
||||||
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
|
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
|
||||||
|
# * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op
|
||||||
|
# * Second, copy updated configs from mounted configsets to this core. If it already existed, this updates core
|
||||||
|
# to the latest configs. If it's a newly created core, this is a no-op.
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
init-var-solr
|
init-var-solr
|
||||||
precreate-core authority /opt/solr/server/solr/configsets/dspace/authority
|
precreate-core authority /opt/solr/server/solr/configsets/dspace/authority
|
||||||
|
cp -r -u /opt/solr/server/solr/configsets/dspace/authority/* authority
|
||||||
precreate-core oai /opt/solr/server/solr/configsets/dspace/oai
|
precreate-core oai /opt/solr/server/solr/configsets/dspace/oai
|
||||||
|
cp -r -u /opt/solr/server/solr/configsets/dspace/oai/* oai
|
||||||
precreate-core search /opt/solr/server/solr/configsets/dspace/search
|
precreate-core search /opt/solr/server/solr/configsets/dspace/search
|
||||||
|
cp -r -u /opt/solr/server/solr/configsets/dspace/search/* search
|
||||||
precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics
|
precreate-core statistics /opt/solr/server/solr/configsets/dspace/statistics
|
||||||
|
cp -r -u /opt/solr/server/solr/configsets/dspace/statistics/* statistics
|
||||||
exec solr -f
|
exec solr -f
|
||||||
volumes:
|
volumes:
|
||||||
assetstore:
|
assetstore:
|
||||||
|
@@ -16,11 +16,15 @@ services:
|
|||||||
dspace-angular:
|
dspace-angular:
|
||||||
container_name: dspace-angular
|
container_name: dspace-angular
|
||||||
environment:
|
environment:
|
||||||
DSPACE_HOST: dspace-angular
|
DSPACE_UI_SSL: 'false'
|
||||||
DSPACE_NAMESPACE: /
|
DSPACE_UI_HOST: dspace-angular
|
||||||
DSPACE_PORT: '4000'
|
DSPACE_UI_PORT: '4000'
|
||||||
DSPACE_SSL: "false"
|
DSPACE_UI_NAMESPACE: /
|
||||||
image: dspace/dspace-angular:latest
|
DSPACE_REST_SSL: 'false'
|
||||||
|
DSPACE_REST_HOST: localhost
|
||||||
|
DSPACE_REST_PORT: 8080
|
||||||
|
DSPACE_REST_NAMESPACE: /server
|
||||||
|
image: dspace/dspace-angular:dspace-7_x
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
@@ -33,5 +37,3 @@ services:
|
|||||||
target: 9876
|
target: 9876
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
volumes:
|
|
||||||
- ./environment.dev.ts:/app/src/environments/environment.dev.ts
|
|
||||||
|
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* 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/
|
|
||||||
*/
|
|
||||||
// This file is based on environment.template.ts provided by Angular UI
|
|
||||||
export const environment = {
|
|
||||||
// Default to using the local REST API (running in Docker)
|
|
||||||
rest: {
|
|
||||||
ssl: false,
|
|
||||||
host: 'localhost',
|
|
||||||
port: 8080,
|
|
||||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
|
||||||
nameSpace: '/server'
|
|
||||||
}
|
|
||||||
};
|
|
@@ -1,6 +0,0 @@
|
|||||||
dspace.dir=/dspace
|
|
||||||
db.url=jdbc:postgresql://dspacedb:5432/dspace
|
|
||||||
dspace.server.url=http://localhost:8080/server
|
|
||||||
dspace.ui.url=http://localhost:4000
|
|
||||||
dspace.name=DSpace Started with Docker Compose
|
|
||||||
solr.server=http://dspacesolr:8983/solr
|
|
@@ -1,26 +1,30 @@
|
|||||||
# Configuration
|
# Configuration
|
||||||
|
|
||||||
Default configuration file is located in `src/environments/` folder. All configuration options should be listed in the default configuration file `src/environments/environment.common.ts`. Please do not change this file directly! To change the default configuration values, create local files that override the parameters you need to change. You can use `environment.template.ts` as a starting point.
|
Default configuration file is located at `config/config.yml`. All configuration options should be listed in the default typescript file `src/config/default-app-config.ts`. Please do not change this file directly! To override the default configuration values, create local files that override the parameters you need to change. You can use `config.example.yml` as a starting point.
|
||||||
|
|
||||||
- Create a new `environment.dev.ts` file in `src/environments/` for `development` environment;
|
- Create a new `config.(dev or development).yml` file in `config/` for `development` environment;
|
||||||
- Create a new `environment.prod.ts` file in `src/environments/` for `production` environment;
|
- Create a new `config.(prod or production).yml` file in `config/` for `production` environment;
|
||||||
|
|
||||||
Some few configuration options can be overridden by setting environment variables. These and the variable names are listed below.
|
Alternatively, create a desired app config file at an external location and set the path as environment variable `DSPACE_APP_CONFIG_PATH`.
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
```
|
||||||
|
DSPACE_APP_CONFIG_PATH=/usr/local/dspace/config/config.yml
|
||||||
|
```
|
||||||
|
|
||||||
|
Configuration options can be overridden by setting environment variables.
|
||||||
|
|
||||||
## Nodejs server
|
## Nodejs server
|
||||||
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itsself to, and if ssl should be enabled not. By default it listens on `localhost:4000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
|
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itsself to, and if ssl should be enabled not. By default it listens on `localhost:4000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
|
||||||
|
|
||||||
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
||||||
```
|
|
||||||
export const environment = {
|
```yaml
|
||||||
// Angular UI settings.
|
ui:
|
||||||
ui: {
|
ssl: false
|
||||||
ssl: false,
|
host: localhost
|
||||||
host: 'localhost',
|
port: 4000
|
||||||
port: 4000,
|
nameSpace: /
|
||||||
nameSpace: '/'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||||
@@ -30,21 +34,24 @@ Alternately you can set the following environment variables. If any of these are
|
|||||||
DSPACE_PORT=4000
|
DSPACE_PORT=4000
|
||||||
DSPACE_NAMESPACE=/
|
DSPACE_NAMESPACE=/
|
||||||
```
|
```
|
||||||
|
or
|
||||||
|
```
|
||||||
|
DSPACE_UI_SSL=true
|
||||||
|
DSPACE_UI_HOST=localhost
|
||||||
|
DSPACE_UI_PORT=4000
|
||||||
|
DSPACE_UI_NAMESPACE=/
|
||||||
|
```
|
||||||
|
|
||||||
## DSpace's REST endpoint
|
## DSpace's REST endpoint
|
||||||
dspace-angular connects to your DSpace installation by using its REST endpoint. To do so, you have to define the ip address, port and if ssl should be enabled. You can do this in a configuration file (see above) by adding the following options:
|
dspace-angular connects to your DSpace installation by using its REST endpoint. To do so, you have to define the ip address, port and if ssl should be enabled. You can do this in a configuration file (see above) by adding the following options:
|
||||||
|
|
||||||
```
|
```yaml
|
||||||
export const environment = {
|
rest:
|
||||||
// The REST API server settings.
|
ssl: true
|
||||||
rest: {
|
host: api7.dspace.org
|
||||||
ssl: true,
|
port: 443
|
||||||
host: 'api7.dspace.org',
|
nameSpace: /server
|
||||||
port: 443,
|
}
|
||||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
|
||||||
nameSpace: '/server'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||||
@@ -55,6 +62,21 @@ Alternately you can set the following environment variables. If any of these are
|
|||||||
DSPACE_REST_NAMESPACE=/server
|
DSPACE_REST_NAMESPACE=/server
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Environment variable naming convention
|
||||||
|
|
||||||
|
Settings can be set using the following convention for naming the environment variables:
|
||||||
|
|
||||||
|
1. replace all `.` with `_`
|
||||||
|
2. convert all characters to upper case
|
||||||
|
3. prefix with `DSPACE_`
|
||||||
|
|
||||||
|
e.g.
|
||||||
|
|
||||||
|
```
|
||||||
|
cache.msToLive.default => DSPACE_CACHE_MSTOLIVE_DEFAULT
|
||||||
|
auth.ui.timeUntilIdle => DSPACE_AUTH_UI_TIMEUNTILIDLE
|
||||||
|
```
|
||||||
|
|
||||||
## Supporting analytics services other than Google Analytics
|
## Supporting analytics services other than Google Analytics
|
||||||
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.
|
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.
|
||||||
|
|
||||||
|
@@ -22,7 +22,7 @@ module.exports = function (config) {
|
|||||||
reports: ['html', 'lcovonly', 'text-summary'],
|
reports: ['html', 'lcovonly', 'text-summary'],
|
||||||
fixWebpackSourcePaths: true
|
fixWebpackSourcePaths: true
|
||||||
},
|
},
|
||||||
reporters: ['mocha', 'kjhtml'],
|
reporters: ['mocha', 'kjhtml', 'coverage-istanbul'],
|
||||||
mochaReporter: {
|
mochaReporter: {
|
||||||
ignoreSkipped: true,
|
ignoreSkipped: true,
|
||||||
output: 'autowatch'
|
output: 'autowatch'
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"watch": ["src/environments/mock-environment.ts"],
|
|
||||||
"ext": "ts",
|
|
||||||
"exec": "ts-node --project ./tsconfig.ts-node.json scripts/set-mock-env.ts"
|
|
||||||
}
|
|
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"watch": ["src/environments"],
|
"watch": [
|
||||||
"ext": "ts",
|
"config"
|
||||||
"ignore": ["src/environments/environment.ts", "src/environments/mock-environment.ts"],
|
],
|
||||||
"exec": "ts-node --project ./tsconfig.ts-node.json scripts/set-env.ts --dev"
|
"ext": "json"
|
||||||
}
|
}
|
||||||
|
234
package.json
234
package.json
@@ -1,56 +1,45 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "0.0.0",
|
"version": "7.4.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:dev": "ts-node --project ./tsconfig.ts-node.json scripts/set-env.ts --dev",
|
"config:watch": "nodemon",
|
||||||
"config:prod": "ts-node --project ./tsconfig.ts-node.json scripts/set-env.ts --prod",
|
"test:rest": "ts-node --project ./tsconfig.ts-node.json scripts/test-rest.ts",
|
||||||
"config:test": "ts-node --project ./tsconfig.ts-node.json scripts/set-mock-env.ts",
|
|
||||||
"config:test:watch": "nodemon --config mock-nodemon.json",
|
|
||||||
"config:dev:watch": "nodemon",
|
|
||||||
"config:check:rest": "yarn run config:prod && ts-node --project ./tsconfig.ts-node.json scripts/test-rest.ts",
|
|
||||||
"prestart:dev": "yarn run config:dev",
|
|
||||||
"prebuild": "yarn run config:dev",
|
|
||||||
"pretest": "yarn run config:test",
|
|
||||||
"pretest:watch": "yarn run config:test",
|
|
||||||
"pretest:headless": "yarn run config:test",
|
|
||||||
"prebuild:prod": "yarn run config:prod",
|
|
||||||
"pree2e": "yarn run config:prod",
|
|
||||||
"start": "yarn run start:prod",
|
"start": "yarn run start:prod",
|
||||||
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
"start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"",
|
||||||
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
"start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr",
|
||||||
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
|
||||||
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
|
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
|
||||||
|
"preserve": "yarn base-href",
|
||||||
|
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||||
|
"serve:ssr": "node dist/server/main",
|
||||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||||
"build": "ng build",
|
"build": "ng build --configuration development",
|
||||||
"build:stats": "ng build --stats-json",
|
"build:stats": "ng build --stats-json",
|
||||||
"build:prod": "yarn run build:ssr",
|
"build:prod": "yarn run build:ssr",
|
||||||
"build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server",
|
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||||
"build:client-and-server-bundles": "ng build --prod && ng run dspace-angular:server:production --bundleDependencies true",
|
"test": "ng test --sourceMap=true --watch=false --configuration test",
|
||||||
"test:watch": "npm-run-all --parallel config:test:watch test",
|
"test:watch": "nodemon --exec \"ng test --sourceMap=true --watch=true --configuration test\"",
|
||||||
"test": "ng test --sourceMap=true --watch=true",
|
"test:headless": "ng test --sourceMap=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||||
"test:headless": "ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
|
|
||||||
"lint": "ng lint",
|
"lint": "ng lint",
|
||||||
"lint-fix": "ng lint --fix=true",
|
"lint-fix": "ng lint --fix=true",
|
||||||
"e2e": "ng e2e",
|
"e2e": "ng e2e",
|
||||||
"compile:server": "webpack --config webpack.server.config.js --progress --color",
|
"clean:dev:config": "rimraf src/assets/config.json",
|
||||||
"serve:ssr": "node dist/server",
|
|
||||||
"clean:coverage": "rimraf coverage",
|
"clean:coverage": "rimraf coverage",
|
||||||
"clean:dist": "rimraf dist",
|
"clean:dist": "rimraf dist",
|
||||||
"clean:doc": "rimraf doc",
|
"clean:doc": "rimraf doc",
|
||||||
"clean:log": "rimraf *.log*",
|
"clean:log": "rimraf *.log*",
|
||||||
"clean:json": "rimraf *.records.json",
|
"clean:json": "rimraf *.records.json",
|
||||||
"clean:bld": "rimraf build",
|
|
||||||
"clean:node": "rimraf node_modules",
|
"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 && yarn run clean:bld",
|
"clean:prod": "yarn run clean:dist && yarn run clean:log && yarn run clean:doc && yarn run clean:coverage && yarn run clean:json",
|
||||||
"clean": "yarn run clean:prod && yarn run clean:env && yarn run clean:node",
|
"clean": "yarn run clean:prod && yarn run clean:dev:config && yarn run clean:node",
|
||||||
"clean:env": "rimraf src/environments/environment.ts",
|
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||||
"sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
|
||||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||||
"merge-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||||
"postinstall": "ngcc",
|
|
||||||
"cypress:open": "cypress open",
|
"cypress:open": "cypress open",
|
||||||
"cypress:run": "cypress run"
|
"cypress:run": "cypress run",
|
||||||
|
"env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts",
|
||||||
|
"base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts",
|
||||||
|
"check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./"
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
"fs": false,
|
"fs": false,
|
||||||
@@ -61,41 +50,45 @@
|
|||||||
"private": true,
|
"private": true,
|
||||||
"resolutions": {
|
"resolutions": {
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
"webdriver-manager": "^12.1.8"
|
"webdriver-manager": "^12.1.8",
|
||||||
|
"ts-node": "10.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "~10.2.3",
|
"@angular/animations": "~13.2.6",
|
||||||
"@angular/cdk": "^10.2.6",
|
"@angular/cdk": "^13.2.6",
|
||||||
"@angular/common": "~10.2.3",
|
"@angular/common": "~13.2.6",
|
||||||
"@angular/compiler": "~10.2.3",
|
"@angular/compiler": "~13.2.6",
|
||||||
"@angular/core": "~10.2.3",
|
"@angular/core": "~13.2.6",
|
||||||
"@angular/forms": "~10.2.3",
|
"@angular/forms": "~13.2.6",
|
||||||
"@angular/localize": "10.2.3",
|
"@angular/localize": "13.2.6",
|
||||||
"@angular/platform-browser": "~10.2.3",
|
"@angular/platform-browser": "~13.2.6",
|
||||||
"@angular/platform-browser-dynamic": "~10.2.3",
|
"@angular/platform-browser-dynamic": "~13.2.6",
|
||||||
"@angular/platform-server": "~10.2.3",
|
"@angular/platform-server": "~13.2.6",
|
||||||
"@angular/router": "~10.2.3",
|
"@angular/router": "~13.2.6",
|
||||||
"@angularclass/bootloader": "1.0.1",
|
"@babel/runtime": "^7.17.2",
|
||||||
"@kolkov/ngx-gallery": "^1.2.3",
|
"@kolkov/ngx-gallery": "^2.0.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "7.0.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
"@ng-dynamic-forms/core": "^12.0.0",
|
"@material-ui/icons": "^4.9.1",
|
||||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^12.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@ngrx/effects": "^10.0.1",
|
"@ng-dynamic-forms/core": "^15.0.0",
|
||||||
"@ngrx/router-store": "^10.0.1",
|
"@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0",
|
||||||
"@ngrx/store": "^10.0.1",
|
"@ngrx/effects": "^13.0.2",
|
||||||
"@nguniversal/express-engine": "10.1.0",
|
"@ngrx/router-store": "^13.0.2",
|
||||||
|
"@ngrx/store": "^13.0.2",
|
||||||
|
"@nguniversal/express-engine": "^13.0.2",
|
||||||
"@ngx-translate/core": "^13.0.0",
|
"@ngx-translate/core": "^13.0.0",
|
||||||
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
||||||
|
"@types/grecaptcha": "^3.0.4",
|
||||||
"angular-idle-preload": "3.0.0",
|
"angular-idle-preload": "3.0.0",
|
||||||
"angular2-text-mask": "9.0.0",
|
"angulartics2": "^12.0.0",
|
||||||
"angulartics2": "^10.0.0",
|
"axios": "^0.27.2",
|
||||||
"bootstrap": "4.3.1",
|
"bootstrap": "4.3.1",
|
||||||
"caniuse-lite": "^1.0.30001165",
|
"caniuse-lite": "^1.0.30001165",
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
"cli-progress": "^3.8.0",
|
"cli-progress": "^3.8.0",
|
||||||
|
"compression": "^1.7.4",
|
||||||
"cookie-parser": "1.4.5",
|
"cookie-parser": "1.4.5",
|
||||||
"core-js": "^3.7.0",
|
"core-js": "^3.7.0",
|
||||||
"debug-loader": "^0.0.1",
|
|
||||||
"deepmerge": "^4.2.2",
|
"deepmerge": "^4.2.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
"express-rate-limit": "^5.1.3",
|
"express-rate-limit": "^5.1.3",
|
||||||
@@ -103,102 +96,127 @@
|
|||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"font-awesome": "4.7.0",
|
"font-awesome": "4.7.0",
|
||||||
|
"http-proxy-middleware": "^1.0.5",
|
||||||
"https": "1.0.0",
|
"https": "1.0.0",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
|
"js-yaml": "^4.1.0",
|
||||||
"json5": "^2.1.3",
|
"json5": "^2.1.3",
|
||||||
"jsonschema": "1.4.0",
|
"jsonschema": "1.4.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.10",
|
"klaro": "^0.7.10",
|
||||||
"mirador": "^3.0.0",
|
"lodash": "^4.17.21",
|
||||||
|
"markdown-it": "^13.0.1",
|
||||||
|
"markdown-it-mathjax3": "^4.3.1",
|
||||||
|
"mirador": "^3.3.0",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.10.0",
|
"mirador-share-plugin": "^0.11.0",
|
||||||
"moment": "^2.29.1",
|
"moment": "^2.29.4",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"ng-mocks": "10.5.4",
|
"ng-mocks": "^13.1.1",
|
||||||
"ng2-file-upload": "1.4.0",
|
"ng2-file-upload": "1.4.0",
|
||||||
"ng2-nouislider": "^1.8.2",
|
"ng2-nouislider": "^1.8.3",
|
||||||
"ngx-infinite-scroll": "^10.0.1",
|
"ngx-infinite-scroll": "^10.0.1",
|
||||||
"ngx-moment": "^5.0.0",
|
"ngx-moment": "^5.0.0",
|
||||||
"ngx-pagination": "5.0.0",
|
"ngx-pagination": "5.0.0",
|
||||||
"ngx-sortablejs": "^10.0.0",
|
"ngx-sortablejs": "^11.1.0",
|
||||||
|
"ngx-ui-switch": "^11.0.1",
|
||||||
"nouislider": "^14.6.3",
|
"nouislider": "^14.6.3",
|
||||||
"pem": "1.14.4",
|
"pem": "1.14.4",
|
||||||
"postcss-cli": "^8.3.0",
|
"postcss-cli": "^9.1.0",
|
||||||
"react": "^16.14.0",
|
"prop-types": "^15.7.2",
|
||||||
"react-dom": "^16.14.0",
|
"react-copy-to-clipboard": "^5.0.1",
|
||||||
"reflect-metadata": "^0.1.13",
|
"reflect-metadata": "^0.1.13",
|
||||||
"rxjs": "^6.6.3",
|
"rxjs": "^7.5.5",
|
||||||
"rxjs-spy": "^7.5.3",
|
"sanitize-html": "^2.7.2",
|
||||||
"sass-resources-loader": "^2.1.1",
|
|
||||||
"sortablejs": "1.13.0",
|
"sortablejs": "1.13.0",
|
||||||
"tslib": "^2.0.0",
|
"tslib": "^2.0.0",
|
||||||
|
"url-parse": "^1.5.6",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
"webfontloader": "1.6.28",
|
"webfontloader": "1.6.28",
|
||||||
"zone.js": "^0.10.3"
|
"zone.js": "~0.11.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "10.0.1",
|
"@angular-builders/custom-webpack": "~13.1.0",
|
||||||
"@angular-devkit/build-angular": "~0.1002.0",
|
"@angular-devkit/build-angular": "~13.2.6",
|
||||||
"@angular/cli": "~10.2.0",
|
"@angular-eslint/builder": "13.1.0",
|
||||||
"@angular/compiler-cli": "~10.2.3",
|
"@angular-eslint/eslint-plugin": "13.1.0",
|
||||||
"@angular/language-service": "~10.2.3",
|
"@angular-eslint/eslint-plugin-template": "13.1.0",
|
||||||
|
"@angular-eslint/schematics": "13.1.0",
|
||||||
|
"@angular-eslint/template-parser": "13.1.0",
|
||||||
|
"@angular/cli": "~13.2.6",
|
||||||
|
"@angular/compiler-cli": "~13.2.6",
|
||||||
|
"@angular/language-service": "~13.2.6",
|
||||||
"@cypress/schematic": "^1.5.0",
|
"@cypress/schematic": "^1.5.0",
|
||||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||||
"@ngrx/store-devtools": "^10.0.1",
|
"@ngrx/store-devtools": "^13.0.2",
|
||||||
"@ngtools/webpack": "10.2.0",
|
"@ngtools/webpack": "^13.2.6",
|
||||||
"@nguniversal/builders": "~10.1.0",
|
"@nguniversal/builders": "^13.0.2",
|
||||||
"@types/deep-freeze": "0.1.2",
|
"@types/deep-freeze": "0.1.2",
|
||||||
"@types/express": "^4.17.9",
|
"@types/express": "^4.17.9",
|
||||||
"@types/file-saver": "^2.0.1",
|
"@types/file-saver": "^2.0.1",
|
||||||
"@types/jasmine": "^3.6.2",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/jasminewd2": "~2.0.8",
|
"@types/jasminewd2": "~2.0.8",
|
||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.14.165",
|
"@types/lodash": "^4.14.165",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.14.9",
|
||||||
|
"@types/sanitize-html": "^2.6.2",
|
||||||
|
"@typescript-eslint/eslint-plugin": "5.11.0",
|
||||||
|
"@typescript-eslint/parser": "5.11.0",
|
||||||
"axe-core": "^4.3.3",
|
"axe-core": "^4.3.3",
|
||||||
"codelyzer": "^6.0.1",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"compression-webpack-plugin": "^3.0.1",
|
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"css-loader": "3.4.0",
|
"cross-env": "^7.0.3",
|
||||||
"cssnano": "^4.1.10",
|
"css-loader": "^6.2.0",
|
||||||
"cypress": "8.6.0",
|
"css-minimizer-webpack-plugin": "^3.4.1",
|
||||||
"cypress-axe": "^0.13.0",
|
"cssnano": "^5.0.6",
|
||||||
|
"cypress": "9.5.1",
|
||||||
|
"cypress-axe": "^0.14.0",
|
||||||
|
"debug-loader": "^0.0.1",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"dotenv": "^8.2.0",
|
"dotenv": "^8.2.0",
|
||||||
|
"eslint": "^8.2.0",
|
||||||
|
"eslint-plugin-deprecation": "^1.3.2",
|
||||||
|
"eslint-plugin-import": "^2.25.4",
|
||||||
|
"eslint-plugin-jsdoc": "^38.0.6",
|
||||||
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
|
"express-static-gzip": "^2.1.5",
|
||||||
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
||||||
"html-loader": "^1.3.2",
|
"html-loader": "^1.3.2",
|
||||||
"html-webpack-plugin": "^4.5.0",
|
"jasmine-core": "^3.8.0",
|
||||||
"http-proxy-middleware": "^1.0.5",
|
"jasmine-marbles": "0.9.2",
|
||||||
"jasmine-core": "^3.6.0",
|
"jasmine-spec-reporter": "~5.0.0",
|
||||||
"jasmine-marbles": "0.6.0",
|
"karma": "^6.3.14",
|
||||||
"jasmine-spec-reporter": "^6.0.0",
|
"karma-chrome-launcher": "~3.1.0",
|
||||||
"karma": "^5.2.3",
|
|
||||||
"karma-chrome-launcher": "^3.1.0",
|
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||||
"karma-jasmine": "^4.0.1",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.4",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"nodemon": "^2.0.2",
|
"ngx-mask": "^13.1.7",
|
||||||
"npm-run-all": "^4.1.5",
|
"nodemon": "^2.0.15",
|
||||||
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
"postcss": "^8.1",
|
||||||
"postcss-apply": "0.11.0",
|
"postcss-apply": "0.12.0",
|
||||||
"postcss-import": "^12.0.1",
|
"postcss-import": "^14.0.0",
|
||||||
"postcss-loader": "^3.0.0",
|
"postcss-loader": "^4.0.3",
|
||||||
"postcss-preset-env": "6.7.0",
|
"postcss-preset-env": "^7.4.2",
|
||||||
"postcss-responsive-type": "1.0.0",
|
"postcss-responsive-type": "1.0.0",
|
||||||
"protractor": "^7.0.0",
|
"protractor": "^7.0.0",
|
||||||
"protractor-istanbul-plugin": "2.0.0",
|
"protractor-istanbul-plugin": "2.0.0",
|
||||||
"raw-loader": "0.5.1",
|
"raw-loader": "0.5.1",
|
||||||
|
"react": "^16.14.0",
|
||||||
|
"react-dom": "^16.14.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"script-ext-html-webpack-plugin": "2.1.5",
|
"rxjs-spy": "^8.0.2",
|
||||||
"string-replace-loader": "^2.3.0",
|
"sass": "~1.32.6",
|
||||||
|
"sass-loader": "^12.6.0",
|
||||||
|
"sass-resources-loader": "^2.1.1",
|
||||||
|
"string-replace-loader": "^3.1.0",
|
||||||
"terser-webpack-plugin": "^2.3.1",
|
"terser-webpack-plugin": "^2.3.1",
|
||||||
"ts-loader": "^5.2.0",
|
"ts-loader": "^5.2.0",
|
||||||
"ts-node": "^8.8.1",
|
"ts-node": "^8.10.2",
|
||||||
"tslint": "^6.1.3",
|
"typescript": "~4.5.5",
|
||||||
"typescript": "~4.0.5",
|
"webpack": "^5.69.1",
|
||||||
"webpack": "^4.44.2",
|
|
||||||
"webpack-bundle-analyzer": "^4.4.0",
|
"webpack-bundle-analyzer": "^4.4.0",
|
||||||
"webpack-cli": "^4.2.0",
|
"webpack-cli": "^4.2.0",
|
||||||
"webpack-node-externals": "1.7.2"
|
"webpack-dev-server": "^4.5.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
36
scripts/base-href.ts
Normal file
36
scripts/base-href.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { AppConfig } from '../src/config/app-config.interface';
|
||||||
|
import { buildAppConfig } from '../src/config/config.server';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to set baseHref as `ui.nameSpace` for development mode. Adds `baseHref` to angular.json build options.
|
||||||
|
*
|
||||||
|
* Usage (see package.json):
|
||||||
|
*
|
||||||
|
* yarn base-href
|
||||||
|
*/
|
||||||
|
|
||||||
|
const appConfig: AppConfig = buildAppConfig();
|
||||||
|
|
||||||
|
const angularJsonPath = join(process.cwd(), 'angular.json');
|
||||||
|
|
||||||
|
if (!fs.existsSync(angularJsonPath)) {
|
||||||
|
console.error(`Error:\n${angularJsonPath} does not exist\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const angularJson = require(angularJsonPath);
|
||||||
|
|
||||||
|
const baseHref = `${appConfig.ui.nameSpace}${appConfig.ui.nameSpace.endsWith('/') ? '' : '/'}`;
|
||||||
|
|
||||||
|
console.log(`Setting baseHref to ${baseHref} in angular.json`);
|
||||||
|
|
||||||
|
angularJson.projects['dspace-angular'].architect.build.options.baseHref = baseHref;
|
||||||
|
|
||||||
|
fs.writeFileSync(angularJsonPath, JSON.stringify(angularJson, null, 2) + '\n');
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
39
scripts/env-to-yaml.ts
Normal file
39
scripts/env-to-yaml.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import * as fs from 'fs';
|
||||||
|
import * as yaml from 'js-yaml';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Script to help convert previous version environment.*.ts to yaml.
|
||||||
|
*
|
||||||
|
* Usage (see package.json):
|
||||||
|
*
|
||||||
|
* yarn env:yaml [relative path to environment.ts file] (optional relative path to write yaml file) *
|
||||||
|
*/
|
||||||
|
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
if (args[0] === undefined) {
|
||||||
|
console.log(`Usage:\n\tyarn env:yaml [relative path to environment.ts file] (optional relative path to write yaml file)\n`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
const envFullPath = join(process.cwd(), args[0]);
|
||||||
|
|
||||||
|
if (!fs.existsSync(envFullPath)) {
|
||||||
|
console.error(`Error:\n${envFullPath} does not exist\n`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const env = require(envFullPath).environment;
|
||||||
|
|
||||||
|
const config = yaml.dump(env);
|
||||||
|
if (args[1]) {
|
||||||
|
const ymlFullPath = join(process.cwd(), args[1]);
|
||||||
|
fs.writeFileSync(ymlFullPath, config);
|
||||||
|
} else {
|
||||||
|
console.log(config);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
|
@@ -1,11 +1,15 @@
|
|||||||
import { environment } from '../src/environments/environment';
|
|
||||||
|
|
||||||
import * as child from 'child_process';
|
import * as child from 'child_process';
|
||||||
|
|
||||||
|
import { AppConfig } from '../src/config/app-config.interface';
|
||||||
|
import { buildAppConfig } from '../src/config/config.server';
|
||||||
|
|
||||||
|
const appConfig: AppConfig = buildAppConfig();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calls `ng serve` with the following arguments configured for the UI in the environment file: host, port, nameSpace, ssl
|
* Calls `ng serve` with the following arguments configured for the UI in the app config: host, port, nameSpace, ssl
|
||||||
|
* Any CLI arguments given to this script are patched through to `ng serve` as well.
|
||||||
*/
|
*/
|
||||||
child.spawn(
|
child.spawn(
|
||||||
`ng serve --host ${environment.ui.host} --port ${environment.ui.port} --servePath ${environment.ui.nameSpace} --ssl ${environment.ui.ssl}`,
|
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl} ${process.argv.slice(2).join(' ')} --configuration development`,
|
||||||
{ stdio:'inherit', shell: true }
|
{ stdio: 'inherit', shell: true }
|
||||||
);
|
);
|
||||||
|
@@ -1,116 +0,0 @@
|
|||||||
import { writeFile } from 'fs';
|
|
||||||
import { environment as commonEnv } from '../src/environments/environment.common';
|
|
||||||
import { GlobalConfig } from '../src/config/global-config.interface';
|
|
||||||
import { ServerConfig } from '../src/config/server-config.interface';
|
|
||||||
import { hasValue } from '../src/app/shared/empty.util';
|
|
||||||
|
|
||||||
// Configure Angular `environment.ts` file path
|
|
||||||
const targetPath = './src/environments/environment.ts';
|
|
||||||
// Load node modules
|
|
||||||
const colors = require('colors');
|
|
||||||
require('dotenv').config();
|
|
||||||
const merge = require('deepmerge');
|
|
||||||
const mergeOptions = { arrayMerge: (destinationArray, sourceArray, options) => sourceArray };
|
|
||||||
const environment = process.argv[2];
|
|
||||||
let environmentFilePath;
|
|
||||||
let production = false;
|
|
||||||
|
|
||||||
switch (environment) {
|
|
||||||
case '--prod':
|
|
||||||
case '--production':
|
|
||||||
production = true;
|
|
||||||
console.log(`Building ${colors.red.bold(`production`)} environment`);
|
|
||||||
environmentFilePath = '../src/environments/environment.prod.ts';
|
|
||||||
break;
|
|
||||||
case '--test':
|
|
||||||
console.log(`Building ${colors.blue.bold(`test`)} environment`);
|
|
||||||
environmentFilePath = '../src/environments/environment.test.ts';
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.log(`Building ${colors.green.bold(`development`)} environment`);
|
|
||||||
environmentFilePath = '../src/environments/environment.dev.ts';
|
|
||||||
}
|
|
||||||
|
|
||||||
const processEnv = {
|
|
||||||
ui: createServerConfig(
|
|
||||||
process.env.DSPACE_HOST,
|
|
||||||
process.env.DSPACE_PORT,
|
|
||||||
process.env.DSPACE_NAMESPACE,
|
|
||||||
process.env.DSPACE_SSL),
|
|
||||||
rest: createServerConfig(
|
|
||||||
process.env.DSPACE_REST_HOST,
|
|
||||||
process.env.DSPACE_REST_PORT,
|
|
||||||
process.env.DSPACE_REST_NAMESPACE,
|
|
||||||
process.env.DSPACE_REST_SSL)
|
|
||||||
} as GlobalConfig;
|
|
||||||
|
|
||||||
import(environmentFilePath)
|
|
||||||
.then((file) => generateEnvironmentFile(merge.all([commonEnv, file.environment, processEnv], mergeOptions)))
|
|
||||||
.catch(() => {
|
|
||||||
console.log(colors.yellow.bold(`No specific environment file found for ` + environment));
|
|
||||||
generateEnvironmentFile(merge(commonEnv, processEnv, mergeOptions))
|
|
||||||
});
|
|
||||||
|
|
||||||
function generateEnvironmentFile(file: GlobalConfig): void {
|
|
||||||
file.production = production;
|
|
||||||
buildBaseUrls(file);
|
|
||||||
const contents = `export const environment = ` + JSON.stringify(file);
|
|
||||||
writeFile(targetPath, contents, (err) => {
|
|
||||||
if (err) {
|
|
||||||
throw console.error(err);
|
|
||||||
} else {
|
|
||||||
console.log(`Angular ${colors.bold('environment.ts')} file generated correctly at ${colors.bold(targetPath)} \n`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// allow to override a few important options by environment variables
|
|
||||||
function createServerConfig(host?: string, port?: string, nameSpace?: string, ssl?: string): ServerConfig {
|
|
||||||
const result = {} as any;
|
|
||||||
if (hasValue(host)) {
|
|
||||||
result.host = host;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(nameSpace)) {
|
|
||||||
result.nameSpace = nameSpace;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(port)) {
|
|
||||||
result.port = Number(port);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasValue(ssl)) {
|
|
||||||
result.ssl = ssl.trim().match(/^(true|1|yes)$/i) ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildBaseUrls(config: GlobalConfig): void {
|
|
||||||
for (const key in config) {
|
|
||||||
if (config.hasOwnProperty(key) && config[key].host) {
|
|
||||||
config[key].baseUrl = [
|
|
||||||
getProtocol(config[key].ssl),
|
|
||||||
getHost(config[key].host),
|
|
||||||
getPort(config[key].port),
|
|
||||||
getNameSpace(config[key].nameSpace)
|
|
||||||
].join('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProtocol(ssl: boolean): string {
|
|
||||||
return ssl ? 'https://' : 'http://';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getHost(host: string): string {
|
|
||||||
return host;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getPort(port: number): string {
|
|
||||||
return port ? (port !== 80 && port !== 443) ? ':' + port : '' : '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getNameSpace(nameSpace: string): string {
|
|
||||||
return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
|
|
||||||
}
|
|
@@ -1,11 +0,0 @@
|
|||||||
import { copyFile } from 'fs';
|
|
||||||
|
|
||||||
// Configure Angular `environment.ts` file path
|
|
||||||
const sourcePath = './src/environments/mock-environment.ts';
|
|
||||||
const targetPath = './src/environments/environment.ts';
|
|
||||||
|
|
||||||
// destination.txt will be created or overwritten by default.
|
|
||||||
copyFile(sourcePath, targetPath, (err) => {
|
|
||||||
if (err) throw err;
|
|
||||||
console.log(sourcePath + ' was copied to ' + targetPath);
|
|
||||||
});
|
|
10
scripts/sync-i18n-files.ts
Executable file → Normal file
10
scripts/sync-i18n-files.ts
Executable file → Normal file
@@ -1,4 +1,5 @@
|
|||||||
import { projectRoot} from '../webpack/helpers';
|
import { projectRoot } from '../webpack/helpers';
|
||||||
|
|
||||||
const commander = require('commander');
|
const commander = require('commander');
|
||||||
const fs = require('fs');
|
const fs = require('fs');
|
||||||
const JSON5 = require('json5');
|
const JSON5 = require('json5');
|
||||||
@@ -119,7 +120,7 @@ function syncFileWithSource(pathToTargetFile, pathToOutputFile) {
|
|||||||
outputChunks.forEach(function (chunk) {
|
outputChunks.forEach(function (chunk) {
|
||||||
progressBar.increment();
|
progressBar.increment();
|
||||||
chunk.split("\n").forEach(function (line) {
|
chunk.split("\n").forEach(function (line) {
|
||||||
file.write(" " + line + "\n");
|
file.write((line === '' ? '' : ` ${line}`) + "\n");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
file.write("\n}");
|
file.write("\n}");
|
||||||
@@ -192,7 +193,10 @@ function createNewChunkComparingSourceAndTarget(correspondingTargetChunk, source
|
|||||||
|
|
||||||
const targetList = correspondingTargetChunk.split("\n");
|
const targetList = correspondingTargetChunk.split("\n");
|
||||||
const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*");
|
const oldKeyValueInTargetComments = getSubStringWithRegex(correspondingTargetChunk, "\\s*\\/\\/\\s*\".*");
|
||||||
const keyValueTarget = targetList[targetList.length - 1];
|
let keyValueTarget = targetList[targetList.length - 1];
|
||||||
|
if (!keyValueTarget.endsWith(",")) {
|
||||||
|
keyValueTarget = keyValueTarget + ",";
|
||||||
|
}
|
||||||
|
|
||||||
if (oldKeyValueInTargetComments != null) {
|
if (oldKeyValueInTargetComments != null) {
|
||||||
const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0];
|
const oldKeyValueUncommented = getSubStringWithRegex(oldKeyValueInTargetComments[0], "\".*")[0];
|
||||||
|
@@ -1,24 +1,34 @@
|
|||||||
import * as http from 'http';
|
import * as http from 'http';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import { environment } from '../src/environments/environment';
|
|
||||||
|
import { AppConfig } from '../src/config/app-config.interface';
|
||||||
|
import { buildAppConfig } from '../src/config/config.server';
|
||||||
|
|
||||||
|
const appConfig: AppConfig = buildAppConfig();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Script to test the connection with the configured REST API (in the 'rest' settings of your environment.*.ts)
|
* Script to test the connection with the configured REST API (in the 'rest' settings of your config.*.yaml)
|
||||||
*
|
*
|
||||||
* This script is useful to test for any Node.js connection issues with your REST API.
|
* This script is useful to test for any Node.js connection issues with your REST API.
|
||||||
*
|
*
|
||||||
* Usage (see package.json): yarn test:rest-api
|
* Usage (see package.json): yarn test:rest
|
||||||
*/
|
*/
|
||||||
|
|
||||||
// Get root URL of configured REST API
|
// Get root URL of configured REST API
|
||||||
const restUrl = environment.rest.baseUrl + '/api';
|
const restUrl = appConfig.rest.baseUrl + '/api';
|
||||||
console.log(`...Testing connection to REST API at ${restUrl}...\n`);
|
console.log(`...Testing connection to REST API at ${restUrl}...\n`);
|
||||||
|
|
||||||
// If SSL enabled, test via HTTPS, else via HTTP
|
// If SSL enabled, test via HTTPS, else via HTTP
|
||||||
if (environment.rest.ssl) {
|
if (appConfig.rest.ssl) {
|
||||||
const req = https.request(restUrl, (res) => {
|
const req = https.request(restUrl, (res) => {
|
||||||
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
||||||
res.on('data', (data) => {
|
// We will keep reading data until the 'end' event fires.
|
||||||
|
// This ensures we don't just read the first chunk.
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
checkJSONResponse(data);
|
checkJSONResponse(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -31,7 +41,13 @@ if (environment.rest.ssl) {
|
|||||||
} else {
|
} else {
|
||||||
const req = http.request(restUrl, (res) => {
|
const req = http.request(restUrl, (res) => {
|
||||||
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
||||||
res.on('data', (data) => {
|
// We will keep reading data until the 'end' event fires.
|
||||||
|
// This ensures we don't just read the first chunk.
|
||||||
|
let data = '';
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
res.on('end', () => {
|
||||||
checkJSONResponse(data);
|
checkJSONResponse(data);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -55,7 +71,7 @@ function checkJSONResponse(responseData: any): any {
|
|||||||
console.log(`\t"dspaceVersion" = ${parsedData.dspaceVersion}`);
|
console.log(`\t"dspaceVersion" = ${parsedData.dspaceVersion}`);
|
||||||
console.log(`\t"dspaceUI" = ${parsedData.dspaceUI}`);
|
console.log(`\t"dspaceUI" = ${parsedData.dspaceUI}`);
|
||||||
console.log(`\t"dspaceServer" = ${parsedData.dspaceServer}`);
|
console.log(`\t"dspaceServer" = ${parsedData.dspaceServer}`);
|
||||||
console.log(`\t"dspaceServer" property matches UI's "rest" config? ${(parsedData.dspaceServer === environment.rest.baseUrl)}`);
|
console.log(`\t"dspaceServer" property matches UI's "rest" config? ${(parsedData.dspaceServer === appConfig.rest.baseUrl)}`);
|
||||||
// Check for "authn" and "sites" in "_links" section as they should always exist (even if no data)!
|
// Check for "authn" and "sites" in "_links" section as they should always exist (even if no data)!
|
||||||
const linksFound: string[] = Object.keys(parsedData._links);
|
const linksFound: string[] = Object.keys(parsedData._links);
|
||||||
console.log(`\tDoes "/api" endpoint have HAL links ("_links" section)? ${linksFound.includes('authn') && linksFound.includes('sites')}`);
|
console.log(`\tDoes "/api" endpoint have HAL links ("_links" section)? ${linksFound.includes('authn') && linksFound.includes('sites')}`);
|
||||||
|
131
server.ts
131
server.ts
@@ -15,28 +15,41 @@
|
|||||||
* import for `ngExpressEngine`.
|
* import for `ngExpressEngine`.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import 'zone.js/dist/zone-node';
|
import 'zone.js/node';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import 'rxjs';
|
import 'rxjs';
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import axios from 'axios';
|
||||||
import * as pem from 'pem';
|
import * as pem from 'pem';
|
||||||
import * as https from 'https';
|
import * as https from 'https';
|
||||||
import * as morgan from 'morgan';
|
import * as morgan from 'morgan';
|
||||||
import * as express from 'express';
|
import * as express from 'express';
|
||||||
import * as bodyParser from 'body-parser';
|
import * as bodyParser from 'body-parser';
|
||||||
import * as compression from 'compression';
|
import * as compression from 'compression';
|
||||||
|
import * as expressStaticGzip from 'express-static-gzip';
|
||||||
|
|
||||||
|
import { existsSync, readFileSync } from 'fs';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
|
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
import { existsSync } from 'fs';
|
|
||||||
|
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
import { environment } from './src/environments/environment';
|
import { environment } from './src/environments/environment';
|
||||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
import { hasValue, hasNoValue } from './src/app/shared/empty.util';
|
import { hasNoValue, hasValue } from './src/app/shared/empty.util';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import { UIServerConfig } from './src/config/ui-server-config.interface';
|
import { UIServerConfig } from './src/config/ui-server-config.interface';
|
||||||
|
|
||||||
|
import { ServerAppModule } from './src/main.server';
|
||||||
|
|
||||||
|
import { buildAppConfig } from './src/config/config.server';
|
||||||
|
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
||||||
|
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||||
|
import { logStartupMessage } from './startup-message';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
*/
|
*/
|
||||||
@@ -46,28 +59,39 @@ const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
|
|||||||
|
|
||||||
const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index';
|
const indexHtml = existsSync(join(DIST_FOLDER, 'index.html')) ? 'index.html' : 'index';
|
||||||
|
|
||||||
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
|
|
||||||
const { ServerAppModule, ngExpressEngine } = require('./dist/server/main');
|
|
||||||
|
|
||||||
const cookieParser = require('cookie-parser');
|
const cookieParser = require('cookie-parser');
|
||||||
|
|
||||||
|
const appConfig: AppConfig = buildAppConfig(join(DIST_FOLDER, 'assets/config.json'));
|
||||||
|
|
||||||
|
// extend environment with app config for server
|
||||||
|
extendEnvironmentWithAppConfig(environment, appConfig);
|
||||||
|
|
||||||
// The Express app is exported so that it can be used by serverless Functions.
|
// The Express app is exported so that it can be used by serverless Functions.
|
||||||
export function app() {
|
export function app() {
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Create a new express application
|
* Create a new express application
|
||||||
*/
|
*/
|
||||||
const server = express();
|
const server = express();
|
||||||
|
|
||||||
|
// Tell Express to trust X-FORWARDED-* headers from proxies
|
||||||
|
// See https://expressjs.com/en/guide/behind-proxies.html
|
||||||
|
server.set('trust proxy', environment.ui.useProxies);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* If production mode is enabled in the environment file:
|
* If production mode is enabled in the environment file:
|
||||||
* - Enable Angular's production mode
|
* - Enable Angular's production mode
|
||||||
* - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression)
|
* - Enable compression for SSR reponses. See [compression](https://github.com/expressjs/compression)
|
||||||
*/
|
*/
|
||||||
if (environment.production) {
|
if (environment.production) {
|
||||||
enableProdMode();
|
enableProdMode();
|
||||||
server.use(compression());
|
server.use(compression({
|
||||||
|
// only compress responses we've marked as SSR
|
||||||
|
// otherwise, this middleware may compress files we've chosen not to compress via compression-webpack-plugin
|
||||||
|
filter: (_, res) => res.locals.ssr,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -101,7 +125,11 @@ export function app() {
|
|||||||
provide: RESPONSE,
|
provide: RESPONSE,
|
||||||
useValue: (options as any).req.res,
|
useValue: (options as any).req.res,
|
||||||
},
|
},
|
||||||
],
|
{
|
||||||
|
provide: APP_CONFIG,
|
||||||
|
useValue: environment
|
||||||
|
}
|
||||||
|
]
|
||||||
})(_, (options as any), callback)
|
})(_, (options as any), callback)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -118,7 +146,11 @@ export function app() {
|
|||||||
/**
|
/**
|
||||||
* Proxy the sitemaps
|
* Proxy the sitemaps
|
||||||
*/
|
*/
|
||||||
server.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true }));
|
router.use('/sitemap**', createProxyMiddleware({
|
||||||
|
target: `${environment.rest.baseUrl}/sitemaps`,
|
||||||
|
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||||
|
changeOrigin: true
|
||||||
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the rateLimiter property is present
|
* Checks if the rateLimiter property is present
|
||||||
@@ -135,15 +167,28 @@ export function app() {
|
|||||||
|
|
||||||
/*
|
/*
|
||||||
* Serve static resources (images, i18n messages, …)
|
* Serve static resources (images, i18n messages, …)
|
||||||
|
* Handle pre-compressed files with [express-static-gzip](https://github.com/tkoenig89/express-static-gzip)
|
||||||
*/
|
*/
|
||||||
server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
|
router.get('*.*', cacheControl, expressStaticGzip(DIST_FOLDER, {
|
||||||
|
index: false,
|
||||||
|
enableBrotli: true,
|
||||||
|
orderPreference: ['br', 'gzip'],
|
||||||
|
}));
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Fallthrough to the IIIF viewer (must be included in the build).
|
* Fallthrough to the IIIF viewer (must be included in the build).
|
||||||
*/
|
*/
|
||||||
server.use('/iiif', express.static(IIIF_VIEWER, {index:false}));
|
router.use('/iiif', express.static(IIIF_VIEWER, { index: false }));
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checking server status
|
||||||
|
*/
|
||||||
|
server.get('/app/health', healthCheck);
|
||||||
|
|
||||||
// Register the ngApp callback function to handle incoming requests
|
// Register the ngApp callback function to handle incoming requests
|
||||||
server.get('*', ngApp);
|
router.get('*', ngApp);
|
||||||
|
|
||||||
|
server.use(environment.ui.nameSpace, router);
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
}
|
}
|
||||||
@@ -165,6 +210,7 @@ function ngApp(req, res) {
|
|||||||
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }]
|
||||||
}, (err, data) => {
|
}, (err, data) => {
|
||||||
if (hasNoValue(err) && hasValue(data)) {
|
if (hasNoValue(err) && hasValue(data)) {
|
||||||
|
res.locals.ssr = true; // mark response as SSR
|
||||||
res.send(data);
|
res.send(data);
|
||||||
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
||||||
// When this error occurs we can't fall back to CSR because the response has already been
|
// When this error occurs we can't fall back to CSR because the response has already been
|
||||||
@@ -176,13 +222,25 @@ function ngApp(req, res) {
|
|||||||
if (hasValue(err)) {
|
if (hasValue(err)) {
|
||||||
console.warn('Error details : ', err);
|
console.warn('Error details : ', err);
|
||||||
}
|
}
|
||||||
res.sendFile(DIST_FOLDER + '/index.html');
|
res.render(indexHtml, {
|
||||||
|
req,
|
||||||
|
providers: [{
|
||||||
|
provide: APP_BASE_HREF,
|
||||||
|
useValue: req.baseUrl
|
||||||
|
}]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If preboot is disabled, just serve the client
|
// If preboot is disabled, just serve the client
|
||||||
console.log('Universal off, serving for direct CSR');
|
console.log('Universal off, serving for direct CSR');
|
||||||
res.sendFile(DIST_FOLDER + '/index.html');
|
res.render(indexHtml, {
|
||||||
|
req,
|
||||||
|
providers: [{
|
||||||
|
provide: APP_BASE_HREF,
|
||||||
|
useValue: req.baseUrl
|
||||||
|
}]
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,24 +285,27 @@ function run() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
function start() {
|
||||||
|
logStartupMessage(environment);
|
||||||
|
|
||||||
|
/*
|
||||||
* If SSL is enabled
|
* If SSL is enabled
|
||||||
* - Read credentials from configuration files
|
* - Read credentials from configuration files
|
||||||
* - Call script to start an HTTPS server with these credentials
|
* - Call script to start an HTTPS server with these credentials
|
||||||
* When SSL is disabled
|
* When SSL is disabled
|
||||||
* - Start an HTTP server on the configured port and host
|
* - Start an HTTP server on the configured port and host
|
||||||
*/
|
*/
|
||||||
if (environment.ui.ssl) {
|
if (environment.ui.ssl) {
|
||||||
let serviceKey;
|
let serviceKey;
|
||||||
try {
|
try {
|
||||||
serviceKey = fs.readFileSync('./config/ssl/key.pem');
|
serviceKey = readFileSync('./config/ssl/key.pem');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Service key not found at ./config/ssl/key.pem');
|
console.warn('Service key not found at ./config/ssl/key.pem');
|
||||||
}
|
}
|
||||||
|
|
||||||
let certificate;
|
let certificate;
|
||||||
try {
|
try {
|
||||||
certificate = fs.readFileSync('./config/ssl/cert.pem');
|
certificate = readFileSync('./config/ssl/cert.pem');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn('Certificate not found at ./config/ssl/key.pem');
|
console.warn('Certificate not found at ./config/ssl/key.pem');
|
||||||
}
|
}
|
||||||
@@ -266,8 +327,34 @@ if (environment.ui.ssl) {
|
|||||||
createHttpsServer(keys);
|
createHttpsServer(keys);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
run();
|
run();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* The callback function to serve health check requests
|
||||||
|
*/
|
||||||
|
function healthCheck(req, res) {
|
||||||
|
const baseUrl = `${environment.rest.baseUrl}${environment.actuators.endpointPath}`;
|
||||||
|
axios.get(baseUrl)
|
||||||
|
.then((response) => {
|
||||||
|
res.status(response.status).send(response.data);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
res.status(error.response.status).send({
|
||||||
|
error: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Webpack will replace 'require' with '__webpack_require__'
|
||||||
|
// '__non_webpack_require__' is a proxy to Node 'require'
|
||||||
|
// The below code is to ensure that the server is run only when not requiring the bundle.
|
||||||
|
declare const __non_webpack_require__: NodeRequire;
|
||||||
|
const mainModule = __non_webpack_require__.main;
|
||||||
|
const moduleFilename = (mainModule && mainModule.filename) || '';
|
||||||
|
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
|
||||||
|
start();
|
||||||
}
|
}
|
||||||
|
|
||||||
export * from './src/main.server';
|
export * from './src/main.server';
|
||||||
|
@@ -9,13 +9,15 @@ import { GroupFormComponent } from './group-registry/group-form/group-form.compo
|
|||||||
import { MembersListComponent } from './group-registry/group-form/members-list/members-list.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 { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
|
||||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
|
import { FormModule } from '../shared/form/form.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
AccessControlRoutingModule
|
AccessControlRoutingModule,
|
||||||
|
FormModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
EPeopleRegistryComponent,
|
EPeopleRegistryComponent,
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
import { type } from '../../shared/ngrx/type';
|
import { type } from '../../shared/ngrx/type';
|
||||||
@@ -16,7 +17,6 @@ export const EPeopleRegistryActionTypes = {
|
|||||||
CANCEL_EDIT_EPERSON: type('dspace/epeople-registry/CANCEL_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
|
* Used to edit an EPerson in the EPeople registry
|
||||||
*/
|
*/
|
||||||
@@ -37,7 +37,6 @@ export class EPeopleRegistryCancelEPersonAction implements Action {
|
|||||||
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
|
type = EPeopleRegistryActionTypes.CANCEL_EDIT_EPERSON;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export a type alias of all actions in this action group
|
* Export a type alias of all actions in this action group
|
||||||
|
@@ -45,7 +45,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ds-loading *ngIf="searching$ | async"></ds-loading>
|
<ds-themed-loading *ngIf="searching$ | async"></ds-themed-loading>
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(searching$ | async)"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
|
@@ -9,7 +9,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
|||||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { FindListOptions } from '../../core/data/request.models';
|
|
||||||
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../core/eperson/eperson-data.service';
|
||||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||||
import { PageInfo } from '../../core/shared/page-info.model';
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
@@ -27,6 +26,7 @@ import { AuthorizationDataService } from '../../core/data/feature-authorization/
|
|||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
|
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||||
|
|
||||||
describe('EPeopleRegistryComponent', () => {
|
describe('EPeopleRegistryComponent', () => {
|
||||||
let component: EPeopleRegistryComponent;
|
let component: EPeopleRegistryComponent;
|
||||||
|
@@ -238,7 +238,6 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
|||||||
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
this.epersonService.deleteEPerson(ePerson).pipe(getFirstCompletedRemoteData()).subscribe((restResponse: RemoteData<NoContent>) => {
|
||||||
if (restResponse.hasSucceeded) {
|
if (restResponse.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
|
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', {name: ePerson.name}));
|
||||||
this.reset();
|
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.errorMessage);
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,7 @@
|
|||||||
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
class="btn btn-outline-secondary"><i class="fas fa-arrow-left"></i> {{messagePrefix + '.return' | translate}}</button>
|
||||||
</div>
|
</div>
|
||||||
<div between class="btn-group">
|
<div between class="btn-group">
|
||||||
<button class="btn btn-primary" [disabled]="!(canReset$ | async)">
|
<button class="btn btn-primary" [disabled]="!(canReset$ | async)" (click)="resetPassword()">
|
||||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,9 +36,13 @@
|
|||||||
</button>
|
</button>
|
||||||
</ds-form>
|
</ds-form>
|
||||||
|
|
||||||
|
<ds-themed-loading [showMessage]="false" *ngIf="!formGroup"></ds-themed-loading>
|
||||||
|
|
||||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||||
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
<h5>{{messagePrefix + '.groupsEPersonIsMemberOf' | translate}}</h5>
|
||||||
|
|
||||||
|
<ds-themed-loading [showMessage]="false" *ngIf="!(groups | async)"></ds-themed-loading>
|
||||||
|
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
*ngIf="(groups | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
|
@@ -2,18 +2,18 @@ import { Observable, of as observableOf } from 'rxjs';
|
|||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { BrowserModule, By } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||||
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
|
||||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { EPeopleRegistryComponent } from '../epeople-registry.component';
|
||||||
import { EPersonFormComponent } from './eperson-form.component';
|
import { EPersonFormComponent } from './eperson-form.component';
|
||||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
|
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
@@ -28,9 +28,9 @@ import { createPaginatedList } from '../../../shared/testing/utils.test';
|
|||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
import { FormArray, FormControl, FormGroup,Validators, NG_VALIDATORS, NG_ASYNC_VALIDATORS } from '@angular/forms';
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
|
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||||
|
|
||||||
describe('EPersonFormComponent', () => {
|
describe('EPersonFormComponent', () => {
|
||||||
let component: EPersonFormComponent;
|
let component: EPersonFormComponent;
|
||||||
@@ -42,6 +42,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
let authService: AuthServiceStub;
|
let authService: AuthServiceStub;
|
||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let groupsDataService: GroupDataService;
|
let groupsDataService: GroupDataService;
|
||||||
|
let epersonRegistrationService: EpersonRegistrationService;
|
||||||
|
|
||||||
let paginationService;
|
let paginationService;
|
||||||
|
|
||||||
@@ -176,7 +177,7 @@ describe('EPersonFormComponent', () => {
|
|||||||
|
|
||||||
});
|
});
|
||||||
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
||||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
findListByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||||
getGroupRegistryRouterLink: ''
|
getGroupRegistryRouterLink: ''
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -199,12 +200,18 @@ describe('EPersonFormComponent', () => {
|
|||||||
{ provide: AuthService, useValue: authService },
|
{ provide: AuthService, useValue: authService },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{ provide: PaginationService, useValue: paginationService },
|
{ provide: PaginationService, useValue: paginationService },
|
||||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring']) }
|
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])},
|
||||||
|
{ provide: EpersonRegistrationService, useValue: epersonRegistrationService },
|
||||||
|
EPeopleRegistryComponent
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
epersonRegistrationService = jasmine.createSpyObj('epersonRegistrationService', {
|
||||||
|
registerEmail: createSuccessfulRemoteDataObject$(null)
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
fixture = TestBed.createComponent(EPersonFormComponent);
|
fixture = TestBed.createComponent(EPersonFormComponent);
|
||||||
component = fixture.componentInstance;
|
component = fixture.componentInstance;
|
||||||
@@ -514,4 +521,23 @@ describe('EPersonFormComponent', () => {
|
|||||||
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Reset Password', () => {
|
||||||
|
let ePersonId;
|
||||||
|
let ePersonEmail;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
ePersonId = 'testEPersonId';
|
||||||
|
ePersonEmail = 'person.email@4science.it';
|
||||||
|
component.epersonInitial = Object.assign(new EPerson(), {
|
||||||
|
id: ePersonId,
|
||||||
|
email: ePersonEmail
|
||||||
|
});
|
||||||
|
component.resetPassword();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call epersonRegistrationService.registerEmail', () => {
|
||||||
|
expect(epersonRegistrationService.registerEmail).toHaveBeenCalledWith(ePersonEmail);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -34,6 +34,8 @@ import { NoContent } from '../../../core/shared/NoContent.model';
|
|||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||||
|
import { Registration } from '../../../core/shared/registration.model';
|
||||||
|
import { EpersonRegistrationService } from '../../../core/data/eperson-registration.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-eperson-form',
|
selector: 'ds-eperson-form',
|
||||||
@@ -121,7 +123,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
* Observable whether or not the admin is allowed to reset the EPerson's password
|
* Observable whether or not the admin is allowed to reset the EPerson's password
|
||||||
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
||||||
*/
|
*/
|
||||||
canReset$: Observable<boolean> = observableOf(false);
|
canReset$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable whether or not the admin is allowed to delete the EPerson
|
* Observable whether or not the admin is allowed to delete the EPerson
|
||||||
@@ -167,7 +169,8 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
emailValueChangeSubscribe: Subscription;
|
emailValueChangeSubscribe: Subscription;
|
||||||
|
|
||||||
constructor(protected changeDetectorRef: ChangeDetectorRef,
|
constructor(
|
||||||
|
protected changeDetectorRef: ChangeDetectorRef,
|
||||||
public epersonService: EPersonDataService,
|
public epersonService: EPersonDataService,
|
||||||
public groupsDataService: GroupDataService,
|
public groupsDataService: GroupDataService,
|
||||||
private formBuilderService: FormBuilderService,
|
private formBuilderService: FormBuilderService,
|
||||||
@@ -177,7 +180,9 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
public requestService: RequestService) {
|
public requestService: RequestService,
|
||||||
|
private epersonRegistrationService: EpersonRegistrationService,
|
||||||
|
) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
this.epersonInitial = eperson;
|
this.epersonInitial = eperson;
|
||||||
if (hasValue(eperson)) {
|
if (hasValue(eperson)) {
|
||||||
@@ -260,7 +265,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
if (eperson != null) {
|
if (eperson != null) {
|
||||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, {
|
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
elementsPerPage: this.config.pageSize
|
elementsPerPage: this.config.pageSize
|
||||||
});
|
});
|
||||||
@@ -292,7 +297,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
}),
|
}),
|
||||||
switchMap(([eperson, findListOptions]) => {
|
switchMap(([eperson, findListOptions]) => {
|
||||||
if (eperson != null) {
|
if (eperson != null) {
|
||||||
return this.groupsDataService.findAllByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||||
}
|
}
|
||||||
return observableOf(undefined);
|
return observableOf(undefined);
|
||||||
})
|
})
|
||||||
@@ -310,6 +315,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.canDelete$ = activeEPerson$.pipe(
|
this.canDelete$ = activeEPerson$.pipe(
|
||||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
||||||
);
|
);
|
||||||
|
this.canReset$ = observableOf(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -479,6 +485,26 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
this.isImpersonated = false;
|
this.isImpersonated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends an email to current eperson address with the information
|
||||||
|
* to reset password
|
||||||
|
*/
|
||||||
|
resetPassword() {
|
||||||
|
if (hasValue(this.epersonInitial.email)) {
|
||||||
|
this.epersonRegistrationService.registerEmail(this.epersonInitial.email).pipe(getFirstCompletedRemoteData())
|
||||||
|
.subscribe((response: RemoteData<Registration>) => {
|
||||||
|
if (response.hasSucceeded) {
|
||||||
|
this.notificationsService.success(this.translateService.get('admin.access-control.epeople.actions.reset'),
|
||||||
|
this.translateService.get('forgot-email.form.success.content', {email: this.epersonInitial.email}));
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(this.translateService.get('forgot-email.form.error.head'),
|
||||||
|
this.translateService.get('forgot-email.form.error.content', {email: this.epersonInitial.email}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -528,7 +554,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
private updateGroups(options) {
|
private updateGroups(options) {
|
||||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||||
this.groups = this.groupsDataService.findAllByHref(eperson._links.groups.href, options);
|
this.groups = this.groupsDataService.findListByHref(eperson._links.groups.href, options);
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,8 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormControl, FormGroup, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms';
|
||||||
import { BrowserModule } from '@angular/platform-browser';
|
import { BrowserModule, By } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -34,6 +34,8 @@ import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mo
|
|||||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||||
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
|
|
||||||
describe('GroupFormComponent', () => {
|
describe('GroupFormComponent', () => {
|
||||||
let component: GroupFormComponent;
|
let component: GroupFormComponent;
|
||||||
@@ -86,6 +88,9 @@ describe('GroupFormComponent', () => {
|
|||||||
patch(group: Group, operations: Operation[]) {
|
patch(group: Group, operations: Operation[]) {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return createSuccessfulRemoteDataObject$({});
|
||||||
|
},
|
||||||
cancelEditGroup(): void {
|
cancelEditGroup(): void {
|
||||||
this.activeGroup = null;
|
this.activeGroup = null;
|
||||||
},
|
},
|
||||||
@@ -117,7 +122,69 @@ describe('GroupFormComponent', () => {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
builderService = getMockFormBuilderService();
|
builderService = Object.assign(getMockFormBuilderService(),{
|
||||||
|
createFormGroup(formModel, options = null) {
|
||||||
|
const controls = {};
|
||||||
|
formModel.forEach( model => {
|
||||||
|
model.parent = parent;
|
||||||
|
const controlModel = model;
|
||||||
|
const controlState = { value: controlModel.value, disabled: controlModel.disabled };
|
||||||
|
const controlOptions = this.createAbstractControlOptions(controlModel.validators, controlModel.asyncValidators, controlModel.updateOn);
|
||||||
|
controls[model.id] = new FormControl(controlState, controlOptions);
|
||||||
|
});
|
||||||
|
return new FormGroup(controls, options);
|
||||||
|
},
|
||||||
|
createAbstractControlOptions(validatorsConfig = null, asyncValidatorsConfig = null, updateOn = null) {
|
||||||
|
return {
|
||||||
|
validators: validatorsConfig !== null ? this.getValidators(validatorsConfig) : null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getValidators(validatorsConfig) {
|
||||||
|
return this.getValidatorFns(validatorsConfig);
|
||||||
|
},
|
||||||
|
getValidatorFns(validatorsConfig, validatorsToken = this._NG_VALIDATORS) {
|
||||||
|
let validatorFns = [];
|
||||||
|
if (this.isObject(validatorsConfig)) {
|
||||||
|
validatorFns = Object.keys(validatorsConfig).map(validatorConfigKey => {
|
||||||
|
const validatorConfigValue = validatorsConfig[validatorConfigKey];
|
||||||
|
if (this.isValidatorDescriptor(validatorConfigValue)) {
|
||||||
|
const descriptor = validatorConfigValue;
|
||||||
|
return this.getValidatorFn(descriptor.name, descriptor.args, validatorsToken);
|
||||||
|
}
|
||||||
|
return this.getValidatorFn(validatorConfigKey, validatorConfigValue, validatorsToken);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return validatorFns;
|
||||||
|
},
|
||||||
|
getValidatorFn(validatorName, validatorArgs = null, validatorsToken = this._NG_VALIDATORS) {
|
||||||
|
let validatorFn;
|
||||||
|
if (Validators.hasOwnProperty(validatorName)) { // Built-in Angular Validators
|
||||||
|
validatorFn = Validators[validatorName];
|
||||||
|
} else { // Custom Validators
|
||||||
|
if (this._DYNAMIC_VALIDATORS && this._DYNAMIC_VALIDATORS.has(validatorName)) {
|
||||||
|
validatorFn = this._DYNAMIC_VALIDATORS.get(validatorName);
|
||||||
|
} else if (validatorsToken) {
|
||||||
|
validatorFn = validatorsToken.find(validator => validator.name === validatorName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (validatorFn === undefined) { // throw when no validator could be resolved
|
||||||
|
throw new Error(`validator '${validatorName}' is not provided via NG_VALIDATORS, NG_ASYNC_VALIDATORS or DYNAMIC_FORM_VALIDATORS`);
|
||||||
|
}
|
||||||
|
if (validatorArgs !== null) {
|
||||||
|
return validatorFn(validatorArgs);
|
||||||
|
}
|
||||||
|
return validatorFn;
|
||||||
|
},
|
||||||
|
isValidatorDescriptor(value) {
|
||||||
|
if (this.isObject(value)) {
|
||||||
|
return value.hasOwnProperty('name') && value.hasOwnProperty('args');
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
isObject(value) {
|
||||||
|
return typeof value === 'object' && value !== null;
|
||||||
|
}
|
||||||
|
});
|
||||||
translateService = getMockTranslateService();
|
translateService = getMockTranslateService();
|
||||||
router = new RouterMock();
|
router = new RouterMock();
|
||||||
notificationService = new NotificationsServiceStub();
|
notificationService = new NotificationsServiceStub();
|
||||||
@@ -217,4 +284,114 @@ describe('GroupFormComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
describe('check form validation', () => {
|
||||||
|
let groupCommunity;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
groupName = 'testName';
|
||||||
|
groupCommunity = 'testgroupCommunity';
|
||||||
|
groupDescription = 'testgroupDescription';
|
||||||
|
|
||||||
|
expected = Object.assign(new Group(), {
|
||||||
|
name: groupName,
|
||||||
|
metadata: {
|
||||||
|
'dc.description': [
|
||||||
|
{
|
||||||
|
value: groupDescription
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
spyOn(component.submitForm, 'emit');
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
component.initialisePage();
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
describe('groupName, groupCommunity and groupDescription should be required', () => {
|
||||||
|
it('form should be invalid because the groupName is required', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.groupName.valid).toBeFalse();
|
||||||
|
expect(component.formGroup.controls.groupName.errors.required).toBeTrue();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after inserting information groupName,groupCommunity and groupDescription not required', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.formGroup.controls.groupName.setValue('test');
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
it('groupName should be valid because the groupName is set', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.groupName.valid).toBeTrue();
|
||||||
|
expect(component.formGroup.controls.groupName.errors).toBeNull();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('after already utilized groupName', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
const groupsDataServiceStubWithGroup = Object.assign(groupsDataServiceStub,{
|
||||||
|
searchGroups(query: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [expected]));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
component.formGroup.controls.groupName.setValue('testName');
|
||||||
|
component.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(groupsDataServiceStubWithGroup));
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('groupName should not be valid because groupName is already taken', waitForAsync(() => {
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.formGroup.controls.groupName.valid).toBeFalse();
|
||||||
|
expect(component.formGroup.controls.groupName.errors.groupExists).toBeTruthy();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
let deleteButton;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
component.initialisePage();
|
||||||
|
|
||||||
|
component.canEdit$ = observableOf(true);
|
||||||
|
component.groupBeingEdited = {
|
||||||
|
permanent: false
|
||||||
|
} as Group;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
deleteButton = fixture.debugElement.query(By.css('.delete-button')).nativeElement;
|
||||||
|
|
||||||
|
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||||
|
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf({ id: 'active-group' }));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if confirmed via modal', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
deleteButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
(document as any).querySelector('.modal-footer .confirm').click();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call GroupDataService.delete', () => {
|
||||||
|
expect(groupsDataServiceStub.delete).toHaveBeenCalledWith('active-group');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if canceled via modal', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
deleteButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
(document as any).querySelector('.modal-footer .cancel').click();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not call GroupDataService.delete', () => {
|
||||||
|
expect(groupsDataServiceStub.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
|
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output, ChangeDetectorRef } from '@angular/core';
|
||||||
import { FormGroup } from '@angular/forms';
|
import { FormGroup } from '@angular/forms';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
of as observableOf,
|
of as observableOf,
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { catchError, map, switchMap, take, filter } from 'rxjs/operators';
|
import { catchError, map, switchMap, take, filter, debounceTime } from 'rxjs/operators';
|
||||||
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
|
import { getCollectionEditRolesRoute } from '../../../collection-page/collection-page-routing-paths';
|
||||||
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
|
import { getCommunityEditRolesRoute } from '../../../community-page/community-page-routing-paths';
|
||||||
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../../core/data/dspace-object-data.service';
|
||||||
@@ -45,6 +45,7 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
|||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { Operation } from 'fast-json-patch';
|
import { Operation } from 'fast-json-patch';
|
||||||
|
import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-group-form',
|
selector: 'ds-group-form',
|
||||||
@@ -126,6 +127,12 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
public AlertTypeEnum = AlertType;
|
public AlertTypeEnum = AlertType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription to email field value change
|
||||||
|
*/
|
||||||
|
groupNameValueChangeSubscribe: Subscription;
|
||||||
|
|
||||||
|
|
||||||
constructor(public groupDataService: GroupDataService,
|
constructor(public groupDataService: GroupDataService,
|
||||||
private ePersonDataService: EPersonDataService,
|
private ePersonDataService: EPersonDataService,
|
||||||
private dSpaceObjectDataService: DSpaceObjectDataService,
|
private dSpaceObjectDataService: DSpaceObjectDataService,
|
||||||
@@ -136,7 +143,8 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
protected router: Router,
|
protected router: Router,
|
||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
public requestService: RequestService) {
|
public requestService: RequestService,
|
||||||
|
protected changeDetectorRef: ChangeDetectorRef) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -192,6 +200,14 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
this.groupDescription,
|
this.groupDescription,
|
||||||
];
|
];
|
||||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||||
|
|
||||||
|
if (!!this.formGroup.controls.groupName) {
|
||||||
|
this.formGroup.controls.groupName.setAsyncValidators(ValidateGroupExists.createValidator(this.groupDataService));
|
||||||
|
this.groupNameValueChangeSubscribe = this.groupName.valueChanges.pipe(debounceTime(300)).subscribe(() => {
|
||||||
|
this.changeDetectorRef.detectChanges();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
this.subs.push(
|
this.subs.push(
|
||||||
observableCombineLatest(
|
observableCombineLatest(
|
||||||
this.groupDataService.getActiveGroup(),
|
this.groupDataService.getActiveGroup(),
|
||||||
@@ -201,6 +217,10 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
).subscribe(([activeGroup, canEdit, linkedObject]) => {
|
||||||
|
|
||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
|
|
||||||
|
// Disable group name exists validator
|
||||||
|
this.formGroup.controls.groupName.clearAsyncValidators();
|
||||||
|
|
||||||
this.groupBeingEdited = activeGroup;
|
this.groupBeingEdited = activeGroup;
|
||||||
|
|
||||||
if (linkedObject?.name) {
|
if (linkedObject?.name) {
|
||||||
@@ -406,7 +426,7 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
.subscribe((rd: RemoteData<NoContent>) => {
|
.subscribe((rd: RemoteData<NoContent>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name }));
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.deleted.success', { name: group.name }));
|
||||||
this.reset();
|
this.onCancel();
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(
|
this.notificationsService.error(
|
||||||
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }),
|
this.translateService.get(this.messagePrefix + '.notification.deleted.failure.title', { name: group.name }),
|
||||||
@@ -419,16 +439,6 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will ensure that the page gets reset and that the cache is cleared
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.groupDataService.getBrowseEndpoint().pipe(take(1)).subscribe((href: string) => {
|
|
||||||
this.requestService.removeByHrefSubstring(href);
|
|
||||||
});
|
|
||||||
this.onCancel();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||||
*/
|
*/
|
||||||
@@ -436,6 +446,11 @@ export class GroupFormComponent implements OnInit, OnDestroy {
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.groupDataService.cancelEditGroup();
|
this.groupDataService.cancelEditGroup();
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
|
|
||||||
|
if ( hasValue(this.groupNameValueChangeSubscribe) ) {
|
||||||
|
this.groupNameValueChangeSubscribe.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -53,7 +53,7 @@ describe('MembersListComponent', () => {
|
|||||||
activeGroup: activeGroup,
|
activeGroup: activeGroup,
|
||||||
epersonMembers: epersonMembers,
|
epersonMembers: epersonMembers,
|
||||||
subgroupMembers: subgroupMembers,
|
subgroupMembers: subgroupMembers,
|
||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
return createSuccessfulRemoteDataObject$(buildPaginatedList<EPerson>(new PageInfo(), groupsDataServiceStub.getEPersonMembers()));
|
||||||
},
|
},
|
||||||
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
searchByScope(scope: string, query: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
|
@@ -10,7 +10,7 @@ import {
|
|||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
ObservedValueOf,
|
ObservedValueOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { map, mergeMap, switchMap, take } from 'rxjs/operators';
|
import { defaultIfEmpty, map, mergeMap, switchMap, take } from 'rxjs/operators';
|
||||||
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
|
import {buildPaginatedList, PaginatedList} from '../../../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||||
@@ -129,7 +129,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
this.subs.set(SubKey.MembersDTO,
|
this.subs.set(SubKey.MembersDTO,
|
||||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
switchMap((currentPagination) => {
|
switchMap((currentPagination) => {
|
||||||
return this.ePersonDataService.findAllByHref(this.groupBeingEdited._links.epersons.href, {
|
return this.ePersonDataService.findListByHref(this.groupBeingEdited._links.epersons.href, {
|
||||||
currentPage: currentPagination.currentPage,
|
currentPage: currentPagination.currentPage,
|
||||||
elementsPerPage: currentPagination.pageSize
|
elementsPerPage: currentPagination.pageSize
|
||||||
}
|
}
|
||||||
@@ -144,7 +144,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||||
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
|
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||||
@@ -153,8 +153,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return epersonDtoModel;
|
return epersonDtoModel;
|
||||||
});
|
});
|
||||||
return dto$;
|
return dto$;
|
||||||
}));
|
})]);
|
||||||
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
|
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||||
}));
|
}));
|
||||||
}))
|
}))
|
||||||
@@ -171,10 +171,10 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return this.groupDataService.getActiveGroup().pipe(take(1),
|
return this.groupDataService.getActiveGroup().pipe(take(1),
|
||||||
mergeMap((group: Group) => {
|
mergeMap((group: Group) => {
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href, {
|
return this.ePersonDataService.findListByHref(group._links.epersons.href, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
elementsPerPage: 9999
|
elementsPerPage: 9999
|
||||||
}, false)
|
})
|
||||||
.pipe(
|
.pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
@@ -209,7 +209,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
if (activeGroup != null) {
|
if (activeGroup != null) {
|
||||||
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
const response = this.groupDataService.deleteMemberFromGroup(activeGroup, ePerson.eperson);
|
||||||
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup);
|
this.showNotifications('deleteMember', response, ePerson.eperson.name, activeGroup);
|
||||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.noActiveGroup'));
|
||||||
}
|
}
|
||||||
@@ -274,7 +273,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
switchMap((epersonListRD: RemoteData<PaginatedList<EPerson>>) => {
|
||||||
const dtos$ = observableCombineLatest(...epersonListRD.payload.page.map((member: EPerson) => {
|
const dtos$ = observableCombineLatest([...epersonListRD.payload.page.map((member: EPerson) => {
|
||||||
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
const dto$: Observable<EpersonDtoModel> = observableCombineLatest(
|
||||||
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
this.isMemberOfGroup(member), (isMember: ObservedValueOf<Observable<boolean>>) => {
|
||||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||||
@@ -283,8 +282,8 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
return epersonDtoModel;
|
return epersonDtoModel;
|
||||||
});
|
});
|
||||||
return dto$;
|
return dto$;
|
||||||
}));
|
})]);
|
||||||
return dtos$.pipe(map((dtos: EpersonDtoModel[]) => {
|
return dtos$.pipe(defaultIfEmpty([]), map((dtos: EpersonDtoModel[]) => {
|
||||||
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
return buildPaginatedList(epersonListRD.payload.pageInfo, dtos);
|
||||||
}));
|
}));
|
||||||
}))
|
}))
|
||||||
@@ -315,7 +314,6 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
|||||||
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<any>) => {
|
response.pipe(getFirstCompletedRemoteData()).subscribe((rd: RemoteData<any>) => {
|
||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + '.notification.success.' + messageSuffix, { name: nameObject }));
|
||||||
this.ePersonDataService.clearLinkRequests(activeGroup._links.epersons.href);
|
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
|
this.notificationsService.error(this.translateService.get(this.messagePrefix + '.notification.failure.' + messageSuffix, { name: nameObject }));
|
||||||
}
|
}
|
||||||
|
@@ -65,7 +65,7 @@ describe('SubgroupsListComponent', () => {
|
|||||||
getSubgroups(): Group {
|
getSubgroups(): Group {
|
||||||
return this.activeGroup;
|
return this.activeGroup;
|
||||||
},
|
},
|
||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.subgroups$.pipe(
|
return this.subgroups$.pipe(
|
||||||
map((currentGroups: Group[]) => {
|
map((currentGroups: Group[]) => {
|
||||||
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
return createSuccessfulRemoteDataObject(buildPaginatedList<Group>(new PageInfo(), currentGroups));
|
||||||
|
@@ -115,7 +115,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
this.subs.set(
|
this.subs.set(
|
||||||
SubKey.Members,
|
SubKey.Members,
|
||||||
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
this.paginationService.getCurrentPagination(this.config.id, this.config).pipe(
|
||||||
switchMap((config) => this.groupDataService.findAllByHref(this.groupBeingEdited._links.subgroups.href, {
|
switchMap((config) => this.groupDataService.findListByHref(this.groupBeingEdited._links.subgroups.href, {
|
||||||
currentPage: config.currentPage,
|
currentPage: config.currentPage,
|
||||||
elementsPerPage: config.pageSize
|
elementsPerPage: config.pageSize
|
||||||
},
|
},
|
||||||
@@ -139,7 +139,7 @@ export class SubgroupsListComponent implements OnInit, OnDestroy {
|
|||||||
if (activeGroup.uuid === possibleSubgroup.uuid) {
|
if (activeGroup.uuid === possibleSubgroup.uuid) {
|
||||||
return observableOf(false);
|
return observableOf(false);
|
||||||
} else {
|
} else {
|
||||||
return this.groupDataService.findAllByHref(activeGroup._links.subgroups.href, {
|
return this.groupDataService.findListByHref(activeGroup._links.subgroups.href, {
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
elementsPerPage: 9999
|
elementsPerPage: 9999
|
||||||
})
|
})
|
||||||
|
@@ -0,0 +1,33 @@
|
|||||||
|
import { AbstractControl, ValidationErrors } from '@angular/forms';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
|
import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators';
|
||||||
|
import { Group } from '../../../../core/eperson/models/group.model';
|
||||||
|
|
||||||
|
export class ValidateGroupExists {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method will create the validator with the groupDataService requested from component
|
||||||
|
* @param groupDataService the service with DI in the component that this validator is being utilized.
|
||||||
|
* @return Observable<ValidationErrors | null>
|
||||||
|
*/
|
||||||
|
static createValidator(groupDataService: GroupDataService) {
|
||||||
|
return (control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> => {
|
||||||
|
return groupDataService.searchGroups(control.value, {
|
||||||
|
currentPage: 1,
|
||||||
|
elementsPerPage: 100
|
||||||
|
})
|
||||||
|
.pipe(
|
||||||
|
getFirstSucceededRemoteListPayload(),
|
||||||
|
map( (groups: Group[]) => {
|
||||||
|
return groups.filter(group => group.name === control.value);
|
||||||
|
}),
|
||||||
|
map( (groups: Group[]) => {
|
||||||
|
return groups.length > 0 ? { groupExists: true } : null;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
import { Group } from '../../core/eperson/models/group.model';
|
import { Group } from '../../core/eperson/models/group.model';
|
||||||
import { type } from '../../shared/ngrx/type';
|
import { type } from '../../shared/ngrx/type';
|
||||||
@@ -16,7 +17,6 @@ export const GroupRegistryActionTypes = {
|
|||||||
CANCEL_EDIT_GROUP: type('dspace/epeople-registry/CANCEL_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
|
* Used to edit a Group in the Group registry
|
||||||
*/
|
*/
|
||||||
@@ -37,7 +37,6 @@ export class GroupRegistryCancelGroupAction implements Action {
|
|||||||
type = GroupRegistryActionTypes.CANCEL_EDIT_GROUP;
|
type = GroupRegistryActionTypes.CANCEL_EDIT_GROUP;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export a type alias of all actions in this action group
|
* Export a type alias of all actions in this action group
|
||||||
|
@@ -33,7 +33,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<ds-loading *ngIf="loading$ | async"></ds-loading>
|
<ds-themed-loading *ngIf="loading$ | async"></ds-themed-loading>
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && !(loading$ | async)"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
<button *ngIf="!groupDto.group?.permanent && groupDto.ableToDelete"
|
||||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm"
|
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
|
||||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: groupDto.group.name} }}">
|
||||||
<i class="fas fa-trash-alt fa-fw"></i>
|
<i class="fas fa-trash-alt fa-fw"></i>
|
||||||
</button>
|
</button>
|
||||||
|
@@ -31,6 +31,7 @@ import { RouterMock } from '../../shared/mocks/router.mock';
|
|||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../shared/testing/pagination-service.stub';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
|
import { NoContent } from '../../core/shared/NoContent.model';
|
||||||
|
|
||||||
describe('GroupRegistryComponent', () => {
|
describe('GroupRegistryComponent', () => {
|
||||||
let component: GroupsRegistryComponent;
|
let component: GroupsRegistryComponent;
|
||||||
@@ -68,7 +69,7 @@ describe('GroupRegistryComponent', () => {
|
|||||||
mockGroups = [GroupMock, GroupMock2];
|
mockGroups = [GroupMock, GroupMock2];
|
||||||
mockEPeople = [EPersonMock, EPersonMock2];
|
mockEPeople = [EPersonMock, EPersonMock2];
|
||||||
ePersonDataServiceStub = {
|
ePersonDataServiceStub = {
|
||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
findListByHref(href: string): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
switch (href) {
|
switch (href) {
|
||||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/epersons':
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
||||||
@@ -96,7 +97,7 @@ describe('GroupRegistryComponent', () => {
|
|||||||
};
|
};
|
||||||
groupsDataServiceStub = {
|
groupsDataServiceStub = {
|
||||||
allGroups: mockGroups,
|
allGroups: mockGroups,
|
||||||
findAllByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
findListByHref(href: string): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
switch (href) {
|
switch (href) {
|
||||||
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
case 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups/testgroupid2/groups':
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo({
|
||||||
@@ -145,7 +146,10 @@ describe('GroupRegistryComponent', () => {
|
|||||||
totalPages: 1,
|
totalPages: 1,
|
||||||
currentPage: 1
|
currentPage: 1
|
||||||
}), [result]));
|
}), [result]));
|
||||||
}
|
},
|
||||||
|
delete(objectId: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
|
return createSuccessfulRemoteDataObject$({});
|
||||||
|
},
|
||||||
};
|
};
|
||||||
dsoDataServiceStub = {
|
dsoDataServiceStub = {
|
||||||
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||||
@@ -301,4 +305,29 @@ describe('GroupRegistryComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
let deleteButton;
|
||||||
|
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||||
|
|
||||||
|
setIsAuthorized(true, true);
|
||||||
|
|
||||||
|
// force rerender after setup changes
|
||||||
|
component.search({ query: '' });
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// only mockGroup[0] is deletable, so we should only get one button
|
||||||
|
deleteButton = fixture.debugElement.query(By.css('.btn-delete')).nativeElement;
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call GroupDataService.delete', () => {
|
||||||
|
deleteButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(groupsDataServiceStub.delete).toHaveBeenCalledWith(mockGroups[0].id);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -5,11 +5,12 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
|
EMPTY,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
Subscription
|
Subscription
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { catchError, map, switchMap, take, tap } from 'rxjs/operators';
|
import { catchError, defaultIfEmpty, map, switchMap, tap } from 'rxjs/operators';
|
||||||
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
@@ -144,7 +145,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||||
switchMap((isSiteAdmin: boolean) => {
|
switchMap((isSiteAdmin: boolean) => {
|
||||||
return observableCombineLatest(groups.page.map((group: Group) => {
|
return observableCombineLatest([...groups.page.map((group: Group) => {
|
||||||
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
|
if (hasValue(group) && !this.deletedGroupsIds.includes(group.id)) {
|
||||||
return observableCombineLatest([
|
return observableCombineLatest([
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
|
this.authorizationService.isAuthorized(FeatureID.CanDelete, group.self),
|
||||||
@@ -165,8 +166,10 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
}
|
}
|
||||||
})).pipe(map((dtos: GroupDtoModel[]) => {
|
})]).pipe(defaultIfEmpty([]), map((dtos: GroupDtoModel[]) => {
|
||||||
return buildPaginatedList(groups.pageInfo, dtos);
|
return buildPaginatedList(groups.pageInfo, dtos);
|
||||||
}));
|
}));
|
||||||
})
|
})
|
||||||
@@ -199,7 +202,6 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
if (rd.hasSucceeded) {
|
if (rd.hasSucceeded) {
|
||||||
this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id];
|
this.deletedGroupsIds = [...this.deletedGroupsIds, group.group.id];
|
||||||
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name }));
|
this.notificationsService.success(this.translateService.get(this.messagePrefix + 'notification.deleted.success', { name: group.group.name }));
|
||||||
this.reset();
|
|
||||||
} else {
|
} else {
|
||||||
this.notificationsService.error(
|
this.notificationsService.error(
|
||||||
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }),
|
this.translateService.get(this.messagePrefix + 'notification.deleted.failure.title', { name: group.group.name }),
|
||||||
@@ -209,23 +211,12 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will set everything to stale, which will cause the lists on this page to update.
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.groupService.getBrowseEndpoint().pipe(
|
|
||||||
take(1)
|
|
||||||
).subscribe((href: string) => {
|
|
||||||
this.requestService.setStaleByHrefSubstring(href);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the members (epersons embedded value of a group)
|
* Get the members (epersons embedded value of a group)
|
||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
getMembers(group: Group): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||||
return this.ePersonDataService.findAllByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
return this.ePersonDataService.findListByHref(group._links.epersons.href).pipe(getFirstSucceededRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -233,7 +224,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
|||||||
* @param group
|
* @param group
|
||||||
*/
|
*/
|
||||||
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
getSubgroups(group: Group): Observable<RemoteData<PaginatedList<Group>>> {
|
||||||
return this.groupService.findAllByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
return this.groupService.findListByHref(group._links.subgroups.href).pipe(getFirstSucceededRemoteData());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
<div class="container">
|
||||||
|
<h2 id="header">{{'admin.batch-import.page.header' | translate}}</h2>
|
||||||
|
<p>{{'admin.batch-import.page.help' | translate}}</p>
|
||||||
|
<p *ngIf="dso">
|
||||||
|
selected collection: <b>{{getDspaceObjectName()}}</b>
|
||||||
|
<a href="javascript:void(0)" (click)="removeDspaceObject()">{{'admin.batch-import.page.remove' | translate}}</a>
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
<button class="btn btn-primary" (click)="this.selectCollection();">{{'admin.metadata-import.page.button.select-collection' | translate}}</button>
|
||||||
|
</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
|
||||||
|
<label class="form-check-label" for="validateOnly">
|
||||||
|
{{'admin.metadata-import.page.validateOnly' | translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small id="validateOnlyHelpBlock" class="form-text text-muted">
|
||||||
|
{{'admin.batch-import.page.validateOnly.hint' | translate}}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ds-file-dropzone-no-uploader
|
||||||
|
(onFileAdded)="setFile($event)"
|
||||||
|
[dropMessageLabel]="'admin.batch-import.page.dropMsg'"
|
||||||
|
[dropMessageLabelReplacement]="'admin.batch-import.page.dropMsgReplace'">
|
||||||
|
</ds-file-dropzone-no-uploader>
|
||||||
|
|
||||||
|
<div class="space-children-mr">
|
||||||
|
<button class="btn btn-secondary" id="backButton"
|
||||||
|
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||||
|
<button class="btn btn-primary" id="proceedButton"
|
||||||
|
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,151 @@
|
|||||||
|
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { BatchImportPageComponent } from './batch-import-page.component';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
|
||||||
|
import { FileValidator } from '../../shared/utils/require-file.validator';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import {
|
||||||
|
BATCH_IMPORT_SCRIPT_NAME,
|
||||||
|
ScriptDataService
|
||||||
|
} from '../../core/data/processes/script-data.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
|
|
||||||
|
describe('BatchImportPageComponent', () => {
|
||||||
|
let component: BatchImportPageComponent;
|
||||||
|
let fixture: ComponentFixture<BatchImportPageComponent>;
|
||||||
|
|
||||||
|
let notificationService: NotificationsServiceStub;
|
||||||
|
let scriptService: any;
|
||||||
|
let router;
|
||||||
|
let locationStub;
|
||||||
|
|
||||||
|
function init() {
|
||||||
|
notificationService = new NotificationsServiceStub();
|
||||||
|
scriptService = jasmine.createSpyObj('scriptService',
|
||||||
|
{
|
||||||
|
invoke: createSuccessfulRemoteDataObject$({ processId: '46' })
|
||||||
|
}
|
||||||
|
);
|
||||||
|
router = jasmine.createSpyObj('router', {
|
||||||
|
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||||
|
});
|
||||||
|
locationStub = jasmine.createSpyObj('location', {
|
||||||
|
back: jasmine.createSpy('back')
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
FormsModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
RouterTestingModule.withRoutes([])
|
||||||
|
],
|
||||||
|
declarations: [BatchImportPageComponent, FileValueAccessorDirective, FileValidator],
|
||||||
|
providers: [
|
||||||
|
{ provide: NotificationsService, useValue: notificationService },
|
||||||
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: Router, useValue: router },
|
||||||
|
{ provide: Location, useValue: locationStub },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(BatchImportPageComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if back button is pressed', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('should do location.back', () => {
|
||||||
|
expect(locationStub.back).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if file is set', () => {
|
||||||
|
let fileMock: File;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fileMock = new File([''], 'filename.zip', { type: 'application/zip' });
|
||||||
|
component.setFile(fileMock);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed button is pressed without validate only', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
component.validateOnly = false;
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('metadata-import script is invoked with --zip fileName and the mockFile', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
|
||||||
|
];
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--add' }));
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed button is pressed with validate only', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
component.validateOnly = true;
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('metadata-import script is invoked with --zip fileName and the mockFile and -v validate-only', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: 'filename.zip' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--add' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
|
||||||
|
];
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/46');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if proceed is pressed; but script invoke fails', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
spyOn(scriptService, 'invoke').and.returnValue(createFailedRemoteDataObject$('Error', 500));
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('error notification is shown', () => {
|
||||||
|
expect(notificationService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,124 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Location } from '@angular/common';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { BATCH_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../core/shared/operators';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Process } from '../../process-page/processes/process.model';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { getProcessDetailRoute } from '../../process-page/process-page-routing.paths';
|
||||||
|
import {
|
||||||
|
ImportBatchSelectorComponent
|
||||||
|
} from '../../shared/dso-selector/modal-wrappers/import-batch-selector/import-batch-selector.component';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-batch-import-page',
|
||||||
|
templateUrl: './batch-import-page.component.html'
|
||||||
|
})
|
||||||
|
export class BatchImportPageComponent {
|
||||||
|
/**
|
||||||
|
* The current value of the file
|
||||||
|
*/
|
||||||
|
fileObject: File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validate only flag
|
||||||
|
*/
|
||||||
|
validateOnly = true;
|
||||||
|
/**
|
||||||
|
* dso object for community or collection
|
||||||
|
*/
|
||||||
|
dso: DSpaceObject = null;
|
||||||
|
|
||||||
|
public constructor(private location: Location,
|
||||||
|
protected translate: TranslateService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
private scriptDataService: ScriptDataService,
|
||||||
|
private router: Router,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
private dsoNameService: DSONameService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set file
|
||||||
|
* @param file
|
||||||
|
*/
|
||||||
|
setFile(file) {
|
||||||
|
this.fileObject = file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When return button is pressed go to previous location
|
||||||
|
*/
|
||||||
|
public onReturn() {
|
||||||
|
this.location.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectCollection() {
|
||||||
|
const modalRef = this.modalService.open(ImportBatchSelectorComponent);
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((dso) => {
|
||||||
|
this.dso = dso || null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts import-metadata script with --zip fileName (and the selected file)
|
||||||
|
*/
|
||||||
|
public importMetadata() {
|
||||||
|
if (this.fileObject == null) {
|
||||||
|
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
||||||
|
} else {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--zip', value: this.fileObject.name }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '--add' })
|
||||||
|
];
|
||||||
|
if (this.dso) {
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '--collection', value: this.dso.uuid }));
|
||||||
|
}
|
||||||
|
if (this.validateOnly) {
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true }));
|
||||||
|
}
|
||||||
|
|
||||||
|
this.scriptDataService.invoke(BATCH_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
).subscribe((rd: RemoteData<Process>) => {
|
||||||
|
if (rd.hasSucceeded) {
|
||||||
|
const title = this.translate.get('process.new.notification.success.title');
|
||||||
|
const content = this.translate.get('process.new.notification.success.content');
|
||||||
|
this.notificationsService.success(title, content);
|
||||||
|
if (isNotEmpty(rd.payload)) {
|
||||||
|
this.router.navigateByUrl(getProcessDetailRoute(rd.payload.processId));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const title = this.translate.get('process.new.notification.error.title');
|
||||||
|
const content = this.translate.get('process.new.notification.error.content');
|
||||||
|
this.notificationsService.error(title, content);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* return selected dspace object name
|
||||||
|
*/
|
||||||
|
getDspaceObjectName(): string {
|
||||||
|
if (this.dso) {
|
||||||
|
return this.dsoNameService.getName(this.dso);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* remove selected dso object
|
||||||
|
*/
|
||||||
|
removeDspaceObject(): void {
|
||||||
|
this.dso = null;
|
||||||
|
}
|
||||||
|
}
|
@@ -1,6 +1,17 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
|
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
|
||||||
<p>{{'admin.metadata-import.page.help' | translate}}</p>
|
<p>{{'admin.metadata-import.page.help' | translate}}</p>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="form-check">
|
||||||
|
<input class="form-check-input" type="checkbox" id="validateOnly" [(ngModel)]="validateOnly">
|
||||||
|
<label class="form-check-label" for="validateOnly">
|
||||||
|
{{'admin.metadata-import.page.validateOnly' | translate}}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<small id="validateOnlyHelpBlock" class="form-text text-muted">
|
||||||
|
{{'admin.metadata-import.page.validateOnly.hint' | translate}}
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
<ds-file-dropzone-no-uploader
|
<ds-file-dropzone-no-uploader
|
||||||
(onFileAdded)="setFile($event)"
|
(onFileAdded)="setFile($event)"
|
||||||
@@ -8,8 +19,10 @@
|
|||||||
[dropMessageLabelReplacement]="'admin.metadata-import.page.dropMsgReplace'">
|
[dropMessageLabelReplacement]="'admin.metadata-import.page.dropMsgReplace'">
|
||||||
</ds-file-dropzone-no-uploader>
|
</ds-file-dropzone-no-uploader>
|
||||||
|
|
||||||
|
<div class="space-children-mr">
|
||||||
<button class="btn btn-secondary" id="backButton"
|
<button class="btn btn-secondary" id="backButton"
|
||||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||||
<button class="btn btn-primary" id="proceedButton"
|
<button class="btn btn-primary" id="proceedButton"
|
||||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -87,8 +87,9 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
comp.setFile(fileMock);
|
comp.setFile(fileMock);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('if proceed button is pressed', () => {
|
describe('if proceed button is pressed without validate only', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
|
comp.validateOnly = false;
|
||||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
proceed.click();
|
proceed.click();
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
@@ -107,6 +108,28 @@ describe('MetadataImportPageComponent', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('if proceed button is pressed with validate only', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
comp.validateOnly = true;
|
||||||
|
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||||
|
proceed.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('metadata-import script is invoked with -f fileName and the mockFile and -v validate-only', () => {
|
||||||
|
const parameterValues: ProcessParameter[] = [
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
|
||||||
|
Object.assign(new ProcessParameter(), { name: '-v', value: true }),
|
||||||
|
];
|
||||||
|
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||||
|
});
|
||||||
|
it('success notification is shown', () => {
|
||||||
|
expect(notificationService.success).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('redirected to process page', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('if proceed is pressed; but script invoke fails', () => {
|
describe('if proceed is pressed; but script invoke fails', () => {
|
||||||
beforeEach(fakeAsync(() => {
|
beforeEach(fakeAsync(() => {
|
||||||
jasmine.getEnv().allowRespy(true);
|
jasmine.getEnv().allowRespy(true);
|
||||||
|
@@ -30,6 +30,11 @@ export class MetadataImportPageComponent {
|
|||||||
*/
|
*/
|
||||||
fileObject: File;
|
fileObject: File;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The validate only flag
|
||||||
|
*/
|
||||||
|
validateOnly = true;
|
||||||
|
|
||||||
public constructor(private location: Location,
|
public constructor(private location: Location,
|
||||||
protected translate: TranslateService,
|
protected translate: TranslateService,
|
||||||
protected notificationsService: NotificationsService,
|
protected notificationsService: NotificationsService,
|
||||||
@@ -62,6 +67,9 @@ export class MetadataImportPageComponent {
|
|||||||
const parameterValues: ProcessParameter[] = [
|
const parameterValues: ProcessParameter[] = [
|
||||||
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
||||||
];
|
];
|
||||||
|
if (this.validateOnly) {
|
||||||
|
parameterValues.push(Object.assign(new ProcessParameter(), { name: '-v', value: true }));
|
||||||
|
}
|
||||||
|
|
||||||
this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject]).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
|
@@ -8,6 +8,7 @@ import { SharedModule } from '../../shared/shared.module';
|
|||||||
import { MetadataSchemaFormComponent } from './metadata-registry/metadata-schema-form/metadata-schema-form.component';
|
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';
|
import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.module';
|
||||||
|
import { FormModule } from '../../shared/form/form.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -15,7 +16,8 @@ import { BitstreamFormatsModule } from './bitstream-formats/bitstream-formats.mo
|
|||||||
SharedModule,
|
SharedModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
BitstreamFormatsModule,
|
BitstreamFormatsModule,
|
||||||
AdminRegistriesRoutingModule
|
AdminRegistriesRoutingModule,
|
||||||
|
FormModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
MetadataRegistryComponent,
|
MetadataRegistryComponent,
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
import { type } from '../../../shared/ngrx/type';
|
import { type } from '../../../shared/ngrx/type';
|
||||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
@@ -17,7 +18,6 @@ export const BitstreamFormatsRegistryActionTypes = {
|
|||||||
DESELECT_ALL_FORMAT: type('dspace/bitstream-formats-registry/DESELECT_ALL_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
|
* Used to select a single bitstream format in the bitstream format registry
|
||||||
*/
|
*/
|
||||||
@@ -51,7 +51,6 @@ export class BitstreamFormatsRegistryDeselectAllAction implements Action {
|
|||||||
type = BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT;
|
type = BitstreamFormatsRegistryActionTypes.DESELECT_ALL_FORMAT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export a type alias of all actions in this action group
|
* Export a type alias of all actions in this action group
|
||||||
|
@@ -20,12 +20,10 @@ import { TestScheduler } from 'rxjs/testing';
|
|||||||
import {
|
import {
|
||||||
createNoContentRemoteDataObject$,
|
createNoContentRemoteDataObject$,
|
||||||
createSuccessfulRemoteDataObject,
|
createSuccessfulRemoteDataObject,
|
||||||
createSuccessfulRemoteDataObject$
|
createSuccessfulRemoteDataObject$,
|
||||||
|
createFailedRemoteDataObject$
|
||||||
} from '../../../shared/remote-data.utils';
|
} from '../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
|
||||||
@@ -85,10 +83,6 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
];
|
];
|
||||||
const mockFormatsRD = createSuccessfulRemoteDataObject(createPaginatedList(mockFormatsList));
|
const mockFormatsRD = createSuccessfulRemoteDataObject(createPaginatedList(mockFormatsList));
|
||||||
|
|
||||||
const pagination = Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 });
|
|
||||||
const sort = new SortOptions('score', SortDirection.DESC);
|
|
||||||
const findlistOptions = Object.assign(new FindListOptions(), { currentPage: 1, elementsPerPage: 20 });
|
|
||||||
|
|
||||||
const initAsync = () => {
|
const initAsync = () => {
|
||||||
notificationsServiceStub = new NotificationsServiceStub();
|
notificationsServiceStub = new NotificationsServiceStub();
|
||||||
|
|
||||||
@@ -246,7 +240,7 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
beforeEach(initBeforeEach);
|
beforeEach(initBeforeEach);
|
||||||
it('should clear bitstream formats ', () => {
|
it('should clear bitstream formats and show a success notification', () => {
|
||||||
comp.deleteFormats();
|
comp.deleteFormats();
|
||||||
|
|
||||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
@@ -275,7 +269,7 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
selectBitstreamFormat: {},
|
selectBitstreamFormat: {},
|
||||||
deselectBitstreamFormat: {},
|
deselectBitstreamFormat: {},
|
||||||
deselectAllBitstreamFormats: {},
|
deselectAllBitstreamFormats: {},
|
||||||
delete: observableOf(false),
|
delete: createFailedRemoteDataObject$(),
|
||||||
clearBitStreamFormatRequests: observableOf('cleared')
|
clearBitStreamFormatRequests: observableOf('cleared')
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -295,7 +289,7 @@ describe('BitstreamFormatsComponent', () => {
|
|||||||
));
|
));
|
||||||
|
|
||||||
beforeEach(initBeforeEach);
|
beforeEach(initBeforeEach);
|
||||||
it('should clear bitstream formats ', () => {
|
it('should clear bitstream formats and show an error notification', () => {
|
||||||
comp.deleteFormats();
|
comp.deleteFormats();
|
||||||
|
|
||||||
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
expect(bitstreamFormatService.clearBitStreamFormatRequests).toHaveBeenCalled();
|
||||||
|
@@ -5,14 +5,15 @@ import { PaginatedList } from '../../../core/data/paginated-list.model';
|
|||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
import { map, mergeMap, switchMap, take, toArray } from 'rxjs/operators';
|
||||||
import { map, switchMap, take } from 'rxjs/operators';
|
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
|
import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders a list of bitstream formats
|
* This component renders a list of bitstream formats
|
||||||
@@ -58,18 +59,26 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
* Deletes the currently selected formats from the registry and updates the presented list
|
* Deletes the currently selected formats from the registry and updates the presented list
|
||||||
*/
|
*/
|
||||||
deleteFormats() {
|
deleteFormats() {
|
||||||
this.bitstreamFormatService.clearBitStreamFormatRequests().subscribe();
|
this.bitstreamFormatService.clearBitStreamFormatRequests();
|
||||||
this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(take(1)).subscribe(
|
this.bitstreamFormatService.getSelectedBitstreamFormats().pipe(
|
||||||
(formats) => {
|
take(1),
|
||||||
const tasks$ = [];
|
// emit all formats in the array one at a time
|
||||||
for (const format of formats) {
|
mergeMap((formats: BitstreamFormat[]) => formats),
|
||||||
if (hasValue(format.id)) {
|
// delete each format
|
||||||
tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RemoteData<NoContent>) => response.hasSucceeded)));
|
mergeMap((format: BitstreamFormat) => this.bitstreamFormatService.delete(format.id).pipe(
|
||||||
}
|
// wait for each response to come back
|
||||||
}
|
getFirstCompletedRemoteData(),
|
||||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
// return a boolean to indicate whether a response succeeded
|
||||||
|
map((response: RemoteData<NoContent>) => response.hasSucceeded),
|
||||||
|
)),
|
||||||
|
// wait for all responses to come in and return them as a single array
|
||||||
|
toArray()
|
||||||
|
).subscribe((results: boolean[]) => {
|
||||||
|
// Count the number of succeeded and failed deletions
|
||||||
const successResponses = results.filter((result: boolean) => result);
|
const successResponses = results.filter((result: boolean) => result);
|
||||||
const failedResponses = results.filter((result: boolean) => !result);
|
const failedResponses = results.filter((result: boolean) => !result);
|
||||||
|
|
||||||
|
// Show a notification indicating the number of succeeded and failed deletions
|
||||||
if (successResponses.length > 0) {
|
if (successResponses.length > 0) {
|
||||||
this.showNotification(true, successResponses.length);
|
this.showNotification(true, successResponses.length);
|
||||||
}
|
}
|
||||||
@@ -77,13 +86,13 @@ export class BitstreamFormatsComponent implements OnInit, OnDestroy {
|
|||||||
this.showNotification(false, failedResponses.length);
|
this.showNotification(false, failedResponses.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// reset the selection
|
||||||
this.deselectAll();
|
this.deselectAll();
|
||||||
|
|
||||||
|
// reload the page
|
||||||
this.paginationService.resetPage(this.pageConfig.id);
|
this.paginationService.resetPage(this.pageConfig.id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Deselects all selecetd bitstream formats
|
* Deselects all selecetd bitstream formats
|
||||||
|
@@ -7,13 +7,15 @@ import { FormatFormComponent } from './format-form/format-form.component';
|
|||||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||||
import { BitstreamFormatsRoutingModule } from './bitstream-formats-routing.module';
|
import { BitstreamFormatsRoutingModule } from './bitstream-formats-routing.module';
|
||||||
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||||
|
import { FormModule } from '../../../shared/form/form.module';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
RouterModule,
|
RouterModule,
|
||||||
BitstreamFormatsRoutingModule
|
BitstreamFormatsRoutingModule,
|
||||||
|
FormModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
BitstreamFormatsComponent,
|
BitstreamFormatsComponent,
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable max-classes-per-file */
|
||||||
import { Action } from '@ngrx/store';
|
import { Action } from '@ngrx/store';
|
||||||
import { type } from '../../../shared/ngrx/type';
|
import { type } from '../../../shared/ngrx/type';
|
||||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||||
@@ -26,7 +27,6 @@ export const MetadataRegistryActionTypes = {
|
|||||||
DESELECT_ALL_FIELD: type('dspace/metadata-registry/DESELECT_ALL_FIELD')
|
DESELECT_ALL_FIELD: type('dspace/metadata-registry/DESELECT_ALL_FIELD')
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
|
||||||
/**
|
/**
|
||||||
* Used to edit a metadata schema in the metadata registry
|
* Used to edit a metadata schema in the metadata registry
|
||||||
*/
|
*/
|
||||||
@@ -133,7 +133,6 @@ export class MetadataRegistryDeselectAllFieldAction implements Action {
|
|||||||
type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD;
|
type = MetadataRegistryActionTypes.DESELECT_ALL_FIELD;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Export a type alias of all actions in this action group
|
* Export a type alias of all actions in this action group
|
||||||
|
@@ -21,8 +21,8 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
|
|||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
|
|
||||||
describe('MetadataRegistryComponent', () => {
|
describe('MetadataRegistryComponent', () => {
|
||||||
let comp: MetadataRegistryComponent;
|
let comp: MetadataRegistryComponent;
|
||||||
@@ -52,7 +52,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
||||||
/* tslint:disable:no-empty */
|
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getMetadataSchemas: () => mockSchemas,
|
getMetadataSchemas: () => mockSchemas,
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
getActiveMetadataSchema: () => observableOf(undefined),
|
||||||
@@ -66,7 +66,7 @@ describe('MetadataRegistryComponent', () => {
|
|||||||
},
|
},
|
||||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||||
};
|
};
|
||||||
/* tslint:enable:no-empty */
|
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||||
|
|
||||||
paginationService = new PaginationServiceStub();
|
paginationService = new PaginationServiceStub();
|
||||||
|
|
||||||
|
@@ -128,7 +128,6 @@ export class MetadataRegistryComponent {
|
|||||||
* Delete all the selected metadata schemas
|
* Delete all the selected metadata schemas
|
||||||
*/
|
*/
|
||||||
deleteSchemas() {
|
deleteSchemas() {
|
||||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
|
||||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||||
(schemas) => {
|
(schemas) => {
|
||||||
const tasks$ = [];
|
const tasks$ = [];
|
||||||
@@ -148,7 +147,6 @@ export class MetadataRegistryComponent {
|
|||||||
}
|
}
|
||||||
this.registryService.deselectAllMetadataSchema();
|
this.registryService.deselectAllMetadataSchema();
|
||||||
this.registryService.cancelEditMetadataSchema();
|
this.registryService.cancelEditMetadataSchema();
|
||||||
this.forceUpdateSchemas();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -17,7 +17,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
let fixture: ComponentFixture<MetadataSchemaFormComponent>;
|
||||||
let registryService: RegistryService;
|
let registryService: RegistryService;
|
||||||
|
|
||||||
/* tslint:disable:no-empty */
|
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getActiveMetadataSchema: () => observableOf(undefined),
|
getActiveMetadataSchema: () => observableOf(undefined),
|
||||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||||
@@ -33,7 +33,7 @@ describe('MetadataSchemaFormComponent', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/* tslint:enable:no-empty */
|
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
@@ -24,7 +24,7 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
prefix: 'fake'
|
prefix: 'fake'
|
||||||
});
|
});
|
||||||
|
|
||||||
/* tslint:disable:no-empty */
|
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getActiveMetadataField: () => observableOf(undefined),
|
getActiveMetadataField: () => observableOf(undefined),
|
||||||
createMetadataField: (field: MetadataField) => observableOf(field),
|
createMetadataField: (field: MetadataField) => observableOf(field),
|
||||||
@@ -43,7 +43,7 @@ describe('MetadataFieldFormComponent', () => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
/* tslint:enable:no-empty */
|
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
|
@@ -25,9 +25,9 @@ import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.u
|
|||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
|
||||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||||
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
|
||||||
|
import { FindListOptions } from '../../../core/data/find-list-options.model';
|
||||||
|
|
||||||
describe('MetadataSchemaComponent', () => {
|
describe('MetadataSchemaComponent', () => {
|
||||||
let comp: MetadataSchemaComponent;
|
let comp: MetadataSchemaComponent;
|
||||||
@@ -106,7 +106,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
const mockSchemas = createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockSchemasList));
|
||||||
/* tslint:disable:no-empty */
|
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
|
||||||
const registryServiceStub = {
|
const registryServiceStub = {
|
||||||
getMetadataSchemas: () => mockSchemas,
|
getMetadataSchemas: () => mockSchemas,
|
||||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(buildPaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
||||||
@@ -122,7 +122,7 @@ describe('MetadataSchemaComponent', () => {
|
|||||||
},
|
},
|
||||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||||
};
|
};
|
||||||
/* tslint:enable:no-empty */
|
/* eslint-enable no-empty, @typescript-eslint/no-empty-function */
|
||||||
const schemaNameParam = 'mock';
|
const schemaNameParam = 'mock';
|
||||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
params: observableOf({
|
params: observableOf({
|
||||||
|
@@ -174,15 +174,12 @@ export class MetadataSchemaComponent implements OnInit {
|
|||||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
if (successResponses.length > 0) {
|
if (successResponses.length > 0) {
|
||||||
this.showNotification(true, successResponses.length);
|
this.showNotification(true, successResponses.length);
|
||||||
this.registryService.clearMetadataFieldRequests();
|
|
||||||
|
|
||||||
}
|
}
|
||||||
if (failedResponses.length > 0) {
|
if (failedResponses.length > 0) {
|
||||||
this.showNotification(false, failedResponses.length);
|
this.showNotification(false, failedResponses.length);
|
||||||
}
|
}
|
||||||
this.registryService.deselectAllMetadataField();
|
this.registryService.deselectAllMetadataField();
|
||||||
this.registryService.cancelEditMetadataField();
|
this.registryService.cancelEditMetadataField();
|
||||||
this.forceUpdateFields();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
@@ -7,6 +7,7 @@ import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow
|
|||||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||||
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||||
|
import { BatchImportPageComponent } from './admin-import-batch-page/batch-import-page.component';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -40,6 +41,12 @@ import { REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
|||||||
component: MetadataImportPageComponent,
|
component: MetadataImportPageComponent,
|
||||||
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'batch-import',
|
||||||
|
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||||
|
component: BatchImportPageComponent,
|
||||||
|
data: { title: 'admin.batch-import.title', breadcrumbKey: 'admin.batch-import' }
|
||||||
|
},
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
|
@@ -14,6 +14,14 @@ import { By } from '@angular/platform-browser';
|
|||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
|
||||||
|
import { FileService } from '../../../../../core/shared/file.service';
|
||||||
|
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
|
||||||
|
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
|
|
||||||
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
||||||
let component: CollectionAdminSearchResultGridElementComponent;
|
let component: CollectionAdminSearchResultGridElementComponent;
|
||||||
@@ -45,7 +53,11 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: BitstreamDataService, useValue: {} },
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
{ provide: LinkService, useValue: linkService }
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: FileService, useClass: FileServiceStub },
|
||||||
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -16,6 +16,14 @@ import { CommunitySearchResult } from '../../../../../shared/object-collection/s
|
|||||||
import { Community } from '../../../../../core/shared/community.model';
|
import { Community } from '../../../../../core/shared/community.model';
|
||||||
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
||||||
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../../core/cache/builders/link.service';
|
||||||
|
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
|
||||||
|
import { FileService } from '../../../../../core/shared/file.service';
|
||||||
|
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
|
||||||
|
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
|
||||||
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
|
|
||||||
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
||||||
let component: CommunityAdminSearchResultGridElementComponent;
|
let component: CommunityAdminSearchResultGridElementComponent;
|
||||||
@@ -47,7 +55,11 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: BitstreamDataService, useValue: {} },
|
{ provide: BitstreamDataService, useValue: {} },
|
||||||
{ provide: LinkService, useValue: linkService }
|
{ provide: LinkService, useValue: linkService },
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: FileService, useClass: FileServiceStub },
|
||||||
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@@ -18,6 +18,14 @@ import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-r
|
|||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||||
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
import { getMockThemeService } from '../../../../../shared/mocks/theme-service.mock';
|
||||||
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../../../../shared/theme-support/theme.service';
|
||||||
|
import { AccessStatusDataService } from '../../../../../core/data/access-status-data.service';
|
||||||
|
import { AccessStatusObject } from '../../../../../shared/object-list/access-status-badge/access-status.model';
|
||||||
|
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||||
|
import { AuthServiceStub } from '../../../../../shared/testing/auth-service.stub';
|
||||||
|
import { FileService } from '../../../../../core/shared/file.service';
|
||||||
|
import { FileServiceStub } from '../../../../../shared/testing/file-service.stub';
|
||||||
|
import { AuthorizationDataService } from '../../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { AuthorizationDataServiceStub } from '../../../../../shared/testing/authorization-service.stub';
|
||||||
|
|
||||||
describe('ItemAdminSearchResultGridElementComponent', () => {
|
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||||
let component: ItemAdminSearchResultGridElementComponent;
|
let component: ItemAdminSearchResultGridElementComponent;
|
||||||
@@ -31,6 +39,12 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockAccessStatusDataService = {
|
||||||
|
findAccessStatusFor(item: Item): Observable<RemoteData<AccessStatusObject>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new AccessStatusObject());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const mockThemeService = getMockThemeService();
|
const mockThemeService = getMockThemeService();
|
||||||
|
|
||||||
function init() {
|
function init() {
|
||||||
@@ -55,6 +69,10 @@ describe('ItemAdminSearchResultGridElementComponent', () => {
|
|||||||
{ provide: TruncatableService, useValue: mockTruncatableService },
|
{ provide: TruncatableService, useValue: mockTruncatableService },
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||||
{ provide: ThemeService, useValue: mockThemeService },
|
{ provide: ThemeService, useValue: mockThemeService },
|
||||||
|
{ provide: AccessStatusDataService, useValue: mockAccessStatusDataService },
|
||||||
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
|
{ provide: FileService, useClass: FileServiceStub },
|
||||||
|
{ provide: AuthorizationDataService, useClass: AuthorizationDataServiceStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
|
@@ -13,6 +13,8 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
import { getCollectionEditRoute } from '../../../../../collection-page/collection-page-routing-paths';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('CollectionAdminSearchResultListElementComponent', () => {
|
describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||||
let component: CollectionAdminSearchResultListElementComponent;
|
let component: CollectionAdminSearchResultListElementComponent;
|
||||||
@@ -36,7 +38,8 @@ describe('CollectionAdminSearchResultListElementComponent', () => {
|
|||||||
],
|
],
|
||||||
declarations: [CollectionAdminSearchResultListElementComponent],
|
declarations: [CollectionAdminSearchResultListElementComponent],
|
||||||
providers: [{ provide: TruncatableService, useValue: {} },
|
providers: [{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -13,6 +13,8 @@ import { Community } from '../../../../../core/shared/community.model';
|
|||||||
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
import { getCommunityEditRoute } from '../../../../../community-page/community-page-routing-paths';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('CommunityAdminSearchResultListElementComponent', () => {
|
describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||||
let component: CommunityAdminSearchResultListElementComponent;
|
let component: CommunityAdminSearchResultListElementComponent;
|
||||||
@@ -36,7 +38,8 @@ describe('CommunityAdminSearchResultListElementComponent', () => {
|
|||||||
],
|
],
|
||||||
declarations: [CommunityAdminSearchResultListElementComponent],
|
declarations: [CommunityAdminSearchResultListElementComponent],
|
||||||
providers: [{ provide: TruncatableService, useValue: {} },
|
providers: [{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -10,6 +10,8 @@ import { ItemAdminSearchResultListElementComponent } from './item-admin-search-r
|
|||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
import { DSONameServiceMock } from '../../../../../shared/mocks/dso-name.service.mock';
|
||||||
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../../environments/environment';
|
||||||
|
|
||||||
describe('ItemAdminSearchResultListElementComponent', () => {
|
describe('ItemAdminSearchResultListElementComponent', () => {
|
||||||
let component: ItemAdminSearchResultListElementComponent;
|
let component: ItemAdminSearchResultListElementComponent;
|
||||||
@@ -33,7 +35,8 @@ describe('ItemAdminSearchResultListElementComponent', () => {
|
|||||||
],
|
],
|
||||||
declarations: [ItemAdminSearchResultListElementComponent],
|
declarations: [ItemAdminSearchResultListElementComponent],
|
||||||
providers: [{ provide: TruncatableService, useValue: {} },
|
providers: [{ provide: TruncatableService, useValue: {} },
|
||||||
{ provide: DSONameService, useClass: DSONameServiceMock }],
|
{ provide: DSONameService, useClass: DSONameServiceMock },
|
||||||
|
{ provide: APP_CONFIG, useValue: environment }],
|
||||||
schemas: [NO_ERRORS_SCHEMA]
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
})
|
})
|
||||||
.compileComponents();
|
.compileComponents();
|
||||||
|
@@ -1,28 +1,30 @@
|
|||||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary my-1 move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
|
<div class="space-children-mr my-1">
|
||||||
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
|
||||||
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary my-1 private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
|
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-secondary private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
|
||||||
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
|
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary my-1 public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
|
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-secondary public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
|
||||||
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
|
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary my-1 edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-secondary edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
|
||||||
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t my-1 withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
|
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-warning t withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
|
||||||
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning my-1 reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
|
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-warning reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
|
||||||
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
|
<a [ngClass]="{'btn-sm': small}" class="btn btn-danger delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
|
||||||
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
|
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
|
||||||
</a>
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@@ -10,6 +10,7 @@ import { CollectionAdminSearchResultGridElementComponent } from './admin-search-
|
|||||||
import { ItemAdminSearchResultActionsComponent } from './admin-search-results/item-admin-search-result-actions.component';
|
import { ItemAdminSearchResultActionsComponent } from './admin-search-results/item-admin-search-result-actions.component';
|
||||||
import { JournalEntitiesModule } from '../../entity-groups/journal-entities/journal-entities.module';
|
import { JournalEntitiesModule } from '../../entity-groups/journal-entities/journal-entities.module';
|
||||||
import { ResearchEntitiesModule } from '../../entity-groups/research-entities/research-entities.module';
|
import { ResearchEntitiesModule } from '../../entity-groups/research-entities/research-entities.module';
|
||||||
|
import { SearchModule } from '../../shared/search/search.module';
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put only entry components that use custom decorator
|
// put only entry components that use custom decorator
|
||||||
@@ -24,6 +25,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
|
SearchModule,
|
||||||
SharedModule.withEntryComponents(),
|
SharedModule.withEntryComponents(),
|
||||||
JournalEntitiesModule.withEntryComponents(),
|
JournalEntitiesModule.withEntryComponents(),
|
||||||
ResearchEntitiesModule.withEntryComponents()
|
ResearchEntitiesModule.withEntryComponents()
|
||||||
@@ -36,7 +38,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
export class AdminSearchModule {
|
export class AdminSearchModule {
|
||||||
/**
|
/**
|
||||||
* NOTE: this method allows to resolve issue with components that using a custom decorator
|
* NOTE: this method allows to resolve issue with components that using a custom decorator
|
||||||
* which are not loaded during CSR otherwise
|
* which are not loaded during SSR otherwise
|
||||||
*/
|
*/
|
||||||
static withEntryComponents() {
|
static withEntryComponents() {
|
||||||
return {
|
return {
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
import { Component, Inject, Injector, OnInit } from '@angular/core';
|
||||||
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
|
import { MenuSectionComponent } from '../../../shared/menu/menu-section/menu-section.component';
|
||||||
import { MenuID } from '../../../shared/menu/initial-menus-state';
|
|
||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||||
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
import { LinkMenuItemModel } from '../../../shared/menu/menu-item/models/link.model';
|
||||||
import { MenuSection } from '../../../shared/menu/menu.reducer';
|
import { MenuSection } from '../../../shared/menu/menu-section.model';
|
||||||
|
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||||
import { isNotEmpty } from '../../../shared/empty.util';
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ import { Router } from '@angular/router';
|
|||||||
* Represents a non-expandable section in the admin sidebar
|
* Represents a non-expandable section in the admin sidebar
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
/* tslint:disable:component-selector */
|
/* eslint-disable @angular-eslint/component-selector */
|
||||||
selector: 'li[ds-admin-sidebar-section]',
|
selector: 'li[ds-admin-sidebar-section]',
|
||||||
templateUrl: './admin-sidebar-section.component.html',
|
templateUrl: './admin-sidebar-section.component.html',
|
||||||
styleUrls: ['./admin-sidebar-section.component.scss'],
|
styleUrls: ['./admin-sidebar-section.component.scss'],
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
<nav @slideHorizontal class="navbar navbar-dark p-0"
|
<nav class="navbar navbar-dark p-0"
|
||||||
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
[ngClass]="{'active': sidebarOpen, 'inactive': sidebarClosed}"
|
||||||
[@slideSidebar]="{
|
[@slideSidebar]="{
|
||||||
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
value: (!(sidebarExpanded | async) ? 'collapsed' : 'expanded'),
|
||||||
|
@@ -18,6 +18,10 @@ import { ActivatedRoute } from '@angular/router';
|
|||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import createSpy = jasmine.createSpy;
|
import createSpy = jasmine.createSpy;
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
|
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||||
|
|
||||||
describe('AdminSidebarComponent', () => {
|
describe('AdminSidebarComponent', () => {
|
||||||
let comp: AdminSidebarComponent;
|
let comp: AdminSidebarComponent;
|
||||||
@@ -26,6 +30,28 @@ describe('AdminSidebarComponent', () => {
|
|||||||
let authorizationService: AuthorizationDataService;
|
let authorizationService: AuthorizationDataService;
|
||||||
let scriptService;
|
let scriptService;
|
||||||
|
|
||||||
|
|
||||||
|
const mockItem = Object.assign(new Item(), {
|
||||||
|
id: 'fake-id',
|
||||||
|
uuid: 'fake-id',
|
||||||
|
handle: 'fake/handle',
|
||||||
|
lastModified: '2018',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://localhost:8000/items/fake-id'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
const routeStub = {
|
||||||
|
data: observableOf({
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockItem)
|
||||||
|
}),
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||||
isAuthorized: observableOf(true)
|
isAuthorized: observableOf(true)
|
||||||
@@ -36,12 +62,14 @@ describe('AdminSidebarComponent', () => {
|
|||||||
declarations: [AdminSidebarComponent],
|
declarations: [AdminSidebarComponent],
|
||||||
providers: [
|
providers: [
|
||||||
Injector,
|
Injector,
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
{ provide: MenuService, useValue: menuService },
|
{ provide: MenuService, useValue: menuService },
|
||||||
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
{ provide: CSSVariableService, useClass: CSSVariableServiceStub },
|
||||||
{ provide: AuthService, useClass: AuthServiceStub },
|
{ provide: AuthService, useClass: AuthServiceStub },
|
||||||
{ provide: ActivatedRoute, useValue: {} },
|
{ provide: ActivatedRoute, useValue: {} },
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||||
{ provide: ScriptDataService, useValue: scriptService },
|
{ provide: ScriptDataService, useValue: scriptService },
|
||||||
|
{ provide: ActivatedRoute, useValue: routeStub },
|
||||||
{
|
{
|
||||||
provide: NgbModal, useValue: {
|
provide: NgbModal, useValue: {
|
||||||
open: () => {/*comment*/
|
open: () => {/*comment*/
|
||||||
@@ -157,150 +185,4 @@ describe('AdminSidebarComponent', () => {
|
|||||||
expect(menuService.collapseMenuPreview).toHaveBeenCalled();
|
expect(menuService.collapseMenuPreview).toHaveBeenCalled();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('menu', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
spyOn(menuService, 'addSection');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for regular user', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake(() => {
|
|
||||||
return observableOf(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show site admin section', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'admin_search', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'registries', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
parentID: 'registries', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'curation_tasks', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'workflow', visible: false,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show edit_community', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'edit_community', visible: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show edit_collection', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'edit_collection', visible: false,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show access control section', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'access_control', visible: false,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
parentID: 'access_control', visible: false,
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for site admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
|
||||||
return observableOf(featureID === FeatureID.AdministratorOf);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain site admin section', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'admin_search', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'registries', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
parentID: 'registries', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'curation_tasks', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'workflow', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for community admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
|
||||||
return observableOf(featureID === FeatureID.IsCommunityAdmin);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show edit_community', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'edit_community', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for collection admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
|
||||||
return observableOf(featureID === FeatureID.IsCollectionAdmin);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show edit_collection', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'edit_collection', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('for group admin', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
authorizationService.isAuthorized = createSpy('isAuthorized').and.callFake((featureID: FeatureID) => {
|
|
||||||
return observableOf(featureID === FeatureID.CanManageGroups);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.createMenu();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show access control section', () => {
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
id: 'access_control', visible: true,
|
|
||||||
}));
|
|
||||||
expect(menuService.addSection).toHaveBeenCalledWith(comp.menuID, jasmine.objectContaining({
|
|
||||||
parentID: 'access_control', visible: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -1,26 +1,15 @@
|
|||||||
import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
import { Component, HostListener, Injector, OnInit } from '@angular/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||||
import { combineLatest, combineLatest as observableCombineLatest, Observable, BehaviorSubject } from 'rxjs';
|
import { debounceTime, distinctUntilChanged, first, map, withLatestFrom } from 'rxjs/operators';
|
||||||
import { debounceTime, first, map, take, distinctUntilChanged, withLatestFrom } from 'rxjs/operators';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
import { ScriptDataService } from '../../core/data/processes/script-data.service';
|
import { slideSidebar } from '../../shared/animations/slide';
|
||||||
import { slideHorizontal, slideSidebar } from '../../shared/animations/slide';
|
|
||||||
import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component';
|
|
||||||
import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component';
|
|
||||||
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
|
||||||
import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component';
|
|
||||||
import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component';
|
|
||||||
import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component';
|
|
||||||
import { ExportMetadataSelectorComponent } from '../../shared/dso-selector/modal-wrappers/export-metadata-selector/export-metadata-selector.component';
|
|
||||||
import { MenuID, MenuItemType } from '../../shared/menu/initial-menus-state';
|
|
||||||
import { LinkMenuItemModel } from '../../shared/menu/menu-item/models/link.model';
|
|
||||||
import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model';
|
|
||||||
import { TextMenuItemModel } from '../../shared/menu/menu-item/models/text.model';
|
|
||||||
import { MenuComponent } from '../../shared/menu/menu.component';
|
import { MenuComponent } from '../../shared/menu/menu.component';
|
||||||
import { MenuService } from '../../shared/menu/menu.service';
|
import { MenuService } from '../../shared/menu/menu.service';
|
||||||
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../shared/sass-helper/sass-helper.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { MenuID } from '../../shared/menu/menu-id.model';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Component representing the admin sidebar
|
* Component representing the admin sidebar
|
||||||
@@ -29,7 +18,7 @@ import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
|||||||
selector: 'ds-admin-sidebar',
|
selector: 'ds-admin-sidebar',
|
||||||
templateUrl: './admin-sidebar.component.html',
|
templateUrl: './admin-sidebar.component.html',
|
||||||
styleUrls: ['./admin-sidebar.component.scss'],
|
styleUrls: ['./admin-sidebar.component.scss'],
|
||||||
animations: [slideHorizontal, slideSidebar]
|
animations: [slideSidebar]
|
||||||
})
|
})
|
||||||
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
@@ -62,15 +51,16 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
|
|
||||||
inFocus$: BehaviorSubject<boolean>;
|
inFocus$: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
constructor(protected menuService: MenuService,
|
constructor(
|
||||||
|
protected menuService: MenuService,
|
||||||
protected injector: Injector,
|
protected injector: Injector,
|
||||||
private variableService: CSSVariableService,
|
private variableService: CSSVariableService,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private modalService: NgbModal,
|
public authorizationService: AuthorizationDataService,
|
||||||
private authorizationService: AuthorizationDataService,
|
public route: ActivatedRoute,
|
||||||
private scriptDataService: ScriptDataService,
|
protected themeService: ThemeService
|
||||||
) {
|
) {
|
||||||
super(menuService, injector);
|
super(menuService, injector, authorizationService, route, themeService);
|
||||||
this.inFocus$ = new BehaviorSubject(false);
|
this.inFocus$ = new BehaviorSubject(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,7 +68,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
* Set and calculate all initial values of the instance variables
|
* Set and calculate all initial values of the instance variables
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.createMenu();
|
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
this.sidebarWidth = this.variableService.getVariable('sidebarItemsWidth');
|
||||||
this.authService.isAuthenticated()
|
this.authService.isAuthenticated()
|
||||||
@@ -113,501 +102,6 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize all menu sections and items for this menu
|
|
||||||
*/
|
|
||||||
createMenu() {
|
|
||||||
this.createMainMenuSections();
|
|
||||||
this.createSiteAdministratorMenuSections();
|
|
||||||
this.createExportMenuSections();
|
|
||||||
this.createImportMenuSections();
|
|
||||||
this.createAccessControlMenuSections();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the main menu sections.
|
|
||||||
* edit_community / edit_collection is only included if the current user is a Community or Collection admin
|
|
||||||
*/
|
|
||||||
createMainMenuSections() {
|
|
||||||
combineLatest([
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.IsCollectionAdmin),
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.IsCommunityAdmin),
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf)
|
|
||||||
]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => {
|
|
||||||
const menuList = [
|
|
||||||
/* News */
|
|
||||||
{
|
|
||||||
id: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.new'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'plus',
|
|
||||||
index: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new_community',
|
|
||||||
parentID: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: isCommunityAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.new_community',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(CreateCommunityParentSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new_collection',
|
|
||||||
parentID: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: isCommunityAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.new_collection',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(CreateCollectionParentSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new_item',
|
|
||||||
parentID: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.new_item',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(CreateItemParentSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'new_process',
|
|
||||||
parentID: 'new',
|
|
||||||
active: false,
|
|
||||||
visible: isCollectionAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.new_process',
|
|
||||||
link: '/processes/new'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'new_item_version',
|
|
||||||
// parentID: 'new',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.new_item_version',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Edit */
|
|
||||||
{
|
|
||||||
id: 'edit',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.edit'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'pencil-alt',
|
|
||||||
index: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit_community',
|
|
||||||
parentID: 'edit',
|
|
||||||
active: false,
|
|
||||||
visible: isCommunityAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.edit_community',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(EditCommunitySelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit_collection',
|
|
||||||
parentID: 'edit',
|
|
||||||
active: false,
|
|
||||||
visible: isCollectionAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.edit_collection',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(EditCollectionSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'edit_item',
|
|
||||||
parentID: 'edit',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.edit_item',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(EditItemSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Statistics */
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'statistics_task',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.statistics_task',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// icon: 'chart-bar',
|
|
||||||
// index: 8
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Control Panel */
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'control_panel',
|
|
||||||
// active: false,
|
|
||||||
// visible: isSiteAdmin,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.control_panel',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// icon: 'cogs',
|
|
||||||
// index: 9
|
|
||||||
// },
|
|
||||||
|
|
||||||
/* Processes */
|
|
||||||
{
|
|
||||||
id: 'processes',
|
|
||||||
active: false,
|
|
||||||
visible: isSiteAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.processes',
|
|
||||||
link: '/processes'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'terminal',
|
|
||||||
index: 10
|
|
||||||
},
|
|
||||||
];
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
|
||||||
* the export scripts exist and the current user is allowed to execute them
|
|
||||||
*/
|
|
||||||
createExportMenuSections() {
|
|
||||||
const menuList = [
|
|
||||||
/* Export */
|
|
||||||
{
|
|
||||||
id: 'export',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.export'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'file-export',
|
|
||||||
index: 3,
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
},
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'export_community',
|
|
||||||
// parentID: 'export',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.export_community',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// shouldPersistOnRouteChange: true
|
|
||||||
// },
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'export_collection',
|
|
||||||
// parentID: 'export',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.export_collection',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// shouldPersistOnRouteChange: true
|
|
||||||
// },
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'export_item',
|
|
||||||
// parentID: 'export',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.export_item',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// shouldPersistOnRouteChange: true
|
|
||||||
// },
|
|
||||||
];
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, menuSection));
|
|
||||||
|
|
||||||
observableCombineLatest(
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
|
||||||
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_EXPORT_SCRIPT_NAME)
|
|
||||||
).pipe(
|
|
||||||
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed; otherwise even in production mode, the metadata export button is only available after a refresh (and not in dev mode)
|
|
||||||
// filter(([authorized, metadataExportScriptExists]: boolean[]) => authorized && metadataExportScriptExists),
|
|
||||||
take(1)
|
|
||||||
).subscribe(() => {
|
|
||||||
this.menuService.addSection(this.menuID, {
|
|
||||||
id: 'export_metadata',
|
|
||||||
parentID: 'export',
|
|
||||||
active: true,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.ONCLICK,
|
|
||||||
text: 'menu.section.export_metadata',
|
|
||||||
function: () => {
|
|
||||||
this.modalService.open(ExportMetadataSelectorComponent);
|
|
||||||
}
|
|
||||||
} as OnClickMenuItemModel,
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create menu sections dependent on whether or not the current user is a site administrator and on whether or not
|
|
||||||
* the import scripts exist and the current user is allowed to execute them
|
|
||||||
*/
|
|
||||||
createImportMenuSections() {
|
|
||||||
const menuList = [
|
|
||||||
/* Import */
|
|
||||||
{
|
|
||||||
id: 'import',
|
|
||||||
active: false,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.import'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'file-import',
|
|
||||||
index: 2
|
|
||||||
},
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'import_batch',
|
|
||||||
// parentID: 'import',
|
|
||||||
// active: false,
|
|
||||||
// visible: true,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.import_batch',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// }
|
|
||||||
];
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
})));
|
|
||||||
|
|
||||||
observableCombineLatest(
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
|
||||||
// this.scriptDataService.scriptWithNameExistsAndCanExecute(METADATA_IMPORT_SCRIPT_NAME)
|
|
||||||
).pipe(
|
|
||||||
// TODO uncomment when #635 (https://github.com/DSpace/dspace-angular/issues/635) is fixed
|
|
||||||
// filter(([authorized, metadataImportScriptExists]: boolean[]) => authorized && metadataImportScriptExists),
|
|
||||||
take(1)
|
|
||||||
).subscribe(() => {
|
|
||||||
this.menuService.addSection(this.menuID, {
|
|
||||||
id: 'import_metadata',
|
|
||||||
parentID: 'import',
|
|
||||||
active: true,
|
|
||||||
visible: true,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.import_metadata',
|
|
||||||
link: '/admin/metadata-import'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create menu sections dependent on whether or not the current user is a site administrator
|
|
||||||
*/
|
|
||||||
createSiteAdministratorMenuSections() {
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf).subscribe((authorized) => {
|
|
||||||
const menuList = [
|
|
||||||
/* Admin Search */
|
|
||||||
{
|
|
||||||
id: 'admin_search',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.admin_search',
|
|
||||||
link: '/admin/search'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'search',
|
|
||||||
index: 5
|
|
||||||
},
|
|
||||||
/* Registries */
|
|
||||||
{
|
|
||||||
id: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.registries'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'list',
|
|
||||||
index: 6
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'registries_metadata',
|
|
||||||
parentID: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.registries_metadata',
|
|
||||||
link: 'admin/registries/metadata'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'registries_format',
|
|
||||||
parentID: 'registries',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.registries_format',
|
|
||||||
link: 'admin/registries/bitstream-formats'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Curation tasks */
|
|
||||||
{
|
|
||||||
id: 'curation_tasks',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.curation_task',
|
|
||||||
link: 'admin/curation-tasks'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'filter',
|
|
||||||
index: 7
|
|
||||||
},
|
|
||||||
|
|
||||||
/* Workflow */
|
|
||||||
{
|
|
||||||
id: 'workflow',
|
|
||||||
active: false,
|
|
||||||
visible: authorized,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.workflow',
|
|
||||||
link: '/admin/workflow'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
icon: 'user-check',
|
|
||||||
index: 11
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create menu sections dependent on whether or not the current user can manage access control groups
|
|
||||||
*/
|
|
||||||
createAccessControlMenuSections() {
|
|
||||||
observableCombineLatest(
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.AdministratorOf),
|
|
||||||
this.authorizationService.isAuthorized(FeatureID.CanManageGroups)
|
|
||||||
).subscribe(([isSiteAdmin, canManageGroups]) => {
|
|
||||||
const menuList = [
|
|
||||||
/* Access Control */
|
|
||||||
{
|
|
||||||
id: 'access_control_people',
|
|
||||||
parentID: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: isSiteAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.access_control_people',
|
|
||||||
link: '/access-control/epeople'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'access_control_groups',
|
|
||||||
parentID: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: canManageGroups,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.LINK,
|
|
||||||
text: 'menu.section.access_control_groups',
|
|
||||||
link: '/access-control/groups'
|
|
||||||
} as LinkMenuItemModel,
|
|
||||||
},
|
|
||||||
// TODO: enable this menu item once the feature has been implemented
|
|
||||||
// {
|
|
||||||
// id: 'access_control_authorizations',
|
|
||||||
// parentID: 'access_control',
|
|
||||||
// active: false,
|
|
||||||
// visible: authorized,
|
|
||||||
// model: {
|
|
||||||
// type: MenuItemType.LINK,
|
|
||||||
// text: 'menu.section.access_control_authorizations',
|
|
||||||
// link: ''
|
|
||||||
// } as LinkMenuItemModel,
|
|
||||||
// },
|
|
||||||
{
|
|
||||||
id: 'access_control',
|
|
||||||
active: false,
|
|
||||||
visible: canManageGroups || isSiteAdmin,
|
|
||||||
model: {
|
|
||||||
type: MenuItemType.TEXT,
|
|
||||||
text: 'menu.section.access_control'
|
|
||||||
} as TextMenuItemModel,
|
|
||||||
icon: 'key',
|
|
||||||
index: 4
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
menuList.forEach((menuSection) => this.menuService.addSection(this.menuID, Object.assign(menuSection, {
|
|
||||||
shouldPersistOnRouteChange: true,
|
|
||||||
})));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@HostListener('focusin')
|
@HostListener('focusin')
|
||||||
public handleFocusIn() {
|
public handleFocusIn() {
|
||||||
this.inFocus$.next(true);
|
this.inFocus$.next(true);
|
||||||
|
@@ -4,18 +4,18 @@ import { AdminSidebarSectionComponent } from '../admin-sidebar-section/admin-sid
|
|||||||
import { slide } from '../../../shared/animations/slide';
|
import { slide } from '../../../shared/animations/slide';
|
||||||
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
import { CSSVariableService } from '../../../shared/sass-helper/sass-helper.service';
|
||||||
import { bgColor } from '../../../shared/animations/bgColor';
|
import { bgColor } from '../../../shared/animations/bgColor';
|
||||||
import { MenuID } from '../../../shared/menu/initial-menus-state';
|
|
||||||
import { MenuService } from '../../../shared/menu/menu.service';
|
import { MenuService } from '../../../shared/menu/menu.service';
|
||||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
import { rendersSectionForMenu } from '../../../shared/menu/menu-section.decorator';
|
||||||
|
import { MenuID } from '../../../shared/menu/menu-id.model';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Represents a expandable section in the sidebar
|
* Represents a expandable section in the sidebar
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
/* tslint:disable:component-selector */
|
/* eslint-disable @angular-eslint/component-selector */
|
||||||
selector: 'li[ds-expandable-admin-sidebar-section]',
|
selector: 'li[ds-expandable-admin-sidebar-section]',
|
||||||
templateUrl: './expandable-admin-sidebar-section.component.html',
|
templateUrl: './expandable-admin-sidebar-section.component.html',
|
||||||
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
styleUrls: ['./expandable-admin-sidebar-section.component.scss'],
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { ThemedComponent } from '../../shared/theme-support/themed.component';
|
||||||
|
import { AdminSidebarComponent } from './admin-sidebar.component';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Themed wrapper for AdminSidebarComponent
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-themed-admin-sidebar',
|
||||||
|
styleUrls: [],
|
||||||
|
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||||
|
})
|
||||||
|
export class ThemedAdminSidebarComponent extends ThemedComponent<AdminSidebarComponent> {
|
||||||
|
protected getComponentName(): string {
|
||||||
|
return 'AdminSidebarComponent';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importThemedComponent(themeName: string): Promise<any> {
|
||||||
|
return import(`../../../themes/${themeName}/app/admin/admin-sidebar/admin-sidebar.component`);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected importUnthemedComponent(): Promise<any> {
|
||||||
|
return import('./admin-sidebar.component');
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user