mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Compare commits
1032 Commits
dspace-7.0
...
dspace-7.2
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e4f483c308 | ||
![]() |
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 | ||
![]() |
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 | ||
![]() |
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 | ||
![]() |
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 | ||
![]() |
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 | ||
![]() |
01b200279b | ||
![]() |
f74716a459 | ||
![]() |
b64b7c2607 | ||
![]() |
2bf880b216 | ||
![]() |
efe51aa340 | ||
![]() |
ed806dc3bf | ||
![]() |
5b1c9286ed | ||
![]() |
a6eeceeb67 | ||
![]() |
10622008c4 | ||
![]() |
d955fe7ca3 | ||
![]() |
35584d44aa | ||
![]() |
86c2f389d5 | ||
![]() |
bc999d0b5f | ||
![]() |
0dba48be13 | ||
![]() |
18dd2ad884 | ||
![]() |
46bb3e109c | ||
![]() |
33488ccf40 | ||
![]() |
eb9d72ad72 | ||
![]() |
71f5b46639 | ||
![]() |
c1555326fa | ||
![]() |
46d340a5ce | ||
![]() |
768de1a1e7 | ||
![]() |
77f9b27fcf | ||
![]() |
11ecfd370c | ||
![]() |
ad5a76aedc | ||
![]() |
03fd57e426 | ||
![]() |
47ed6bedb4 | ||
![]() |
ed2c774d86 | ||
![]() |
b7b949b415 | ||
![]() |
c8de6ccb4c | ||
![]() |
e99086c228 | ||
![]() |
ddccae60b5 | ||
![]() |
dcb80b7c9c | ||
![]() |
ab3d53c19f | ||
![]() |
8d8b24f00a | ||
![]() |
dbbb19e37f | ||
![]() |
99af22b621 | ||
![]() |
5e17a4e958 | ||
![]() |
7a567a47b9 | ||
![]() |
6df1ee64f2 | ||
![]() |
7218c450e6 | ||
![]() |
27bce0e5bb | ||
![]() |
6752acbf12 | ||
![]() |
787358d1b0 | ||
![]() |
b6dc7af13e | ||
![]() |
7c23e2ef82 | ||
![]() |
5e8813f5b6 | ||
![]() |
893a306da0 | ||
![]() |
99a2cf926a | ||
![]() |
75c0bf7b61 | ||
![]() |
55216ad5ed | ||
![]() |
a5a91d5139 | ||
![]() |
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 | ||
![]() |
2503b39897 | ||
![]() |
d5c841253c | ||
![]() |
1e2f51ef3e | ||
![]() |
9b2e533038 | ||
![]() |
dd6ec04801 | ||
![]() |
9ea67b1b36 | ||
![]() |
04548237fd | ||
![]() |
fc6678515a | ||
![]() |
2fe9fbd584 | ||
![]() |
26a3ea4178 | ||
![]() |
30b9eae872 | ||
![]() |
035a7826ba | ||
![]() |
e8bebea543 | ||
![]() |
f926b53848 | ||
![]() |
6ff065cd27 | ||
![]() |
d0e4055bf0 | ||
![]() |
b12bc4adc6 | ||
![]() |
45933ba3d0 | ||
![]() |
3065720817 | ||
![]() |
f58e0cd0c4 | ||
![]() |
043decf7e4 | ||
![]() |
9382b16171 | ||
![]() |
fe1c3417c7 | ||
![]() |
936c5cabd3 | ||
![]() |
d1f16cdee3 | ||
![]() |
2c60cc4ca2 | ||
![]() |
07f20b1ad7 | ||
![]() |
551404c08f | ||
![]() |
2b19b1659c | ||
![]() |
2568daedb4 | ||
![]() |
cf0a526187 | ||
![]() |
fd61ac0c41 | ||
![]() |
3b0e1dbcc4 | ||
![]() |
2650e1eb36 | ||
![]() |
52baa819cf | ||
![]() |
238b47b2f9 | ||
![]() |
d12f932351 | ||
![]() |
75fffe2f15 | ||
![]() |
82f6a78511 | ||
![]() |
3cc302bc42 | ||
![]() |
a4c957b8ef | ||
![]() |
6dac82989b | ||
![]() |
a8900f7278 | ||
![]() |
d7a5502d30 | ||
![]() |
9a30b8a9ae | ||
![]() |
0557750987 | ||
![]() |
5f05c1d7c0 | ||
![]() |
78403d9696 | ||
![]() |
376d803b96 | ||
![]() |
898548d399 | ||
![]() |
e142a49479 | ||
![]() |
b0fcdf628f | ||
![]() |
8ac122b151 | ||
![]() |
8f541c37a7 | ||
![]() |
03d9996dcc | ||
![]() |
a3511ab234 | ||
![]() |
bee628921a | ||
![]() |
7cc5bd0280 | ||
![]() |
492a31dd10 | ||
![]() |
0f7fc4907a | ||
![]() |
7c40c3ba68 | ||
![]() |
d264576a07 | ||
![]() |
6a2d856ecf | ||
![]() |
073296eb07 | ||
![]() |
f4cdd6a9ab | ||
![]() |
8a1ff1ef70 | ||
![]() |
305571bc25 | ||
![]() |
4faa850fba | ||
![]() |
fad74a4ca2 | ||
![]() |
3c35c6c8ae | ||
![]() |
dfeee7894a | ||
![]() |
c3dbe00f0b | ||
![]() |
ee70baafd9 | ||
![]() |
66b55bf910 | ||
![]() |
e4d468b17c | ||
![]() |
7b24e3bc8e | ||
![]() |
096a1d8427 | ||
![]() |
ded8a415cf | ||
![]() |
d778f98b0d | ||
![]() |
05b288c8d0 | ||
![]() |
850970e204 | ||
![]() |
4d277991ef | ||
![]() |
80a9770936 | ||
![]() |
24a6a4c25a | ||
![]() |
3a1abe82a1 | ||
![]() |
8c05c6d4df | ||
![]() |
802a212b9b | ||
![]() |
bd02bcd0de | ||
![]() |
4c72fa7da1 | ||
![]() |
13dd4dfc05 | ||
![]() |
478c95f6d4 | ||
![]() |
d08807544f | ||
![]() |
f20776fb8f | ||
![]() |
05261a91ea | ||
![]() |
a745468b0c | ||
![]() |
b738065d88 | ||
![]() |
9c47583a0a | ||
![]() |
6356979994 | ||
![]() |
0fccf4c16a | ||
![]() |
7505a5a208 | ||
![]() |
36be741eb3 | ||
![]() |
363e9b2dcb | ||
![]() |
b4a249245a | ||
![]() |
ab9aed0143 | ||
![]() |
a875f681a7 | ||
![]() |
538e3cbd0f | ||
![]() |
93e0828135 | ||
![]() |
72d1235b2c | ||
![]() |
2d1113626a | ||
![]() |
82b88c85c2 | ||
![]() |
681b10e070 | ||
![]() |
baa94ca1cb | ||
![]() |
bc89692acb | ||
![]() |
b651f4e18f | ||
![]() |
13bce1df60 | ||
![]() |
c28221623c | ||
![]() |
a48074e5f1 | ||
![]() |
46c3ea2a7c | ||
![]() |
1cc1674254 | ||
![]() |
26cfe7d9ea | ||
![]() |
881eb92fa1 | ||
![]() |
ce2790a89b | ||
![]() |
91218bb534 | ||
![]() |
eaa3d96bc7 | ||
![]() |
03ddde9a97 | ||
![]() |
120b9f5ce9 | ||
![]() |
11bf10cbde | ||
![]() |
4feccb9989 | ||
![]() |
90b706239e | ||
![]() |
416aa7adaf | ||
![]() |
506883c960 | ||
![]() |
8173d7d805 | ||
![]() |
5c8448c36e | ||
![]() |
ad15c07a18 | ||
![]() |
08b79f0595 | ||
![]() |
f4c50bc7a2 | ||
![]() |
25ea75cf54 | ||
![]() |
e9bca1b51d | ||
![]() |
b29b87d0f6 | ||
![]() |
b4732f3c31 | ||
![]() |
b7d6c8e557 | ||
![]() |
1d0a5c0e87 | ||
![]() |
74eac1f72c | ||
![]() |
78c88f212f | ||
![]() |
af09479203 | ||
![]() |
69bd118001 | ||
![]() |
d59e6c630d | ||
![]() |
ed3bcaa574 | ||
![]() |
7747a8c4e2 | ||
![]() |
a80d9694d8 | ||
![]() |
a2eb16d328 | ||
![]() |
ee9d54245c | ||
![]() |
e434bb8952 | ||
![]() |
d4a6ed6d08 | ||
![]() |
670a0b8a26 | ||
![]() |
e111f7c70b | ||
![]() |
578b5afa06 | ||
![]() |
8a38d06318 | ||
![]() |
ca9ca0105e | ||
![]() |
ec0e8c7804 | ||
![]() |
abb733b9df | ||
![]() |
3bf9c5f21c | ||
![]() |
53cd1cfcb2 | ||
![]() |
e5e6e9c07a | ||
![]() |
7543af2a38 | ||
![]() |
4f2697bf52 | ||
![]() |
4e9bd86012 | ||
![]() |
f60755b2b0 | ||
![]() |
eb14b6c5ad | ||
![]() |
df8ff19550 | ||
![]() |
d9237acb70 | ||
![]() |
010dee7a16 | ||
![]() |
b22d8358fb | ||
![]() |
93f2274c5d | ||
![]() |
d29c27b400 | ||
![]() |
c6a73f2dcb | ||
![]() |
a170f1f8a9 | ||
![]() |
2b1e3c6b02 | ||
![]() |
50c2d918b0 | ||
![]() |
100166023a | ||
![]() |
2894045134 | ||
![]() |
ac13b8b282 | ||
![]() |
573a133815 | ||
![]() |
e41c66cdda | ||
![]() |
e2c2423e53 | ||
![]() |
4dd3b57539 | ||
![]() |
d10d0b039f | ||
![]() |
6bf3b8e7cd | ||
![]() |
d9db65685b | ||
![]() |
afdbf3541b | ||
![]() |
2e0095a587 | ||
![]() |
51ada1c933 | ||
![]() |
1f941acda4 | ||
![]() |
b20b6d003f | ||
![]() |
eaca1ac228 | ||
![]() |
225a5640e5 | ||
![]() |
681ab1ff88 | ||
![]() |
f1135d8a96 | ||
![]() |
2d638a738e | ||
![]() |
5a136f8865 | ||
![]() |
c816b97525 | ||
![]() |
468ad70751 | ||
![]() |
aee612e06d | ||
![]() |
8633b5207d | ||
![]() |
7214f3fe6d | ||
![]() |
a97f55c32d | ||
![]() |
62fea1fa78 | ||
![]() |
3255a29b30 | ||
![]() |
3c26ecdcdf | ||
![]() |
fbc69832d8 | ||
![]() |
0b62144d97 | ||
![]() |
af12fc1a51 | ||
![]() |
cbab3484e7 | ||
![]() |
ded5e29f10 | ||
![]() |
a547030ce0 | ||
![]() |
523fca2177 | ||
![]() |
8e2ab83d92 | ||
![]() |
97ed08e5c8 | ||
![]() |
55a1a1c0eb | ||
![]() |
025e7f5c38 | ||
![]() |
b1b2bd4562 | ||
![]() |
cf1c73bd32 | ||
![]() |
f858eeef46 | ||
![]() |
b86ae8dd14 | ||
![]() |
a2605cdddd | ||
![]() |
77ff774c97 | ||
![]() |
60bbb9ace4 | ||
![]() |
5d8f625d01 | ||
![]() |
f10f25d129 | ||
![]() |
b4111fe4b1 | ||
![]() |
ffee36607d | ||
![]() |
ce399cb764 | ||
![]() |
44d3558e87 | ||
![]() |
5526cde869 | ||
![]() |
8df8b1c574 | ||
![]() |
ad5ace79fb | ||
![]() |
dd2df9106c | ||
![]() |
05385f3568 | ||
![]() |
706cc47197 | ||
![]() |
d402ab7dcf | ||
![]() |
2f251a3a61 | ||
![]() |
49773140c6 | ||
![]() |
1c3dcefcec | ||
![]() |
3be2e6f08b | ||
![]() |
879c23e002 | ||
![]() |
c40958f450 | ||
![]() |
285551674d | ||
![]() |
c9ac591dc3 | ||
![]() |
17ed492a5f | ||
![]() |
5a82e1b60c | ||
![]() |
fab1e3dbb0 | ||
![]() |
432fb4e3cc | ||
![]() |
173bf5460c | ||
![]() |
15e7f75e39 | ||
![]() |
72bfab16df | ||
![]() |
95ffc7a016 | ||
![]() |
4d4aadd83f | ||
![]() |
6f24b31b39 | ||
![]() |
a1e312440f | ||
![]() |
f35f671dc3 | ||
![]() |
5b036e84e1 | ||
![]() |
d66cf881fb | ||
![]() |
edb814c49a | ||
![]() |
81988179e9 | ||
![]() |
098779a82d | ||
![]() |
13d8d75cfa | ||
![]() |
fec71ebf99 | ||
![]() |
03a11aeda1 | ||
![]() |
9d118dc2fd | ||
![]() |
3c3f2251bb | ||
![]() |
fbfd4a2769 | ||
![]() |
c86b68a6e0 | ||
![]() |
95a2da6410 | ||
![]() |
668a08be45 | ||
![]() |
4b3e0e8d09 | ||
![]() |
cbbac01313 | ||
![]() |
dfae46d92e | ||
![]() |
befc0d4c9e | ||
![]() |
adb40d8712 | ||
![]() |
2cb9345ed5 | ||
![]() |
714ea5d8f9 | ||
![]() |
9cb8a77bf8 | ||
![]() |
8134e6bc17 | ||
![]() |
3c4f3e9104 | ||
![]() |
9d66bc5167 | ||
![]() |
fb0d51c574 | ||
![]() |
9e710f7093 | ||
![]() |
3d894d02d3 | ||
![]() |
5c070428d3 | ||
![]() |
1bb98119f5 | ||
![]() |
ab6e8be6b1 | ||
![]() |
2a10f6bf9e | ||
![]() |
7ae26d3ebb | ||
![]() |
2fa06d28e3 | ||
![]() |
ed12770494 | ||
![]() |
8274594848 | ||
![]() |
214833af68 | ||
![]() |
c63d049d55 | ||
![]() |
303e69a6b9 | ||
![]() |
129ea726e8 | ||
![]() |
b32226c5ed | ||
![]() |
b84e003f27 | ||
![]() |
4ae526d6b2 | ||
![]() |
1bad7fae3f | ||
![]() |
2265de8d44 | ||
![]() |
d095c98b13 | ||
![]() |
5a12f34593 | ||
![]() |
a45780566f | ||
![]() |
43c03de0ad | ||
![]() |
6019a21ebd | ||
![]() |
9cc3351fcf | ||
![]() |
20477f0aaa | ||
![]() |
9fc7b57157 | ||
![]() |
f0474ce2f2 | ||
![]() |
3664a06d8b | ||
![]() |
85ae3da0c0 | ||
![]() |
232b457eff | ||
![]() |
14f026f8c4 | ||
![]() |
681fa3cbe9 | ||
![]() |
47302acf66 | ||
![]() |
91bb5e2fd4 | ||
![]() |
7fcd055458 | ||
![]() |
4292af4294 | ||
![]() |
8ee12868aa | ||
![]() |
8f43fbd399 | ||
![]() |
e9a03994e2 | ||
![]() |
acf2b0ee57 | ||
![]() |
1c5bfbac24 | ||
![]() |
9483e3ff2c | ||
![]() |
124845bee1 | ||
![]() |
3c1263ada6 | ||
![]() |
fa02d1efe6 | ||
![]() |
d0771715b6 | ||
![]() |
03f6f75e49 | ||
![]() |
146ec49a32 | ||
![]() |
2f6501f1af | ||
![]() |
85fa1c5a9c | ||
![]() |
b33c859d67 | ||
![]() |
83d61a5929 | ||
![]() |
d3fe33a837 | ||
![]() |
580a67a18d | ||
![]() |
f7ebffec45 | ||
![]() |
b04c33a5fb | ||
![]() |
8b88e7f4cd | ||
![]() |
6cadae4e7f | ||
![]() |
aaae62de5f | ||
![]() |
fbde0cbad9 | ||
![]() |
88c591054f | ||
![]() |
d6990b3cd1 | ||
![]() |
73e70e6546 | ||
![]() |
6fc8ef6b50 | ||
![]() |
de352a839e | ||
![]() |
008e0cb66c | ||
![]() |
9ee733ea80 | ||
![]() |
6d13ba42c6 | ||
![]() |
9ef4a11771 | ||
![]() |
aac9f02447 | ||
![]() |
5185c90ad6 | ||
![]() |
ff6f6fea33 | ||
![]() |
ae1d462bec | ||
![]() |
9c41cd7f8f | ||
![]() |
98cecd3e84 | ||
![]() |
28707f9a4c | ||
![]() |
9875b61082 | ||
![]() |
d30fdb1db7 | ||
![]() |
eae89e1be0 | ||
![]() |
ab73bf87b0 | ||
![]() |
186e420db5 | ||
![]() |
de2ba413a1 | ||
![]() |
3b264015cb | ||
![]() |
2bd68b0585 | ||
![]() |
cf7d1ed108 | ||
![]() |
9607a5248b | ||
![]() |
d55ae48e4a | ||
![]() |
2c8d72b971 | ||
![]() |
bfcff12499 | ||
![]() |
dbb4d08ca0 | ||
![]() |
3d6cacadff | ||
![]() |
f5cd878096 | ||
![]() |
0212df5bd1 | ||
![]() |
111170e7d4 | ||
![]() |
873feb9cfe | ||
![]() |
8736c0b572 | ||
![]() |
1dba4c55f5 | ||
![]() |
7670ba8a43 | ||
![]() |
a3ba4e59b3 | ||
![]() |
72e97ca6b4 | ||
![]() |
32003a72fd | ||
![]() |
5b869cce46 | ||
![]() |
c190cdc9bd | ||
![]() |
badf901361 | ||
![]() |
97144f74da | ||
![]() |
193de508cd | ||
![]() |
5fd30e1be9 | ||
![]() |
a91f16ed62 | ||
![]() |
711abfca35 | ||
![]() |
5ed41b3f9b | ||
![]() |
8caa916316 | ||
![]() |
fb8f28f17d | ||
![]() |
50400895de | ||
![]() |
0ee451f3d3 | ||
![]() |
8ad8824e3a | ||
![]() |
1721ba5ddf | ||
![]() |
6c219e72d5 | ||
![]() |
829ce12710 | ||
![]() |
ddcb27da3f | ||
![]() |
c696b78393 | ||
![]() |
4b1f086469 | ||
![]() |
91b4c81986 | ||
![]() |
e88baa1995 | ||
![]() |
38387d1a0f | ||
![]() |
b23522d39f | ||
![]() |
c86f163cb7 | ||
![]() |
de8e306d9f | ||
![]() |
c46abae264 | ||
![]() |
a14230afcc | ||
![]() |
03e2e30510 | ||
![]() |
fedf6aa847 | ||
![]() |
7bb32f31de | ||
![]() |
abf6d26641 | ||
![]() |
b0599fa27c | ||
![]() |
72a6754245 | ||
![]() |
e6c2680d2f | ||
![]() |
87e458bc0f | ||
![]() |
eb0b3ae5f4 | ||
![]() |
c1b1cb2a27 | ||
![]() |
d21d0eae55 | ||
![]() |
acff2186b4 | ||
![]() |
0d5fc8a1c0 | ||
![]() |
7714ef47c8 | ||
![]() |
ff0291d346 | ||
![]() |
c33e3909b9 | ||
![]() |
75d641dc82 | ||
![]() |
ac7c038703 | ||
![]() |
4d12236930 | ||
![]() |
44d2310cdb | ||
![]() |
0724692d40 | ||
![]() |
a089e662fb | ||
![]() |
98b2c75a3c | ||
![]() |
24ffa6c63a | ||
![]() |
980d6a6154 | ||
![]() |
511a2e18ab | ||
![]() |
fc398c15f4 | ||
![]() |
434ef5628f | ||
![]() |
714b713134 | ||
![]() |
f8e4b770e8 | ||
![]() |
1187edb486 | ||
![]() |
b03c7fbe4d | ||
![]() |
3bc5737f6d | ||
![]() |
da4be7b57f | ||
![]() |
0dee03e0bc | ||
![]() |
8be77017e9 | ||
![]() |
4b11a2f1c1 | ||
![]() |
3795ab0495 | ||
![]() |
71c36f59af | ||
![]() |
67ce3e6741 | ||
![]() |
a572b0acea | ||
![]() |
dccf04d4c0 | ||
![]() |
b4cd7bdf26 | ||
![]() |
2681a31e87 | ||
![]() |
601bd5e922 | ||
![]() |
606915f39a | ||
![]() |
56487a44f3 | ||
![]() |
4b6a0d8aa2 | ||
![]() |
a48e77810d | ||
![]() |
3ca1e61a8c | ||
![]() |
7dc4609a83 | ||
![]() |
396393e9b7 | ||
![]() |
217f7b5b21 | ||
![]() |
ea2030666c | ||
![]() |
f6c48494b9 | ||
![]() |
290abf7c43 | ||
![]() |
84274c3911 | ||
![]() |
c1ddf7f667 | ||
![]() |
04f4a25870 | ||
![]() |
2599068ccd | ||
![]() |
b586a264ca | ||
![]() |
4c1a72fdb2 | ||
![]() |
c825f911f5 | ||
![]() |
86cd7ba03f | ||
![]() |
f683d1219e | ||
![]() |
10aa2e14f1 | ||
![]() |
fba5504dd9 | ||
![]() |
2c19d80a16 | ||
![]() |
421c37f010 | ||
![]() |
0ac9d58194 | ||
![]() |
1407ea85d7 | ||
![]() |
b2c002057e | ||
![]() |
73a9fe16c5 | ||
![]() |
ddb7d5181f | ||
![]() |
720126647c | ||
![]() |
627b271d3b | ||
![]() |
3d4f493f9f | ||
![]() |
c5689df73d | ||
![]() |
45ad6b6f0b | ||
![]() |
a617e04290 | ||
![]() |
d9a8b8f3fd | ||
![]() |
7c609820b1 | ||
![]() |
3a8e658d54 | ||
![]() |
e546a4f0b5 | ||
![]() |
cfd6512856 | ||
![]() |
4b541a166f | ||
![]() |
f3faa0df2c | ||
![]() |
8085885da4 | ||
![]() |
aa5a395d91 | ||
![]() |
f20b139841 | ||
![]() |
700afc2709 | ||
![]() |
cf515fe6f0 | ||
![]() |
166784b7f2 | ||
![]() |
b556234207 | ||
![]() |
98598a53d9 | ||
![]() |
f0ae00e7df | ||
![]() |
3fc44f6fc1 | ||
![]() |
3b07738d4d | ||
![]() |
3bc031ff00 | ||
![]() |
a69a4e9696 | ||
![]() |
b85ebb2791 | ||
![]() |
28c0c38d0d | ||
![]() |
15e45c3dec | ||
![]() |
34b117efe3 | ||
![]() |
04b4f1cf58 | ||
![]() |
64049fdcf7 | ||
![]() |
2ed16aa66e | ||
![]() |
6a4e56322f | ||
![]() |
81b76dd327 | ||
![]() |
304d8f7386 | ||
![]() |
67f8ce7849 | ||
![]() |
6f7b76ec39 | ||
![]() |
cb4446b79d | ||
![]() |
1caba78b4d | ||
![]() |
9fe5a91bc2 | ||
![]() |
dde57b9387 | ||
![]() |
737728f603 | ||
![]() |
d37d043531 | ||
![]() |
08878941ab | ||
![]() |
c60fa2c441 | ||
![]() |
6e95990431 | ||
![]() |
cb3f5ad259 | ||
![]() |
abe26ce9f8 | ||
![]() |
3e86efc66a | ||
![]() |
5b490203b2 | ||
![]() |
ffb320373d | ||
![]() |
4683df431c | ||
![]() |
d253790c7d | ||
![]() |
22548c1e9d | ||
![]() |
79d5645f81 | ||
![]() |
4c6d355376 | ||
![]() |
5edef6f942 | ||
![]() |
9b80173b87 | ||
![]() |
015f3b9607 | ||
![]() |
586de36835 | ||
![]() |
eead947352 | ||
![]() |
f2a29a6425 | ||
![]() |
2ddda1c766 | ||
![]() |
74a17da5b8 | ||
![]() |
c756c68f28 | ||
![]() |
412822db7c | ||
![]() |
2cd78470ce | ||
![]() |
e7282bdbd7 | ||
![]() |
326bffae7f | ||
![]() |
b104958473 | ||
![]() |
a27a7a4083 | ||
![]() |
bb2892edd8 | ||
![]() |
4862a399c9 | ||
![]() |
eaaad88443 | ||
![]() |
c150fb881e | ||
![]() |
3e53b7c7b1 | ||
![]() |
2dfed863ed | ||
![]() |
95b98d3f79 | ||
![]() |
4ad089ef54 | ||
![]() |
bcfecc53a1 | ||
![]() |
4b238e1842 | ||
![]() |
120ecc6988 | ||
![]() |
bcfb890e1a | ||
![]() |
c717fc5ec8 | ||
![]() |
899b30213e | ||
![]() |
772ac12329 | ||
![]() |
9b95fc5de9 | ||
![]() |
6cbd9dc920 | ||
![]() |
4f38821bb3 | ||
![]() |
41c07e74ca | ||
![]() |
060d0dd556 | ||
![]() |
fce77104d4 | ||
![]() |
21686c86df | ||
![]() |
ca7d45ff0c | ||
![]() |
363d1d74df | ||
![]() |
4567f8cc2c | ||
![]() |
c9ff89a143 | ||
![]() |
4b6e02f773 | ||
![]() |
7a69a23f0c | ||
![]() |
d80da3bbfe | ||
![]() |
55affdebce | ||
![]() |
f85a5e65ad | ||
![]() |
8daf52e8c2 | ||
![]() |
fab567400d | ||
![]() |
579f98d027 | ||
![]() |
926dd46627 | ||
![]() |
3c0cb33bc7 | ||
![]() |
98dde58f9d | ||
![]() |
d6dbbd1f1f | ||
![]() |
e18c66d688 | ||
![]() |
e0edcd64d2 | ||
![]() |
91137a216f | ||
![]() |
d06b76af3f | ||
![]() |
7e129f282f | ||
![]() |
da2bbcee09 | ||
![]() |
3bd8e355f6 | ||
![]() |
68bd8d40e8 | ||
![]() |
3dacbe51a1 | ||
![]() |
5f87d69e9b | ||
![]() |
d3466c3e82 | ||
![]() |
43b8f45eee | ||
![]() |
6631980ee6 | ||
![]() |
4a8becf662 | ||
![]() |
a77ca2f126 | ||
![]() |
a87bf009ef | ||
![]() |
86bf7ff4f2 | ||
![]() |
a69033917d | ||
![]() |
9dc9b5accf | ||
![]() |
7dd2683fc3 | ||
![]() |
abca095b9e | ||
![]() |
f0fb8c1005 | ||
![]() |
042afd9bb8 | ||
![]() |
15ad31bd84 | ||
![]() |
f49a7746fb | ||
![]() |
93450e1dcf | ||
![]() |
b9a8bfb2bd | ||
![]() |
e682997195 | ||
![]() |
340f9518cd | ||
![]() |
fe4fe9e8d3 | ||
![]() |
b5342e0fab | ||
![]() |
eb3cd85680 | ||
![]() |
09a7685a3f | ||
![]() |
6d00cad749 | ||
![]() |
60009144a1 | ||
![]() |
b2b077868e | ||
![]() |
a24cfe4cc7 | ||
![]() |
032231f10e | ||
![]() |
781a88bc4c | ||
![]() |
d2b44318fa | ||
![]() |
ad7824460b | ||
![]() |
8cade4fbf5 | ||
![]() |
fecf59e433 | ||
![]() |
bdc2dd5f9c | ||
![]() |
0fe199a97c | ||
![]() |
aca1c86455 | ||
![]() |
d62d9b0f48 | ||
![]() |
5f45e93d12 | ||
![]() |
4634d2a4a8 | ||
![]() |
6c2a3431c1 | ||
![]() |
4fa6a3e976 | ||
![]() |
eb9a7a15d6 | ||
![]() |
8433f49ed9 | ||
![]() |
7c0d9acbf1 | ||
![]() |
7d0ea04b3e | ||
![]() |
1aa659e6b7 | ||
![]() |
b6ab3d2067 | ||
![]() |
a205aa02b3 | ||
![]() |
cba089081b | ||
![]() |
98571a496c | ||
![]() |
ed7454ffc9 | ||
![]() |
add2aac934 | ||
![]() |
4791cce928 | ||
![]() |
2561d54b2d | ||
![]() |
a86f9444ce | ||
![]() |
37be3530e6 | ||
![]() |
25f02b99d3 | ||
![]() |
fd437eb7ee | ||
![]() |
5fca681222 | ||
![]() |
3504feedf3 | ||
![]() |
a2e00bbd9f | ||
![]() |
23fe338c5d | ||
![]() |
1cbd41ef38 | ||
![]() |
d54b7d9f7c | ||
![]() |
9c85328b17 | ||
![]() |
b4686deb63 | ||
![]() |
4d85c0270f | ||
![]() |
9b8ada0326 | ||
![]() |
c37b31599c | ||
![]() |
6f55225aee | ||
![]() |
247ae73c64 | ||
![]() |
3bc7739098 | ||
![]() |
ce11ae2480 | ||
![]() |
3744abb26f | ||
![]() |
97e8c9b955 | ||
![]() |
0250b182b6 | ||
![]() |
e4a51af46e | ||
![]() |
c0aac8a5e9 | ||
![]() |
f52ed2a218 | ||
![]() |
0330b3ea4c | ||
![]() |
5a8f38abdc | ||
![]() |
45c6f4eb84 | ||
![]() |
187ae50d29 | ||
![]() |
2d694a864e | ||
![]() |
71851fe141 | ||
![]() |
23e7675171 | ||
![]() |
42802bd543 | ||
![]() |
c02983c889 | ||
![]() |
3b5df5d432 | ||
![]() |
aa27ddad02 | ||
![]() |
f377347dcc | ||
![]() |
2ec1096e21 | ||
![]() |
30ff10ea90 | ||
![]() |
6081dabe48 | ||
![]() |
27afd4192f | ||
![]() |
c6bc21f9db | ||
![]() |
7e1c441ee8 | ||
![]() |
a56ebad133 | ||
![]() |
156e9a08b0 |
94
.github/workflows/build.yml
vendored
94
.github/workflows/build.yml
vendored
@@ -16,6 +16,9 @@ jobs:
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: '/server'
|
||||
DSPACE_REST_SSL: false
|
||||
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||
# Comment this out to use the latest release
|
||||
#CHROME_VERSION: "90.0.4430.212-1"
|
||||
strategy:
|
||||
# Create a matrix of Node versions to test against (in parallel)
|
||||
matrix:
|
||||
@@ -26,18 +29,28 @@ jobs:
|
||||
steps:
|
||||
# https://github.com/actions/checkout
|
||||
- name: Checkout codebase
|
||||
uses: actions/checkout@v1
|
||||
uses: actions/checkout@v2
|
||||
|
||||
# https://github.com/actions/setup-node
|
||||
- name: Install Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v1
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
|
||||
- name: Install latest Chrome (for e2e tests)
|
||||
# If CHROME_VERSION env variable specified above, then pin to that version.
|
||||
# Otherwise, just install latest version of Chrome.
|
||||
- name: Install Chrome (for e2e tests)
|
||||
run: |
|
||||
if [[ -z "${CHROME_VERSION}" ]]
|
||||
then
|
||||
echo "Installing latest stable version"
|
||||
sudo apt-get update
|
||||
sudo apt-get --only-upgrade install google-chrome-stable -y
|
||||
else
|
||||
echo "Installing version ${CHROME_VERSION}"
|
||||
wget -q "https://dl.google.com/linux/chrome/deb/pool/main/g/google-chrome-stable/google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||
sudo dpkg -i "google-chrome-stable_${CHROME_VERSION}_amd64.deb"
|
||||
fi
|
||||
google-chrome --version
|
||||
|
||||
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
|
||||
@@ -53,9 +66,6 @@ jobs:
|
||||
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
|
||||
restore-keys: ${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install the latest chromedriver compatible with the installed chrome version
|
||||
run: yarn global add chromedriver --detect_chromedriver_version
|
||||
|
||||
- name: Install Yarn dependencies
|
||||
run: yarn install --frozen-lockfile
|
||||
|
||||
@@ -72,7 +82,7 @@ jobs:
|
||||
# Upload coverage reports to Codecov (for Node v12 only)
|
||||
# https://github.com/codecov/codecov-action
|
||||
- name: Upload coverage to Codecov.io
|
||||
uses: codecov/codecov-action@v1
|
||||
uses: codecov/codecov-action@v2
|
||||
if: matrix.node-version == '12.x'
|
||||
|
||||
# Using docker-compose start backend using CI configuration
|
||||
@@ -83,23 +93,63 @@ jobs:
|
||||
docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||
docker container ls
|
||||
|
||||
# Wait until the REST API returns a 200 response (or for a max of 30 seconds)
|
||||
# https://github.com/nev7n/wait_for_response
|
||||
- name: Wait for DSpace REST Backend to be ready (for e2e tests)
|
||||
uses: nev7n/wait_for_response@v1
|
||||
with:
|
||||
# We use the 'sites' endpoint to also ensure the database is ready
|
||||
url: 'http://localhost:8080/server/api/core/sites'
|
||||
responseCode: 200
|
||||
timeout: 30000
|
||||
|
||||
- name: Get DSpace REST Backend info/properties
|
||||
run: curl http://localhost:8080/server/api
|
||||
|
||||
# Run integration tests via Cypress.io
|
||||
# https://github.com/cypress-io/github-action
|
||||
# (NOTE: to run these e2e tests locally, just use 'ng e2e')
|
||||
- name: Run e2e tests (integration tests)
|
||||
uses: cypress-io/github-action@v2
|
||||
with:
|
||||
# Run tests in Chrome, headless mode
|
||||
browser: chrome
|
||||
headless: true
|
||||
# Start app before running tests (will be stopped automatically after tests finish)
|
||||
start: yarn run serve:ssr
|
||||
# Wait for backend & frontend to be available
|
||||
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
|
||||
wait-on: http://localhost:8080/server/api/core/sites, http://localhost:4000
|
||||
# Wait for 2 mins max for everything to respond
|
||||
wait-on-timeout: 120
|
||||
|
||||
# Cypress always creates a video of all e2e tests (whether they succeeded or failed)
|
||||
# Save those in an Artifact
|
||||
- name: Upload e2e test videos to Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
if: always()
|
||||
with:
|
||||
name: e2e-test-videos
|
||||
path: cypress/videos
|
||||
|
||||
# If e2e tests fail, Cypress creates a screenshot of what happened
|
||||
# Save those in an Artifact
|
||||
- name: Upload e2e test failure screenshots to Artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
if: failure()
|
||||
with:
|
||||
name: e2e-test-screenshots
|
||||
path: cypress/screenshots
|
||||
|
||||
# Start up the app with SSR enabled (run in background)
|
||||
- name: Start app in SSR (server-side rendering) mode
|
||||
run: |
|
||||
chromedriver --url-base='/wd/hub' --port=4444 &
|
||||
yarn run e2e:ci
|
||||
nohup yarn run serve:ssr &
|
||||
printf 'Waiting for app to start'
|
||||
until curl --output /dev/null --silent --head --fail http://localhost:4000/home; do
|
||||
printf '.'
|
||||
sleep 2
|
||||
done
|
||||
echo "App started successfully."
|
||||
|
||||
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
|
||||
# If it does, then SSR is working, as this tag is created by our MetadataService.
|
||||
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
||||
- name: Verify SSR (server-side rendering)
|
||||
run: |
|
||||
result=$(wget -O- -q http://localhost:4000/home)
|
||||
echo "$result"
|
||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
||||
|
||||
- name: Stop running app
|
||||
run: kill -9 $(lsof -t -i:4000)
|
||||
|
||||
- name: Shutdown Docker containers
|
||||
run: docker-compose -f ./docker/docker-compose-ci.yml down
|
||||
|
78
.github/workflows/docker.yml
vendored
Normal file
78
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,78 @@
|
||||
# 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
|
||||
|
||||
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/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
|
||||
# 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 }}
|
4
.gitignore
vendored
4
.gitignore
vendored
@@ -7,10 +7,6 @@ npm-debug.log
|
||||
|
||||
/build/
|
||||
|
||||
/src/environments/environment.ts
|
||||
/src/environments/environment.dev.ts
|
||||
/src/environments/environment.prod.ts
|
||||
|
||||
/coverage
|
||||
|
||||
/dist/
|
||||
|
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
@@ -3,5 +3,6 @@
|
||||
"i18n-ally.localesPaths": [
|
||||
"src/assets/i18n",
|
||||
"src/app/core/locale"
|
||||
]
|
||||
],
|
||||
"typescript.tsdk": "node_modules\\typescript\\lib"
|
||||
}
|
@@ -1,7 +1,7 @@
|
||||
# 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
|
||||
ADD . /app/
|
||||
EXPOSE 4000
|
||||
|
20
LICENSE
20
LICENSE
@@ -1,6 +1,6 @@
|
||||
DSpace source code BSD License:
|
||||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2002-2020, LYRASIS. All rights reserved.
|
||||
Copyright (c) 2002-2021, LYRASIS. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
@@ -13,13 +13,12 @@ notice, this list of conditions and the following disclaimer.
|
||||
notice, this list of conditions and the following disclaimer in the
|
||||
documentation and/or other materials provided with the distribution.
|
||||
|
||||
- Neither the name DuraSpace nor the name of the DSpace Foundation
|
||||
nor the names of its contributors may be used to endorse or promote
|
||||
products derived from this software without specific prior written
|
||||
permission.
|
||||
- Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
``AS IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
|
||||
@@ -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
|
||||
USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
|
||||
DAMAGE.
|
||||
|
||||
|
||||
DSpace uses third-party libraries which may be distributed under
|
||||
different licenses to the above. Information about these licenses
|
||||
is detailed in the LICENSES_THIRD_PARTY file at the root of the source
|
||||
tree. You must agree to the terms of these licenses, in addition to
|
||||
the above DSpace source code license, in order to use this software.
|
||||
|
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.
|
364
README.md
364
README.md
@@ -3,17 +3,39 @@
|
||||
dspace-angular
|
||||
==============
|
||||
|
||||
> The next UI for DSpace 7, based on Angular Universal.
|
||||
> The DSpace User Interface built on [Angular](https://angular.io/), written in [TypeScript](https://www.typescriptlang.org/) and using [Angular Universal](https://angular.io/guide/universal).
|
||||
|
||||
This project is currently under active development. For more information on the DSpace 7 release see the [DSpace 7.0 Release Status wiki page](https://wiki.lyrasis.org/display/DSPACE/DSpace+Release+7.0+Status)
|
||||
Overview
|
||||
--------
|
||||
|
||||
You can find additional information on the DSpace 7 Angular UI on the [wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development).
|
||||
DSpace open source software is a turnkey repository application used by more than
|
||||
2,000 organizations and institutions worldwide to provide durable access to digital resources.
|
||||
For more information, visit http://www.dspace.org/
|
||||
|
||||
DSpace consists of both a Java-based backend and an Angular-based frontend.
|
||||
|
||||
* Backend (https://github.com/DSpace/DSpace/) provides a REST API, along with other machine-based interfaces (e.g. OAI-PMH, SWORD, etc)
|
||||
* The REST Contract is at https://github.com/DSpace/RestContract
|
||||
* Frontend (this codebase) is the User Interface built on the REST API
|
||||
|
||||
Downloads
|
||||
---------
|
||||
|
||||
* Backend (REST API): https://github.com/DSpace/DSpace/releases
|
||||
* Frontend (User Interface): https://github.com/DSpace/dspace-angular/releases
|
||||
|
||||
|
||||
## Documentation / Installation
|
||||
|
||||
Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.lyrasis.org/display/DSDOC/).
|
||||
|
||||
The latest DSpace Installation instructions are available at:
|
||||
https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
||||
|
||||
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) `v12.x`, `v14.x` or `v16.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
|
||||
|
||||
```bash
|
||||
# clone the repo
|
||||
@@ -47,6 +69,9 @@ Table of Contents
|
||||
- [Cleaning](#cleaning)
|
||||
- [Testing](#testing)
|
||||
- [Test a Pull Request](#test-a-pull-request)
|
||||
- [Unit Tests](#unit-tests)
|
||||
- [E2E Tests](#e2e-tests)
|
||||
- [Writing E2E Tests](#writing-e2e-tests)
|
||||
- [Documentation](#documentation)
|
||||
- [Other commands](#other-commands)
|
||||
- [Recommended Editors/IDEs](#recommended-editorsides)
|
||||
@@ -65,60 +90,98 @@ Requirements
|
||||
------------
|
||||
|
||||
- [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 `v12.x`, `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.
|
||||
|
||||
Installing
|
||||
----------
|
||||
|
||||
- `yarn run global` to install the required global dependencies
|
||||
- `yarn install` to install the local dependencies
|
||||
|
||||
### Configuring
|
||||
|
||||
Default configuration file is located in `src/environments/` folder.
|
||||
Default configuration file is located in `config/` folder.
|
||||
|
||||
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 `environment.prod.ts` file in `src/environments/` for a `production` environment;
|
||||
- Create a new `config.(dev or development).yml` file in `config/` for a `development` 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.
|
||||
|
||||
The following settings can be overwritten in this file:
|
||||
The following non-convention settings:
|
||||
|
||||
```bash
|
||||
DSPACE_HOST # The host name of the angular application
|
||||
DSPACE_PORT # The port number of the angular application
|
||||
DSPACE_NAMESPACE # The namespace of the angular application
|
||||
DSPACE_SSL # Whether the angular application uses SSL [true/false]
|
||||
```
|
||||
|
||||
DSPACE_REST_HOST # The host name of the REST application
|
||||
DSPACE_REST_PORT # The port number of the REST application
|
||||
DSPACE_REST_NAMESPACE # The namespace of the REST application
|
||||
DSPACE_REST_SSL # Whether the angular REST uses SSL [true/false]
|
||||
All other 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.
|
||||
|
||||
```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.:
|
||||
```bash
|
||||
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`.
|
||||
|
||||
#### Using environment variables in code
|
||||
To use environment variables in a UI component, use:
|
||||
|
||||
```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
|
||||
@@ -146,6 +209,9 @@ This will build the application and put the result in the `dist` folder. You ca
|
||||
|
||||
|
||||
### Running the application with Docker
|
||||
NOTE: At this time, we do not have production-ready Docker images for DSpace.
|
||||
That said, we do have quick-start Docker Compose scripts for development or testing purposes.
|
||||
|
||||
See [Docker Runtime Options](docker/README.md)
|
||||
|
||||
|
||||
@@ -184,34 +250,68 @@ Once you have tested the Pull Request, please add a comment and/or approval to t
|
||||
|
||||
### Unit Tests
|
||||
|
||||
Unit tests use Karma. You can find the configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `yarn run coverage`.
|
||||
Unit tests use the [Jasmine test framework](https://jasmine.github.io/), and are run via [Karma](https://karma-runner.github.io/).
|
||||
|
||||
You can find the Karma configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `yarn run coverage`.
|
||||
|
||||
The default browser is Google Chrome.
|
||||
|
||||
Place your tests in the same location of the application source code files that they test.
|
||||
Place your tests in the same location of the application source code files that they test, e.g. ending with `*.component.spec.ts`
|
||||
|
||||
and run: `yarn run test`
|
||||
and run: `yarn test`
|
||||
|
||||
### E2E test
|
||||
If you run into odd test errors, see the Angular guide to debugging tests: https://angular.io/guide/test-debugging
|
||||
|
||||
E2E tests use Protractor + Selenium server + browsers. You can find the configuration file at the same level of this README file:`./protractor.conf.js` Protractor is installed as 'local' as a dev dependency.
|
||||
### E2E Tests
|
||||
|
||||
If you are going to use a remote test enviroment you need to edit the './e2e//protractor.conf.js'. Follow the instructions you will find inside it.
|
||||
E2E tests (aka integration tests) use [Cypress.io](https://www.cypress.io/). Configuration for cypress can be found in the `cypress.json` file in the root directory.
|
||||
|
||||
The default browser is Google Chrome.
|
||||
The test files can be found in the `./cypress/integration/` folder.
|
||||
|
||||
Place your tests at the following path: `./e2e`
|
||||
Before you can run e2e tests, two things are required:
|
||||
1. 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 `config.prod.yml` or `config.yml`. You may override this using env variables, see [Configuring](#configuring).
|
||||
2. Your backend MUST include our Entities Test Data set. Some tests run against a (currently hardcoded) Community/Collection/Item UUID. These UUIDs are all valid for our Entities Test Data set. 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
|
||||
|
||||
and run: `ng e2e`
|
||||
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.
|
||||
|
||||
### Continuous Integration (CI) Test
|
||||
#### Writing E2E Tests
|
||||
|
||||
To run all the tests (e.g.: to run tests with Continuous Integration software) you can execute:`yarn run ci` Keep in mind that this command prerequisites are the sum of unit test and E2E tests.
|
||||
All E2E tests must be created under the `./cypress/integration/` folder, and must end in `.spec.ts`. Subfolders are allowed.
|
||||
|
||||
* The easiest way to start creating new tests is by running `ng e2e`. This builds the app and brings up Cypress.
|
||||
* From here, if you are editing an existing test file, you can either open it in your IDE or run it first to see what it already does.
|
||||
* To create a new test file, click `+ New Spec File`. Choose a meaningful name ending in `spec.ts` (Please make sure it ends in `.ts` so that it's a Typescript file, and not plain Javascript)
|
||||
* Start small. Add a basic `describe` and `it` which just [cy.visit](https://docs.cypress.io/api/commands/visit) the page you want to test. For example:
|
||||
```
|
||||
describe('Community/Collection Browse Page', () => {
|
||||
it('should exist as a page', () => {
|
||||
cy.visit('/community-list');
|
||||
});
|
||||
});
|
||||
```
|
||||
* Run your test file from the Cypress window. This starts the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner) in a new browser window.
|
||||
* 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.
|
||||
* 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
|
||||
* 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 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.
|
||||
* 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.
|
||||
|
||||
_Hint: Creating e2e tests is easiest in an IDE (like Visual Studio), as it can help prompt/autocomplete your Cypress commands._
|
||||
|
||||
More Information: [docs.cypress.io](https://docs.cypress.io/) has great guides & documentation helping you learn more about writing/debugging e2e tests in Cypress.
|
||||
|
||||
### Learning how to build tests
|
||||
|
||||
See our [DSpace Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide) for more hints/tips.
|
||||
|
||||
Documentation
|
||||
--------------
|
||||
|
||||
See [`./docs`](docs) for further documentation.
|
||||
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
|
||||
|
||||
Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of htis codebase.
|
||||
|
||||
### Building code documentation
|
||||
|
||||
@@ -234,8 +334,6 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've
|
||||
- Free
|
||||
- [Visual Studio Code](https://code.visualstudio.com/)
|
||||
- [Debugger for Chrome](https://marketplace.visualstudio.com/items?itemName=msjsdiag.debugger-for-chrome)
|
||||
- [Atom](https://atom.io/)
|
||||
- [TypeScript plugin](https://atom.io/packages/atom-typescript)
|
||||
- Paid
|
||||
- [Webstorm](https://www.jetbrains.com/webstorm/download/) or [IntelliJ IDEA Ultimate](https://www.jetbrains.com/idea/)
|
||||
- [Sublime Text](http://www.sublimetext.com/3)
|
||||
@@ -251,101 +349,85 @@ File Structure
|
||||
|
||||
```
|
||||
dspace-angular
|
||||
├── README.md * This document
|
||||
├── app.yaml * Application manifest file
|
||||
├── config * Folder for configuration files
|
||||
│ ├── environment.default.js * Default configuration files
|
||||
│ └── environment.test.js * Test configuration files
|
||||
├── config *
|
||||
│ └── config.yml * Default app config
|
||||
├── cypress * Folder for Cypress (https://cypress.io/) / e2e tests
|
||||
│ ├── downloads *
|
||||
│ ├── fixtures * Folder for e2e/integration test files
|
||||
│ ├── integration * Folder for any fixtures needed by e2e tests
|
||||
│ ├── plugins * Folder for Cypress plugins (if any)
|
||||
│ ├── 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
|
||||
├── e2e * Folder for e2e test files
|
||||
│ ├── app.e2e-spec.ts *
|
||||
│ ├── app.po.ts *
|
||||
│ ├── pagenotfound *
|
||||
│ │ ├── pagenotfound.e2e-spec.ts *
|
||||
│ │ └── pagenotfound.po.ts *
|
||||
│ └── tsconfig.json * TypeScript configuration file for e2e tests
|
||||
│ └── 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
|
||||
├── LICENSE *
|
||||
├── LICENSES_THIRD_PARTY *
|
||||
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
|
||||
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
|
||||
├── postcss.config.js * PostCSS (http://postcss.org/) configuration file
|
||||
├── protractor.conf.js *
|
||||
├── resources * Folder for static resources
|
||||
│ ├── data * Folder for static data
|
||||
│ │ └── en * Folder for i18n English data
|
||||
│ ├── i18n * Folder for i18n translations
|
||||
│ │ └── en.json * i18n translations for English
|
||||
│ └── images * Folder for images
|
||||
│ ├── dspace-logo-old.png *
|
||||
│ ├── dspace-logo.png *
|
||||
│ └── favicon.ico *
|
||||
├── rollup.config.js * Rollup (http://rollupjs.org/) configuration
|
||||
├── spec-bundle.js *
|
||||
├── src * The source of the application
|
||||
│ ├── app *
|
||||
│ │ ├── app-routing.module.ts *
|
||||
│ │ ├── app.component.html *
|
||||
│ │ ├── app.component.scss *
|
||||
│ │ ├── app.component.spec.ts *
|
||||
│ │ ├── app.component.ts *
|
||||
│ │ ├── app.effects.ts *
|
||||
│ │ ├── app.module.ts *
|
||||
│ │ ├── app.reducer.ts *
|
||||
│ │ ├── browser-app.module.ts * The root module for the client
|
||||
│ │ ├── +collection-page * Lazily loaded route for collection module
|
||||
│ │ ├── +community-page * Lazily loaded route for community module
|
||||
│ │ ├── core *
|
||||
│ │ ├── header *
|
||||
│ │ ├── +home * Lazily loaded route for home module
|
||||
│ │ ├── +item-page * Lazily loaded route for item module
|
||||
│ │ ├── object-list *
|
||||
│ │ ├── pagenotfound *
|
||||
│ │ ├── server-app.module.ts * The root module for the server
|
||||
│ │ ├── shared *
|
||||
│ │ ├── store.actions.ts *
|
||||
│ │ ├── store.effects.ts *
|
||||
│ │ ├── thumbnail *
|
||||
│ │ └── typings.d.ts * File that allows you to add custom typings for libraries without TypeScript support
|
||||
│ ├── backend * Folder containing a mock of the REST API, hosted by the express server
|
||||
│ │ ├── api.ts *
|
||||
│ │ ├── cache.ts *
|
||||
│ │ ├── data *
|
||||
│ │ └── db.ts *
|
||||
│ ├── config *
|
||||
│ │ ├── cache-config.interface.ts *
|
||||
│ │ ├── config.interface.ts *
|
||||
│ │ ├── global-config.interface.ts *
|
||||
│ │ ├── server-config.interface.ts *
|
||||
│ │ └── universal-config.interface.ts *
|
||||
│ ├── config.ts * File that loads environmental and shareable settings and makes them available to app components
|
||||
│ ├── 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
|
||||
│ ├── modules *
|
||||
│ │ ├── cookies *
|
||||
│ │ ├── data-loader *
|
||||
│ │ ├── transfer-http *
|
||||
│ │ ├── transfer-state *
|
||||
│ │ ├── transfer-store *
|
||||
│ │ └── translate-universal-loader.ts *
|
||||
│ ├── routes.ts * The routes file for the server
|
||||
│ ├── styles * Folder containing global styles
|
||||
│ │ ├── _mixins.scss *
|
||||
│ │ └── variables.scss * Global sass variables file
|
||||
│ ├── tsconfig.browser.json * TypeScript config for the client build
|
||||
│ ├── tsconfig.server.json * TypeScript config for the server build
|
||||
│ └── tsconfig.test.json * TypeScript config for the test build
|
||||
├── tsconfig.json * TypeScript config
|
||||
├── postcss.config.js * PostCSS (http://postcss.org/) configuration
|
||||
├── README.md * This document
|
||||
├── SECURITY.md *
|
||||
├── server.ts * Angular Universal Node.js Express server
|
||||
├── tsconfig.app.json * TypeScript config for browser (app)
|
||||
├── tsconfig.json * TypeScript common config
|
||||
├── tsconfig.server.json * TypeScript config for server
|
||||
├── tsconfig.spec.json * TypeScript config for tests
|
||||
├── tsconfig.ts-node.json * TypeScript config for using ts-node directly
|
||||
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
|
||||
├── typedoc.json * TYPEDOC configuration
|
||||
├── webpack * Webpack (https://webpack.github.io/) config directory
|
||||
│ ├── webpack.aot.js * Webpack (https://webpack.github.io/) config for AoT build
|
||||
│ ├── webpack.client.js * Webpack (https://webpack.github.io/) config for client build
|
||||
│ ├── webpack.common.js *
|
||||
│ ├── webpack.prod.js * Webpack (https://webpack.github.io/) config for production build
|
||||
│ ├── webpack.server.js * Webpack (https://webpack.github.io/) config for server build
|
||||
│ └── webpack.test.js * Webpack (https://webpack.github.io/) config for test build
|
||||
├── webpack.config.ts *
|
||||
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
|
||||
```
|
||||
|
||||
@@ -403,8 +485,8 @@ Frequently asked questions
|
||||
- You can write your tests next to your component files. e.g. for `src/app/home/home.component.ts` call it `src/app/home/home.component.spec.ts`
|
||||
- How do I start the app when I get `EACCES` and `EADDRINUSE` errors?
|
||||
- The `EADDRINUSE` error means the port `4000` is currently being used and `EACCES` is lack of permission to build files to `./dist/`
|
||||
- What are the naming conventions for Angular 2?
|
||||
- See [the official angular 2 style guide](https://angular.io/styleguide)
|
||||
- What are the naming conventions for Angular?
|
||||
- See [the official angular style guide](https://angular.io/styleguide)
|
||||
- Why is the size of my app larger in development?
|
||||
- The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed.
|
||||
- node-pre-gyp ERR in yarn install (Windows)
|
||||
@@ -415,6 +497,36 @@ Frequently asked questions
|
||||
- then run `git add yarn.lock` to stage the lockfile for commit
|
||||
- and `git commit` to conclude the merge
|
||||
|
||||
Getting Help
|
||||
------------
|
||||
|
||||
DSpace provides public mailing lists where you can post questions or raise topics for discussion.
|
||||
We welcome everyone to participate in these lists:
|
||||
|
||||
* [dspace-community@googlegroups.com](https://groups.google.com/d/forum/dspace-community) : General discussion about DSpace platform, announcements, sharing of best practices
|
||||
* [dspace-tech@googlegroups.com](https://groups.google.com/d/forum/dspace-tech) : Technical support mailing list. See also our guide for [How to troubleshoot an error](https://wiki.lyrasis.org/display/DSPACE/Troubleshoot+an+error).
|
||||
* [dspace-devel@googlegroups.com](https://groups.google.com/d/forum/dspace-devel) : Developers / Development mailing list
|
||||
|
||||
Great Q&A is also available under the [DSpace tag on Stackoverflow](http://stackoverflow.com/questions/tagged/dspace)
|
||||
|
||||
Additional support options are at https://wiki.lyrasis.org/display/DSPACE/Support
|
||||
|
||||
DSpace also has an active service provider network. If you'd rather hire a service provider to
|
||||
install, upgrade, customize or host DSpace, then we recommend getting in touch with one of our
|
||||
[Registered Service Providers](http://www.dspace.org/service-providers).
|
||||
|
||||
|
||||
Issue Tracker
|
||||
-------------
|
||||
|
||||
DSpace uses GitHub to track issues:
|
||||
* Backend (REST API) issues: https://github.com/DSpace/DSpace/issues
|
||||
* Frontend (User Interface) issues: https://github.com/DSpace/dspace-angular/issues
|
||||
|
||||
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.
|
||||
|
15
SECURITY.md
Normal file
15
SECURITY.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# Security Policy
|
||||
|
||||
## Supported Versions
|
||||
|
||||
For information regarding which versions of DSpace are currently under support, please see our DSpace Software Support Policy:
|
||||
|
||||
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you believe you have found a security vulnerability in a supported version of DSpace, we encourage you to let us know right away.
|
||||
We will investigate all legitimate reports and do our best to quickly fix the problem. Please see our DSpace Software Support Policy
|
||||
for information on privately reporting vulnerabilities:
|
||||
|
||||
https://wiki.lyrasis.org/display/DSPACE/DSpace+Software+Support+Policy
|
60
angular.json
60
angular.json
@@ -47,6 +47,7 @@
|
||||
"src/robots.txt"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles/startup.scss",
|
||||
{
|
||||
"input": "src/styles/base-theme.scss",
|
||||
"inject": false,
|
||||
@@ -67,6 +68,12 @@
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.production.ts"
|
||||
}
|
||||
],
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"extractCss": true,
|
||||
@@ -138,6 +145,16 @@
|
||||
}
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"test": {
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.test.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
@@ -146,7 +163,7 @@
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
"cypress/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
@@ -154,10 +171,11 @@
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "dspace-angular:serve"
|
||||
"devServerTarget": "dspace-angular:serve",
|
||||
"watch": true,
|
||||
"headless": false
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
@@ -175,16 +193,19 @@
|
||||
}
|
||||
},
|
||||
"outputPath": "dist/server",
|
||||
"main": "src/main.server.ts",
|
||||
"main": "server.ts",
|
||||
"tsConfig": "tsconfig.server.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"sourceMap": false,
|
||||
"optimization": {
|
||||
"scripts": false,
|
||||
"styles": true
|
||||
"optimization": true,
|
||||
"fileReplacements": [
|
||||
{
|
||||
"replace": "src/environments/environment.ts",
|
||||
"with": "src/environments/environment.production.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -214,9 +235,30 @@
|
||||
"configurations": {
|
||||
"production": {}
|
||||
}
|
||||
},
|
||||
"cypress-run": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"devServerTarget": "dspace-angular:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "dspace-angular:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"cypress-open": {
|
||||
"builder": "@cypress/schematic:cypress",
|
||||
"options": {
|
||||
"watch": true,
|
||||
"headless": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "dspace-angular"
|
||||
"defaultProject": "dspace-angular",
|
||||
"cli": {
|
||||
"analytics": false
|
||||
}
|
||||
}
|
2
config/.gitignore
vendored
Normal file
2
config/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
config.*.yml
|
||||
!config.example.yml
|
236
config/config.example.yml
Normal file
236
config/config.example.yml
Normal file
@@ -0,0 +1,236 @@
|
||||
# NOTE: will log all redux actions and transfers in console
|
||||
debug: false
|
||||
|
||||
# Angular Universal server settings
|
||||
# NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg.
|
||||
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
|
||||
|
||||
# The REST API server settings
|
||||
# NOTE: these must be '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
|
||||
|
||||
# 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
|
||||
|
||||
# Item Page Config
|
||||
item:
|
||||
edit:
|
||||
undoTimeout: 10000 # 10 seconds
|
||||
|
||||
# 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
|
||||
|
||||
# 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
|
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
|
10
cypress.json
Normal file
10
cypress.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"integrationFolder": "cypress/integration",
|
||||
"supportFile": "cypress/support/index.ts",
|
||||
"videosFolder": "cypress/videos",
|
||||
"screenshotsFolder": "cypress/screenshots",
|
||||
"pluginsFile": "cypress/plugins/index.ts",
|
||||
"fixturesFolder": "cypress/fixtures",
|
||||
"baseUrl": "http://localhost:4000",
|
||||
"retries": 2
|
||||
}
|
2
cypress/.gitignore
vendored
Normal file
2
cypress/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
screenshots/
|
||||
videos/
|
5
cypress/fixtures/example.json
Normal file
5
cypress/fixtures/example.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"name": "Using fixtures to represent data",
|
||||
"email": "hello@cypress.io",
|
||||
"body": "Fixtures are a great way to mock data for responses to routes"
|
||||
}
|
15
cypress/integration/breadcrumbs.spec.ts
Normal file
15
cypress/integration/breadcrumbs.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Breadcrumbs', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
// Visit an Item, as those have more breadcrumbs
|
||||
cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION);
|
||||
|
||||
// Wait for breadcrumbs to be visible
|
||||
cy.get('ds-breadcrumbs').should('be.visible');
|
||||
|
||||
// Analyze <ds-breadcrumbs> for accessibility
|
||||
testA11y('ds-breadcrumbs');
|
||||
});
|
||||
});
|
13
cypress/integration/browse-by-author.spec.ts
Normal file
13
cypress/integration/browse-by-author.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Browse By Author', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/author');
|
||||
|
||||
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||
cy.get('ds-browse-by-metadata-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||
testA11y('ds-browse-by-metadata-page');
|
||||
});
|
||||
});
|
13
cypress/integration/browse-by-dateissued.spec.ts
Normal file
13
cypress/integration/browse-by-dateissued.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Browse By Date Issued', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/dateissued');
|
||||
|
||||
// Wait for <ds-browse-by-date-page> to be visible
|
||||
cy.get('ds-browse-by-date-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-browse-by-date-page> for accessibility
|
||||
testA11y('ds-browse-by-date-page');
|
||||
});
|
||||
});
|
13
cypress/integration/browse-by-subject.spec.ts
Normal file
13
cypress/integration/browse-by-subject.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Browse By Subject', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/subject');
|
||||
|
||||
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||
cy.get('ds-browse-by-metadata-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||
testA11y('ds-browse-by-metadata-page');
|
||||
});
|
||||
});
|
13
cypress/integration/browse-by-title.spec.ts
Normal file
13
cypress/integration/browse-by-title.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Browse By Title', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/browse/title');
|
||||
|
||||
// Wait for <ds-browse-by-title-page> to be visible
|
||||
cy.get('ds-browse-by-title-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-browse-by-title-page> for accessibility
|
||||
testA11y('ds-browse-by-title-page');
|
||||
});
|
||||
});
|
15
cypress/integration/collection-page.spec.ts
Normal file
15
cypress/integration/collection-page.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TEST_COLLECTION } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Collection Page', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/collections/' + TEST_COLLECTION);
|
||||
|
||||
// <ds-collection-page> tag must be loaded
|
||||
cy.get('ds-collection-page').should('exist');
|
||||
|
||||
// Analyze <ds-collection-page> for accessibility issues
|
||||
testA11y('ds-collection-page');
|
||||
});
|
||||
});
|
32
cypress/integration/collection-statistics.spec.ts
Normal file
32
cypress/integration/collection-statistics.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { TEST_COLLECTION } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Collection Statistics Page', () => {
|
||||
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/' + TEST_COLLECTION;
|
||||
|
||||
it('should load if you click on "Statistics" from a Collection page', () => {
|
||||
cy.visit('/collections/' + TEST_COLLECTION);
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
|
||||
});
|
||||
|
||||
it('should contain a "Total visits" section', () => {
|
||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_COLLECTION + '_TotalVisits').should('exist');
|
||||
});
|
||||
|
||||
it('should contain a "Total visits per month" section', () => {
|
||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_COLLECTION + '_TotalVisitsPerMonth').should('exist');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(COLLECTIONSTATISTICSPAGE);
|
||||
|
||||
// <ds-collection-statistics-page> tag must be loaded
|
||||
cy.get('ds-collection-statistics-page').should('exist');
|
||||
|
||||
// Analyze <ds-collection-statistics-page> for accessibility issues
|
||||
testA11y('ds-collection-statistics-page');
|
||||
});
|
||||
});
|
25
cypress/integration/community-list.spec.ts
Normal file
25
cypress/integration/community-list.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community List Page', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/community-list');
|
||||
|
||||
// <ds-community-list-page> tag must be loaded
|
||||
cy.get('ds-community-list-page').should('exist');
|
||||
|
||||
// Open first Community (to show Collections)...that way we scan sub-elements as well
|
||||
cy.get('ds-community-list :nth-child(1) > .btn-group > .btn').click();
|
||||
|
||||
// Analyze <ds-community-list-page> for accessibility issues
|
||||
// Disable heading-order checks until it is fixed
|
||||
testA11y('ds-community-list-page',
|
||||
{
|
||||
rules: {
|
||||
'heading-order': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
});
|
||||
});
|
15
cypress/integration/community-page.spec.ts
Normal file
15
cypress/integration/community-page.spec.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { TEST_COMMUNITY } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community Page', () => {
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/communities/' + TEST_COMMUNITY);
|
||||
|
||||
// <ds-community-page> tag must be loaded
|
||||
cy.get('ds-community-page').should('exist');
|
||||
|
||||
// Analyze <ds-community-page> for accessibility issues
|
||||
testA11y('ds-community-page',);
|
||||
});
|
||||
});
|
32
cypress/integration/community-statistics.spec.ts
Normal file
32
cypress/integration/community-statistics.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { TEST_COMMUNITY } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Community Statistics Page', () => {
|
||||
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/' + TEST_COMMUNITY;
|
||||
|
||||
it('should load if you click on "Statistics" from a Community page', () => {
|
||||
cy.visit('/communities/' + TEST_COMMUNITY);
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
||||
});
|
||||
|
||||
it('should contain a "Total visits" section', () => {
|
||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_COMMUNITY + '_TotalVisits').should('exist');
|
||||
});
|
||||
|
||||
it('should contain a "Total visits per month" section', () => {
|
||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_COMMUNITY + '_TotalVisitsPerMonth').should('exist');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(COMMUNITYSTATISTICSPAGE);
|
||||
|
||||
// <ds-community-statistics-page> tag must be loaded
|
||||
cy.get('ds-community-statistics-page').should('exist');
|
||||
|
||||
// Analyze <ds-community-statistics-page> for accessibility issues
|
||||
testA11y('ds-community-statistics-page');
|
||||
});
|
||||
});
|
13
cypress/integration/footer.spec.ts
Normal file
13
cypress/integration/footer.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Footer', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/');
|
||||
|
||||
// Footer must first be visible
|
||||
cy.get('ds-footer').should('be.visible');
|
||||
|
||||
// Analyze <ds-footer> for accessibility
|
||||
testA11y('ds-footer');
|
||||
});
|
||||
});
|
19
cypress/integration/header.spec.ts
Normal file
19
cypress/integration/header.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Header', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/');
|
||||
|
||||
// Header must first be visible
|
||||
cy.get('ds-header').should('be.visible');
|
||||
|
||||
// Analyze <ds-header> for accessibility
|
||||
testA11y({
|
||||
include: ['ds-header'],
|
||||
exclude: [
|
||||
['#search-navbar-container'], // search in navbar has duplicative ID. Will be fixed in #1174
|
||||
['.dropdownLogin'] // "Log in" link has color contrast issues. Will be fixed in #1149
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
19
cypress/integration/homepage-statistics.spec.ts
Normal file
19
cypress/integration/homepage-statistics.spec.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Site Statistics Page', () => {
|
||||
it('should load if you click on "Statistics" from homepage', () => {
|
||||
cy.visit('/');
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', '/statistics');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/statistics');
|
||||
|
||||
// <ds-site-statistics-page> tag must be loaded
|
||||
cy.get('ds-site-statistics-page').should('exist');
|
||||
|
||||
// Analyze <ds-site-statistics-page> for accessibility issues
|
||||
testA11y('ds-site-statistics-page');
|
||||
});
|
||||
});
|
32
cypress/integration/homepage.spec.ts
Normal file
32
cypress/integration/homepage.spec.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Homepage', () => {
|
||||
beforeEach(() => {
|
||||
// All tests start with visiting homepage
|
||||
cy.visit('/');
|
||||
});
|
||||
|
||||
it('should display translated title "DSpace Angular :: Home"', () => {
|
||||
cy.title().should('eq', 'DSpace Angular :: Home');
|
||||
});
|
||||
|
||||
it('should contain a news section', () => {
|
||||
cy.get('ds-home-news').should('be.visible');
|
||||
});
|
||||
|
||||
it('should have a working search box', () => {
|
||||
const queryString = 'test';
|
||||
cy.get('ds-search-form input[name="query"]').type(queryString);
|
||||
cy.get('ds-search-form button.search-button').click();
|
||||
cy.url().should('include', '/search');
|
||||
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
// Wait for homepage tag to appear
|
||||
cy.get('ds-home-page').should('be.visible');
|
||||
|
||||
// Analyze <ds-home-page> for accessibility issues
|
||||
testA11y('ds-home-page');
|
||||
});
|
||||
});
|
31
cypress/integration/item-page.spec.ts
Normal file
31
cypress/integration/item-page.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Item Page', () => {
|
||||
const ITEMPAGE = '/items/' + TEST_ENTITY_PUBLICATION;
|
||||
const ENTITYPAGE = '/entities/publication/' + TEST_ENTITY_PUBLICATION;
|
||||
|
||||
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
||||
it('should redirect to the entity page when navigating to an item page', () => {
|
||||
cy.visit(ITEMPAGE);
|
||||
cy.location('pathname').should('eq', ENTITYPAGE);
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(ENTITYPAGE);
|
||||
|
||||
// <ds-item-page> tag must be loaded
|
||||
cy.get('ds-item-page').should('exist');
|
||||
|
||||
// Analyze <ds-item-page> for accessibility issues
|
||||
// Disable heading-order checks until it is fixed
|
||||
testA11y('ds-item-page',
|
||||
{
|
||||
rules: {
|
||||
'heading-order': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
});
|
||||
});
|
38
cypress/integration/item-statistics.spec.ts
Normal file
38
cypress/integration/item-statistics.spec.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { TEST_ENTITY_PUBLICATION } from 'cypress/support';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
describe('Item Statistics Page', () => {
|
||||
const ITEMSTATISTICSPAGE = '/statistics/items/' + TEST_ENTITY_PUBLICATION;
|
||||
|
||||
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
||||
cy.visit('/entities/publication/' + TEST_ENTITY_PUBLICATION);
|
||||
cy.get('ds-navbar ds-link-menu-item a[title="Statistics"]').click();
|
||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||
});
|
||||
|
||||
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
cy.get('ds-item-statistics-page').should('exist');
|
||||
cy.get('ds-item-page').should('not.exist');
|
||||
});
|
||||
|
||||
it('should contain a "Total visits" section', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisits').should('exist');
|
||||
});
|
||||
|
||||
it('should contain a "Total visits per month" section', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
cy.get('.' + TEST_ENTITY_PUBLICATION + '_TotalVisitsPerMonth').should('exist');
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit(ITEMSTATISTICSPAGE);
|
||||
|
||||
// <ds-item-statistics-page> tag must be loaded
|
||||
cy.get('ds-item-statistics-page').should('exist');
|
||||
|
||||
// Analyze <ds-item-statistics-page> for accessibility issues
|
||||
testA11y('ds-item-statistics-page');
|
||||
});
|
||||
});
|
13
cypress/integration/pagenotfound.spec.ts
Normal file
13
cypress/integration/pagenotfound.spec.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
describe('PageNotFound', () => {
|
||||
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
||||
// request an invalid page (UUIDs at root path aren't valid)
|
||||
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
|
||||
cy.get('ds-pagenotfound').should('exist');
|
||||
});
|
||||
|
||||
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
||||
cy.visit('/home');
|
||||
cy.get('ds-pagenotfound').should('not.exist');
|
||||
});
|
||||
|
||||
});
|
49
cypress/integration/search-navbar.spec.ts
Normal file
49
cypress/integration/search-navbar.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
const page = {
|
||||
fillOutQueryInNavBar(query) {
|
||||
// Click the magnifying glass
|
||||
cy.get('.navbar-container #search-navbar-container form a').click();
|
||||
// Fill out a query in input that appears
|
||||
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type(query);
|
||||
},
|
||||
submitQueryByPressingEnter() {
|
||||
cy.get('.navbar-container #search-navbar-container form input[name = "query"]').type('{enter}');
|
||||
},
|
||||
submitQueryByPressingIcon() {
|
||||
cy.get('.navbar-container #search-navbar-container form .submit-icon').click();
|
||||
}
|
||||
};
|
||||
|
||||
describe('Search from Navigation Bar', () => {
|
||||
// NOTE: these tests currently assume this query will return results!
|
||||
const query = 'test';
|
||||
|
||||
it('should go to search page with correct query if submitted (from home)', () => {
|
||||
cy.visit('/');
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingEnter();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
// At least one search result should be displayed
|
||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||
});
|
||||
|
||||
it('should go to search page with correct query if submitted (from search)', () => {
|
||||
cy.visit('/search');
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingEnter();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
// At least one search result should be displayed
|
||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||
});
|
||||
|
||||
it('should allow user to also submit query by clicking icon', () => {
|
||||
cy.visit('/');
|
||||
page.fillOutQueryInNavBar(query);
|
||||
page.submitQueryByPressingIcon();
|
||||
// New URL should include query param
|
||||
cy.url().should('include', 'query=' + query);
|
||||
// At least one search result should be displayed
|
||||
cy.get('ds-item-search-result-list-element').should('be.visible');
|
||||
});
|
||||
});
|
72
cypress/integration/search-page.spec.ts
Normal file
72
cypress/integration/search-page.spec.ts
Normal file
@@ -0,0 +1,72 @@
|
||||
import { Options } from 'cypress-axe';
|
||||
import { testA11y } from 'cypress/support/utils';
|
||||
|
||||
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', () => {
|
||||
const queryString = 'Another interesting query string';
|
||||
cy.visit('/search');
|
||||
// Type query in searchbox & click search button
|
||||
cy.get(SEARCHFORM_ID + ' input[name="query"]').type(queryString);
|
||||
cy.get(SEARCHFORM_ID + ' button.search-button').click();
|
||||
cy.url().should('include', 'query=' + encodeURI(queryString));
|
||||
});
|
||||
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.visit('/search');
|
||||
|
||||
// <ds-search-page> tag must be loaded
|
||||
cy.get('ds-search-page').should('exist');
|
||||
|
||||
// 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-search-page> for accessibility issues
|
||||
testA11y(
|
||||
{
|
||||
include: ['ds-search-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 pass accessibility tests in Grid view', () => {
|
||||
cy.visit('/search');
|
||||
|
||||
// Click to display grid view
|
||||
// TODO: These buttons should likely have an easier way to uniquely select
|
||||
cy.get('#search-sidebar-content > ds-view-mode-switch > .btn-group > [href="/search?view=grid"] > .fas').click();
|
||||
|
||||
// <ds-search-page> tag must be loaded
|
||||
cy.get('ds-search-page').should('exist');
|
||||
|
||||
// Analyze <ds-search-page> for accessibility issues
|
||||
testA11y('ds-search-page',
|
||||
{
|
||||
rules: {
|
||||
// Search filters fail these two "moderate" impact rules
|
||||
'heading-order': { enabled: false },
|
||||
'landmark-unique': { enabled: false }
|
||||
}
|
||||
} as Options
|
||||
);
|
||||
});
|
||||
});
|
16
cypress/plugins/index.ts
Normal file
16
cypress/plugins/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
|
||||
// For more info, visit https://on.cypress.io/plugins-api
|
||||
module.exports = (on, config) => {
|
||||
// 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
|
||||
on('task', {
|
||||
log(message: string) {
|
||||
console.log(message);
|
||||
return null;
|
||||
},
|
||||
table(message: string) {
|
||||
console.table(message);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
};
|
43
cypress/support/commands.ts
Normal file
43
cypress/support/commands.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// ***********************************************
|
||||
// This example namespace declaration will help
|
||||
// with Intellisense and code completion in your
|
||||
// IDE or Text Editor.
|
||||
// ***********************************************
|
||||
// declare namespace Cypress {
|
||||
// interface Chainable<Subject = any> {
|
||||
// customCommand(param: any): typeof customCommand;
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// function customCommand(param: any): void {
|
||||
// console.warn(param);
|
||||
// }
|
||||
//
|
||||
// NOTE: You can use it like so:
|
||||
// Cypress.Commands.add('customCommand', customCommand);
|
||||
//
|
||||
// ***********************************************
|
||||
// This example commands.js shows you how to
|
||||
// create various custom commands and overwrite
|
||||
// existing commands.
|
||||
//
|
||||
// For more comprehensive examples of custom
|
||||
// commands please read more here:
|
||||
// https://on.cypress.io/custom-commands
|
||||
// ***********************************************
|
||||
//
|
||||
//
|
||||
// -- This is a parent command --
|
||||
// Cypress.Commands.add("login", (email, password) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a child command --
|
||||
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This is a dual command --
|
||||
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
|
||||
//
|
||||
//
|
||||
// -- This will overwrite an existing command --
|
||||
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
|
26
cypress/support/index.ts
Normal file
26
cypress/support/index.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
// ***********************************************************
|
||||
// This example support/index.js is processed and
|
||||
// loaded automatically before your test files.
|
||||
//
|
||||
// This is a great place to put global configuration and
|
||||
// behavior that modifies Cypress.
|
||||
//
|
||||
// You can change the location of this file or turn off
|
||||
// automatically serving support files with the
|
||||
// 'supportFile' configuration option.
|
||||
//
|
||||
// You can read more here:
|
||||
// https://on.cypress.io/configuration
|
||||
// ***********************************************************
|
||||
|
||||
// When a command from ./commands is ready to use, import with `import './commands'` syntax
|
||||
// import './commands';
|
||||
|
||||
// Import Cypress Axe tools for all tests
|
||||
// https://github.com/component-driven/cypress-axe
|
||||
import 'cypress-axe';
|
||||
|
||||
// Global constants used in tests
|
||||
export const TEST_COLLECTION = '282164f5-d325-4740-8dd1-fa4d6d3e7200';
|
||||
export const TEST_COMMUNITY = '0958c910-2037-42a9-81c7-dca80e3892b4';
|
||||
export const TEST_ENTITY_PUBLICATION = 'e98b0f27-5c19-49a0-960d-eb6ad5287067';
|
44
cypress/support/utils.ts
Normal file
44
cypress/support/utils.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { Result } from 'axe-core';
|
||||
import { Options } from 'cypress-axe';
|
||||
|
||||
// Log violations to terminal/commandline in a table format.
|
||||
// Uses 'log' and 'table' tasks defined in ../plugins/index.ts
|
||||
// Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file
|
||||
function terminalLog(violations: Result[]) {
|
||||
cy.task(
|
||||
'log',
|
||||
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`
|
||||
);
|
||||
// pluck specific keys to keep the table readable
|
||||
const violationData = violations.map(
|
||||
({ id, impact, description, helpUrl, nodes }) => ({
|
||||
id,
|
||||
impact,
|
||||
description,
|
||||
helpUrl,
|
||||
nodes: nodes.length,
|
||||
html: nodes.map(node => node.html)
|
||||
})
|
||||
);
|
||||
|
||||
// Print violations as an array, since 'node.html' above often breaks table alignment
|
||||
cy.task('log', violationData);
|
||||
// Optionally, uncomment to print as a table
|
||||
// cy.task('table', violationData);
|
||||
|
||||
}
|
||||
|
||||
// Custom "testA11y()" method which checks accessibility using cypress-axe
|
||||
// while also ensuring any violations are logged to the terminal (see terminalLog above)
|
||||
// This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load
|
||||
export const testA11y = (context?: any, options?: Options) => {
|
||||
cy.injectAxe();
|
||||
cy.configureAxe({
|
||||
rules: [
|
||||
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
|
||||
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
|
||||
{ id: 'color-contrast', enabled: false },
|
||||
]
|
||||
});
|
||||
cy.checkA11y(context, options, terminalLog);
|
||||
};
|
13
cypress/tsconfig.json
Normal file
13
cypress/tsconfig.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": [
|
||||
"**/*.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"cypress",
|
||||
"cypress-axe",
|
||||
"node"
|
||||
]
|
||||
}
|
||||
}
|
@@ -1,5 +1,23 @@
|
||||
# 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.
|
||||
***
|
||||
|
||||
## '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-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.
|
||||
@@ -11,10 +29,6 @@
|
||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||
- cli.assetstore.yml
|
||||
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
|
||||
- environment.dev.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
|
||||
|
@@ -18,10 +18,19 @@ services:
|
||||
dspace-cli:
|
||||
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
||||
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:
|
||||
- "assetstore:/dspace/assetstore"
|
||||
- "./local.cfg:/dspace/config/local.cfg"
|
||||
entrypoint: /dspace/bin/dspace
|
||||
command: help
|
||||
networks:
|
||||
|
@@ -17,6 +17,19 @@ services:
|
||||
# DSpace (backend) webapp container
|
||||
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:
|
||||
- dspacedb
|
||||
image: dspace/dspace:dspace-7_x-test
|
||||
@@ -29,7 +42,6 @@ services:
|
||||
tty: true
|
||||
volumes:
|
||||
- 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)
|
||||
- solr_configs:/dspace/solr
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
@@ -64,7 +76,7 @@ services:
|
||||
dspacesolr:
|
||||
container_name: dspacesolr
|
||||
# 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
|
||||
depends_on:
|
||||
- dspace
|
||||
|
@@ -13,10 +13,32 @@
|
||||
version: '3.7'
|
||||
networks:
|
||||
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:
|
||||
# DSpace (backend) webapp container
|
||||
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
|
||||
depends_on:
|
||||
- dspacedb
|
||||
@@ -29,7 +51,6 @@ services:
|
||||
tty: true
|
||||
volumes:
|
||||
- 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)
|
||||
- solr_configs:/dspace/solr
|
||||
# Ensure that the database is ready BEFORE starting tomcat
|
||||
@@ -62,7 +83,7 @@ services:
|
||||
dspacesolr:
|
||||
container_name: dspacesolr
|
||||
# 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
|
||||
depends_on:
|
||||
- dspace
|
||||
@@ -81,15 +102,22 @@ services:
|
||||
# Keep Solr data directory between reboots
|
||||
- solr_data:/var/solr/data
|
||||
# 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:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
- |
|
||||
init-var-solr
|
||||
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
|
||||
cp -r -u /opt/solr/server/solr/configsets/dspace/oai/* oai
|
||||
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
|
||||
cp -r -u /opt/solr/server/solr/configsets/dspace/statistics/* statistics
|
||||
exec solr -f
|
||||
volumes:
|
||||
assetstore:
|
||||
|
@@ -16,11 +16,15 @@ services:
|
||||
dspace-angular:
|
||||
container_name: dspace-angular
|
||||
environment:
|
||||
DSPACE_HOST: dspace-angular
|
||||
DSPACE_NAMESPACE: /
|
||||
DSPACE_PORT: '4000'
|
||||
DSPACE_SSL: "false"
|
||||
image: dspace/dspace-angular:latest
|
||||
DSPACE_UI_SSL: 'false'
|
||||
DSPACE_UI_HOST: dspace-angular
|
||||
DSPACE_UI_PORT: '4000'
|
||||
DSPACE_UI_NAMESPACE: /
|
||||
DSPACE_REST_SSL: 'false'
|
||||
DSPACE_REST_HOST: localhost
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: /server
|
||||
image: dspace/dspace-angular:dspace-7_x
|
||||
build:
|
||||
context: ..
|
||||
dockerfile: Dockerfile
|
||||
@@ -33,5 +37,3 @@ services:
|
||||
target: 9876
|
||||
stdin_open: 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
|
||||
|
||||
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 `environment.prod.ts` file in `src/environments/` for `production` environment;
|
||||
- Create a new `config.(dev or development).yml` file in `config/` for `development` 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
|
||||
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):
|
||||
```
|
||||
export const environment = {
|
||||
// Angular UI settings.
|
||||
ui: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
port: 4000,
|
||||
nameSpace: '/'
|
||||
}
|
||||
};
|
||||
|
||||
```yaml
|
||||
ui:
|
||||
ssl: false
|
||||
host: localhost
|
||||
port: 4000
|
||||
nameSpace: /
|
||||
```
|
||||
|
||||
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_NAMESPACE=/
|
||||
```
|
||||
or
|
||||
```
|
||||
DSPACE_UI_SSL=true
|
||||
DSPACE_UI_HOST=localhost
|
||||
DSPACE_UI_PORT=4000
|
||||
DSPACE_UI_NAMESPACE=/
|
||||
```
|
||||
|
||||
## 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:
|
||||
|
||||
```
|
||||
export const environment = {
|
||||
// The REST API server settings.
|
||||
rest: {
|
||||
ssl: true,
|
||||
host: 'api7.dspace.org',
|
||||
port: 443,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: '/server'
|
||||
}
|
||||
};
|
||||
```yaml
|
||||
rest:
|
||||
ssl: true
|
||||
host: api7.dspace.org
|
||||
port: 443
|
||||
nameSpace: /server
|
||||
}
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
|
||||
## 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
|
||||
This project makes use of [Angulartics](https://angulartics.github.io/angulartics2/) to track usage events and send them to Google Analytics.
|
||||
|
||||
|
@@ -1,14 +0,0 @@
|
||||
const config = require('./protractor.conf').config;
|
||||
|
||||
config.capabilities = {
|
||||
browserName: 'chrome',
|
||||
chromeOptions: {
|
||||
args: ['--headless', '--no-sandbox', '--disable-gpu']
|
||||
}
|
||||
};
|
||||
|
||||
// don't use protractor's webdriver, as it may be incompatible with the installed chrome version
|
||||
config.directConnect = false;
|
||||
config.seleniumAddress = 'http://localhost:4444/wd/hub';
|
||||
|
||||
exports.config = config;
|
@@ -1,92 +0,0 @@
|
||||
// Protractor configuration file, see link for more information
|
||||
// https://github.com/angular/protractor/blob/master/docs/referenceConf.js
|
||||
|
||||
/*global jasmine */
|
||||
var SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
||||
|
||||
exports.config = {
|
||||
allScriptsTimeout: 600000,
|
||||
// -----------------------------------------------------------------
|
||||
// Uncomment to run tests using a remote Selenium server
|
||||
//seleniumAddress: 'http://selenium.address:4444/wd/hub',
|
||||
// Change to 'false' to run tests using a remote Selenium server
|
||||
directConnect: true,
|
||||
// Change if the website to test is not on the localhost
|
||||
baseUrl: 'http://localhost:4000/',
|
||||
// -----------------------------------------------------------------
|
||||
specs: [
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
// -----------------------------------------------------------------
|
||||
// Browser and Capabilities: PhantomJS
|
||||
// -----------------------------------------------------------------
|
||||
// capabilities: {
|
||||
// 'browserName': 'phantomjs',
|
||||
// 'version': '',
|
||||
// 'platform': 'ANY'
|
||||
// },
|
||||
// -----------------------------------------------------------------
|
||||
// Browser and Capabilities: Chrome
|
||||
// -----------------------------------------------------------------
|
||||
capabilities: {
|
||||
'browserName': 'chrome',
|
||||
'version': '',
|
||||
'platform': 'ANY',
|
||||
'chromeOptions': {
|
||||
'args': [ '--headless', '--disable-gpu' ]
|
||||
}
|
||||
},
|
||||
// -----------------------------------------------------------------
|
||||
// Browser and Capabilities: Firefox
|
||||
// -----------------------------------------------------------------
|
||||
// capabilities: {
|
||||
// 'browserName': 'firefox',
|
||||
// 'version': '',
|
||||
// 'platform': 'ANY'
|
||||
// },
|
||||
|
||||
// -----------------------------------------------------------------
|
||||
// Browser and Capabilities: MultiCapabilities
|
||||
// -----------------------------------------------------------------
|
||||
//multiCapabilities: [
|
||||
// {
|
||||
// 'browserName': 'phantomjs',
|
||||
// 'version': '',
|
||||
// 'platform': 'ANY'
|
||||
// },
|
||||
// {
|
||||
// 'browserName': 'chrome',
|
||||
// 'version': '',
|
||||
// 'platform': 'ANY'
|
||||
// }
|
||||
// {
|
||||
// 'browserName': 'firefox',
|
||||
// 'version': '',
|
||||
// 'platform': 'ANY'
|
||||
// }
|
||||
//],
|
||||
|
||||
plugins: [{
|
||||
path: '../node_modules/protractor-istanbul-plugin'
|
||||
}],
|
||||
|
||||
framework: 'jasmine',
|
||||
jasmineNodeOpts: {
|
||||
showColors: true,
|
||||
defaultTimeoutInterval: 600000,
|
||||
print: function () {}
|
||||
},
|
||||
useAllAngular2AppRoots: true,
|
||||
beforeLaunch: function () {
|
||||
require('ts-node').register({
|
||||
project: './e2e/tsconfig.json'
|
||||
});
|
||||
},
|
||||
onPrepare: function () {
|
||||
jasmine.getEnv().addReporter(new SpecReporter({
|
||||
spec: {
|
||||
displayStacktrace: true
|
||||
}
|
||||
}));
|
||||
}
|
||||
};
|
@@ -1,22 +0,0 @@
|
||||
import { ProtractorPage } from './app.po';
|
||||
|
||||
describe('protractor App', () => {
|
||||
let page: ProtractorPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new ProtractorPage();
|
||||
});
|
||||
|
||||
it('should display translated title "DSpace Angular :: Home"', () => {
|
||||
page.navigateTo();
|
||||
page.waitUntilNotLoading();
|
||||
expect<any>(page.getPageTitleText()).toEqual('DSpace Angular :: Home');
|
||||
});
|
||||
|
||||
it('should contain a news section', () => {
|
||||
page.navigateTo();
|
||||
page.waitUntilNotLoading();
|
||||
const text = page.getHomePageNewsText();
|
||||
expect<any>(text).toBeDefined();
|
||||
});
|
||||
});
|
@@ -1,23 +0,0 @@
|
||||
import { browser, element, by, protractor, promise } from 'protractor';
|
||||
|
||||
export class ProtractorPage {
|
||||
navigateTo() {
|
||||
return browser.get('/')
|
||||
.then(() => browser.waitForAngular());
|
||||
}
|
||||
|
||||
getPageTitleText() {
|
||||
return browser.getTitle();
|
||||
}
|
||||
|
||||
getHomePageNewsText() {
|
||||
return element(by.css('ds-home-news')).getText();
|
||||
}
|
||||
|
||||
waitUntilNotLoading(): promise.Promise<unknown> {
|
||||
const loading = element(by.css('.loader'));
|
||||
const EC = protractor.ExpectedConditions;
|
||||
const notLoading = EC.not(EC.presenceOf(loading));
|
||||
return browser.wait(notLoading, 10000);
|
||||
}
|
||||
}
|
@@ -1,19 +0,0 @@
|
||||
import { ProtractorPage } from './pagenotfound.po';
|
||||
|
||||
describe('protractor PageNotFound', () => {
|
||||
let page: ProtractorPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new ProtractorPage();
|
||||
});
|
||||
|
||||
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
|
||||
page.navigateToNonExistingPage();
|
||||
expect<any>(page.elementTagExists('ds-pagenotfound')).toEqual(true);
|
||||
});
|
||||
|
||||
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
|
||||
page.navigateToExistingPage();
|
||||
expect<any>(page.elementTagExists('ds-pagenotfound')).toEqual(false);
|
||||
});
|
||||
});
|
@@ -1,18 +0,0 @@
|
||||
import { browser, element, by } from 'protractor';
|
||||
|
||||
export class ProtractorPage {
|
||||
HOMEPAGE = '/home';
|
||||
NONEXISTINGPAGE = '/e9019a69-d4f1-4773-b6a3-bd362caa46f2';
|
||||
|
||||
navigateToNonExistingPage() {
|
||||
return browser.get(this.NONEXISTINGPAGE);
|
||||
}
|
||||
navigateToExistingPage() {
|
||||
return browser.get(this.HOMEPAGE);
|
||||
}
|
||||
|
||||
elementTagExists(tag: string) {
|
||||
return element(by.tagName(tag)).isPresent();
|
||||
}
|
||||
|
||||
}
|
@@ -1,46 +0,0 @@
|
||||
import { ProtractorPage } from './search-navbar.po';
|
||||
import { browser } from 'protractor';
|
||||
|
||||
describe('protractor SearchNavbar', () => {
|
||||
let page: ProtractorPage;
|
||||
let queryString: string;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new ProtractorPage();
|
||||
queryString = 'the test query';
|
||||
});
|
||||
|
||||
it('should go to search page with correct query if submitted (from home)', () => {
|
||||
page.navigateToHome();
|
||||
return checkIfSearchWorks();
|
||||
});
|
||||
|
||||
it('should go to search page with correct query if submitted (from search)', () => {
|
||||
page.navigateToSearch();
|
||||
return checkIfSearchWorks();
|
||||
});
|
||||
|
||||
it('check if can submit search box with pressing button', () => {
|
||||
page.navigateToHome();
|
||||
page.expandAndFocusSearchBox();
|
||||
page.setCurrentQuery(queryString);
|
||||
page.submitNavbarSearchForm();
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function checkIfSearchWorks(): boolean {
|
||||
page.setCurrentQuery(queryString);
|
||||
page.submitByPressingEnter();
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
||||
});
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
});
|
@@ -1,35 +0,0 @@
|
||||
import { browser, by, element, protractor } from 'protractor';
|
||||
import { promise } from 'selenium-webdriver';
|
||||
|
||||
export class ProtractorPage {
|
||||
HOME = '/home';
|
||||
SEARCH = '/search';
|
||||
|
||||
navigateToHome() {
|
||||
return browser.get(this.HOME);
|
||||
}
|
||||
|
||||
navigateToSearch() {
|
||||
return browser.get(this.SEARCH);
|
||||
}
|
||||
|
||||
getCurrentQuery(): promise.Promise<string> {
|
||||
return element(by.css('.navbar-container #search-navbar-container form input')).getAttribute('value');
|
||||
}
|
||||
|
||||
expandAndFocusSearchBox() {
|
||||
element(by.css('.navbar-container #search-navbar-container form a')).click();
|
||||
}
|
||||
|
||||
setCurrentQuery(query: string) {
|
||||
element(by.css('.navbar-container #search-navbar-container form input[name="query"]')).sendKeys(query);
|
||||
}
|
||||
|
||||
submitNavbarSearchForm() {
|
||||
element(by.css('.navbar-container #search-navbar-container form .submit-icon')).click();
|
||||
}
|
||||
|
||||
submitByPressingEnter() {
|
||||
element(by.css('.navbar-container #search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER);
|
||||
}
|
||||
}
|
@@ -1,60 +0,0 @@
|
||||
import { ProtractorPage } from './search-page.po';
|
||||
import { browser } from 'protractor';
|
||||
|
||||
describe('protractor SearchPage', () => {
|
||||
let page: ProtractorPage;
|
||||
|
||||
beforeEach(() => {
|
||||
page = new ProtractorPage();
|
||||
});
|
||||
|
||||
it('should contain query value when navigating to page with query parameter', () => {
|
||||
const queryString = 'Interesting query string';
|
||||
page.navigateToSearchWithQueryParameter(queryString)
|
||||
.then(() => page.getCurrentQuery())
|
||||
.then((query: string) => {
|
||||
expect<string>(query).toEqual(queryString);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have right scope selected when navigating to page with scope parameter', () => {
|
||||
page.navigateToSearch()
|
||||
.then(() => page.getRandomScopeOption())
|
||||
.then((scopeString: string) => {
|
||||
page.navigateToSearchWithScopeParameter(scopeString);
|
||||
page.waitUntilNotLoading();
|
||||
page.getCurrentScope()
|
||||
.then((s: string) => {
|
||||
expect<string>(s).toEqual(scopeString);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to the correct url when scope was set and submit button was triggered', () => {
|
||||
page.navigateToSearch()
|
||||
.then(() => page.getRandomScopeOption())
|
||||
.then((scopeString: string) => {
|
||||
page.setCurrentScope(scopeString)
|
||||
.then(() => page.submitSearchForm())
|
||||
.then(() => page.waitUntilNotLoading())
|
||||
.then(() => () => {
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('scope=' + encodeURI(scopeString)) !== -1;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to the correct url when query was set and submit button was triggered', () => {
|
||||
const queryString = 'Another interesting query string';
|
||||
page.setCurrentQuery(queryString);
|
||||
page.submitSearchForm();
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,55 +0,0 @@
|
||||
import { browser, by, element, protractor } from 'protractor';
|
||||
import { promise } from 'selenium-webdriver';
|
||||
|
||||
export class ProtractorPage {
|
||||
SEARCH = '/search';
|
||||
|
||||
navigateToSearch() {
|
||||
return browser.get(this.SEARCH);
|
||||
}
|
||||
|
||||
navigateToSearchWithQueryParameter(query: string) {
|
||||
return browser.get(this.SEARCH + '?query=' + query);
|
||||
}
|
||||
|
||||
navigateToSearchWithScopeParameter(scope: string) {
|
||||
return browser.get(this.SEARCH + '?scope=' + scope);
|
||||
}
|
||||
|
||||
getCurrentScope(): promise.Promise<string> {
|
||||
const scopeSelect = element(by.css('#search-form select'));
|
||||
browser.wait(protractor.ExpectedConditions.presenceOf(scopeSelect), 10000);
|
||||
return scopeSelect.getAttribute('value');
|
||||
}
|
||||
|
||||
getCurrentQuery(): promise.Promise<string> {
|
||||
return element(by.css('#search-form input')).getAttribute('value');
|
||||
}
|
||||
|
||||
setCurrentScope(scope: string) {
|
||||
return element(by.css('#search-form option[value="' + scope + '"]')).click();
|
||||
}
|
||||
|
||||
setCurrentQuery(query: string) {
|
||||
element(by.css('#search-form input[name="query"]')).sendKeys(query);
|
||||
}
|
||||
|
||||
submitSearchForm() {
|
||||
return element(by.css('#search-form button.search-button')).click();
|
||||
}
|
||||
|
||||
getRandomScopeOption(): promise.Promise<string> {
|
||||
const options = element(by.css('select[name="scope"]')).all(by.tagName('option'));
|
||||
return options.count().then((c: number) => {
|
||||
const index: number = Math.floor(Math.random() * (c - 1));
|
||||
return options.get(index + 1).getAttribute('value');
|
||||
});
|
||||
}
|
||||
|
||||
waitUntilNotLoading(): promise.Promise<unknown> {
|
||||
const loading = element(by.css('.loader'));
|
||||
const EC = protractor.ExpectedConditions;
|
||||
const notLoading = EC.not(EC.presenceOf(loading));
|
||||
return browser.wait(notLoading, 10000);
|
||||
}
|
||||
}
|
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"declaration": false,
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"module": "commonjs",
|
||||
"moduleResolution": "node",
|
||||
"outDir": "../dist/out-tsc-e2e",
|
||||
"sourceMap": true,
|
||||
"target": "es2018",
|
||||
"typeRoots": [
|
||||
"../node_modules/@types"
|
||||
]
|
||||
}
|
||||
}
|
@@ -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"],
|
||||
"ext": "ts",
|
||||
"ignore": ["src/environments/environment.ts", "src/environments/mock-environment.ts"],
|
||||
"exec": "ts-node --project ./tsconfig.ts-node.json scripts/set-env.ts --dev"
|
||||
"watch": [
|
||||
"config"
|
||||
],
|
||||
"ext": "json"
|
||||
}
|
||||
|
157
package.json
157
package.json
@@ -3,50 +3,40 @@
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"config:dev": "ts-node --project ./tsconfig.ts-node.json scripts/set-env.ts --dev",
|
||||
"config:prod": "ts-node --project ./tsconfig.ts-node.json scripts/set-env.ts --prod",
|
||||
"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",
|
||||
"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",
|
||||
"pree2e:ci": "yarn run config:prod",
|
||||
"config:watch": "nodemon",
|
||||
"test:rest": "ts-node --project ./tsconfig.ts-node.json scripts/test-rest.ts",
|
||||
"start": "yarn run start:prod",
|
||||
"start:dev": "nodemon --exec \"cross-env NODE_ENV=development yarn run serve\"",
|
||||
"start:prod": "yarn run build:prod && cross-env NODE_ENV=production yarn run serve:ssr",
|
||||
"start:mirador:prod": "yarn run build:mirador && yarn run start:prod",
|
||||
"serve": "ts-node --project ./tsconfig.ts-node.json scripts/serve.ts",
|
||||
"start:dev": "npm-run-all --parallel config:dev:watch serve",
|
||||
"start:prod": "yarn run build:prod && yarn run serve:ssr",
|
||||
"serve:ssr": "node dist/server/main",
|
||||
"analyze": "webpack-bundle-analyzer dist/browser/stats.json",
|
||||
"build": "ng build",
|
||||
"build:stats": "ng build --stats-json",
|
||||
"build:prod": "yarn run build:ssr",
|
||||
"build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server",
|
||||
"build:client-and-server-bundles": "ng build --prod && ng run dspace-angular:server:production --bundleDependencies true",
|
||||
"test:watch": "npm-run-all --parallel config:test:watch test",
|
||||
"test": "ng test --sourceMap=true --watch=true",
|
||||
"test:headless": "ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
|
||||
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||
"test": "ng test --sourceMap=true --watch=false --configuration test",
|
||||
"test:watch": "nodemon --exec \"ng test --sourceMap=true --watch=true --configuration test\"",
|
||||
"test:headless": "ng test --sourceMap=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||
"lint": "ng lint",
|
||||
"lint-fix": "ng lint --fix=true",
|
||||
"e2e": "ng e2e",
|
||||
"e2e:ci": "ng e2e --webdriver-update=false --protractor-config=./e2e/protractor-ci.conf.js",
|
||||
"compile:server": "webpack --config webpack.server.config.js --progress --color",
|
||||
"serve:ssr": "node dist/server",
|
||||
"clean:dev:config": "rimraf src/assets/config.json",
|
||||
"clean:coverage": "rimraf coverage",
|
||||
"clean:dist": "rimraf dist",
|
||||
"clean:doc": "rimraf doc",
|
||||
"clean:log": "rimraf *.log*",
|
||||
"clean:json": "rimraf *.records.json",
|
||||
"clean:bld": "rimraf build",
|
||||
"clean:node": "rimraf node_modules",
|
||||
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json && yarn run clean:bld",
|
||||
"clean": "yarn run clean:prod && yarn run clean:env && yarn run clean:node",
|
||||
"clean:env": "rimraf src/environments/environment.ts",
|
||||
"sync-i18n": "yarn run config:dev && ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||
"postinstall": "ngcc"
|
||||
"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:dev:config && yarn run clean:node",
|
||||
"sync-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/sync-i18n-files.ts",
|
||||
"build:mirador": "webpack --config webpack/webpack.mirador.config.ts",
|
||||
"merge-i18n": "ts-node --project ./tsconfig.ts-node.json scripts/merge-i18n-files.ts",
|
||||
"cypress:open": "cypress open",
|
||||
"cypress:run": "cypress run",
|
||||
"env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts"
|
||||
},
|
||||
"browser": {
|
||||
"fs": false,
|
||||
@@ -56,28 +46,29 @@
|
||||
},
|
||||
"private": true,
|
||||
"resolutions": {
|
||||
"minimist": "^1.2.5"
|
||||
"minimist": "^1.2.5",
|
||||
"webdriver-manager": "^12.1.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "~10.2.3",
|
||||
"@angular/cdk": "^10.2.6",
|
||||
"@angular/common": "~10.2.3",
|
||||
"@angular/compiler": "~10.2.3",
|
||||
"@angular/core": "~10.2.3",
|
||||
"@angular/forms": "~10.2.3",
|
||||
"@angular/localize": "10.2.3",
|
||||
"@angular/platform-browser": "~10.2.3",
|
||||
"@angular/platform-browser-dynamic": "~10.2.3",
|
||||
"@angular/platform-server": "~10.2.3",
|
||||
"@angular/router": "~10.2.3",
|
||||
"@angularclass/bootloader": "1.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "7.0.0",
|
||||
"@ng-dynamic-forms/core": "^12.0.0",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^12.0.0",
|
||||
"@ngrx/effects": "^10.0.1",
|
||||
"@ngrx/router-store": "^10.0.1",
|
||||
"@ngrx/store": "^10.0.1",
|
||||
"@nguniversal/express-engine": "10.1.0",
|
||||
"@angular/animations": "~11.2.14",
|
||||
"@angular/cdk": "^11.2.13",
|
||||
"@angular/common": "~11.2.14",
|
||||
"@angular/compiler": "~11.2.14",
|
||||
"@angular/core": "~11.2.14",
|
||||
"@angular/forms": "~11.2.14",
|
||||
"@angular/localize": "11.2.14",
|
||||
"@angular/platform-browser": "~11.2.14",
|
||||
"@angular/platform-browser-dynamic": "~11.2.14",
|
||||
"@angular/platform-server": "~11.2.14",
|
||||
"@angular/router": "~11.2.14",
|
||||
"@kolkov/ngx-gallery": "^1.2.3",
|
||||
"@ng-bootstrap/ng-bootstrap": "9.1.3",
|
||||
"@ng-dynamic-forms/core": "^13.0.0",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^13.0.0",
|
||||
"@ngrx/effects": "^11.1.1",
|
||||
"@ngrx/router-store": "^11.1.1",
|
||||
"@ngrx/store": "^11.1.1",
|
||||
"@nguniversal/express-engine": "11.2.1",
|
||||
"@ngx-translate/core": "^13.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^9.0.0",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
@@ -87,9 +78,9 @@
|
||||
"caniuse-lite": "^1.0.30001165",
|
||||
"cerialize": "0.1.18",
|
||||
"cli-progress": "^3.8.0",
|
||||
"compression": "^1.7.4",
|
||||
"cookie-parser": "1.4.5",
|
||||
"core-js": "^3.7.0",
|
||||
"debug-loader": "^0.0.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"express": "^4.17.1",
|
||||
"express-rate-limit": "^5.1.3",
|
||||
@@ -97,73 +88,83 @@
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "^6.1.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"https": "1.0.0",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.1.3",
|
||||
"jsonschema": "1.4.0",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"klaro": "^0.7.10",
|
||||
"lodash": "^4.17.21",
|
||||
"mirador": "^3.3.0",
|
||||
"mirador-dl-plugin": "^0.13.0",
|
||||
"mirador-share-plugin": "^0.11.0",
|
||||
"moment": "^2.29.1",
|
||||
"morgan": "^1.10.0",
|
||||
"ng-mocks": "10.5.4",
|
||||
"ng-mocks": "11.11.2",
|
||||
"ng2-file-upload": "1.4.0",
|
||||
"ng2-nouislider": "^1.8.2",
|
||||
"ng2-nouislider": "^1.8.3",
|
||||
"ngx-infinite-scroll": "^10.0.1",
|
||||
"ngx-moment": "^5.0.0",
|
||||
"ngx-pagination": "5.0.0",
|
||||
"ngx-sortablejs": "^10.0.0",
|
||||
"ngx-sortablejs": "^11.1.0",
|
||||
"nouislider": "^14.6.3",
|
||||
"pem": "1.14.4",
|
||||
"postcss-cli": "^8.3.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^6.6.3",
|
||||
"rxjs-spy": "^7.5.3",
|
||||
"sass-resources-loader": "^2.1.1",
|
||||
"sortablejs": "1.13.0",
|
||||
"tslib": "^2.0.0",
|
||||
"url-parse": "^1.5.3",
|
||||
"uuid": "^8.3.2",
|
||||
"webfontloader": "1.6.28",
|
||||
"zone.js": "^0.10.3",
|
||||
"@kolkov/ngx-gallery": "^1.2.3"
|
||||
"zone.js": "^0.10.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "10.0.1",
|
||||
"@angular-devkit/build-angular": "~0.1002.0",
|
||||
"@angular/cli": "~10.2.0",
|
||||
"@angular/compiler-cli": "~10.2.3",
|
||||
"@angular/language-service": "~10.2.3",
|
||||
"@angular-devkit/build-angular": "~0.1102.15",
|
||||
"@angular/cli": "~11.2.15",
|
||||
"@angular/compiler-cli": "~11.2.14",
|
||||
"@angular/language-service": "~11.2.14",
|
||||
"@cypress/schematic": "^1.5.0",
|
||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||
"@ngrx/store-devtools": "^10.0.1",
|
||||
"@ngtools/webpack": "10.2.0",
|
||||
"@nguniversal/builders": "~10.1.0",
|
||||
"@ngrx/store-devtools": "^11.1.1",
|
||||
"@ngtools/webpack": "10.2.3",
|
||||
"@nguniversal/builders": "~11.2.1",
|
||||
"@types/deep-freeze": "0.1.2",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/file-saver": "^2.0.1",
|
||||
"@types/jasmine": "^3.6.2",
|
||||
"@types/jasmine": "~3.6.0",
|
||||
"@types/jasminewd2": "~2.0.8",
|
||||
"@types/js-cookie": "2.2.6",
|
||||
"@types/lodash": "^4.14.165",
|
||||
"@types/node": "^14.14.9",
|
||||
"codelyzer": "^6.0.1",
|
||||
"axe-core": "^4.3.3",
|
||||
"codelyzer": "^6.0.0",
|
||||
"compression-webpack-plugin": "^3.0.1",
|
||||
"copy-webpack-plugin": "^6.4.1",
|
||||
"cross-env": "^7.0.3",
|
||||
"css-loader": "3.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"cypress": "8.6.0",
|
||||
"cypress-axe": "^0.13.0",
|
||||
"debug-loader": "^0.0.1",
|
||||
"deep-freeze": "0.0.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^6.0.3",
|
||||
"html-loader": "^1.3.2",
|
||||
"html-webpack-plugin": "^4.5.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"jasmine-core": "^3.6.0",
|
||||
"jasmine-core": "~3.6.0",
|
||||
"jasmine-marbles": "0.6.0",
|
||||
"jasmine-spec-reporter": "^6.0.0",
|
||||
"jasmine-spec-reporter": "~5.0.0",
|
||||
"karma": "^5.2.3",
|
||||
"karma-chrome-launcher": "^3.1.0",
|
||||
"karma-chrome-launcher": "~3.1.0",
|
||||
"karma-coverage-istanbul-reporter": "~3.0.2",
|
||||
"karma-jasmine": "^4.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.5.4",
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"nodemon": "^2.0.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"nodemon": "^2.0.15",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.4",
|
||||
"postcss-apply": "0.11.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
@@ -173,17 +174,21 @@
|
||||
"protractor": "^7.0.0",
|
||||
"protractor-istanbul-plugin": "2.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"react": "^16.14.0",
|
||||
"react-dom": "^16.14.0",
|
||||
"rimraf": "^3.0.2",
|
||||
"rxjs-spy": "^7.5.3",
|
||||
"sass-resources-loader": "^2.1.1",
|
||||
"script-ext-html-webpack-plugin": "2.1.5",
|
||||
"string-replace-loader": "^2.3.0",
|
||||
"terser-webpack-plugin": "^2.3.1",
|
||||
"ts-loader": "^5.2.0",
|
||||
"ts-node": "^8.8.1",
|
||||
"ts-node": "^8.10.2",
|
||||
"tslint": "^6.1.3",
|
||||
"typescript": "~4.0.5",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-bundle-analyzer": "^4.4.0",
|
||||
"webpack-cli": "^4.2.0",
|
||||
"webpack-node-externals": "1.7.2"
|
||||
"webpack-dev-server": "^4.5.0"
|
||||
}
|
||||
}
|
||||
|
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);
|
||||
|
||||
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);
|
||||
}
|
||||
|
99
scripts/merge-i18n-files.ts
Normal file
99
scripts/merge-i18n-files.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import { projectRoot} from '../webpack/helpers';
|
||||
const commander = require('commander');
|
||||
const fs = require('fs');
|
||||
const JSON5 = require('json5');
|
||||
const _cliProgress = require('cli-progress');
|
||||
const _ = require('lodash');
|
||||
|
||||
const program = new commander.Command();
|
||||
program.version('1.0.0', '-v, --version');
|
||||
|
||||
const LANGUAGE_FILES_LOCATION = 'src/assets/i18n';
|
||||
|
||||
parseCliInput();
|
||||
|
||||
/**
|
||||
* Purpose: Allows customization of i18n labels from within themes
|
||||
* e.g. Customize the label "menu.section.browse_global" to display "Browse DSpace" rather than "All of DSpace"
|
||||
*
|
||||
* This script uses the i18n files found in a source directory to override settings in files with the same
|
||||
* name in a destination directory. Only the i18n labels to be overridden need be in the source files.
|
||||
*
|
||||
* Execution (using custom theme):
|
||||
* ```
|
||||
* yarn merge-i18n -s src/themes/custom/assets/i18n
|
||||
* ```
|
||||
*
|
||||
* Input parameters:
|
||||
* * Output directory: The directory in which the original i18n files are stored
|
||||
* - Defaults to src/assets/i18n (the default i18n file location)
|
||||
* - This is where the final output files will be written
|
||||
* * Source directory: The directory with override files
|
||||
* - Required
|
||||
* - Recommended to place override files in the theme directory under assets/i18n (but this is not required)
|
||||
* - Files must have matching names in both source and destination directories, for example:
|
||||
* en.json5 in the source directory will be merged with en.json5 in the destination directory
|
||||
* fr.json5 in the source directory will be merged with fr.json5 in the destination directory
|
||||
*/
|
||||
function parseCliInput() {
|
||||
program
|
||||
.option('-d, --output-dir <output-dir>', 'output dir when running script on all language files', projectRoot(LANGUAGE_FILES_LOCATION))
|
||||
.option('-s, --source-dir <source-dir>', 'source dir of transalations to be merged')
|
||||
.usage('(-s <source-dir> [-d <output-dir>])')
|
||||
.parse(process.argv);
|
||||
|
||||
if (program.outputDir && program.sourceDir) {
|
||||
if (!fs.existsSync(program.outputDir) && !fs.lstatSync(program.outputDir).isDirectory() ) {
|
||||
console.error('Output does not exist or is not a directory.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
if (!fs.existsSync(program.sourceDir) && !fs.lstatSync(program.sourceDir).isDirectory() ) {
|
||||
console.error('Source does not exist or is not a directory.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
fs.readdirSync(projectRoot(program.sourceDir)).forEach(file => {
|
||||
if (fs.existsSync(program.outputDir + '/' + file) ) {
|
||||
console.log('Merging: ' + program.outputDir + '/' + file + ' with ' + program.sourceDir + '/' + file);
|
||||
mergeFileWithSource(program.sourceDir + '/' + file, program.outputDir + '/' + file);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.error('Source or Output parameter is missing.');
|
||||
console.log(program.outputHelp());
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads source file and output file to merge the contents
|
||||
* > Iterates over the source file keys
|
||||
* > Updates values for each key and adds new keys as needed
|
||||
* > Updates the output file with the new merged json
|
||||
* @param pathToSourceFile Valid path to source file to merge from
|
||||
* @param pathToOutputFile Valid path to merge and write output
|
||||
*/
|
||||
function mergeFileWithSource(pathToSourceFile, pathToOutputFile) {
|
||||
const progressBar = new _cliProgress.SingleBar({}, _cliProgress.Presets.shades_classic);
|
||||
progressBar.start(100, 0);
|
||||
|
||||
const sourceFile = fs.readFileSync(pathToSourceFile, 'utf8');
|
||||
progressBar.update(10);
|
||||
const outputFile = fs.readFileSync(pathToOutputFile, 'utf8');
|
||||
progressBar.update(20);
|
||||
|
||||
const parsedSource = JSON5.parse(sourceFile);
|
||||
progressBar.update(30);
|
||||
const parsedOutput = JSON5.parse(outputFile);
|
||||
progressBar.update(40);
|
||||
|
||||
for (const key of Object.keys(parsedSource)) {
|
||||
parsedOutput[key] = parsedSource[key];
|
||||
}
|
||||
progressBar.update(80);
|
||||
fs.writeFileSync(pathToOutputFile,JSON5.stringify(parsedOutput,{ space:'\n ', quote: '"' }), { encoding:'utf8' });
|
||||
|
||||
progressBar.update(100);
|
||||
progressBar.stop();
|
||||
}
|
@@ -1,11 +1,14 @@
|
||||
import { environment } from '../src/environments/environment';
|
||||
|
||||
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
|
||||
*/
|
||||
child.spawn(
|
||||
`ng serve --host ${environment.ui.host} --port ${environment.ui.port} --servePath ${environment.ui.nameSpace} --ssl ${environment.ui.ssl}`,
|
||||
{ stdio:'inherit', shell: true }
|
||||
`ng serve --host ${appConfig.ui.host} --port ${appConfig.ui.port} --serve-path ${appConfig.ui.nameSpace} --ssl ${appConfig.ui.ssl}`,
|
||||
{ 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);
|
||||
});
|
70
scripts/test-rest.ts
Normal file
70
scripts/test-rest.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as http from 'http';
|
||||
import * as https from 'https';
|
||||
|
||||
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 config.*.yaml)
|
||||
*
|
||||
* This script is useful to test for any Node.js connection issues with your REST API.
|
||||
*
|
||||
* Usage (see package.json): yarn test:rest
|
||||
*/
|
||||
|
||||
// Get root URL of configured REST API
|
||||
const restUrl = appConfig.rest.baseUrl + '/api';
|
||||
console.log(`...Testing connection to REST API at ${restUrl}...\n`);
|
||||
|
||||
// If SSL enabled, test via HTTPS, else via HTTP
|
||||
if (appConfig.rest.ssl) {
|
||||
const req = https.request(restUrl, (res) => {
|
||||
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
||||
res.on('data', (data) => {
|
||||
checkJSONResponse(data);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', error => {
|
||||
console.error('ERROR connecting to REST API\n' + error);
|
||||
});
|
||||
|
||||
req.end();
|
||||
} else {
|
||||
const req = http.request(restUrl, (res) => {
|
||||
console.log(`RESPONSE: ${res.statusCode} ${res.statusMessage} \n`);
|
||||
res.on('data', (data) => {
|
||||
checkJSONResponse(data);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', error => {
|
||||
console.error('ERROR connecting to REST API\n' + error);
|
||||
});
|
||||
|
||||
req.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check JSON response from REST API to see if it looks valid. Log useful information
|
||||
* @param responseData response data
|
||||
*/
|
||||
function checkJSONResponse(responseData: any): any {
|
||||
let parsedData;
|
||||
try {
|
||||
parsedData = JSON.parse(responseData);
|
||||
console.log('Checking JSON returned for validity...');
|
||||
console.log(`\t"dspaceVersion" = ${parsedData.dspaceVersion}`);
|
||||
console.log(`\t"dspaceUI" = ${parsedData.dspaceUI}`);
|
||||
console.log(`\t"dspaceServer" = ${parsedData.dspaceServer}`);
|
||||
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)!
|
||||
const linksFound: string[] = Object.keys(parsedData._links);
|
||||
console.log(`\tDoes "/api" endpoint have HAL links ("_links" section)? ${linksFound.includes('authn') && linksFound.includes('sites')}`);
|
||||
} catch (err) {
|
||||
console.error('ERROR: INVALID DSPACE REST API! Response is not valid JSON!');
|
||||
console.error(`Response returned:\n${responseData}`);
|
||||
}
|
||||
}
|
59
server.ts
59
server.ts
@@ -19,36 +19,50 @@ import 'zone.js/dist/zone-node';
|
||||
import 'reflect-metadata';
|
||||
import 'rxjs';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as pem from 'pem';
|
||||
import * as https from 'https';
|
||||
import * as morgan from 'morgan';
|
||||
import * as express from 'express';
|
||||
import * as bodyParser from 'body-parser';
|
||||
import * as compression from 'compression';
|
||||
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
import { enableProdMode } from '@angular/core';
|
||||
import { existsSync } from 'fs';
|
||||
|
||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||
|
||||
import { environment } from './src/environments/environment';
|
||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||
import { hasValue, hasNoValue } from './src/app/shared/empty.util';
|
||||
import { APP_BASE_HREF } from '@angular/common';
|
||||
|
||||
import { UIServerConfig } from './src/config/ui-server-config.interface';
|
||||
|
||||
import { ServerAppModule } from './src/main.server';
|
||||
|
||||
import { buildAppConfig } from './src/config/config.server';
|
||||
import { AppConfig, APP_CONFIG } from './src/config/app-config.interface';
|
||||
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||
|
||||
/*
|
||||
* Set path for the browser application's dist folder
|
||||
*/
|
||||
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
|
||||
// Set path fir IIIF viewer.
|
||||
const IIIF_VIEWER = join(process.cwd(), 'dist/iiif');
|
||||
|
||||
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 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.
|
||||
export function app() {
|
||||
|
||||
@@ -57,7 +71,6 @@ export function app() {
|
||||
*/
|
||||
const server = express();
|
||||
|
||||
|
||||
/*
|
||||
* If production mode is enabled in the environment file:
|
||||
* - Enable Angular's production mode
|
||||
@@ -99,7 +112,11 @@ export function app() {
|
||||
provide: RESPONSE,
|
||||
useValue: (options as any).req.res,
|
||||
},
|
||||
],
|
||||
{
|
||||
provide: APP_CONFIG,
|
||||
useValue: environment
|
||||
}
|
||||
]
|
||||
})(_, (options as any), callback)
|
||||
);
|
||||
|
||||
@@ -135,6 +152,10 @@ export function app() {
|
||||
* Serve static resources (images, i18n messages, …)
|
||||
*/
|
||||
server.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
|
||||
/*
|
||||
* Fallthrough to the IIIF viewer (must be included in the build).
|
||||
*/
|
||||
server.use('/iiif', express.static(IIIF_VIEWER, {index:false}));
|
||||
|
||||
// Register the ngApp callback function to handle incoming requests
|
||||
server.get('*', ngApp);
|
||||
@@ -221,24 +242,25 @@ function run() {
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
function start() {
|
||||
/*
|
||||
* If SSL is enabled
|
||||
* - Read credentials from configuration files
|
||||
* - Call script to start an HTTPS server with these credentials
|
||||
* When SSL is disabled
|
||||
* - Start an HTTP server on the configured port and host
|
||||
*/
|
||||
if (environment.ui.ssl) {
|
||||
if (environment.ui.ssl) {
|
||||
let serviceKey;
|
||||
try {
|
||||
serviceKey = fs.readFileSync('./config/ssl/key.pem');
|
||||
serviceKey = readFileSync('./config/ssl/key.pem');
|
||||
} catch (e) {
|
||||
console.warn('Service key not found at ./config/ssl/key.pem');
|
||||
}
|
||||
|
||||
let certificate;
|
||||
try {
|
||||
certificate = fs.readFileSync('./config/ssl/cert.pem');
|
||||
certificate = readFileSync('./config/ssl/cert.pem');
|
||||
} catch (e) {
|
||||
console.warn('Certificate not found at ./config/ssl/key.pem');
|
||||
}
|
||||
@@ -260,8 +282,19 @@ if (environment.ui.ssl) {
|
||||
createHttpsServer(keys);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
} else {
|
||||
run();
|
||||
}
|
||||
}
|
||||
|
||||
// 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';
|
||||
|
@@ -1,11 +0,0 @@
|
||||
<li class="sidebar-section">
|
||||
<a class="nav-item nav-link shortcut-icon" attr.aria-labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" [routerLink]="itemModel.link">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
</a>
|
||||
<div class="sidebar-collapsible">
|
||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
@@ -1,27 +0,0 @@
|
||||
<li class="sidebar-section" [ngClass]="{'expanded': (expanded | async)}"
|
||||
[@bgColor]="{
|
||||
value: ((expanded | async) ? 'endBackground' : 'startBackground'),
|
||||
params: {endColor: (sidebarActiveBg | async)}}">
|
||||
<div class="icon-wrapper">
|
||||
<a class="nav-item nav-link shortcut-icon" attr.aria.labelledby="sidebarName-{{section.id}}" [title]="('menu.section.icon.' + section.id) | translate" (click)="toggleSection($event)" href="#">
|
||||
<i class="fas fa-{{section.icon}} fa-fw"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="sidebar-collapsible">
|
||||
<a class="nav-item nav-link" href="#"
|
||||
(click)="toggleSection($event)">
|
||||
<span id="sidebarName-{{section.id}}" class="section-header-text">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(section.id).component; injector: (sectionMap$ | async).get(section.id).injector;"></ng-container>
|
||||
</span>
|
||||
<i class="fas fa-chevron-right fa-pull-right"
|
||||
[@rotate]="(expanded | async) ? 'expanded' : 'collapsed'" [title]="('menu.section.toggle.' + section.id) | translate"></i>
|
||||
</a>
|
||||
<ul class="sidebar-sub-level-items list-unstyled" @slide *ngIf="(expanded | async)">
|
||||
<li *ngFor="let subSection of (subSections$ | async)">
|
||||
<ng-container
|
||||
*ngComponentOutlet="(sectionMap$ | async).get(subSection.id).component; injector: (sectionMap$ | async).get(subSection.id).injector;"></ng-container>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</li>
|
@@ -1,38 +0,0 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { EditBitstreamPageComponent } from './edit-bitstream-page/edit-bitstream-page.component';
|
||||
import { AuthenticatedGuard } from '../core/auth/authenticated.guard';
|
||||
import { BitstreamPageResolver } from './bitstream-page.resolver';
|
||||
import { BitstreamDownloadPageComponent } from '../shared/bitstream-download-page/bitstream-download-page.component';
|
||||
|
||||
const EDIT_BITSTREAM_PATH = ':id/edit';
|
||||
|
||||
/**
|
||||
* Routing module to help navigate Bitstream pages
|
||||
*/
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{
|
||||
path:':id/download',
|
||||
component: BitstreamDownloadPageComponent,
|
||||
resolve: {
|
||||
bitstream: BitstreamPageResolver
|
||||
},
|
||||
},
|
||||
{
|
||||
path: EDIT_BITSTREAM_PATH,
|
||||
component: EditBitstreamPageComponent,
|
||||
resolve: {
|
||||
bitstream: BitstreamPageResolver
|
||||
},
|
||||
canActivate: [AuthenticatedGuard]
|
||||
}
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
BitstreamPageResolver,
|
||||
]
|
||||
})
|
||||
export class BitstreamPageRoutingModule {
|
||||
}
|
@@ -1,254 +0,0 @@
|
||||
import { EditBitstreamPageComponent } from './edit-bitstream-page.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { DynamicFormControlModel, DynamicFormService } from '@ng-dynamic-forms/core';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { BitstreamDataService } from '../../core/data/bitstream-data.service';
|
||||
import { ChangeDetectorRef, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { BitstreamFormatDataService } from '../../core/data/bitstream-format-data.service';
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
||||
import { INotification, Notification } from '../../shared/notifications/models/notification.model';
|
||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatSupportLevel } from '../../core/shared/bitstream-format-support-level';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { FormControl, FormGroup } from '@angular/forms';
|
||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||
import { VarDirective } from '../../shared/utils/var.directive';
|
||||
import {
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { RouterStub } from '../../shared/testing/router.stub';
|
||||
import { getEntityEditRoute, getItemEditRoute } from '../../+item-page/item-page-routing-paths';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info');
|
||||
const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning');
|
||||
const successNotification: INotification = new Notification('id', NotificationType.Success, 'success');
|
||||
|
||||
let notificationsService: NotificationsService;
|
||||
let formService: DynamicFormService;
|
||||
let bitstreamService: BitstreamDataService;
|
||||
let bitstreamFormatService: BitstreamFormatDataService;
|
||||
let bitstream: Bitstream;
|
||||
let selectedFormat: BitstreamFormat;
|
||||
let allFormats: BitstreamFormat[];
|
||||
let router: Router;
|
||||
let routerStub;
|
||||
|
||||
describe('EditBitstreamPageComponent', () => {
|
||||
let comp: EditBitstreamPageComponent;
|
||||
let fixture: ComponentFixture<EditBitstreamPageComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
allFormats = [
|
||||
Object.assign({
|
||||
id: '1',
|
||||
shortDescription: 'Unknown',
|
||||
description: 'Unknown format',
|
||||
supportLevel: BitstreamFormatSupportLevel.Unknown,
|
||||
_links: {
|
||||
self: { href: 'format-selflink-1' }
|
||||
}
|
||||
}),
|
||||
Object.assign({
|
||||
id: '2',
|
||||
shortDescription: 'PNG',
|
||||
description: 'Portable Network Graphics',
|
||||
supportLevel: BitstreamFormatSupportLevel.Known,
|
||||
_links: {
|
||||
self: { href: 'format-selflink-2' }
|
||||
}
|
||||
}),
|
||||
Object.assign({
|
||||
id: '3',
|
||||
shortDescription: 'GIF',
|
||||
description: 'Graphics Interchange Format',
|
||||
supportLevel: BitstreamFormatSupportLevel.Known,
|
||||
_links: {
|
||||
self: { href: 'format-selflink-3' }
|
||||
}
|
||||
})
|
||||
] as BitstreamFormat[];
|
||||
selectedFormat = allFormats[1];
|
||||
notificationsService = jasmine.createSpyObj('notificationsService',
|
||||
{
|
||||
info: infoNotification,
|
||||
warning: warningNotification,
|
||||
success: successNotification
|
||||
}
|
||||
);
|
||||
formService = Object.assign({
|
||||
createFormGroup: (fModel: DynamicFormControlModel[]) => {
|
||||
const controls = {};
|
||||
if (hasValue(fModel)) {
|
||||
fModel.forEach((controlModel) => {
|
||||
controls[controlModel.id] = new FormControl((controlModel as any).value);
|
||||
});
|
||||
return new FormGroup(controls);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
});
|
||||
bitstream = Object.assign(new Bitstream(), {
|
||||
metadata: {
|
||||
'dc.description': [
|
||||
{
|
||||
value: 'Bitstream description'
|
||||
}
|
||||
],
|
||||
'dc.title': [
|
||||
{
|
||||
value: 'Bitstream title'
|
||||
}
|
||||
]
|
||||
},
|
||||
format: createSuccessfulRemoteDataObject$(selectedFormat),
|
||||
_links: {
|
||||
self: 'bitstream-selflink'
|
||||
},
|
||||
bundle: createSuccessfulRemoteDataObject$({
|
||||
item: createSuccessfulRemoteDataObject$(Object.assign(new Item(), {
|
||||
uuid: 'some-uuid'
|
||||
}))
|
||||
})
|
||||
});
|
||||
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
||||
findById: createSuccessfulRemoteDataObject$(bitstream),
|
||||
update: createSuccessfulRemoteDataObject$(bitstream),
|
||||
updateFormat: createSuccessfulRemoteDataObject$(bitstream),
|
||||
commitUpdates: {},
|
||||
patch: {}
|
||||
});
|
||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||
findAll: createSuccessfulRemoteDataObject$(createPaginatedList(allFormats))
|
||||
});
|
||||
|
||||
const itemPageUrl = `fake-url/some-uuid`;
|
||||
routerStub = Object.assign(new RouterStub(), {
|
||||
url: `${itemPageUrl}`
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule],
|
||||
declarations: [EditBitstreamPageComponent, FileSizePipe, VarDirective],
|
||||
providers: [
|
||||
{ provide: NotificationsService, useValue: notificationsService },
|
||||
{ provide: DynamicFormService, useValue: formService },
|
||||
{ provide: ActivatedRoute, useValue: { data: observableOf({ bitstream: createSuccessfulRemoteDataObject(bitstream) }), snapshot: { queryParams: {} } } },
|
||||
{ provide: BitstreamDataService, useValue: bitstreamService },
|
||||
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
ChangeDetectorRef
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditBitstreamPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
router = (comp as any).router;
|
||||
});
|
||||
|
||||
describe('on startup', () => {
|
||||
let rawForm;
|
||||
|
||||
beforeEach(() => {
|
||||
rawForm = comp.formGroup.getRawValue();
|
||||
});
|
||||
|
||||
it('should fill in the bitstream\'s title', () => {
|
||||
expect(rawForm.fileNamePrimaryContainer.fileName).toEqual(bitstream.name);
|
||||
});
|
||||
|
||||
it('should fill in the bitstream\'s description', () => {
|
||||
expect(rawForm.descriptionContainer.description).toEqual(bitstream.firstMetadataValue('dc.description'));
|
||||
});
|
||||
|
||||
it('should select the correct format', () => {
|
||||
expect(rawForm.formatContainer.selectedFormat).toEqual(selectedFormat.id);
|
||||
});
|
||||
|
||||
it('should put the \"New Format\" input on invisible', () => {
|
||||
expect(comp.formLayout.newFormat.grid.host).toContain('invisible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('when an unknown format is selected', () => {
|
||||
beforeEach(() => {
|
||||
comp.updateNewFormatLayout(allFormats[0].id);
|
||||
});
|
||||
|
||||
it('should remove the invisible class from the \"New Format\" input', () => {
|
||||
expect(comp.formLayout.newFormat.grid.host).not.toContain('invisible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('onSubmit', () => {
|
||||
describe('when selected format hasn\'t changed', () => {
|
||||
beforeEach(() => {
|
||||
comp.onSubmit();
|
||||
});
|
||||
|
||||
it('should call update', () => {
|
||||
expect(bitstreamService.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should commit the updates', () => {
|
||||
expect(bitstreamService.commitUpdates).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when selected format has changed', () => {
|
||||
beforeEach(() => {
|
||||
comp.formGroup.patchValue({
|
||||
formatContainer: {
|
||||
selectedFormat: allFormats[2].id
|
||||
}
|
||||
});
|
||||
fixture.detectChanges();
|
||||
comp.onSubmit();
|
||||
});
|
||||
|
||||
it('should call update', () => {
|
||||
expect(bitstreamService.update).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should call updateFormat', () => {
|
||||
expect(bitstreamService.updateFormat).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should commit the updates', () => {
|
||||
expect(bitstreamService.commitUpdates).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('when the cancel button is clicked', () => {
|
||||
it('should call navigateToItemEditBitstreams method', () => {
|
||||
spyOn(comp, 'navigateToItemEditBitstreams');
|
||||
comp.onCancel();
|
||||
expect(comp.navigateToItemEditBitstreams).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
describe('when navigateToItemEditBitstreams is called, and the component has an itemId', () => {
|
||||
it('should redirect to the item edit page on the bitstreams tab with the itemId from the component', () => {
|
||||
comp.itemId = 'some-uuid1';
|
||||
comp.navigateToItemEditBitstreams();
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid1'), 'bitstreams']);
|
||||
});
|
||||
});
|
||||
describe('when navigateToItemEditBitstreams is called, and the component does not have an itemId', () => {
|
||||
it('should redirect to the item edit page on the bitstreams tab with the itemId from the bundle links ', () => {
|
||||
comp.itemId = undefined;
|
||||
comp.navigateToItemEditBitstreams();
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith([getEntityEditRoute(null, 'some-uuid'), 'bitstreams']);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,12 +0,0 @@
|
||||
import { BrowseByType, rendersBrowseBy } from './browse-by-decorator';
|
||||
|
||||
describe('BrowseByDecorator', () => {
|
||||
const titleDecorator = rendersBrowseBy(BrowseByType.Title);
|
||||
const dateDecorator = rendersBrowseBy(BrowseByType.Date);
|
||||
const metadataDecorator = rendersBrowseBy(BrowseByType.Metadata);
|
||||
it('should have a decorator for all types', () => {
|
||||
expect(titleDecorator.length).not.toEqual(0);
|
||||
expect(dateDecorator.length).not.toEqual(0);
|
||||
expect(metadataDecorator.length).not.toEqual(0);
|
||||
});
|
||||
});
|
@@ -1,54 +0,0 @@
|
||||
import { BrowseBySwitcherComponent } from './browse-by-switcher.component';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import * as decorator from './browse-by-decorator';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import createSpy = jasmine.createSpy;
|
||||
|
||||
xdescribe('BrowseBySwitcherComponent', () => {
|
||||
let comp: BrowseBySwitcherComponent;
|
||||
let fixture: ComponentFixture<BrowseBySwitcherComponent>;
|
||||
|
||||
const types = environment.browseBy.types;
|
||||
|
||||
const params = new BehaviorSubject(createParamsWithId('initialValue'));
|
||||
|
||||
const activatedRouteStub = {
|
||||
params: params
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [BrowseBySwitcherComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(BrowseBySwitcherComponent);
|
||||
comp = fixture.componentInstance;
|
||||
spyOnProperty(decorator, 'getComponentByBrowseByType').and.returnValue(createSpy('getComponentByItemType'));
|
||||
}));
|
||||
|
||||
types.forEach((type) => {
|
||||
describe(`when switching to a browse-by page for "${type.id}"`, () => {
|
||||
beforeEach(() => {
|
||||
params.next(createParamsWithId(type.id));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it(`should call getComponentByBrowseByType with type "${type.type}"`, () => {
|
||||
expect(decorator.getComponentByBrowseByType).toHaveBeenCalledWith(type.type);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export function createParamsWithId(id) {
|
||||
return { id: id };
|
||||
}
|
@@ -1,39 +0,0 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { getComponentByBrowseByType } from './browse-by-decorator';
|
||||
import { environment } from '../../../environments/environment';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-browse-by-switcher',
|
||||
templateUrl: './browse-by-switcher.component.html'
|
||||
})
|
||||
/**
|
||||
* Component for determining what Browse-By component to use depending on the metadata (browse ID) provided
|
||||
*/
|
||||
export class BrowseBySwitcherComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* Resolved browse-by component
|
||||
*/
|
||||
browseByComponent: Observable<any>;
|
||||
|
||||
public constructor(protected route: ActivatedRoute) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the correct browse-by component by using the relevant config from environment.js
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.browseByComponent = this.route.params.pipe(
|
||||
map((params) => {
|
||||
const id = params.id;
|
||||
return environment.browseBy.types.find((config: BrowseByTypeConfig) => config.id === id);
|
||||
}),
|
||||
map((config: BrowseByTypeConfig) => getComponentByBrowseByType(config.type))
|
||||
);
|
||||
}
|
||||
|
||||
}
|
@@ -1,59 +0,0 @@
|
||||
import { ActivatedRouteSnapshot, CanActivate, RouterStateSnapshot } from '@angular/router';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service';
|
||||
import { hasNoValue, hasValue } from '../shared/empty.util';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { getFirstSucceededRemoteData } from '../core/shared/operators';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { environment } from '../../environments/environment';
|
||||
|
||||
@Injectable()
|
||||
/**
|
||||
* A guard taking care of the correct route.data being set for the Browse-By components
|
||||
*/
|
||||
export class BrowseByGuard implements CanActivate {
|
||||
|
||||
constructor(protected dsoService: DSpaceObjectDataService,
|
||||
protected translate: TranslateService) {
|
||||
}
|
||||
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
|
||||
const title = route.data.title;
|
||||
const id = route.params.id || route.queryParams.id || route.data.id;
|
||||
let metadataField = route.data.metadataField;
|
||||
if (hasNoValue(metadataField) && hasValue(id)) {
|
||||
const config = environment.browseBy.types.find((conf) => conf.id === id);
|
||||
if (hasValue(config) && hasValue(config.metadataField)) {
|
||||
metadataField = config.metadataField;
|
||||
}
|
||||
}
|
||||
const scope = route.queryParams.scope;
|
||||
const value = route.queryParams.value;
|
||||
const metadataTranslated = this.translate.instant('browse.metadata.' + id);
|
||||
if (hasValue(scope)) {
|
||||
const dsoAndMetadata$ = this.dsoService.findById(scope).pipe(getFirstSucceededRemoteData());
|
||||
return dsoAndMetadata$.pipe(
|
||||
map((dsoRD) => {
|
||||
const name = dsoRD.payload.name;
|
||||
route.data = this.createData(title, id, metadataField, name, metadataTranslated, value, route);
|
||||
return true;
|
||||
})
|
||||
);
|
||||
} else {
|
||||
route.data = this.createData(title, id, metadataField, '', metadataTranslated, value, route);
|
||||
return observableOf(true);
|
||||
}
|
||||
}
|
||||
|
||||
private createData(title, id, metadataField, collection, field, value, route) {
|
||||
return Object.assign({}, route.data, {
|
||||
title: title,
|
||||
id: id,
|
||||
metadataField: metadataField,
|
||||
collection: collection,
|
||||
field: field,
|
||||
value: hasValue(value) ? `"${value}"` : ''
|
||||
});
|
||||
}
|
||||
}
|
@@ -1,87 +0,0 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormService,
|
||||
DynamicInputModel,
|
||||
DynamicTextAreaModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { RequestService } from '../../core/data/request.service';
|
||||
import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||
|
||||
/**
|
||||
* Form used for creating and editing collections
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-collection-form',
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
|
||||
})
|
||||
export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
/**
|
||||
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
|
||||
*/
|
||||
@Input() dso: Collection = new Collection();
|
||||
|
||||
/**
|
||||
* @type {Collection.type} This is a collection-type form
|
||||
*/
|
||||
type = Collection.type;
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a collection
|
||||
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||
*/
|
||||
formModel: DynamicFormControlModel[] = [
|
||||
new DynamicInputModel({
|
||||
id: 'title',
|
||||
name: 'dc.title',
|
||||
required: true,
|
||||
validators: {
|
||||
required: null
|
||||
},
|
||||
errorMessages: {
|
||||
required: 'Please enter a name for this title'
|
||||
},
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'description',
|
||||
name: 'dc.description',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'abstract',
|
||||
name: 'dc.description.abstract',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'rights',
|
||||
name: 'dc.rights',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'tableofcontents',
|
||||
name: 'dc.description.tableofcontents',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'license',
|
||||
name: 'dc.rights.license',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'provenance',
|
||||
name: 'dc.description.provenance',
|
||||
}),
|
||||
];
|
||||
|
||||
public constructor(protected formService: DynamicFormService,
|
||||
protected translate: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected authService: AuthService,
|
||||
protected dsoService: CommunityDataService,
|
||||
protected requestService: RequestService,
|
||||
protected objectCache: ObjectCacheService) {
|
||||
super(formService, translate, notificationsService, authService, requestService, objectCache);
|
||||
}
|
||||
}
|
@@ -1,22 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<ng-container *ngVar="(dsoRD$ | async)?.payload as dso">
|
||||
<div class="col-12 pb-4">
|
||||
<h2 id="header" class="border-bottom pb-2">{{ 'community.delete.head' | translate}}</h2>
|
||||
<p class="pb-2">{{ 'community.delete.text' | translate:{ dso: dso.name } }}</p>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right">
|
||||
<button class="btn btn-outline-secondary" (click)="onCancel(dso)">
|
||||
<i class="fas fa-times"></i> {{'community.delete.cancel' | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger mr-2" (click)="onConfirm(dso)">
|
||||
<i class="fas fa-trash"></i> {{'community.delete.confirm' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
@@ -1,57 +0,0 @@
|
||||
<div class="container-fluid">
|
||||
<div class="d-inline-block float-right">
|
||||
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||
(click)="onSubmit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<h4>{{ 'collection.edit.tabs.source.head' | translate }}</h4>
|
||||
<div *ngIf="contentSource" class="form-check mb-4">
|
||||
<input type="checkbox" class="form-check-input" id="externalSourceCheck" [checked]="(contentSource?.harvestType !== harvestTypeNone)" (change)="changeExternalSource()">
|
||||
<label class="form-check-label" for="externalSourceCheck">{{ 'collection.edit.tabs.source.external' | translate }}</label>
|
||||
</div>
|
||||
<ds-loading *ngIf="!contentSource" [message]="'loading.content-source' | translate"></ds-loading>
|
||||
<h4 *ngIf="contentSource && (contentSource?.harvestType !== harvestTypeNone)">{{ 'collection.edit.tabs.source.form.head' | translate }}</h4>
|
||||
</div>
|
||||
<ds-form *ngIf="formGroup && contentSource && (contentSource?.harvestType !== harvestTypeNone)"
|
||||
[formId]="'collection-source-form-id'"
|
||||
[formGroup]="formGroup"
|
||||
[formModel]="formModel"
|
||||
[formLayout]="formLayout"
|
||||
[displaySubmit]="false"
|
||||
[displayCancel]="false"
|
||||
(dfChange)="onChange($event)"
|
||||
(submitForm)="onSubmit()"
|
||||
(cancel)="onCancel()"></ds-form>
|
||||
<div class="container-fluid" *ngIf="(contentSource?.harvestType !== harvestTypeNone)">
|
||||
<div class="d-inline-block float-right">
|
||||
<button class=" btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !isValid() || (initialHarvestType === harvestTypeNone && contentSource.harvestType === initialHarvestType)"
|
||||
(click)="onSubmit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
@@ -1,66 +0,0 @@
|
||||
<div class="item-metadata">
|
||||
<div class="button-row top d-flex mb-2">
|
||||
<button class="mr-auto btn btn-success"
|
||||
(click)="add()"><i
|
||||
class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.add-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || !(isValid() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<table class="table table-responsive table-striped table-bordered" *ngIf="((updates$ | async)| dsObjectValues).length > 0">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th>{{'item.edit.metadata.headers.field' | translate}}</th>
|
||||
<th>{{'item.edit.metadata.headers.value' | translate}}</th>
|
||||
<th class="text-center">{{'item.edit.metadata.headers.language' | translate}}</th>
|
||||
<th class="text-center">{{'item.edit.metadata.headers.edit' | translate}}</th>
|
||||
</tr>
|
||||
<tr *ngFor="let updateValue of ((updates$ | async)| dsObjectValues); trackBy: trackUpdate"
|
||||
ds-edit-in-place-field
|
||||
[fieldUpdate]="updateValue || {}"
|
||||
[url]="url"
|
||||
[ngClass]="{
|
||||
'table-warning': updateValue.changeType === 0,
|
||||
'table-danger': updateValue.changeType === 2,
|
||||
'table-success': updateValue.changeType === 1
|
||||
}">
|
||||
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div *ngIf="((updates$ | async)| dsObjectValues).length == 0">
|
||||
<ds-alert [content]="'item.edit.metadata.empty'" [type]="AlertTypeEnum.Info"></ds-alert>
|
||||
</div>
|
||||
<div class="button-row bottom">
|
||||
<div class="mt-2 float-right">
|
||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
||||
(click)="reinstate()"><i
|
||||
class="fas fa-undo-alt"></i> {{"item.edit.metadata.reinstate-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-primary mr-0" [disabled]="!(hasChanges() | async)"
|
||||
(click)="submit()"><i
|
||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||
</button>
|
||||
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
|
||||
[disabled]="!(hasChanges() | async)"
|
||||
(click)="discard()"><i
|
||||
class="fas fa-times"></i> {{"item.edit.metadata.discard-button" | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,48 +0,0 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2>{{'item.edit.move.head' | translate: {id: (itemRD$ | async)?.payload?.handle} }}</h2>
|
||||
<p>{{'item.edit.move.description' | translate}}</p>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<ds-dso-input-suggestions #f id="search-form"
|
||||
[suggestions]="(collectionSearchResults | async)"
|
||||
[placeholder]="'item.edit.move.search.placeholder'| translate"
|
||||
[action]="getCurrentUrl()"
|
||||
[name]="'item-move'"
|
||||
[(ngModel)]="selectedCollectionName"
|
||||
(clickSuggestion)="onClick($event)"
|
||||
(typeSuggestion)="resetCollection($event)"
|
||||
(findSuggestions)="findSuggestions($event)"
|
||||
(click)="f.open()"
|
||||
ngDefaultControl>
|
||||
</ds-dso-input-suggestions>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<p>
|
||||
<input type="checkbox" name="tc" [(ngModel)]="inheritPolicies" id="inheritPoliciesCheckbox">
|
||||
<label for="inheritPoliciesCheckbox">{{'item.edit.move.inheritpolicies.checkbox' |
|
||||
translate}}</label>
|
||||
</p>
|
||||
<p>
|
||||
{{'item.edit.move.inheritpolicies.description' | translate}}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button (click)="moveCollection()" class="btn btn-primary" [disabled]=!canSubmit>
|
||||
<span *ngIf="!processing"> {{'item.edit.move.move' | translate}}</span>
|
||||
<span *ngIf="processing"><i class='fas fa-circle-notch fa-spin'></i>
|
||||
{{'item.edit.move.processing' | translate}}
|
||||
</span>
|
||||
</button>
|
||||
<button [routerLink]="[(itemPageRoute$ | async), 'edit']"
|
||||
class="btn btn-outline-secondary">
|
||||
{{'item.edit.move.cancel' | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,15 +0,0 @@
|
||||
<div class="col-3 float-left d-flex h-100 action-label">
|
||||
<span class="justify-content-center align-self-center">
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.label' | translate}}
|
||||
</span>
|
||||
</div>
|
||||
<div *ngIf="!operation.disabled" class="col-9 float-left action-button">
|
||||
<a class="btn btn-outline-primary" [routerLink]="operation.operationUrl">
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||
</a>
|
||||
</div>
|
||||
<div *ngIf="operation.disabled" class="col-9 float-left action-button">
|
||||
<span class="btn btn-danger">
|
||||
{{'item.edit.tabs.status.buttons.' + operation.operationKey + '.button' | translate}}
|
||||
</span>
|
||||
</div>
|
@@ -1,26 +0,0 @@
|
||||
<h5>
|
||||
{{getRelationshipMessageKey() | async | translate}}
|
||||
<button class="ml-2 btn btn-success" (click)="openLookup()">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
||||
</button>
|
||||
</h5>
|
||||
<ng-container *ngVar="updates$ | async as updates">
|
||||
<ng-container *ngIf="updates">
|
||||
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
||||
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||
class="relationship-row d-block alert"
|
||||
[fieldUpdate]="updateValue || {}"
|
||||
[url]="url"
|
||||
[editItem]="item"
|
||||
[ngClass]="{
|
||||
'alert-success': updateValue.changeType === 1,
|
||||
'alert-warning': updateValue.changeType === 0,
|
||||
'alert-danger': updateValue.changeType === 2
|
||||
}">
|
||||
</ds-edit-relationship>
|
||||
<div *ngIf="updateValues.length === 0">{{"item.edit.relationships.no-relationships" | translate}}</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="!updates"></ds-loading>
|
||||
</ng-container>
|
@@ -1,344 +0,0 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||
import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
|
||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||
import { combineLatest as observableCombineLatest, Observable, of } from 'rxjs';
|
||||
import {
|
||||
FieldUpdate,
|
||||
FieldUpdates,
|
||||
RelationshipIdentifiable
|
||||
} from '../../../../core/data/object-updates/object-updates.reducer';
|
||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { defaultIfEmpty, map, mergeMap, switchMap, take, startWith } from 'rxjs/operators';
|
||||
import { hasValue, hasValueOperator } from '../../../../shared/empty.util';
|
||||
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
|
||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||
import {
|
||||
getAllSucceededRemoteData,
|
||||
getRemoteDataPayload,
|
||||
getFirstSucceededRemoteData,
|
||||
} from '../../../../core/shared/operators';
|
||||
import { ItemType } from '../../../../core/shared/item-relationships/item-type.model';
|
||||
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||
import { RelationshipOptions } from '../../../../shared/form/builder/models/relationship-options.model';
|
||||
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||
import { SearchResult } from '../../../../shared/search/search-result.model';
|
||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-relationship-list',
|
||||
styleUrls: ['./edit-relationship-list.component.scss'],
|
||||
templateUrl: './edit-relationship-list.component.html',
|
||||
})
|
||||
/**
|
||||
* A component creating a list of editable relationships of a certain type
|
||||
* The relationships are rendered as a list of related items
|
||||
*/
|
||||
export class EditRelationshipListComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The item to display related items for
|
||||
*/
|
||||
@Input() item: Item;
|
||||
|
||||
@Input() itemType: ItemType;
|
||||
|
||||
/**
|
||||
* The URL to the current page
|
||||
* Used to fetch updates for the current item from the store
|
||||
*/
|
||||
@Input() url: string;
|
||||
|
||||
/**
|
||||
* The label of the relationship-type we're rendering a list for
|
||||
*/
|
||||
@Input() relationshipType: RelationshipType;
|
||||
|
||||
private relatedEntityType$: Observable<ItemType>;
|
||||
|
||||
/**
|
||||
* The list ID to save selected entities under
|
||||
*/
|
||||
listId: string;
|
||||
|
||||
/**
|
||||
* The FieldUpdates for the relationships in question
|
||||
*/
|
||||
updates$: Observable<FieldUpdates>;
|
||||
|
||||
/**
|
||||
* A reference to the lookup window
|
||||
*/
|
||||
modalRef: NgbModalRef;
|
||||
|
||||
constructor(
|
||||
protected objectUpdatesService: ObjectUpdatesService,
|
||||
protected linkService: LinkService,
|
||||
protected relationshipService: RelationshipService,
|
||||
protected modalService: NgbModal,
|
||||
protected selectableListService: SelectableListService,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the i18n message key for this relationship type
|
||||
*/
|
||||
public getRelationshipMessageKey(): Observable<string> {
|
||||
|
||||
return observableCombineLatest(
|
||||
this.getLabel(),
|
||||
this.relatedEntityType$,
|
||||
).pipe(
|
||||
map(([label, relatedEntityType]) => {
|
||||
if (hasValue(label) && label.indexOf('is') > -1 && label.indexOf('Of') > -1) {
|
||||
const relationshipLabel = `${label.substring(2, label.indexOf('Of'))}`;
|
||||
if (relationshipLabel !== relatedEntityType.label) {
|
||||
return `relationships.is${relationshipLabel}Of.${relatedEntityType.label}`;
|
||||
} else {
|
||||
return `relationships.is${relationshipLabel}Of`;
|
||||
}
|
||||
} else {
|
||||
return label;
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relevant label for this relationship type
|
||||
*/
|
||||
private getLabel(): Observable<string> {
|
||||
return observableCombineLatest([
|
||||
this.relationshipType.leftType,
|
||||
this.relationshipType.rightType,
|
||||
].map((itemTypeRD) => itemTypeRD.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
))).pipe(
|
||||
map((itemTypes: ItemType[]) => [
|
||||
this.relationshipType.leftwardType,
|
||||
this.relationshipType.rightwardType,
|
||||
][itemTypes.findIndex((itemType) => itemType.id === this.itemType.id)]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent unnecessary rerendering so fields don't lose focus
|
||||
*/
|
||||
trackUpdate(index, update: FieldUpdate) {
|
||||
return update && update.field ? update.field.uuid : undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the dynamic lookup modal to search for items to add as relationships
|
||||
*/
|
||||
openLookup() {
|
||||
|
||||
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
|
||||
size: 'lg'
|
||||
});
|
||||
const modalComp: DsDynamicLookupRelationModalComponent = this.modalRef.componentInstance;
|
||||
modalComp.repeatable = true;
|
||||
modalComp.listId = this.listId;
|
||||
modalComp.item = this.item;
|
||||
modalComp.select = (...selectableObjects: SearchResult<Item>[]) => {
|
||||
selectableObjects.forEach((searchResult) => {
|
||||
const relatedItem: Item = searchResult.indexableObject;
|
||||
this.getFieldUpdatesForRelatedItem(relatedItem)
|
||||
.subscribe((identifiables) => {
|
||||
identifiables.forEach((identifiable) =>
|
||||
this.objectUpdatesService.removeSingleFieldUpdate(this.url, identifiable.uuid)
|
||||
);
|
||||
if (identifiables.length === 0) {
|
||||
this.relationshipService.getNameVariant(this.listId, relatedItem.uuid)
|
||||
.subscribe((nameVariant) => {
|
||||
const update = {
|
||||
uuid: this.relationshipType.id + '-' + relatedItem.uuid,
|
||||
nameVariant,
|
||||
type: this.relationshipType,
|
||||
relatedItem,
|
||||
} as RelationshipIdentifiable;
|
||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
modalComp.deselect = (...selectableObjects: SearchResult<Item>[]) => {
|
||||
selectableObjects.forEach((searchResult) => {
|
||||
const relatedItem: Item = searchResult.indexableObject;
|
||||
this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.relationshipType.id + '-' + relatedItem.uuid);
|
||||
this.getFieldUpdatesForRelatedItem(relatedItem)
|
||||
.subscribe((identifiables) =>
|
||||
identifiables.forEach((identifiable) =>
|
||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, identifiable)
|
||||
)
|
||||
);
|
||||
});
|
||||
};
|
||||
this.relatedEntityType$
|
||||
.pipe(take(1))
|
||||
.subscribe((relatedEntityType) => {
|
||||
modalComp.relationshipOptions = Object.assign(
|
||||
new RelationshipOptions(), {
|
||||
relationshipType: relatedEntityType.label,
|
||||
// filter: this.getRelationshipMessageKey(),
|
||||
searchConfiguration: relatedEntityType.label.toLowerCase(),
|
||||
nameVariants: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
this.selectableListService.deselectAll(this.listId);
|
||||
this.updates$.pipe(
|
||||
switchMap((updates) =>
|
||||
Object.values(updates).length > 0 ?
|
||||
observableCombineLatest(
|
||||
Object.values(updates)
|
||||
.filter((update) => update.changeType !== FieldChangeType.REMOVE)
|
||||
.map((update) => {
|
||||
const field = update.field as RelationshipIdentifiable;
|
||||
if (field.relationship) {
|
||||
return this.getRelatedItem(field.relationship);
|
||||
} else {
|
||||
return of(field.relatedItem);
|
||||
}
|
||||
})
|
||||
) : of([])
|
||||
),
|
||||
take(1),
|
||||
map((items) => items.map((item) => {
|
||||
const searchResult = new ItemSearchResult();
|
||||
searchResult.indexableObject = item;
|
||||
searchResult.hitHighlights = {};
|
||||
return searchResult;
|
||||
})),
|
||||
).subscribe((items) => {
|
||||
this.selectableListService.select(this.listId, items);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the existing field updates regarding a relationship with a given item
|
||||
* @param relatedItem The item for which to get the existing field updates
|
||||
*/
|
||||
private getFieldUpdatesForRelatedItem(relatedItem: Item): Observable<RelationshipIdentifiable[]> {
|
||||
|
||||
return this.updates$.pipe(
|
||||
take(1),
|
||||
map((updates) => Object.values(updates)
|
||||
.map((update) => update.field as RelationshipIdentifiable)
|
||||
.filter((field) => field.relationship)
|
||||
),
|
||||
mergeMap((identifiables) =>
|
||||
observableCombineLatest(
|
||||
identifiables.map((identifiable) => this.getRelatedItem(identifiable.relationship))
|
||||
).pipe(
|
||||
defaultIfEmpty([]),
|
||||
map((relatedItems) =>
|
||||
identifiables.filter((identifiable, index) => relatedItems[index].uuid === relatedItem.uuid)
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the related item for a given relationship
|
||||
* @param relationship The relationship for which to get the related item
|
||||
*/
|
||||
private getRelatedItem(relationship: Relationship): Observable<Item> {
|
||||
return this.relationshipService.isLeftItem(relationship, this.item).pipe(
|
||||
switchMap((isLeftItem) => isLeftItem ? relationship.rightItem : relationship.leftItem),
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
) as Observable<Item>;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
|
||||
this.relatedEntityType$ =
|
||||
observableCombineLatest([
|
||||
this.relationshipType.leftType,
|
||||
this.relationshipType.rightType,
|
||||
].map((type) => type.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
))).pipe(
|
||||
map((relatedTypes: ItemType[]) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)),
|
||||
hasValueOperator()
|
||||
);
|
||||
|
||||
this.relatedEntityType$.pipe(
|
||||
take(1)
|
||||
).subscribe(
|
||||
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`
|
||||
);
|
||||
|
||||
this.updates$ = this.getItemRelationships().pipe(
|
||||
switchMap((relationships) =>
|
||||
observableCombineLatest(
|
||||
relationships.map((relationship) => this.relationshipService.isLeftItem(relationship, this.item))
|
||||
).pipe(
|
||||
defaultIfEmpty([]),
|
||||
map((isLeftItemArray) => isLeftItemArray.map((isLeftItem, index) => {
|
||||
const relationship = relationships[index];
|
||||
const nameVariant = isLeftItem ? relationship.rightwardValue : relationship.leftwardValue;
|
||||
return {
|
||||
uuid: relationship.id,
|
||||
type: this.relationshipType,
|
||||
relationship,
|
||||
nameVariant,
|
||||
} as RelationshipIdentifiable;
|
||||
})),
|
||||
)),
|
||||
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields).pipe(
|
||||
map((fieldUpdates) => {
|
||||
const fieldUpdatesFiltered: FieldUpdates = {};
|
||||
Object.keys(fieldUpdates).forEach((uuid) => {
|
||||
if (hasValue(fieldUpdates[uuid])) {
|
||||
const field = fieldUpdates[uuid].field;
|
||||
if ((field as RelationshipIdentifiable).type.id === this.relationshipType.id) {
|
||||
fieldUpdatesFiltered[uuid] = fieldUpdates[uuid];
|
||||
}
|
||||
}
|
||||
});
|
||||
return fieldUpdatesFiltered;
|
||||
}),
|
||||
)),
|
||||
startWith({}),
|
||||
);
|
||||
}
|
||||
|
||||
private getItemRelationships() {
|
||||
this.linkService.resolveLink(this.item,
|
||||
followLink('relationships', undefined, true, true, true,
|
||||
followLink('relationshipType'),
|
||||
followLink('leftItem'),
|
||||
followLink('rightItem'),
|
||||
));
|
||||
return this.item.relationships.pipe(
|
||||
getAllSucceededRemoteData(),
|
||||
map((relationships: RemoteData<PaginatedList<Relationship>>) => relationships.payload.page.filter((relationship: Relationship) => hasValue(relationship))),
|
||||
switchMap((itemRelationships: Relationship[]) =>
|
||||
observableCombineLatest(
|
||||
itemRelationships
|
||||
.map((relationship) => relationship.relationshipType.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
))
|
||||
).pipe(
|
||||
defaultIfEmpty([]),
|
||||
map((relationshipTypes) => itemRelationships.filter(
|
||||
(relationship, index) => relationshipTypes[index].id === this.relationshipType.id)
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,6 +0,0 @@
|
||||
<div class="mt-4">
|
||||
<ds-alert [content]="'item.edit.tabs.versionhistory.under-construction'" [type]="AlertTypeEnum.Warning"></ds-alert>
|
||||
</div>
|
||||
<div class="mt-2" *ngVar="(itemRD$ | async)?.payload as item">
|
||||
<ds-item-versions *ngIf="item" [item]="item" [displayWhenEmpty]="true" [displayTitle]="false"></ds-item-versions>
|
||||
</div>
|
@@ -1,7 +0,0 @@
|
||||
<ds-metadata-field-wrapper *ngIf="(this.collectionsRD$ | async)?.hasSucceeded" [label]="label | translate">
|
||||
<div class="collections">
|
||||
<a *ngFor="let collection of (this.collectionsRD$ | async)?.payload?.page; let last=last;" [routerLink]="['/collections', collection.id]">
|
||||
<span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span>
|
||||
</a>
|
||||
</div>
|
||||
</ds-metadata-field-wrapper>
|
@@ -1,91 +0,0 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { CollectionsComponent } from './collections.component';
|
||||
|
||||
let collectionsComponent: CollectionsComponent;
|
||||
let fixture: ComponentFixture<CollectionsComponent>;
|
||||
|
||||
let collectionDataServiceStub;
|
||||
|
||||
const mockCollection1: Collection = Object.assign(new Collection(), {
|
||||
metadata: {
|
||||
'dc.description.abstract': [
|
||||
{
|
||||
language: 'en_US',
|
||||
value: 'Short description'
|
||||
}
|
||||
]
|
||||
},
|
||||
_links: {
|
||||
self: { href: 'collection-selflink' }
|
||||
}
|
||||
});
|
||||
|
||||
const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: createSuccessfulRemoteDataObject$(mockCollection1)});
|
||||
const failedMockItem: Item = Object.assign(new Item(), {owningCollection: createFailedRemoteDataObject$('error', 500)});
|
||||
|
||||
describe('CollectionsComponent', () => {
|
||||
collectionDataServiceStub = {
|
||||
findOwningCollectionFor(item: Item) {
|
||||
if (item === succeededMockItem) {
|
||||
return createSuccessfulRemoteDataObject$(mockCollection1);
|
||||
} else {
|
||||
return createFailedRemoteDataObject$('error', 500);
|
||||
}
|
||||
}
|
||||
};
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [ CollectionsComponent ],
|
||||
providers: [
|
||||
{ provide: RemoteDataBuildService, useValue: getMockRemoteDataBuildService()},
|
||||
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
||||
],
|
||||
|
||||
schemas: [ NO_ERRORS_SCHEMA ]
|
||||
}).overrideComponent(CollectionsComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(CollectionsComponent);
|
||||
collectionsComponent = fixture.componentInstance;
|
||||
collectionsComponent.label = 'test.test';
|
||||
collectionsComponent.separator = '<br/>';
|
||||
|
||||
}));
|
||||
|
||||
describe('When the requested item request has succeeded', () => {
|
||||
beforeEach(() => {
|
||||
collectionsComponent.item = succeededMockItem;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should show the collection', () => {
|
||||
const collectionField = fixture.debugElement.query(By.css('ds-metadata-field-wrapper div.collections'));
|
||||
expect(collectionField).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('When the requested item request has failed', () => {
|
||||
beforeEach(() => {
|
||||
collectionsComponent.item = failedMockItem;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should not show the collection', () => {
|
||||
const collectionField = fixture.debugElement.query(By.css('ds-metadata-field-wrapper div.collections'));
|
||||
expect(collectionField).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,68 +0,0 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* This component renders the parent collections section of the item
|
||||
* inside a 'ds-metadata-field-wrapper' component.
|
||||
*/
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-page-collections',
|
||||
templateUrl: './collections.component.html'
|
||||
})
|
||||
export class CollectionsComponent implements OnInit {
|
||||
|
||||
@Input() item: Item;
|
||||
|
||||
label = 'item.page.collections';
|
||||
|
||||
separator = '<br/>';
|
||||
|
||||
collectionsRD$: Observable<RemoteData<PaginatedList<Collection>>>;
|
||||
|
||||
constructor(private cds: CollectionDataService) {
|
||||
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
// this.collections = this.item.parents.payload;
|
||||
|
||||
// TODO: this should use parents, but the collections
|
||||
// for an Item aren't returned by the REST API yet,
|
||||
// only the owning collection
|
||||
this.collectionsRD$ = this.cds.findOwningCollectionFor(this.item).pipe(
|
||||
map((rd: RemoteData<Collection>) => {
|
||||
if (hasValue(rd.payload)) {
|
||||
return new RemoteData(
|
||||
rd.timeCompleted,
|
||||
rd.msToLive,
|
||||
rd.lastUpdated,
|
||||
rd.state,
|
||||
rd.errorMessage,
|
||||
buildPaginatedList({
|
||||
elementsPerPage: 10,
|
||||
totalPages: 1,
|
||||
currentPage: 1,
|
||||
totalElements: 1,
|
||||
_links: {
|
||||
self: rd.payload._links.self
|
||||
}
|
||||
} as PageInfo, [rd.payload]),
|
||||
rd.statusCode
|
||||
);
|
||||
} else {
|
||||
return rd as any;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,95 +0,0 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { waitForAsync, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@Component({
|
||||
selector: 'ds-component-without-content',
|
||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||
'</ds-metadata-field-wrapper>'
|
||||
})
|
||||
class NoContentComponent {}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-component-with-empty-spans',
|
||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||
' <span></span>\n' +
|
||||
' <span></span>\n' +
|
||||
'</ds-metadata-field-wrapper>'
|
||||
})
|
||||
class SpanContentComponent {}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-component-with-text',
|
||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||
' <span>The quick brown fox jumps over the lazy dog</span>\n' +
|
||||
'</ds-metadata-field-wrapper>'
|
||||
})
|
||||
class TextContentComponent {}
|
||||
|
||||
@Component({
|
||||
selector: 'ds-component-with-image',
|
||||
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||
' <img src="https://some/image.png" alt="an alt text">\n' +
|
||||
'</ds-metadata-field-wrapper>'
|
||||
})
|
||||
class ImgContentComponent {}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
describe('MetadataFieldWrapperComponent', () => {
|
||||
let component: MetadataFieldWrapperComponent;
|
||||
let fixture: ComponentFixture<MetadataFieldWrapperComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
declarations: [MetadataFieldWrapperComponent, NoContentComponent, SpanContentComponent, TextContentComponent, ImgContentComponent]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetadataFieldWrapperComponent);
|
||||
component = fixture.componentInstance;
|
||||
});
|
||||
|
||||
const wrapperSelector = '.simple-view-element';
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not show the component when there is no content', () => {
|
||||
const parentFixture = TestBed.createComponent(NoContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||
});
|
||||
|
||||
it('should not show the component when there is DOM content but not text or an image', () => {
|
||||
const parentFixture = TestBed.createComponent(SpanContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
|
||||
});
|
||||
|
||||
it('should show the component when there is text content', () => {
|
||||
const parentFixture = TestBed.createComponent(TextContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
parentFixture.detectChanges();
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||
});
|
||||
|
||||
it('should show the component when there is img content', () => {
|
||||
const parentFixture = TestBed.createComponent(ImgContentComponent);
|
||||
parentFixture.detectChanges();
|
||||
const parentNative = parentFixture.nativeElement;
|
||||
const nativeWrapper = parentNative.querySelector(wrapperSelector);
|
||||
parentFixture.detectChanges();
|
||||
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
|
||||
});
|
||||
|
||||
});
|
@@ -1,39 +0,0 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { environment } from '../../../../../environments/environment';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
||||
import { getItemPageRoute } from '../../../item-page-routing-paths';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item',
|
||||
template: ''
|
||||
})
|
||||
/**
|
||||
* A generic component for displaying metadata and relations of an item
|
||||
*/
|
||||
export class ItemComponent implements OnInit {
|
||||
@Input() object: Item;
|
||||
|
||||
/**
|
||||
* Route to the item page
|
||||
*/
|
||||
itemPageRoute: string;
|
||||
mediaViewer = environment.mediaViewer;
|
||||
|
||||
constructor(protected bitstreamDataService: BitstreamDataService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.itemPageRoute = getItemPageRoute(this.object);
|
||||
}
|
||||
|
||||
// TODO refactor to return RemoteData, and thumbnail template to deal with loading
|
||||
getThumbnail(): Observable<Bitstream> {
|
||||
return this.bitstreamDataService.getThumbnailFor(this.object).pipe(
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,112 +0,0 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../../../../core/cache/object-cache.service';
|
||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
||||
import { CommunityDataService } from '../../../../core/data/community-data.service';
|
||||
import { DefaultChangeAnalyzer } from '../../../../core/data/default-change-analyzer.service';
|
||||
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
||||
import { buildPaginatedList } from '../../../../core/data/paginated-list.model';
|
||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { MetadataMap } from '../../../../core/shared/metadata.models';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||
import { TruncatableService } from '../../../../shared/truncatable/truncatable.service';
|
||||
import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
|
||||
import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component';
|
||||
import { createRelationshipsObservable } from '../shared/item.component.spec';
|
||||
import { UntypedItemComponent } from './untyped-item.component';
|
||||
|
||||
const mockItem: Item = Object.assign(new Item(), {
|
||||
bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])),
|
||||
metadata: new MetadataMap(),
|
||||
relationships: createRelationshipsObservable()
|
||||
});
|
||||
|
||||
describe('UntypedItemComponent', () => {
|
||||
let comp: UntypedItemComponent;
|
||||
let fixture: ComponentFixture<UntypedItemComponent>;
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
const mockBitstreamDataService = {
|
||||
getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> {
|
||||
return createSuccessfulRemoteDataObject$(new Bitstream());
|
||||
}
|
||||
};
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
declarations: [UntypedItemComponent, GenericItemPageFieldComponent, TruncatePipe],
|
||||
providers: [
|
||||
{ provide: ItemDataService, useValue: {} },
|
||||
{ provide: TruncatableService, useValue: {} },
|
||||
{ provide: RelationshipService, useValue: {} },
|
||||
{ provide: ObjectCacheService, useValue: {} },
|
||||
{ provide: UUIDService, useValue: {} },
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: {} },
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
{ provide: HttpClient, useValue: {} },
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(UntypedItemComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(UntypedItemComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.object = mockItem;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
it('should contain a component to display the date', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-date-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the author', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-author-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the abstract', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-abstract-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the uri', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-uri-field'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('should contain a component to display the collections', () => {
|
||||
const fields = fixture.debugElement.queryAll(By.css('ds-item-page-collections'));
|
||||
expect(fields.length).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
});
|
@@ -1,22 +0,0 @@
|
||||
<ngb-tabset *ngIf="relationTypes.length > 1" [destroyOnHide]="true" #tabs="ngbTabset" [activeId]="activeTab$ | async" (tabChange)="onTabChange($event)">
|
||||
<ngb-tab *ngFor="let relationType of relationTypes" title="{{'item.page.relationships.' + relationType.label | translate}}" [id]="relationType.filter">
|
||||
<ng-template ngbTabContent>
|
||||
<div class="mt-4">
|
||||
<ds-related-entities-search [item]="item"
|
||||
[relationType]="relationType.filter"
|
||||
[configuration]="relationType.configuration"
|
||||
[searchEnabled]="searchEnabled"
|
||||
[sideBarWidth]="sideBarWidth">
|
||||
</ds-related-entities-search>
|
||||
</div>
|
||||
</ng-template>
|
||||
</ngb-tab>
|
||||
</ngb-tabset>
|
||||
<div *ngIf="relationTypes.length === 1" class="mt-4">
|
||||
<ds-related-entities-search *ngVar="relationTypes[0] as relationType" [item]="item"
|
||||
[relationType]="relationType.filter"
|
||||
[configuration]="relationType.configuration"
|
||||
[searchEnabled]="searchEnabled"
|
||||
[sideBarWidth]="sideBarWidth">
|
||||
</ds-related-entities-search>
|
||||
</div>
|
@@ -1 +0,0 @@
|
||||
@import '../+login-page/login-page.component.scss';
|
@@ -1,21 +0,0 @@
|
||||
<div class="parent mb-3">
|
||||
<div class="upload">
|
||||
<ds-uploader *ngIf="uploadFilesOptions.url !== ''"
|
||||
[uploadFilesOptions]="uploadFilesOptions"
|
||||
(onCompleteItem)="onCompleteItem($event)"
|
||||
(onUploadError)="onUploadError($event)"
|
||||
(onFileSelected)="afterFileLoaded($event)"></ds-uploader>
|
||||
|
||||
</div>
|
||||
<div class="add">
|
||||
<button class="btn btn-lg btn-primary mt-1 ml-2" (click)="openDialog()" attr.aria-label="'mydspace.new-submission' | translate" title="{{'mydspace.new-submission' | translate}}">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="add">
|
||||
<a class="btn btn-lg btn-outline-primary mt-1 ml-2" [routerLink]="['/import-external']" role="button" attr.aria-label="{{'mydspace.new-submission-external' | translate}}" title="{{'mydspace.new-submission-external' | translate}}">
|
||||
<i class="fa fa-file-import" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
</div>
|
@@ -1,52 +0,0 @@
|
||||
<div class="container">
|
||||
<ds-my-dspace-new-submission *dsShowOnlyForRole="[roleTypeEnum.Submitter]"></ds-my-dspace-new-submission>
|
||||
<div class="search-page row">
|
||||
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-3 sidebar-md-sticky"
|
||||
id="search-sidebar"
|
||||
[configurationList]="(configurationList$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
[viewModeList]="viewModeList"
|
||||
[refreshFilters]="refreshFilters.asObservable()"
|
||||
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
|
||||
<div class="col-12 col-md-9">
|
||||
<ds-search-form id="search-form"
|
||||
[query]="(searchOptions$ | async)?.query"
|
||||
[scope]="(searchOptions$ | async)?.scope"
|
||||
[currentUrl]="getSearchLink()"
|
||||
[scopes]="(scopeListRD$ | async)"
|
||||
[inPlaceSearch]="inPlaceSearch"
|
||||
[searchPlaceholder]="'mydspace.search-form.placeholder' | translate">
|
||||
</ds-search-form>
|
||||
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||
<div class="row">
|
||||
<div id="search-body"
|
||||
class="row-offcanvas row-offcanvas-left w-100"
|
||||
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
||||
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
|
||||
id="search-sidebar-sm"
|
||||
[configurationList]="(configurationList$ | async)"
|
||||
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
|
||||
(toggleSidebar)="closeSidebar()"
|
||||
[ngClass]="{'active': !(isSidebarCollapsed() | async)}"
|
||||
[refreshFilters]="refreshFilters.asObservable()"
|
||||
[inPlaceSearch]="inPlaceSearch">
|
||||
</ds-search-sidebar>
|
||||
<div id="search-content" class="col-12">
|
||||
<div class="d-block d-md-none search-controls clearfix">
|
||||
<ds-view-mode-switch [viewModeList]="viewModeList" [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
|
||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||
class="fas fa-sliders"></i> {{"search.sidebar.open"
|
||||
| translate}}
|
||||
</button>
|
||||
</div>
|
||||
<ds-my-dspace-results [searchResults]="resultsRD$ | async"
|
||||
[searchConfig]="searchOptions$ | async"
|
||||
[context]="context$ | async"
|
||||
(contentChange)="onResultsContentChange()"></ds-my-dspace-results>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1 +0,0 @@
|
||||
@import '../+search-page/search.component.scss';
|
@@ -1,191 +0,0 @@
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
|
||||
import { Store } from '@ngrx/store';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { MyDSpacePageComponent, SEARCH_CONFIG_SERVICE } from './my-dspace-page.component';
|
||||
import { RouteService } from '../core/services/route.service';
|
||||
import { routeServiceStub } from '../shared/testing/route-service.stub';
|
||||
import { SearchConfigurationServiceStub } from '../shared/testing/search-configuration-service.stub';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { SearchFilterService } from '../core/shared/search/search-filter.service';
|
||||
import { RoleDirective } from '../shared/roles/role.directive';
|
||||
import { RoleService } from '../core/roles/role.service';
|
||||
import { RoleServiceMock } from '../shared/mocks/role-service.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { SidebarServiceStub } from '../shared/testing/sidebar-service.stub';
|
||||
|
||||
describe('MyDSpacePageComponent', () => {
|
||||
let comp: MyDSpacePageComponent;
|
||||
let fixture: ComponentFixture<MyDSpacePageComponent>;
|
||||
let searchServiceObject: SearchService;
|
||||
let searchConfigurationServiceObject: SearchConfigurationService;
|
||||
const store: Store<MyDSpacePageComponent> = jasmine.createSpyObj('store', {
|
||||
/* tslint:disable:no-empty */
|
||||
dispatch: {},
|
||||
/* tslint:enable:no-empty */
|
||||
select: observableOf(true)
|
||||
});
|
||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||
pagination.id = 'mydspace-results-pagination';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
|
||||
const mockResults = createSuccessfulRemoteDataObject$(['test', 'data']);
|
||||
const searchServiceStub = jasmine.createSpyObj('SearchService', {
|
||||
search: mockResults,
|
||||
getEndpoint: observableOf('discover/search/objects'),
|
||||
getSearchLink: '/mydspace',
|
||||
getScopes: observableOf(['test-scope']),
|
||||
setServiceOptions: {}
|
||||
});
|
||||
const configurationParam = 'default';
|
||||
const queryParam = 'test query';
|
||||
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||
const paginatedSearchOptions = new PaginatedSearchOptions({
|
||||
configuration: configurationParam,
|
||||
query: queryParam,
|
||||
scope: scopeParam,
|
||||
pagination,
|
||||
sort
|
||||
});
|
||||
const activatedRouteStub = {
|
||||
snapshot: {
|
||||
queryParamMap: new Map([
|
||||
['query', queryParam],
|
||||
['scope', scopeParam]
|
||||
])
|
||||
},
|
||||
queryParams: observableOf({
|
||||
query: queryParam,
|
||||
scope: scopeParam
|
||||
})
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, NgbCollapseModule],
|
||||
declarations: [MyDSpacePageComponent, RoleDirective],
|
||||
providers: [
|
||||
{ provide: SearchService, useValue: searchServiceStub },
|
||||
{
|
||||
provide: CommunityDataService,
|
||||
useValue: jasmine.createSpyObj('communityService', ['findById', 'findAll'])
|
||||
},
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{
|
||||
provide: Store, useValue: store
|
||||
},
|
||||
{
|
||||
provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService',
|
||||
{
|
||||
isXs: observableOf(true),
|
||||
isSm: observableOf(false),
|
||||
isXsOrSm: observableOf(true)
|
||||
})
|
||||
},
|
||||
{
|
||||
provide: SidebarService,
|
||||
useValue: SidebarServiceStub
|
||||
},
|
||||
{
|
||||
provide: SearchFilterService,
|
||||
useValue: {}
|
||||
}, {
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useValue: new SearchConfigurationServiceStub()
|
||||
},
|
||||
{
|
||||
provide: RoleService,
|
||||
useValue: new RoleServiceMock()
|
||||
},
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(MyDSpacePageComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MyDSpacePageComponent);
|
||||
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||
fixture.detectChanges();
|
||||
searchServiceObject = (comp as any).service;
|
||||
searchConfigurationServiceObject = (comp as any).searchConfigService;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
comp = null;
|
||||
searchServiceObject = null;
|
||||
searchConfigurationServiceObject = null;
|
||||
});
|
||||
|
||||
it('should get the scope and query from the route parameters', () => {
|
||||
|
||||
searchConfigurationServiceObject.paginatedSearchOptions.next(paginatedSearchOptions);
|
||||
expect(comp.searchOptions$).toBeObservable(cold('b', {
|
||||
b: paginatedSearchOptions
|
||||
}));
|
||||
|
||||
});
|
||||
|
||||
describe('when the open sidebar button is clicked in mobile view', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(comp, 'openSidebar');
|
||||
const openSidebarButton = fixture.debugElement.query(By.css('.open-sidebar'));
|
||||
openSidebarButton.triggerEventHandler('click', null);
|
||||
});
|
||||
|
||||
it('should trigger the openSidebar function', () => {
|
||||
expect(comp.openSidebar).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is true in mobile view', () => {
|
||||
let menu: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||
comp.isSidebarCollapsed = () => observableOf(true);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should close the sidebar', () => {
|
||||
expect(menu.classList).not.toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
describe('when sidebarCollapsed is false in mobile view', () => {
|
||||
let menu: HTMLElement;
|
||||
|
||||
beforeEach(() => {
|
||||
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||
comp.isSidebarCollapsed = () => observableOf(false);
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should open the menu', () => {
|
||||
expect(menu.classList).toContain('active');
|
||||
});
|
||||
|
||||
});
|
||||
});
|
@@ -1,202 +0,0 @@
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
Component,
|
||||
Inject,
|
||||
InjectionToken,
|
||||
Input,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
|
||||
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
|
||||
import { map, switchMap, tap, } from 'rxjs/operators';
|
||||
|
||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||
import { pushInOut } from '../shared/animations/push';
|
||||
import { HostWindowService } from '../shared/host-window.service';
|
||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
||||
import { hasValue } from '../shared/empty.util';
|
||||
import { getFirstSucceededRemoteData } from '../core/shared/operators';
|
||||
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
|
||||
import { SearchConfigurationOption } from '../shared/search/search-switch-configuration/search-configuration-option.model';
|
||||
import { RoleType } from '../core/roles/role-types';
|
||||
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
|
||||
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||
import { ViewMode } from '../core/shared/view-mode.model';
|
||||
import { MyDSpaceRequest } from '../core/data/request.models';
|
||||
import { SearchResult } from '../shared/search/search-result.model';
|
||||
import { Context } from '../core/shared/context.model';
|
||||
|
||||
export const MYDSPACE_ROUTE = '/mydspace';
|
||||
export const SEARCH_CONFIG_SERVICE: InjectionToken<SearchConfigurationService> = new InjectionToken<SearchConfigurationService>('searchConfigurationService');
|
||||
|
||||
/**
|
||||
* This component represents the whole mydspace page
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-my-dspace-page',
|
||||
styleUrls: ['./my-dspace-page.component.scss'],
|
||||
templateUrl: './my-dspace-page.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
animations: [pushInOut],
|
||||
providers: [
|
||||
{
|
||||
provide: SEARCH_CONFIG_SERVICE,
|
||||
useClass: MyDSpaceConfigurationService
|
||||
}
|
||||
]
|
||||
})
|
||||
export class MyDSpacePageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* True when the search component should show results on the current page
|
||||
*/
|
||||
@Input() inPlaceSearch = true;
|
||||
|
||||
/**
|
||||
* The list of available configuration options
|
||||
*/
|
||||
configurationList$: Observable<SearchConfigurationOption[]>;
|
||||
|
||||
/**
|
||||
* The current search results
|
||||
*/
|
||||
resultsRD$: BehaviorSubject<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> = new BehaviorSubject(null);
|
||||
|
||||
/**
|
||||
* The current paginated search options
|
||||
*/
|
||||
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||
|
||||
/**
|
||||
* The current relevant scopes
|
||||
*/
|
||||
scopeListRD$: Observable<DSpaceObject[]>;
|
||||
|
||||
/**
|
||||
* Emits true if were on a small screen
|
||||
*/
|
||||
isXsOrSm$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Subscription to unsubscribe from
|
||||
*/
|
||||
sub: Subscription;
|
||||
|
||||
/**
|
||||
* Variable for enumeration RoleType
|
||||
*/
|
||||
roleTypeEnum = RoleType;
|
||||
|
||||
/**
|
||||
* List of available view mode
|
||||
*/
|
||||
viewModeList = [ViewMode.ListElement, ViewMode.DetailedListElement];
|
||||
|
||||
/**
|
||||
* The current context of this page: workspace or workflow
|
||||
*/
|
||||
context$: Observable<Context>;
|
||||
|
||||
/**
|
||||
* Emit an event every time search sidebars must refresh their contents.
|
||||
*/
|
||||
refreshFilters: Subject<any> = new Subject<any>();
|
||||
|
||||
constructor(private service: SearchService,
|
||||
private sidebarService: SidebarService,
|
||||
private windowService: HostWindowService,
|
||||
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
|
||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
||||
this.service.setServiceOptions(MyDSpaceResponseParsingService, MyDSpaceRequest);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize available configuration list
|
||||
*
|
||||
* Listening to changes in the paginated search options
|
||||
* If something changes, update the search results
|
||||
*
|
||||
* Listen to changes in the scope
|
||||
* If something changes, update the list of scopes for the dropdown
|
||||
*
|
||||
* Listen to changes in the configuration
|
||||
* If something changes, update the current context
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.configurationList$ = this.searchConfigService.getAvailableConfigurationOptions();
|
||||
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||
this.sub = this.searchOptions$.pipe(
|
||||
tap(() => this.resultsRD$.next(null)),
|
||||
switchMap((options: PaginatedSearchOptions) => this.service.search(options).pipe(getFirstSucceededRemoteData())))
|
||||
.subscribe((results) => {
|
||||
this.resultsRD$.next(results);
|
||||
});
|
||||
|
||||
this.scopeListRD$ = this.searchConfigService.getCurrentScope('').pipe(
|
||||
switchMap((scopeId) => this.service.getScopes(scopeId))
|
||||
);
|
||||
|
||||
this.context$ = this.searchConfigService.getCurrentConfiguration('workspace')
|
||||
.pipe(
|
||||
map((configuration: string) => {
|
||||
if (configuration === 'workspace') {
|
||||
return Context.Workspace;
|
||||
} else {
|
||||
return Context.Workflow;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the contentChange event from within the my dspace content.
|
||||
* Notify search sidebars to refresh their content.
|
||||
*/
|
||||
onResultsContentChange() {
|
||||
this.refreshFilters.next();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to a collapsed state
|
||||
*/
|
||||
public closeSidebar(): void {
|
||||
this.sidebarService.collapse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the sidebar to an expanded state
|
||||
*/
|
||||
public openSidebar(): void {
|
||||
this.sidebarService.expand();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sidebar is collapsed
|
||||
* @returns {Observable<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
|
||||
*/
|
||||
public isSidebarCollapsed(): Observable<boolean> {
|
||||
return this.sidebarService.isCollapsed;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns {string} The base path to the search page
|
||||
*/
|
||||
public getSearchLink(): string {
|
||||
return this.service.getSearchLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Unsubscribe from the subscription
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
if (hasValue(this.sub)) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
this.refreshFilters.complete();
|
||||
}
|
||||
}
|
@@ -1,14 +0,0 @@
|
||||
<div *ngIf="searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0" @fadeIn>
|
||||
<ds-viewable-collection
|
||||
[config]="searchConfig.pagination"
|
||||
[hasBorder]="hasBorder"
|
||||
[sortConfig]="searchConfig.sort"
|
||||
[objects]="searchResults"
|
||||
[hideGear]="true"
|
||||
[context]="context"
|
||||
(contentChange)="contentChange.emit()">
|
||||
</ds-viewable-collection>
|
||||
</div>
|
||||
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>
|
||||
<ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.errorMessage || searchResults?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error>
|
||||
<h3 *ngIf="searchResults?.payload?.page.length == 0" class="text-center text-muted" ><span>{{'mydspace.results.no-results' | translate}}</span></h3>
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user