mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Compare commits
891 Commits
dspace-7.0
...
dspace-7.0
Author | SHA1 | Date | |
---|---|---|---|
![]() |
d9634a84ae | ||
![]() |
5bf6d0f287 | ||
![]() |
d843230158 | ||
![]() |
bb935be0b6 | ||
![]() |
fbe6ec61de | ||
![]() |
03bba0f578 | ||
![]() |
098eb291b6 | ||
![]() |
4f16f21a7d | ||
![]() |
6511920d0c | ||
![]() |
b4120f2053 | ||
![]() |
2621454cdb | ||
![]() |
5615390ce5 | ||
![]() |
3e191bb56e | ||
![]() |
e12559544b | ||
![]() |
24c07536b0 | ||
![]() |
24706f0cc4 | ||
![]() |
764cfc5d95 | ||
![]() |
c67d2e6718 | ||
![]() |
8a07b3bb2b | ||
![]() |
f6fea087c3 | ||
![]() |
b6ff9369e4 | ||
![]() |
15de3a2eeb | ||
![]() |
6e6253c2d8 | ||
![]() |
6ff9429bc0 | ||
![]() |
aa84a56c2b | ||
![]() |
e7e408dacb | ||
![]() |
b7382de29a | ||
![]() |
1ef3d40904 | ||
![]() |
1fba45a08b | ||
![]() |
456551fd94 | ||
![]() |
a0414e11bc | ||
![]() |
3de2b50d10 | ||
![]() |
ddc1cbbd73 | ||
![]() |
cfadb4314e | ||
![]() |
dfbbd98862 | ||
![]() |
047fbfee1f | ||
![]() |
6960597381 | ||
![]() |
124ed4aff2 | ||
![]() |
dd7f34e541 | ||
![]() |
5afd4a6930 | ||
![]() |
f8dab74b64 | ||
![]() |
2d2fb0210c | ||
![]() |
05f3b7e7c6 | ||
![]() |
9886ba8dc9 | ||
![]() |
8e04dff09d | ||
![]() |
9757e6651e | ||
![]() |
47edd9b6d8 | ||
![]() |
f96549d5c7 | ||
![]() |
9c01c1b3f3 | ||
![]() |
1b10ec63d6 | ||
![]() |
9f396f7120 | ||
![]() |
54e61e00ce | ||
![]() |
11b62daa44 | ||
![]() |
4e10db8a53 | ||
![]() |
b68fe23310 | ||
![]() |
d5927a76fc | ||
![]() |
b77dce25ce | ||
![]() |
ade3bffcb7 | ||
![]() |
4214ddec8c | ||
![]() |
5db42a6da8 | ||
![]() |
75058a735a | ||
![]() |
8c4f5002f5 | ||
![]() |
2e8a78151e | ||
![]() |
5e21c5fd26 | ||
![]() |
fc35c9fd67 | ||
![]() |
e53548a95a | ||
![]() |
eee57f5b41 | ||
![]() |
1647c95600 | ||
![]() |
abab73b909 | ||
![]() |
7eb88f9fda | ||
![]() |
0be3c61eeb | ||
![]() |
e7ef068352 | ||
![]() |
c5cb1a2837 | ||
![]() |
a178e215f0 | ||
![]() |
fab0a09727 | ||
![]() |
1e8669a738 | ||
![]() |
a6dd87f78c | ||
![]() |
cced7a2914 | ||
![]() |
3f4a0d1fd9 | ||
![]() |
4b11e1d9cd | ||
![]() |
b82e840935 | ||
![]() |
c5e975601a | ||
![]() |
99b84223e2 | ||
![]() |
d4027cc1d1 | ||
![]() |
90f0597baa | ||
![]() |
48ffb2105f | ||
![]() |
d7f6885232 | ||
![]() |
8d29090f24 | ||
![]() |
c19f49fb96 | ||
![]() |
19ca179e7a | ||
![]() |
f6e8448164 | ||
![]() |
b3b934033c | ||
![]() |
3e1fb243a1 | ||
![]() |
33aeccdd43 | ||
![]() |
0b3b82b485 | ||
![]() |
472154c0df | ||
![]() |
de9f642bf9 | ||
![]() |
f0940aad57 | ||
![]() |
4141ae7ad7 | ||
![]() |
b89c21ec3b | ||
![]() |
ac86976115 | ||
![]() |
6feb344aa0 | ||
![]() |
b58defc0f3 | ||
![]() |
18fdf7585e | ||
![]() |
aa1b568bf4 | ||
![]() |
dd037459cf | ||
![]() |
7997100a7e | ||
![]() |
5965f4e929 | ||
![]() |
1e82943f4a | ||
![]() |
dcbb002630 | ||
![]() |
810f009582 | ||
![]() |
b602a736de | ||
![]() |
10b314cf20 | ||
![]() |
1f4ae7a035 | ||
![]() |
2ebb1640b4 | ||
![]() |
4a0444d669 | ||
![]() |
a6c0a60bd7 | ||
![]() |
77b2506112 | ||
![]() |
bd522038a1 | ||
![]() |
eda1a34da5 | ||
![]() |
a0759098ea | ||
![]() |
a76e0796c6 | ||
![]() |
99c0f6ceaa | ||
![]() |
59def51ebe | ||
![]() |
5e970bcff2 | ||
![]() |
3d08eb0ae1 | ||
![]() |
e9a2b4b368 | ||
![]() |
b8138ee3b9 | ||
![]() |
b15113ad96 | ||
![]() |
a504b7cbd3 | ||
![]() |
306d6d15c5 | ||
![]() |
7d22bcc59c | ||
![]() |
94119668dd | ||
![]() |
5ebd4fcbc8 | ||
![]() |
cd6c5b70c8 | ||
![]() |
16a68dae42 | ||
![]() |
be79663df9 | ||
![]() |
17249e9541 | ||
![]() |
f4946003e2 | ||
![]() |
f167d5a629 | ||
![]() |
724e5d1f12 | ||
![]() |
bc0be9e235 | ||
![]() |
f222cd8b33 | ||
![]() |
a63bfdf2fd | ||
![]() |
43ae3fc952 | ||
![]() |
cb3ef1dde4 | ||
![]() |
bbc7844b5d | ||
![]() |
76b9fd4702 | ||
![]() |
61e0b9efb0 | ||
![]() |
21e8879f77 | ||
![]() |
beca98b441 | ||
![]() |
32cf92eba9 | ||
![]() |
81fc26b844 | ||
![]() |
a3df6ce474 | ||
![]() |
55c45f5f6c | ||
![]() |
e9d77c4d2c | ||
![]() |
e89d064934 | ||
![]() |
92207cf66d | ||
![]() |
7fbae8997d | ||
![]() |
4080cce8fd | ||
![]() |
ee2f11f602 | ||
![]() |
87bed6d288 | ||
![]() |
3178e5578c | ||
![]() |
67bd19a340 | ||
![]() |
ab190e381d | ||
![]() |
7367f91176 | ||
![]() |
4b35828bbe | ||
![]() |
94b4115ecd | ||
![]() |
cc618ebadd | ||
![]() |
547ade276c | ||
![]() |
de1e1a70d1 | ||
![]() |
ecf75efe99 | ||
![]() |
d46355e274 | ||
![]() |
708b5495c7 | ||
![]() |
0fc757ba4d | ||
![]() |
8768645e4a | ||
![]() |
6b90ac073c | ||
![]() |
1247088df9 | ||
![]() |
a38d365bab | ||
![]() |
18fb47c9b6 | ||
![]() |
cfd819cd62 | ||
![]() |
3f8a670dcf | ||
![]() |
0a06368340 | ||
![]() |
a2fa51cef5 | ||
![]() |
66cabcb342 | ||
![]() |
8f822ba4f7 | ||
![]() |
05a92f8e5b | ||
![]() |
82d780b6c2 | ||
![]() |
cb7335208f | ||
![]() |
0553f68607 | ||
![]() |
25d912b006 | ||
![]() |
f6f87a5738 | ||
![]() |
f1db0c044b | ||
![]() |
9c83cd9b53 | ||
![]() |
4f608eb3cf | ||
![]() |
42978931de | ||
![]() |
261f79daab | ||
![]() |
d560e08b20 | ||
![]() |
8bfccaa624 | ||
![]() |
c2fd019ebd | ||
![]() |
a29de15f71 | ||
![]() |
6422288207 | ||
![]() |
73b5a376b3 | ||
![]() |
4a30e5d7d5 | ||
![]() |
9b69aaaa07 | ||
![]() |
e06ff5057a | ||
![]() |
4338250d8d | ||
![]() |
26905860ba | ||
![]() |
3e0f4a54e6 | ||
![]() |
66cdf9dd18 | ||
![]() |
e160d46b81 | ||
![]() |
9e095d09b6 | ||
![]() |
930512efaa | ||
![]() |
393bdf0786 | ||
![]() |
9ae68fee32 | ||
![]() |
2898cbac6d | ||
![]() |
d295fa422f | ||
![]() |
8f28e989a1 | ||
![]() |
311d93efad | ||
![]() |
2f946ba2a3 | ||
![]() |
d2523ade9b | ||
![]() |
902d4c2330 | ||
![]() |
793698a6d8 | ||
![]() |
eb9809801f | ||
![]() |
34da1dc055 | ||
![]() |
0582bc5622 | ||
![]() |
79db49043f | ||
![]() |
2ed144bd1c | ||
![]() |
c9519f44fb | ||
![]() |
9ced3530ab | ||
![]() |
090fb94c4d | ||
![]() |
85506238b3 | ||
![]() |
1a9d20161c | ||
![]() |
7dae82cbbd | ||
![]() |
7e7ce7b06e | ||
![]() |
ce5247253c | ||
![]() |
90f68eed7e | ||
![]() |
76ad72f536 | ||
![]() |
23354b45c1 | ||
![]() |
adb51bc06c | ||
![]() |
8b639bc7ba | ||
![]() |
4779dd209e | ||
![]() |
3f77ffa1e6 | ||
![]() |
2529c91868 | ||
![]() |
1a46031e4d | ||
![]() |
a25ab5620f | ||
![]() |
fb1b6c7a7a | ||
![]() |
d88a863a0d | ||
![]() |
47ab023e7a | ||
![]() |
095d5f3299 | ||
![]() |
725481687d | ||
![]() |
3e8ff05d2d | ||
![]() |
2162153ae9 | ||
![]() |
091ec6c917 | ||
![]() |
c8c7e92717 | ||
![]() |
cb11e3c702 | ||
![]() |
6fecdfadb2 | ||
![]() |
9d3f58770a | ||
![]() |
ee8ad1293f | ||
![]() |
a68428cc12 | ||
![]() |
d467af4a35 | ||
![]() |
b3e90cbce9 | ||
![]() |
84451b1f6f | ||
![]() |
cf2759f6ca | ||
![]() |
607318f6c5 | ||
![]() |
5950fb2e32 | ||
![]() |
efb2922bb3 | ||
![]() |
c48b817c2d | ||
![]() |
1c31deb3df | ||
![]() |
9225819046 | ||
![]() |
57f436e4ff | ||
![]() |
b8abedb5d6 | ||
![]() |
dda2668109 | ||
![]() |
c7ef818454 | ||
![]() |
c33a63aa90 | ||
![]() |
a283476403 | ||
![]() |
130a58870e | ||
![]() |
a38df3811b | ||
![]() |
7ff250fb0b | ||
![]() |
31e97eec9c | ||
![]() |
2959b73632 | ||
![]() |
5088da484e | ||
![]() |
eeec39bf3f | ||
![]() |
8d399fdde8 | ||
![]() |
23001b6d26 | ||
![]() |
47e735270b | ||
![]() |
b8a5edd2f3 | ||
![]() |
522ad4b3dc | ||
![]() |
0da19b7c75 | ||
![]() |
1d7a9cd78b | ||
![]() |
d558a056aa | ||
![]() |
1bfc12fe9b | ||
![]() |
08484d31ca | ||
![]() |
3afe8c0b1d | ||
![]() |
35ca785695 | ||
![]() |
e5742c49d9 | ||
![]() |
4648348bd1 | ||
![]() |
e94fbfdcd0 | ||
![]() |
6cd6c096e3 | ||
![]() |
e053607a17 | ||
![]() |
b05420ea88 | ||
![]() |
0fa1e17078 | ||
![]() |
7f6bd680b2 | ||
![]() |
7787a2d0e9 | ||
![]() |
02fb4a4e4e | ||
![]() |
9a666731e6 | ||
![]() |
73370fa00d | ||
![]() |
5784493279 | ||
![]() |
95163aa226 | ||
![]() |
3560bc60aa | ||
![]() |
56d8a99c51 | ||
![]() |
5a1b82d93c | ||
![]() |
52289d8d23 | ||
![]() |
797a640814 | ||
![]() |
deabffd429 | ||
![]() |
a74dafb959 | ||
![]() |
d85b0cb1b1 | ||
![]() |
bfeeb7c13e | ||
![]() |
5650a4a235 | ||
![]() |
a38e5cf632 | ||
![]() |
7c36051fcc | ||
![]() |
aa265c02a5 | ||
![]() |
9ec5709410 | ||
![]() |
633dfaaa6d | ||
![]() |
d6a5d9d1dd | ||
![]() |
58f18737f7 | ||
![]() |
2da96aac25 | ||
![]() |
9015d50f59 | ||
![]() |
bd9a6c487f | ||
![]() |
b731301b39 | ||
![]() |
d6ab8eff45 | ||
![]() |
fe785f6d2f | ||
![]() |
0eb7eb6962 | ||
![]() |
3fb4be27df | ||
![]() |
2238599a7d | ||
![]() |
4fa8d02fb2 | ||
![]() |
a68dfc1c01 | ||
![]() |
a414f04e96 | ||
![]() |
c2298f6fee | ||
![]() |
e46ee579a3 | ||
![]() |
cece770e8f | ||
![]() |
8c5347ab0f | ||
![]() |
d562b19d57 | ||
![]() |
599ddf4653 | ||
![]() |
ba3c8d2b9e | ||
![]() |
3f0ab2cf13 | ||
![]() |
2ad62dd3e9 | ||
![]() |
5e191ff4d1 | ||
![]() |
531ef1fb1b | ||
![]() |
f655f5d149 | ||
![]() |
92e47dee07 | ||
![]() |
30d53c5954 | ||
![]() |
e36cb83622 | ||
![]() |
e8237f196f | ||
![]() |
b77fd69ac7 | ||
![]() |
e234c3a44d | ||
![]() |
6d802f5078 | ||
![]() |
e9993c8e16 | ||
![]() |
4370f197ec | ||
![]() |
44381d7653 | ||
![]() |
2fe9965411 | ||
![]() |
7cfa0f17a5 | ||
![]() |
df5d55c246 | ||
![]() |
68ecc7ac31 | ||
![]() |
9bbdf95bee | ||
![]() |
c36877ae3a | ||
![]() |
2cffdc8a86 | ||
![]() |
0e1874d269 | ||
![]() |
71ff276079 | ||
![]() |
bcd93de708 | ||
![]() |
09f1b7e01f | ||
![]() |
1d0f0b8304 | ||
![]() |
7e80bcf9e9 | ||
![]() |
ca42e2280c | ||
![]() |
81f0391400 | ||
![]() |
e0604026c5 | ||
![]() |
ce61addc9b | ||
![]() |
e0990aeb4b | ||
![]() |
71da279d2f | ||
![]() |
5884308037 | ||
![]() |
9a4f962a21 | ||
![]() |
9f91fd2ad7 | ||
![]() |
150d2cbb71 | ||
![]() |
d35f2a419e | ||
![]() |
5b46ec1570 | ||
![]() |
199d4bf38f | ||
![]() |
ea28998bc1 | ||
![]() |
722fdc0124 | ||
![]() |
a8b7035aff | ||
![]() |
0348d23aac | ||
![]() |
09665dfbe2 | ||
![]() |
c7994f9d90 | ||
![]() |
6ef3f685ca | ||
![]() |
f4387ac212 | ||
![]() |
3e6195a124 | ||
![]() |
f992fe1afd | ||
![]() |
6433e211a8 | ||
![]() |
94e3f2d5e0 | ||
![]() |
53779cf69e | ||
![]() |
1dfe7ec170 | ||
![]() |
3f39752550 | ||
![]() |
b1e44a7fa5 | ||
![]() |
6ef60351a5 | ||
![]() |
99a6bf52ff | ||
![]() |
31eadf9c94 | ||
![]() |
d611761548 | ||
![]() |
fc1ffd0e18 | ||
![]() |
ea16289013 | ||
![]() |
3225966600 | ||
![]() |
4cc1a3eecd | ||
![]() |
1cb0bb72e4 | ||
![]() |
df26b85c50 | ||
![]() |
bb4aedc472 | ||
![]() |
066e6cd142 | ||
![]() |
e119967325 | ||
![]() |
775becd656 | ||
![]() |
1a0e5f4098 | ||
![]() |
2863837bf8 | ||
![]() |
746a72392f | ||
![]() |
bcb1682e1d | ||
![]() |
5b82a8a07b | ||
![]() |
ca564b2ad1 | ||
![]() |
0888d81310 | ||
![]() |
fd59e02786 | ||
![]() |
02ddf7c8c5 | ||
![]() |
806843640f | ||
![]() |
8df505b4a0 | ||
![]() |
32c8d9de03 | ||
![]() |
0ca1798537 | ||
![]() |
9677e053f5 | ||
![]() |
0c56e0182a | ||
![]() |
6ad7cee7d1 | ||
![]() |
e094663a77 | ||
![]() |
9c27be085e | ||
![]() |
573968adc0 | ||
![]() |
5642a2798a | ||
![]() |
e6b0c20377 | ||
![]() |
f37de548c0 | ||
![]() |
97b4b90585 | ||
![]() |
59564eb44d | ||
![]() |
f24bd9fe36 | ||
![]() |
bfdd943d45 | ||
![]() |
bb705919f3 | ||
![]() |
9ada61db28 | ||
![]() |
f71fd5737a | ||
![]() |
63cca76b49 | ||
![]() |
73c25998e3 | ||
![]() |
3117916d5b | ||
![]() |
fcdd1a8ef2 | ||
![]() |
882ff3ea18 | ||
![]() |
ba46f243c8 | ||
![]() |
080ddf8a1f | ||
![]() |
78a5bd5fce | ||
![]() |
0cef62ff48 | ||
![]() |
c7cf11f421 | ||
![]() |
c3192047e1 | ||
![]() |
c25085b299 | ||
![]() |
ea5f727449 | ||
![]() |
74d1e0bccb | ||
![]() |
0f4e881717 | ||
![]() |
1f54094d5a | ||
![]() |
0b858805f2 | ||
![]() |
43f5e08530 | ||
![]() |
1156bd3934 | ||
![]() |
e1b80bcbaf | ||
![]() |
c2c650fa1a | ||
![]() |
0e29fbc2ba | ||
![]() |
b4ec056d03 | ||
![]() |
2f04b6ae83 | ||
![]() |
e7ef9dab20 | ||
![]() |
d66d5b6a46 | ||
![]() |
a49607ed61 | ||
![]() |
24858fceab | ||
![]() |
a281583f6e | ||
![]() |
05b2489da5 | ||
![]() |
b578aa408b | ||
![]() |
bbaaaed4b5 | ||
![]() |
72381f4083 | ||
![]() |
2653980beb | ||
![]() |
4c1b589158 | ||
![]() |
5cef15eb20 | ||
![]() |
63cff331d2 | ||
![]() |
89afaaa4a5 | ||
![]() |
bbbeddc875 | ||
![]() |
fb40b8f031 | ||
![]() |
f841e45019 | ||
![]() |
e963aa9af0 | ||
![]() |
8350833b61 | ||
![]() |
0721b844f5 | ||
![]() |
28891211e4 | ||
![]() |
82a3014af4 | ||
![]() |
709726e041 | ||
![]() |
136e36afab | ||
![]() |
baec130cf3 | ||
![]() |
f33f391eb3 | ||
![]() |
ec998fce30 | ||
![]() |
be2d496335 | ||
![]() |
3b581b93c5 | ||
![]() |
39e0c03593 | ||
![]() |
94cd5b5767 | ||
![]() |
752cf97787 | ||
![]() |
294f5e5f31 | ||
![]() |
efc476ab31 | ||
![]() |
f5959e9857 | ||
![]() |
e277a72ebf | ||
![]() |
5f0f665501 | ||
![]() |
be16134329 | ||
![]() |
9e05072290 | ||
![]() |
c6ee46fdea | ||
![]() |
16d3d0e063 | ||
![]() |
6f9f4ec968 | ||
![]() |
b8ab83ce99 | ||
![]() |
7421eef223 | ||
![]() |
470c21d8d8 | ||
![]() |
3db47f5791 | ||
![]() |
8b8d7ba91a | ||
![]() |
ee9f5ec7f1 | ||
![]() |
c835d9b9c7 | ||
![]() |
6bd04c94ec | ||
![]() |
6c1e636f5c | ||
![]() |
405815ac5a | ||
![]() |
d27034d634 | ||
![]() |
9a4fc65331 | ||
![]() |
ea26597b6a | ||
![]() |
31b6bfc332 | ||
![]() |
035a4b6e16 | ||
![]() |
9f64fedc7b | ||
![]() |
ce04825c4a | ||
![]() |
ca7a76b80f | ||
![]() |
a51a683215 | ||
![]() |
44386823a3 | ||
![]() |
b79a3d37f3 | ||
![]() |
f8f0e5f149 | ||
![]() |
bcb77c8dcc | ||
![]() |
697be3c610 | ||
![]() |
9b7a33cc69 | ||
![]() |
3ece498b81 | ||
![]() |
b1d39feeda | ||
![]() |
ba9996a3b0 | ||
![]() |
fcb610412d | ||
![]() |
23e8c8b449 | ||
![]() |
c423b474ab | ||
![]() |
7b4a481f43 | ||
![]() |
ce6b259a3a | ||
![]() |
db5ade547d | ||
![]() |
2fbf3afd4a | ||
![]() |
45247ff07c | ||
![]() |
d869c6282a | ||
![]() |
d9749c80c4 | ||
![]() |
acd6e6079e | ||
![]() |
4ae2977ef2 | ||
![]() |
56ee4bbaa0 | ||
![]() |
1295e127fd | ||
![]() |
d24c51e1cd | ||
![]() |
7227d5eefd | ||
![]() |
f0bf87b7f9 | ||
![]() |
36acb7578f | ||
![]() |
d483d46370 | ||
![]() |
8fc2309d48 | ||
![]() |
bca1e8bdd4 | ||
![]() |
96d76073ef | ||
![]() |
f136ea7d4f | ||
![]() |
5421d97bc5 | ||
![]() |
a57d5d7c4f | ||
![]() |
a9cb6aeaa6 | ||
![]() |
f881c2f428 | ||
![]() |
e7043f8734 | ||
![]() |
e4a83f0704 | ||
![]() |
780b8b7db9 | ||
![]() |
92b1a4477d | ||
![]() |
7b64d6bc31 | ||
![]() |
0b67bd54fb | ||
![]() |
5f6fdf00ec | ||
![]() |
460e20284e | ||
![]() |
b61490341e | ||
![]() |
4a65051641 | ||
![]() |
1dde3087ff | ||
![]() |
c5bf8f6267 | ||
![]() |
5c7cd4a27d | ||
![]() |
b41acfdb33 | ||
![]() |
8ef77df651 | ||
![]() |
51620df76c | ||
![]() |
09ee329f17 | ||
![]() |
e1716751d4 | ||
![]() |
b70f8e12f6 | ||
![]() |
b5d92590ee | ||
![]() |
49bb61b2ae | ||
![]() |
18007048a3 | ||
![]() |
59205b174f | ||
![]() |
79eecc7c76 | ||
![]() |
8761d0ac27 | ||
![]() |
6d88381ead | ||
![]() |
1071506133 | ||
![]() |
e802185657 | ||
![]() |
245cb45503 | ||
![]() |
ff0750d053 | ||
![]() |
172d0d986b | ||
![]() |
8b78ed3a97 | ||
![]() |
ad26ececbf | ||
![]() |
b28f59f087 | ||
![]() |
03f910b45d | ||
![]() |
64478870c0 | ||
![]() |
ad8f31d44a | ||
![]() |
e42785a7a4 | ||
![]() |
0e9d624519 | ||
![]() |
c97e3e0515 | ||
![]() |
cd46f33909 | ||
![]() |
1afa906b21 | ||
![]() |
3894b3615d | ||
![]() |
aa9570c776 | ||
![]() |
03668e347e | ||
![]() |
f548d5bf20 | ||
![]() |
7677a673aa | ||
![]() |
7ced70b7d3 | ||
![]() |
101f1a76dd | ||
![]() |
889a91ba52 | ||
![]() |
bdc0304d5a | ||
![]() |
33be8a4a91 | ||
![]() |
a58d6da8e0 | ||
![]() |
6220c51aa6 | ||
![]() |
67b73ff921 | ||
![]() |
e8f418b077 | ||
![]() |
a561f37b00 | ||
![]() |
4f7afc578f | ||
![]() |
bb0f28c9c4 | ||
![]() |
4580f1b18a | ||
![]() |
4fe2c75810 | ||
![]() |
b249a91aaa | ||
![]() |
f5f4570116 | ||
![]() |
b5e1394fce | ||
![]() |
07c8c593ec | ||
![]() |
519046e769 | ||
![]() |
3b30af3e4d | ||
![]() |
bacb778fa7 | ||
![]() |
b338332d13 | ||
![]() |
84d9e4b063 | ||
![]() |
4c48088920 | ||
![]() |
f3ecd3837e | ||
![]() |
ef91468310 | ||
![]() |
db169826d5 | ||
![]() |
766015d64c | ||
![]() |
2a6e0c796a | ||
![]() |
c87d0895b0 | ||
![]() |
afe394e2a4 | ||
![]() |
673926c36b | ||
![]() |
df2cc22172 | ||
![]() |
c2789dfbf7 | ||
![]() |
8635209959 | ||
![]() |
7ec7d3a455 | ||
![]() |
0f43ff4fea | ||
![]() |
f1d1761fd7 | ||
![]() |
e4c4a1ecd4 | ||
![]() |
facd2284d2 | ||
![]() |
adc7649820 | ||
![]() |
056bac885d | ||
![]() |
3aefa690b9 | ||
![]() |
ce9fed7507 | ||
![]() |
51dd1b1b2b | ||
![]() |
ecf4b165b1 | ||
![]() |
2caf88bebe | ||
![]() |
2e367cb87c | ||
![]() |
dd7fc2aa6f | ||
![]() |
1465cfca37 | ||
![]() |
53fd559a7c | ||
![]() |
2a353efa5f | ||
![]() |
6196a99a98 | ||
![]() |
ad4b7fa254 | ||
![]() |
42740809c0 | ||
![]() |
172d3891f0 | ||
![]() |
cad9dcaf98 | ||
![]() |
267db6ddce | ||
![]() |
616a5fe0bd | ||
![]() |
145b26d262 | ||
![]() |
c4d02ff67d | ||
![]() |
37e4e63f81 | ||
![]() |
bbac79bdfc | ||
![]() |
92dca8d4e8 | ||
![]() |
5aa09eb363 | ||
![]() |
19bd36a80f | ||
![]() |
450bfe6b3f | ||
![]() |
b29161988d | ||
![]() |
c03146e415 | ||
![]() |
a6d7b6444c | ||
![]() |
cb61b0adda | ||
![]() |
9a68969eec | ||
![]() |
5e94cf379c | ||
![]() |
98ade2eb63 | ||
![]() |
7df5025aa5 | ||
![]() |
4deaf145b6 | ||
![]() |
b81d8ed900 | ||
![]() |
4ac3eb5f9b | ||
![]() |
83a1f9d31d | ||
![]() |
ceaa14e9b2 | ||
![]() |
ee38de488c | ||
![]() |
e7d0c96a26 | ||
![]() |
4cd11bcbca | ||
![]() |
2c3d8fd031 | ||
![]() |
d02c41c089 | ||
![]() |
c48bb2cbb0 | ||
![]() |
d6087e3620 | ||
![]() |
e43aa15a70 | ||
![]() |
66a996bb0f | ||
![]() |
634f4e718e | ||
![]() |
06cc028c80 | ||
![]() |
00f2aa5e1c | ||
![]() |
46498992a7 | ||
![]() |
638793ca5e | ||
![]() |
44801701c9 | ||
![]() |
3d482aed26 | ||
![]() |
76790be925 | ||
![]() |
3ac575c360 | ||
![]() |
6b0f76c012 | ||
![]() |
9cfed06784 | ||
![]() |
e0d4eb7c9c | ||
![]() |
e43f04e564 | ||
![]() |
85bc11176e | ||
![]() |
74db9030b0 | ||
![]() |
558b0b688c | ||
![]() |
1fd7989a41 | ||
![]() |
e5053c85b0 | ||
![]() |
37fe6d2193 | ||
![]() |
99c1234a7d | ||
![]() |
0b65bdac46 | ||
![]() |
e078071dfb | ||
![]() |
b210a36e53 | ||
![]() |
4c391bbae4 | ||
![]() |
493cbc65fe | ||
![]() |
61624bf606 | ||
![]() |
807500db41 | ||
![]() |
d003400c16 | ||
![]() |
2a58f2480a | ||
![]() |
5cb0570bc0 | ||
![]() |
9339ae2d7c | ||
![]() |
6314e17f27 | ||
![]() |
de1b57d8d9 | ||
![]() |
f64eff42f4 | ||
![]() |
2eb3d11cd2 | ||
![]() |
62002f94ef | ||
![]() |
1ac52edbf7 | ||
![]() |
b9de6a7a7d | ||
![]() |
41d6255998 | ||
![]() |
5a37891184 | ||
![]() |
4f14e546a5 | ||
![]() |
fab11e9055 | ||
![]() |
7f6c88164b | ||
![]() |
8913a45a6b | ||
![]() |
b603c7fc05 | ||
![]() |
258b1228f3 | ||
![]() |
f664cf4c90 | ||
![]() |
924e623b6a | ||
![]() |
97953912c5 | ||
![]() |
b3ba67e31d | ||
![]() |
5bb4065580 | ||
![]() |
f93df688e4 | ||
![]() |
29e6f6f72f | ||
![]() |
45beb3f93f | ||
![]() |
6e49ef6859 | ||
![]() |
6dbc2ef880 | ||
![]() |
285fe2299a | ||
![]() |
a2df95a679 | ||
![]() |
987e2d98fd | ||
![]() |
2989ca4d96 | ||
![]() |
5bcc700cc9 | ||
![]() |
4a52513a38 | ||
![]() |
27fcdab8ad | ||
![]() |
894bee2583 | ||
![]() |
69ba0865a8 | ||
![]() |
6b6068ffb8 | ||
![]() |
3a73de90ba | ||
![]() |
c225c9015e | ||
![]() |
b666e3f208 | ||
![]() |
d4609b87ea | ||
![]() |
6b53c448c8 | ||
![]() |
5e890aca2c | ||
![]() |
3bac6cc1b3 | ||
![]() |
cf492a92c8 | ||
![]() |
b4bb3acb63 | ||
![]() |
07a5dceed1 | ||
![]() |
eacc13a95c | ||
![]() |
428abc8282 | ||
![]() |
e7a5daad18 | ||
![]() |
ef3a235178 | ||
![]() |
e38aec831f | ||
![]() |
3e4704af0d | ||
![]() |
ee649debfb | ||
![]() |
68e2a08af2 | ||
![]() |
7c9c45b7cb | ||
![]() |
1cb39cef41 | ||
![]() |
1f26a7b634 | ||
![]() |
18d38ca737 | ||
![]() |
be8923d572 | ||
![]() |
2e94b349f9 | ||
![]() |
1af5958fa9 | ||
![]() |
f61f92e1ad | ||
![]() |
e48b8ad147 | ||
![]() |
e87c173e96 | ||
![]() |
ac323f48cc | ||
![]() |
1caec64115 | ||
![]() |
a3b97f7bd9 | ||
![]() |
0850c264b6 | ||
![]() |
1b7fdfc59c | ||
![]() |
e53727248d | ||
![]() |
e396fe054e | ||
![]() |
1a318b6f7a | ||
![]() |
9043e1375c | ||
![]() |
02c67f4ec0 | ||
![]() |
c870b3d02c | ||
![]() |
56318ad86b | ||
![]() |
dd0396465e | ||
![]() |
8bf241c182 | ||
![]() |
a8d5ad9c37 | ||
![]() |
644dcf8ce6 | ||
![]() |
212d626e36 | ||
![]() |
bc8bacab54 | ||
![]() |
9d059e190e | ||
![]() |
c98a4a1c3f | ||
![]() |
1a6f67d12e | ||
![]() |
1b8cb5b10c | ||
![]() |
6053d0691f | ||
![]() |
46ab613481 | ||
![]() |
e3facbd8c8 | ||
![]() |
1885845fef | ||
![]() |
0f539ea7d7 | ||
![]() |
2fc1e6e827 | ||
![]() |
32f8842af8 | ||
![]() |
6cfcff5503 | ||
![]() |
405143388c | ||
![]() |
fa0a0dd78c | ||
![]() |
34c5d93a98 | ||
![]() |
d1b7f07c68 | ||
![]() |
360abb78de | ||
![]() |
28747314e6 | ||
![]() |
6792f75c38 | ||
![]() |
53355d1fc4 | ||
![]() |
d98ce7c97e | ||
![]() |
98937bd240 | ||
![]() |
a714958c04 | ||
![]() |
4ae5ee21b1 | ||
![]() |
24fc3e6c76 | ||
![]() |
4f048f59f7 | ||
![]() |
8a6fe08c22 | ||
![]() |
002813e25e | ||
![]() |
ee3de31d69 | ||
![]() |
0854701550 | ||
![]() |
a4d5e0d20e | ||
![]() |
db96da55d8 | ||
![]() |
a449b79746 | ||
![]() |
c92fb9262c | ||
![]() |
ab966c08ac | ||
![]() |
e768169add | ||
![]() |
23d4b16243 | ||
![]() |
3e9eb9eedc | ||
![]() |
3dfba2ad12 | ||
![]() |
11d3f33276 | ||
![]() |
2a686a2fc4 | ||
![]() |
5778b3ec6d | ||
![]() |
747606b40f | ||
![]() |
c002d8c640 | ||
![]() |
5ef538a9e8 | ||
![]() |
692a4a4a3d | ||
![]() |
aa7b3b7bb4 | ||
![]() |
e3355455af | ||
![]() |
b7f18a1275 | ||
![]() |
4cdb1370e2 | ||
![]() |
b705e4a0f6 | ||
![]() |
55f14b2885 | ||
![]() |
cce9832037 | ||
![]() |
453a2919d1 | ||
![]() |
b728463e56 | ||
![]() |
84bb44ae62 | ||
![]() |
9804161ff1 | ||
![]() |
a5047f2f76 | ||
![]() |
fa40cac7e7 | ||
![]() |
e9f002785a | ||
![]() |
6bba88019e | ||
![]() |
28a0654d20 | ||
![]() |
b3371c5c14 | ||
![]() |
b3b8ea1dee | ||
![]() |
7fd4b41460 | ||
![]() |
088304fce7 | ||
![]() |
cfdc227563 | ||
![]() |
2eae641703 | ||
![]() |
90beee9a85 | ||
![]() |
644ab2cfab | ||
![]() |
cf8bba7112 | ||
![]() |
ba436a6467 | ||
![]() |
4cc1f55272 | ||
![]() |
cddaf539aa | ||
![]() |
143604fa4d | ||
![]() |
ef9976cf09 | ||
![]() |
12be9101e5 |
29
.codecov.yml
Normal file
29
.codecov.yml
Normal file
@@ -0,0 +1,29 @@
|
||||
# DSpace configuration for Codecov.io coverage reports
|
||||
# These override the default YAML settings at
|
||||
# https://docs.codecov.io/docs/codecov-yaml#section-default-yaml
|
||||
# Can be validated via instructions at:
|
||||
# https://docs.codecov.io/docs/codecov-yaml#validate-your-repository-yaml
|
||||
|
||||
# Settings related to code coverage analysis
|
||||
coverage:
|
||||
status:
|
||||
# Configuration for project-level checks. This checks how the PR changes overall coverage.
|
||||
project:
|
||||
default:
|
||||
# For each PR, auto compare coverage to previous commit.
|
||||
# Require that overall (project) coverage does NOT drop more than 0.5%
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
# Configuration for patch-level checks. This checks the relative coverage of the new PR code ONLY.
|
||||
patch:
|
||||
default:
|
||||
# For each PR, make sure the coverage of the new code is within 1% of current overall coverage.
|
||||
# We let 'patch' be more lenient as we only require *project* coverage to not drop significantly.
|
||||
target: auto
|
||||
threshold: 1%
|
||||
|
||||
# Turn PR comments "off". This feature adds the code coverage summary as a
|
||||
# comment on each PR. See https://docs.codecov.io/docs/pull-request-comments
|
||||
# However, this same info is available from the Codecov checks in the PR's
|
||||
# "Checks" tab in GitHub. So, the comment is unnecessary.
|
||||
comment: false
|
@@ -1,5 +1,4 @@
|
||||
# http://editorconfig.org
|
||||
|
||||
# Editor configuration, see https://editorconfig.org
|
||||
root = true
|
||||
|
||||
[*]
|
||||
|
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
22
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,22 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: ''
|
||||
labels: bug, needs triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is. Include the version(s) of DSpace where you've seen this problem & what *web browser* you were using. Link to examples if they are public.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. Do this
|
||||
2. Then this...
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Related work**
|
||||
Link to any related tickets or PRs here.
|
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest a new feature for this project
|
||||
title: ''
|
||||
labels: new feature, needs triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives or workarounds you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
26
.github/disabled-workflows/pull_request_opened.yml
vendored
Normal file
26
.github/disabled-workflows/pull_request_opened.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
# This workflow runs whenever a new pull request is created
|
||||
# TEMPORARILY DISABLED. Unfortunately this doesn't work for PRs created from forked repositories (which is how we tend to create PRs).
|
||||
# There is no known workaround yet. See https://github.community/t/how-to-use-github-token-for-prs-from-forks/16818
|
||||
name: Pull Request opened
|
||||
|
||||
# Only run for newly opened PRs against the "main" branch
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened]
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
automation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Assign the PR to whomever created it. This is useful for visualizing assignments on project boards
|
||||
# See https://github.com/marketplace/actions/pull-request-assigner
|
||||
- name: Assign PR to creator
|
||||
uses: thomaseizinger/assign-pr-creator-action@v1.0.0
|
||||
# Note, this authentication token is created automatically
|
||||
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
|
||||
with:
|
||||
repo-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Ignore errors. It is possible the PR was created by someone who cannot be assigned
|
||||
continue-on-error: true
|
14
.github/pull_request_template.md
vendored
14
.github/pull_request_template.md
vendored
@@ -1,7 +1,7 @@
|
||||
## References
|
||||
_Add references/links to any related tickets or PRs. These may include:_
|
||||
* Link to [Angular issue or PR](https://github.com/DSpace/dspace-angular/issues) related to this PR, if any
|
||||
* Link to [JIRA](https://jira.lyrasis.org/projects/DS/summary) ticket(s), if any
|
||||
_Add references/links to any related issues or PRs. These may include:_
|
||||
* Fixes [GitHub issue](https://github.com/DSpace/dspace-angular/issues), if any
|
||||
* Requires [REST API PR](https://github.com/DSpace/DSpace/pulls), if any
|
||||
|
||||
## Description
|
||||
Short summary of changes (1-2 sentences).
|
||||
@@ -20,9 +20,7 @@ _This checklist provides a reminder of what we are going to look for when review
|
||||
|
||||
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
|
||||
- [ ] My PR passes [TSLint](https://palantir.github.io/tslint/) validation using `yarn run lint`
|
||||
- [ ] My PR doesn't introduce circular dependencies
|
||||
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
|
||||
- [ ] My PR passes all specs/tests and includes new/updated specs for any bug fixes, improvements or new features. A few reminders about what constitutes good tests:
|
||||
* Include tests for different user types (if behavior differs), including: (1) Anonymous user, (2) Logged in user (non-admin), and (3) Administrator.
|
||||
* Include tests for error scenarios, e.g. when errors/warnings should appear (or buttons should be disabled).
|
||||
* For bug fixes, include a test that reproduces the bug and proves it is fixed. For clarity, it may be useful to provide the test in a separate commit from the bug fix.
|
||||
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/master/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
||||
- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
|
||||
- [ ] If my PR includes new, third-party dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
|
||||
|
29
.github/workflows/issue_opened.yml
vendored
Normal file
29
.github/workflows/issue_opened.yml
vendored
Normal file
@@ -0,0 +1,29 @@
|
||||
# This workflow runs whenever a new issue is created
|
||||
name: Issue opened
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
jobs:
|
||||
automation:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# Add the new issue to a project board, if it needs triage
|
||||
# See https://github.com/marketplace/actions/create-project-card-action
|
||||
- name: Add issue to project board
|
||||
# Only add to project board if issue is flagged as "needs triage" or has no labels
|
||||
# NOTE: By default we flag new issues as "needs triage" in our issue template
|
||||
if: (contains(github.event.issue.labels.*.name, 'needs triage') || join(github.event.issue.labels.*.name) == '')
|
||||
uses: technote-space/create-project-card-action@v1
|
||||
# Note, the authentication token below is an ORG level Secret.
|
||||
# It must be created/recreated manually via a personal access token with "public_repo" and "admin:org" permissions
|
||||
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token#permissions-for-the-github_token
|
||||
# This is necessary because the "DSpace Backlog" project is an org level project (i.e. not repo specific)
|
||||
with:
|
||||
GITHUB_TOKEN: ${{ secrets.ORG_PROJECT_TOKEN }}
|
||||
PROJECT: DSpace Backlog
|
||||
COLUMN: Triage
|
||||
CHECK_ORG_PROJECT: true
|
||||
# Ignore errors
|
||||
continue-on-error: true
|
25
.github/workflows/label_merge_conflicts.yml
vendored
Normal file
25
.github/workflows/label_merge_conflicts.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
# This workflow checks open PRs for merge conflicts and labels them when conflicts are found
|
||||
name: Check for merge conflicts
|
||||
|
||||
# Run whenever the "main" branch is updated
|
||||
# NOTE: This means merge conflicts are only checked for when a PR is merged to main.
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
triage:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
# See: https://github.com/mschilde/auto-label-merge-conflicts/
|
||||
- name: Auto-label PRs with merge conflicts
|
||||
uses: mschilde/auto-label-merge-conflicts@v2.0
|
||||
# Add "merge conflict" label if a merge conflict is detected. Remove it when resolved.
|
||||
# Note, the authentication token is created automatically
|
||||
# See: https://docs.github.com/en/actions/configuring-and-managing-workflows/authenticating-with-the-github_token
|
||||
with:
|
||||
CONFLICT_LABEL_NAME: 'merge conflict'
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# Ignore errors
|
||||
continue-on-error: true
|
7
.gitignore
vendored
7
.gitignore
vendored
@@ -7,8 +7,9 @@ npm-debug.log
|
||||
|
||||
/build/
|
||||
|
||||
/config/environment.dev.js
|
||||
/config/environment.prod.js
|
||||
/src/environments/environment.ts
|
||||
/src/environments/environment.dev.ts
|
||||
/src/environments/environment.prod.ts
|
||||
|
||||
/coverage
|
||||
|
||||
@@ -36,3 +37,5 @@ yarn-error.log
|
||||
package-lock.json
|
||||
|
||||
.java-version
|
||||
|
||||
.env
|
||||
|
104
.travis.yml
104
.travis.yml
@@ -1,58 +1,66 @@
|
||||
sudo: required
|
||||
os: linux
|
||||
dist: bionic
|
||||
|
||||
env:
|
||||
# Install the latest docker-compose version for ci testing.
|
||||
# The default installation in travis is not compatible with the latest docker-compose file version.
|
||||
COMPOSE_VERSION: 1.24.1
|
||||
# The ci step will test the dspace-angular code against DSpace REST.
|
||||
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
||||
DSPACE_REST_HOST: localhost
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: '/server/api'
|
||||
DSPACE_REST_SSL: false
|
||||
|
||||
services:
|
||||
- xvfb
|
||||
|
||||
before_install:
|
||||
# Docker Compose Install
|
||||
- curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
|
||||
- chmod +x docker-compose
|
||||
- sudo mv docker-compose /usr/local/bin
|
||||
|
||||
install:
|
||||
# update chrome
|
||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install google-chrome-stable
|
||||
# Start up DSpace 7 using the entities database dump
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
||||
# Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update
|
||||
- docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||
- travis_retry yarn install
|
||||
|
||||
before_script:
|
||||
# The following line could be enabled to verify that the rest server is responding.
|
||||
# Currently, "yarn run build" takes enough time to run to allow the service to be available
|
||||
#- curl http://localhost:8080/
|
||||
|
||||
after_script:
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml down
|
||||
|
||||
language: node_js
|
||||
|
||||
# Enable caching for yarn & node_modules
|
||||
cache:
|
||||
yarn: true
|
||||
|
||||
node_js:
|
||||
- "10"
|
||||
- "12"
|
||||
|
||||
cache:
|
||||
yarn: true
|
||||
# Install latest chrome (for e2e headless testing). Run an update if needed.
|
||||
addons:
|
||||
apt:
|
||||
sources:
|
||||
- google-chrome
|
||||
packages:
|
||||
- google-chrome-stable
|
||||
update: true
|
||||
|
||||
bundler_args: --retry 5
|
||||
env:
|
||||
# The ci step will test the dspace-angular code against DSpace REST.
|
||||
# Direct that step to utilize a DSpace REST service that has been started in docker.
|
||||
DSPACE_REST_HOST: localhost
|
||||
DSPACE_REST_PORT: 8080
|
||||
DSPACE_REST_NAMESPACE: '/server'
|
||||
DSPACE_REST_SSL: false
|
||||
|
||||
before_install:
|
||||
# Check our versions of everything
|
||||
- echo "Check versions"
|
||||
- yarn -v
|
||||
- docker-compose -v
|
||||
- google-chrome-stable --version
|
||||
|
||||
install:
|
||||
# Start up a test DSpace 7 REST backend using the entities database dump
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
||||
# Use the dspace-cli image to populate the assetstore. Triggers a discovery and oai update
|
||||
- docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||
# Install all local dependencies (retry if initially fails)
|
||||
- travis_retry yarn install
|
||||
|
||||
before_script:
|
||||
- echo "Check Docker containers"
|
||||
- docker container ls
|
||||
# The following line could be enabled to verify that the rest server is responding.
|
||||
#- echo "Check REST API available (via Docker)"
|
||||
#- curl http://localhost:8080/server/
|
||||
|
||||
script:
|
||||
- yarn run build
|
||||
- yarn run ci
|
||||
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js
|
||||
# build app and run all tests
|
||||
- ng lint || travis_terminate 1;
|
||||
- travis_wait yarn run build:prod || travis_terminate 1;
|
||||
- yarn test:headless || travis_terminate 1;
|
||||
- yarn run e2e:ci || travis_terminate 1;
|
||||
|
||||
after_script:
|
||||
# Shutdown docker after everything runs
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml down
|
||||
|
||||
# After a successful build and test (see 'script'), send code coverage reports to codecov.io
|
||||
# These code coverage reports are generated by the codecov node module in our package.json
|
||||
after_success:
|
||||
- codecov
|
||||
|
@@ -4,9 +4,9 @@
|
||||
FROM node:12-alpine
|
||||
WORKDIR /app
|
||||
ADD . /app/
|
||||
EXPOSE 3000
|
||||
EXPOSE 4000
|
||||
|
||||
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
|
||||
# See, for example https://github.com/yarnpkg/yarn/issues/5540
|
||||
RUN yarn install --network-timeout 300000
|
||||
CMD yarn run watch
|
||||
CMD yarn run start:dev
|
||||
|
82
README.md
82
README.md
@@ -1,4 +1,4 @@
|
||||
[](https://travis-ci.org/DSpace/dspace-angular) [](https://coveralls.io/github/DSpace/dspace-angular?branch=master) [](https://github.com/angular/universal)
|
||||
[](https://travis-ci.com/DSpace/dspace-angular) [](https://codecov.io/gh/DSpace/dspace-angular) [](https://github.com/angular/universal)
|
||||
|
||||
dspace-angular
|
||||
==============
|
||||
@@ -29,7 +29,7 @@ yarn install
|
||||
yarn start
|
||||
```
|
||||
|
||||
Then go to [http://localhost:3000](http://localhost:3000) in your browser
|
||||
Then go to [http://localhost:4000](http://localhost:4000) in your browser
|
||||
|
||||
Not sure where to start? watch the training videos linked in the [Introduction to the technology](#introduction-to-the-technology) section below.
|
||||
|
||||
@@ -59,13 +59,13 @@ Table of Contents
|
||||
Introduction to the technology
|
||||
------------------------------
|
||||
|
||||
You can find more information on the technologies used in this project (Angular.io, Typescript, Angular Universal, RxJS, etc) on the [LYRASIS wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+UI+Technology+Stack)
|
||||
You can find more information on the technologies used in this project (Angular.io, Angular CLI, Typescript, Angular Universal, RxJS, etc) on the [LYRASIS wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+UI+Technology+Stack)
|
||||
|
||||
Requirements
|
||||
------------
|
||||
|
||||
- [Node.js](https://nodejs.org), [npm](https://www.npmjs.com/), and [yarn](https://yarnpkg.com)
|
||||
- Ensure you're running node `v10.x` or `v12.x`, npm >= `v5.x` and yarn >= `v1.x`
|
||||
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
||||
- Ensure you're running node `v10.x` or `v12.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.
|
||||
|
||||
@@ -77,25 +77,53 @@ Installing
|
||||
|
||||
### Configuring
|
||||
|
||||
Default configuration file is located in `config/` folder.
|
||||
Default configuration file is located in `src/environments/` folder.
|
||||
|
||||
To change the default configuration values, create local files that override the parameters you need to change:
|
||||
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.
|
||||
|
||||
- Create a new `environment.dev.js` file in `config/` for `devel` environment;
|
||||
- Create a new `environment.prod.js` file in `config/` for `production` environment;
|
||||
- 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;
|
||||
|
||||
To use the configuration parameters in your component:
|
||||
The server settings can also be overwritten using an environment file.
|
||||
|
||||
This file should be called `.env` and be placed in the project root.
|
||||
|
||||
The following settings can be overwritten in this file:
|
||||
|
||||
```bash
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
|
||||
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]
|
||||
|
||||
constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig) {}
|
||||
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]
|
||||
```
|
||||
|
||||
The same settings can also be overwritten by setting system environment variables instead, E.g.:
|
||||
```bash
|
||||
export DSPACE_HOST=dspace7.4science.cloud
|
||||
```
|
||||
|
||||
The priority works as follows: **environment variable** overrides **variable in `.env` file** overrides **`environment.(prod, dev or test).ts`** overrides **`environment.common.ts`**
|
||||
|
||||
|
||||
#### Using environment variables in code
|
||||
To use environment variables in a UI component, use:
|
||||
|
||||
```typescript
|
||||
import { environment } from '../environment.ts';
|
||||
```
|
||||
|
||||
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`
|
||||
|
||||
|
||||
Running the app
|
||||
---------------
|
||||
|
||||
After you have installed all dependencies you can now run the app. Run `yarn run watch` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:3000`.
|
||||
After you have installed all dependencies you can now run the app. Run `yarn run start:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:4000`.
|
||||
|
||||
### Running in production mode
|
||||
|
||||
@@ -115,14 +143,6 @@ yarn run build:prod
|
||||
|
||||
This will build the application and put the result in the `dist` folder
|
||||
|
||||
### Deploy
|
||||
```bash
|
||||
# deploy production in standalone pm2 container
|
||||
yarn run deploy
|
||||
|
||||
# remove production from standalone pm2 container
|
||||
yarn run undeploy
|
||||
```
|
||||
|
||||
### Running the application with Docker
|
||||
See [Docker Runtime Options](docker/README.md)
|
||||
@@ -155,17 +175,15 @@ If you would like to contribute by testing a Pull Request (PR), here's how to do
|
||||
* Click it, and follow "Step 1" of those instructions to checkout the pull down the PR branch.
|
||||
2. `yarn run clean` (This resets your local dependencies to ensure you are up-to-date with this PR)
|
||||
3. `yarn install` (Updates your local dependencies to those in the PR)
|
||||
4. `yarn start` (Rebuilds the project, and deploys to localhost:3000, by default)
|
||||
5. At this point, the code from the PR will be deployed to http://localhost:3000. Test it out, and ensure that it does what is described in the PR (or fixes the bug described in the ticket linked to the PR).
|
||||
4. `yarn start` (Rebuilds the project, and deploys to localhost:4000, by default)
|
||||
5. At this point, the code from the PR will be deployed to http://localhost:4000. Test it out, and ensure that it does what is described in the PR (or fixes the bug described in the ticket linked to the PR).
|
||||
|
||||
Once you have tested the Pull Request, please add a comment and/or approval to the PR to let us know whether you found it to be successful (or not). Thanks!
|
||||
|
||||
|
||||
### 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 enviroment 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`.
|
||||
|
||||
To correctly run the tests you need to run the build once with: `yarn run build`.
|
||||
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`.
|
||||
|
||||
The default browser is Google Chrome.
|
||||
|
||||
@@ -177,17 +195,13 @@ and run: `yarn run test`
|
||||
|
||||
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.
|
||||
|
||||
If you are going to use a remote test enviroment you need to edit the './protractor.conf.js'. Follow the instructions you will find inside it.
|
||||
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.
|
||||
|
||||
The default browser is Google Chrome.
|
||||
|
||||
Protractor needs a functional instance of the DSpace interface to run the E2E tests, so you need to run:`yarn run watch`
|
||||
|
||||
or any command that bring up the DSpace interface.
|
||||
|
||||
Place your tests at the following path: `./e2e`
|
||||
|
||||
and run: `yarn run e2e`
|
||||
and run: `ng e2e`
|
||||
|
||||
### Continuous Integration (CI) Test
|
||||
|
||||
@@ -200,7 +214,7 @@ See [`./docs`](docs) for further documentation.
|
||||
|
||||
### Building code documentation
|
||||
|
||||
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts informations from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
|
||||
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts information from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
|
||||
|
||||
Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder.
|
||||
|
||||
@@ -388,7 +402,7 @@ Frequently asked questions
|
||||
- Where do I write my tests?
|
||||
- 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 `3000` is currently being used and `EACCES` is lack of permission to build files to `./dist/`
|
||||
- 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)
|
||||
- Why is the size of my app larger in development?
|
||||
|
159
angular.json
159
angular.json
@@ -1,13 +1,158 @@
|
||||
{
|
||||
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
|
||||
"version": 1,
|
||||
"cli": {
|
||||
"defaultCollection": "@ngrx/schematics"
|
||||
},
|
||||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"core": {
|
||||
"dspace-angular": {
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
"style": "scss"
|
||||
}
|
||||
},
|
||||
"root": "",
|
||||
"projectType": "application"
|
||||
"sourceRoot": "src",
|
||||
"prefix": "ds",
|
||||
"architect": {
|
||||
"build": {
|
||||
"builder": "@angular-builders/custom-webpack:browser",
|
||||
"options": {
|
||||
"customWebpackConfig": {
|
||||
"path": "./webpack/webpack.browser.ts",
|
||||
"mergeStrategies": {
|
||||
"loaders": "prepend"
|
||||
}
|
||||
},
|
||||
"outputPath": "dist/browser",
|
||||
"index": "src/index.html",
|
||||
"main": "src/main.browser.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"aot": false,
|
||||
"assets": [
|
||||
"src/assets",
|
||||
"src/robots.txt"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"optimization": true,
|
||||
"outputHashing": "all",
|
||||
"extractCss": true,
|
||||
"namedChunks": false,
|
||||
"aot": true,
|
||||
"extractLicenses": true,
|
||||
"vendorChunk": false,
|
||||
"buildOptimizer": true,
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "3mb",
|
||||
"maximumError": "5mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "6kb",
|
||||
"maximumError": "10kb"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||
"options": {
|
||||
"browserTarget": "dspace-angular:build",
|
||||
"port": 4000
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"browserTarget": "dspace-angular:build:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||
"options": {
|
||||
"browserTarget": "dspace-angular:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@angular-builders/custom-webpack:karma",
|
||||
"options": {
|
||||
"customWebpackConfig": {
|
||||
"path": "./webpack/webpack.test.ts",
|
||||
"mergeStrategies": {
|
||||
"loaders": "prepend"
|
||||
}
|
||||
},
|
||||
"main": "src/test.ts",
|
||||
"polyfills": "src/polyfills.ts",
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"karmaConfig": "karma.conf.js",
|
||||
"assets": [
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
},
|
||||
"lint": {
|
||||
"builder": "@angular-devkit/build-angular:tslint",
|
||||
"options": {
|
||||
"tsConfig": [
|
||||
"tsconfig.app.json",
|
||||
"tsconfig.spec.json",
|
||||
"e2e/tsconfig.json"
|
||||
],
|
||||
"exclude": [
|
||||
"**/node_modules/**"
|
||||
]
|
||||
}
|
||||
},
|
||||
"e2e": {
|
||||
"builder": "@angular-devkit/build-angular:protractor",
|
||||
"options": {
|
||||
"protractorConfig": "e2e/protractor.conf.js",
|
||||
"devServerTarget": "dspace-angular:serve"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"devServerTarget": "dspace-angular:serve:production"
|
||||
}
|
||||
}
|
||||
},
|
||||
"server": {
|
||||
"builder": "@angular-builders/custom-webpack:server",
|
||||
"options": {
|
||||
"customWebpackConfig": {
|
||||
"path": "./webpack/webpack.prod.ts",
|
||||
"mergeStrategies": {
|
||||
"loaders": "prepend"
|
||||
}
|
||||
},
|
||||
"outputPath": "dist/server",
|
||||
"main": "src/main.server.ts",
|
||||
"tsConfig": "tsconfig.server.json"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"sourceMap": false,
|
||||
"optimization": {
|
||||
"scripts": false,
|
||||
"styles": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"defaultProject": "dspace-angular"
|
||||
}
|
||||
|
17
app.yaml
17
app.yaml
@@ -1,17 +0,0 @@
|
||||
# Copyright 2015-2016, Google, Inc.
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
# [START app_yaml]
|
||||
runtime: nodejs
|
||||
env: flex
|
||||
# [END app_yaml]
|
11
browserslist
Normal file
11
browserslist
Normal file
@@ -0,0 +1,11 @@
|
||||
# This file is used by the build system to adjust CSS and JS output to support the specified browsers below.
|
||||
# For additional information regarding the format and rule options, please see:
|
||||
# https://github.com/browserslist/browserslist#queries
|
||||
|
||||
# You can see what browsers were selected by your queries by running:
|
||||
# npx browserslist
|
||||
|
||||
> 0.5%
|
||||
last 2 versions
|
||||
Firefox ESR
|
||||
not IE 9-11 # For IE 9-11 support, remove 'not'.
|
@@ -1,4 +0,0 @@
|
||||
// This configuration is currently only being used for unit tests, end-to-end tests use environment.dev.ts
|
||||
module.exports = {
|
||||
|
||||
};
|
@@ -11,7 +11,7 @@
|
||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||
- cli.assetstore.yml
|
||||
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
|
||||
- environment.dev.js
|
||||
- environment.dev.ts
|
||||
- Environment file for running DSpace Angular in Docker
|
||||
- local.cfg
|
||||
- Environment file for running the DSpace 7 REST API in Docker.
|
||||
|
@@ -11,7 +11,7 @@ version: "3.7"
|
||||
services:
|
||||
dspace-cli:
|
||||
environment:
|
||||
- AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/master/dogAndReport.zip
|
||||
- AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/main/dogAndReport.zip
|
||||
- ADMIN_EMAIL=test@test.edu
|
||||
- AIPDIR=/tmp/aip-dir
|
||||
entrypoint:
|
||||
|
@@ -7,7 +7,7 @@ services:
|
||||
environment:
|
||||
DSPACE_HOST: dspace-angular
|
||||
DSPACE_NAMESPACE: /
|
||||
DSPACE_PORT: '3000'
|
||||
DSPACE_PORT: '4000'
|
||||
DSPACE_SSL: "false"
|
||||
image: dspace/dspace-angular:latest
|
||||
build:
|
||||
@@ -16,11 +16,11 @@ services:
|
||||
networks:
|
||||
dspacenet:
|
||||
ports:
|
||||
- published: 3000
|
||||
target: 3000
|
||||
- published: 4000
|
||||
target: 4000
|
||||
- published: 9876
|
||||
target: 9876
|
||||
stdin_open: true
|
||||
tty: true
|
||||
volumes:
|
||||
- ./environment.dev.js:/app/config/environment.dev.js
|
||||
- ./environment.dev.ts:/app/src/environments/environment.dev.ts
|
||||
|
@@ -1,16 +1,18 @@
|
||||
/*
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
module.exports = {
|
||||
// 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/api'
|
||||
nameSpace: '/server'
|
||||
}
|
||||
};
|
@@ -1,5 +1,6 @@
|
||||
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,23 +1,23 @@
|
||||
# Configuration
|
||||
|
||||
Default configuration file is located in `config/` folder. All configuration options should be listed in the default configuration file `config/environment.default.js`. Please do not change this file directly! To change the default configuration values, create local files that override the parameters you need to change:
|
||||
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.
|
||||
|
||||
- Create a new `environment.dev.js` file in `config/` for `devel` environment;
|
||||
- Create a new `environment.prod.js` file in `config/` for `production` environment;
|
||||
- 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;
|
||||
|
||||
Some few configuration options can be overridden by setting environment variables. These and the variable names are listed below.
|
||||
|
||||
## 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:3000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
|
||||
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itsself to, and if ssl should be enabled not. By default it listens on `localhost:4000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
|
||||
|
||||
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):
|
||||
```
|
||||
module.exports = {
|
||||
// Angular Universal server settings.
|
||||
export const environment = {
|
||||
// Angular UI settings.
|
||||
ui: {
|
||||
ssl: false,
|
||||
host: 'localhost',
|
||||
port: 3000,
|
||||
port: 4000,
|
||||
nameSpace: '/'
|
||||
}
|
||||
};
|
||||
@@ -27,7 +27,7 @@ Alternately you can set the following environment variables. If any of these are
|
||||
```
|
||||
DSPACE_SSL=true
|
||||
DSPACE_HOST=localhost
|
||||
DSPACE_PORT=3000
|
||||
DSPACE_PORT=4000
|
||||
DSPACE_NAMESPACE=/
|
||||
```
|
||||
|
||||
@@ -35,14 +35,14 @@ Alternately you can set the following environment variables. If any of these are
|
||||
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:
|
||||
|
||||
```
|
||||
module.exports = {
|
||||
export const environment = {
|
||||
// The REST API server settings.
|
||||
rest: {
|
||||
ssl: true,
|
||||
host: 'dspace7.4science.it',
|
||||
host: 'dspace7.4science.cloud',
|
||||
port: 443,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
nameSpace: '/dspace-spring-rest/api'
|
||||
nameSpace: '/server'
|
||||
}
|
||||
};
|
||||
```
|
||||
@@ -50,9 +50,9 @@ module.exports = {
|
||||
Alternately you can set the following environment variables. If any of these are set, it will override all configuration files:
|
||||
```
|
||||
DSPACE_REST_SSL=true
|
||||
DSPACE_REST_HOST=localhost
|
||||
DSPACE_REST_PORT=3000
|
||||
DSPACE_REST_NAMESPACE=/
|
||||
DSPACE_REST_HOST=dspace7.4science.cloud
|
||||
DSPACE_REST_PORT=443
|
||||
DSPACE_REST_NAMESPACE=/server
|
||||
```
|
||||
|
||||
## Supporting analytics services other than Google Analytics
|
||||
@@ -62,4 +62,71 @@ Angulartics can be configured to work with a number of other services besides Go
|
||||
|
||||
In order to start using one of these services, select it from the [Angulartics Providers page](https://angulartics.github.io/angulartics2/#providers), and follow the instructions on how to configure it.
|
||||
|
||||
The Google Analytics script was added in [`main.browser.ts`](https://github.com/DSpace/dspace-angular/blob/ff04760f4af91ac3e7add5e7424a46cb2439e874/src/main.browser.ts#L33) instead of the `<head>` tag in `index.html` to ensure events get sent when the page is shown in a client's browser, and not when it's rendered on the universal server. Likely you'll want to do the same when adding a new service.
|
||||
The Google Analytics script was added in [`main.browser.ts`](https://github.com/DSpace/dspace-angular/blob/ff04760f4af91ac3e7add5e7424a46cb2439e874/src/main.browser.ts#L33) instead of the `<head>` tag in `index.html` to ensure events get sent when the page is shown in a client's browser, and not when it's rendered on the universal server. Likely you'll want to do the same when adding a new service.
|
||||
|
||||
## SEO when hosting REST Api and UI on different servers
|
||||
|
||||
Indexers such as Google Scholar require that files are hosted on the same domain as the page that links them. In DSpace 7, Bitstreams are served from the REST server. So if you use different servers for the REST api and the UI you'll want to ensure that Bitstream downloads are proxied through the UI server.
|
||||
|
||||
In order to achieve this we'll need to do two things:
|
||||
- **Proxy the Bitstream downloads through the UI server.** You'll need to put a webserver such as httpd or nginx in front of the UI server in order to achieve this. [Below](#apache-http-server-config) you'll find a section explaining how to do it in httpd.
|
||||
- **Update the URLs for Bitstream downloads to match the UI server.** This can be done using a setting in the UI environment file.
|
||||
|
||||
### UI config
|
||||
If you set the property `rewriteDownloadUrls` to `true` in your `environment.prod.ts` file, the [origin](https://developer.mozilla.org/en-US/docs/Glossary/Origin) of any download URL will be replaced by the origin of the UI. This will also happen for the `citation_pdf_url` `<meta>` tag on Item pages.
|
||||
|
||||
The app will determine the UI origin currently in use, so the external UI URL doesn't need to be configured anywhere and rewrites will still work if you host the UI from multiple domains.
|
||||
|
||||
### Apache HTTP Server config
|
||||
|
||||
#### Basics
|
||||
In order to be able to host bitstreams from the UI Server you'll need to enable mod_proxy and add the following to the httpd config of your UI server:
|
||||
|
||||
```
|
||||
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "http://rest.api/server/api/core/bitstreams/$1/content"
|
||||
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "http://rest.api/server/api/core/bitstreams/$1/content"
|
||||
```
|
||||
|
||||
Replace http://rest.api in with the correct origin for your REST server.
|
||||
|
||||
The `ProxyPassMatch` line forwards all requests matching the regular expression for a bitstream download URL to the corresponding path on the REST server
|
||||
|
||||
The `ProxyPassReverse` ensures that if the REST server were to return redirect response, httpd would also swap out its hostname for the hostname of the UI before forwarding the response to the client.
|
||||
|
||||
#### Using HTTPS
|
||||
If your REST server uses https, you'll need to enable mod_ssl and ensure `SSLProxyEngine on` is part of your UI server's httpd config as well
|
||||
|
||||
If the UI hostname doesn't match the CN in the SSL certificate of the REST server (which is likely if they're on different domains), you'll also need to add the following lines
|
||||
|
||||
```
|
||||
SSLProxyCheckPeerCN off
|
||||
SSLProxyCheckPeerName off
|
||||
```
|
||||
These are two names for [the same directive](https://httpd.apache.org/docs/trunk/mod/mod_ssl.html#sslproxycheckpeername) that have been used for various versions of httpd, old versions need the former, then some in-between versions need both, and newer versions only need the latter. Keeping them both doesn't harm anything.
|
||||
|
||||
So the entire config becomes:
|
||||
|
||||
```
|
||||
SSLProxyEngine on
|
||||
SSLProxyCheckPeerCN off
|
||||
SSLProxyCheckPeerName off
|
||||
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
|
||||
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
|
||||
```
|
||||
|
||||
If you don't want httpd to verify the certificate of the REST server, you can also turn all checks off with the following config:
|
||||
|
||||
```
|
||||
SSLProxyEngine on
|
||||
SSLProxyVerify none
|
||||
SSLProxyCheckPeerCN off
|
||||
SSLProxyCheckPeerName off
|
||||
SSLProxyCheckPeerExpire off
|
||||
ProxyPassMatch "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
|
||||
ProxyPassReverse "/server/api/core/bitstreams/([^/]+)/content" "https://rest.api/server/api/core/bitstreams/$1/content"
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
@@ -1,16 +0,0 @@
|
||||
import { browser, element, by } 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();
|
||||
}
|
||||
}
|
10
e2e/protractor-ci.conf.js
Normal file
10
e2e/protractor-ci.conf.js
Normal file
@@ -0,0 +1,10 @@
|
||||
const config = require('./protractor.conf').config;
|
||||
|
||||
config.capabilities = {
|
||||
browserName: 'chrome',
|
||||
chromeOptions: {
|
||||
args: ['--headless', '--no-sandbox', '--disable-gpu']
|
||||
}
|
||||
};
|
||||
|
||||
exports.config = config;
|
@@ -12,10 +12,10 @@ exports.config = {
|
||||
// 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:3000/',
|
||||
baseUrl: 'http://localhost:4000/',
|
||||
// -----------------------------------------------------------------
|
||||
specs: [
|
||||
'./e2e/**/*.e2e-spec.ts'
|
||||
'./src/**/*.e2e-spec.ts'
|
||||
],
|
||||
// -----------------------------------------------------------------
|
||||
// Browser and Capabilities: PhantomJS
|
||||
@@ -67,7 +67,7 @@ exports.config = {
|
||||
//],
|
||||
|
||||
plugins: [{
|
||||
path: 'node_modules/protractor-istanbul-plugin'
|
||||
path: '../node_modules/protractor-istanbul-plugin'
|
||||
}],
|
||||
|
||||
framework: 'jasmine',
|
||||
@@ -79,7 +79,7 @@ exports.config = {
|
||||
useAllAngular2AppRoots: true,
|
||||
beforeLaunch: function () {
|
||||
require('ts-node').register({
|
||||
project: 'e2e'
|
||||
project: './e2e/tsconfig.json'
|
||||
});
|
||||
},
|
||||
onPrepare: function () {
|
@@ -9,11 +9,14 @@ describe('protractor App', () => {
|
||||
|
||||
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()
|
||||
.then(() => expect<any>(page.getHomePageNewsText()).toBeDefined());
|
||||
page.navigateTo();
|
||||
page.waitUntilNotLoading();
|
||||
const text = page.getHomePageNewsText();
|
||||
expect<any>(text).toBeDefined();
|
||||
});
|
||||
});
|
23
e2e/src/app.po.ts
Normal file
23
e2e/src/app.po.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
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,6 +1,5 @@
|
||||
import { ProtractorPage } from './search-page.po';
|
||||
import { browser } from 'protractor';
|
||||
import { promise } from 'selenium-webdriver';
|
||||
|
||||
describe('protractor SearchPage', () => {
|
||||
let page: ProtractorPage;
|
||||
@@ -23,9 +22,11 @@ describe('protractor SearchPage', () => {
|
||||
.then(() => page.getRandomScopeOption())
|
||||
.then((scopeString: string) => {
|
||||
page.navigateToSearchWithScopeParameter(scopeString);
|
||||
page.getCurrentScope().then((s: string) => {
|
||||
expect<string>(s).toEqual(scopeString);
|
||||
});
|
||||
page.waitUntilNotLoading();
|
||||
page.getCurrentScope()
|
||||
.then((s: string) => {
|
||||
expect<string>(s).toEqual(scopeString);
|
||||
})
|
||||
});
|
||||
});
|
||||
|
||||
@@ -33,13 +34,16 @@ describe('protractor SearchPage', () => {
|
||||
page.navigateToSearch()
|
||||
.then(() => page.getRandomScopeOption())
|
||||
.then((scopeString: string) => {
|
||||
page.setCurrentScope(scopeString);
|
||||
page.submitSearchForm();
|
||||
browser.wait(() => {
|
||||
return browser.getCurrentUrl().then((url: string) => {
|
||||
return url.indexOf('scope=' + encodeURI(scopeString)) !== -1;
|
||||
});
|
||||
});
|
||||
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;
|
||||
})
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
|
@@ -27,7 +27,7 @@ export class ProtractorPage {
|
||||
}
|
||||
|
||||
setCurrentScope(scope: string) {
|
||||
element(by.css('#search-form option[value="' + scope + '"]')).click();
|
||||
return element(by.css('#search-form option[value="' + scope + '"]')).click();
|
||||
}
|
||||
|
||||
setCurrentQuery(query: string) {
|
||||
@@ -35,7 +35,7 @@ export class ProtractorPage {
|
||||
}
|
||||
|
||||
submitSearchForm() {
|
||||
element(by.css('#search-form button.search-button')).click();
|
||||
return element(by.css('#search-form button.search-button')).click();
|
||||
}
|
||||
|
||||
getRandomScopeOption(): promise.Promise<string> {
|
||||
@@ -46,4 +46,10 @@ export class ProtractorPage {
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
189
karma.conf.js
189
karma.conf.js
@@ -1,176 +1,37 @@
|
||||
/**
|
||||
* @author: @AngularClass
|
||||
*/
|
||||
// Karma configuration file, see link for more information
|
||||
// https://karma-runner.github.io/1.0/config/configuration-file.html
|
||||
|
||||
module.exports = function (config) {
|
||||
|
||||
var testWebpackConfig = require('./webpack/webpack.test.js')({
|
||||
env: 'test'
|
||||
});
|
||||
|
||||
// Uncomment and change to run tests on a remote Selenium server
|
||||
var webdriverConfig = {
|
||||
hostname: 'localhost',
|
||||
port: 4444
|
||||
};
|
||||
|
||||
var configuration = {
|
||||
client: {
|
||||
jasmine: {
|
||||
random: false
|
||||
}
|
||||
},
|
||||
// base path that will be used to resolve all patterns (e.g. files, exclude)
|
||||
config.set({
|
||||
basePath: '',
|
||||
|
||||
/*
|
||||
* Frameworks to use
|
||||
*
|
||||
* available frameworks: https://npmjs.org/browse/keyword/karma-adapter
|
||||
*/
|
||||
frameworks: ['jasmine'],
|
||||
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
plugins: [
|
||||
require("istanbul-instrumenter-loader"),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-coverage'),
|
||||
require("karma-istanbul-preprocessor"),
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage-istanbul-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma'),
|
||||
require('karma-mocha-reporter'),
|
||||
require('karma-phantomjs-launcher'),
|
||||
require('karma-remap-coverage'),
|
||||
require('karma-remap-istanbul'),
|
||||
require('karma-sourcemap-loader'),
|
||||
require('karma-webdriver-launcher'),
|
||||
require('karma-webpack')
|
||||
],
|
||||
|
||||
// list of files to exclude
|
||||
exclude: [],
|
||||
|
||||
/*
|
||||
* list of files / patterns to load in the browser
|
||||
*
|
||||
* we are building the test environment in ./spec-bundle.js
|
||||
*/
|
||||
files: [{
|
||||
pattern: './spec-bundle.js',
|
||||
watched: false,
|
||||
}],
|
||||
|
||||
/*
|
||||
* preprocess matching files before serving them to the browser
|
||||
* available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
|
||||
*/
|
||||
preprocessors: {
|
||||
'./spec-bundle.js': [
|
||||
'istanbul',
|
||||
'webpack',
|
||||
'sourcemap'
|
||||
]
|
||||
client: {
|
||||
clearContext: false // leave Jasmine Spec Runner output visible in browser
|
||||
},
|
||||
|
||||
// Webpack Config at ./webpack.test.js
|
||||
webpack: testWebpackConfig,
|
||||
|
||||
// save interim raw coverage report in memory
|
||||
coverageReporter: {
|
||||
type: 'in-memory'
|
||||
coverageIstanbulReporter: {
|
||||
dir: require('path').join(__dirname, './coverage/dspace-angular'),
|
||||
reports: ['html', 'lcovonly', 'text-summary'],
|
||||
fixWebpackSourcePaths: true
|
||||
},
|
||||
|
||||
remapCoverageReporter: {
|
||||
'text-summary': null, // to show summary in console
|
||||
html: './coverage/html',
|
||||
cobertura: './coverage/cobertura.xml'
|
||||
},
|
||||
|
||||
remapIstanbulReporter: {
|
||||
remapOptions: {}, //additional remap options
|
||||
reports: {
|
||||
json: './coverage/coverage.json',
|
||||
lcovonly: './coverage/lcov.info',
|
||||
html: './coverage/html/',
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Webpack please don't spam the console when running in karma!
|
||||
*/
|
||||
webpackMiddleware: {
|
||||
/**
|
||||
* webpack-dev-middleware configuration
|
||||
* i.e.
|
||||
*/
|
||||
noInfo: true,
|
||||
/**
|
||||
* and use stats to turn off verbose output
|
||||
*/
|
||||
stats: {
|
||||
/**
|
||||
* options i.e.
|
||||
*/
|
||||
chunks: false
|
||||
}
|
||||
},
|
||||
|
||||
/*
|
||||
* test results reporter to use
|
||||
*
|
||||
* possible values: 'dots', 'progress'
|
||||
* available reporters: https://npmjs.org/browse/keyword/karma-reporter
|
||||
*/
|
||||
reporters: [
|
||||
'mocha',
|
||||
'coverage',
|
||||
'remap-coverage',
|
||||
'karma-remap-istanbul'
|
||||
],
|
||||
|
||||
// Karma web server port
|
||||
port: 9876,
|
||||
|
||||
// enable / disable colors in the output (reporters and logs)
|
||||
colors: true,
|
||||
|
||||
/*
|
||||
* level of logging
|
||||
* possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG
|
||||
*/
|
||||
logLevel: config.LOG_WARN,
|
||||
|
||||
// enable / disable watching file and executing tests whenever any file changes
|
||||
autoWatch: false,
|
||||
|
||||
/*
|
||||
* start these browsers
|
||||
* available browser launchers: https://npmjs.org/browse/keyword/karma-launcher
|
||||
*/
|
||||
browsers: [
|
||||
'Chrome'
|
||||
],
|
||||
|
||||
customLaunchers: {
|
||||
// Remote Selenium Server with Chrome - launcher
|
||||
'SeleniumChrome': {
|
||||
base: 'WebDriver',
|
||||
config: webdriverConfig,
|
||||
browserName: 'chrome'
|
||||
},
|
||||
// Remote Selenium Server with Firefox - launcher
|
||||
'SeleniumFirefox': {
|
||||
base: 'WebDriver',
|
||||
config: webdriverConfig,
|
||||
browserName: 'firefox'
|
||||
}
|
||||
},
|
||||
|
||||
reporters: ['mocha', 'kjhtml'],
|
||||
mochaReporter: {
|
||||
ignoreSkipped: true
|
||||
ignoreSkipped: true,
|
||||
output: 'autowatch'
|
||||
},
|
||||
|
||||
browserNoActivityTimeout: 30000
|
||||
|
||||
};
|
||||
|
||||
config.set(configuration);
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['Chrome'],
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
||||
|
5
mock-nodemon.json
Normal file
5
mock-nodemon.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"watch": ["src/environments/mock-environment.ts"],
|
||||
"ext": "ts",
|
||||
"exec": "ts-node --project ./tsconfig.ts-node.json scripts/set-mock-env.ts"
|
||||
}
|
11
nodemon.json
11
nodemon.json
@@ -1,9 +1,6 @@
|
||||
{
|
||||
"watch": [
|
||||
"dist",
|
||||
"config",
|
||||
"src/index.html"
|
||||
],
|
||||
"ext": "js ts json html",
|
||||
"delay": "50"
|
||||
"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"
|
||||
}
|
||||
|
263
package.json
263
package.json
@@ -1,21 +1,37 @@
|
||||
{
|
||||
"name": "dspace-angular",
|
||||
"version": "0.0.1",
|
||||
"description": "Angular Universal UI for DSpace",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/dspace/dspace-angular.git"
|
||||
},
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": "10.* || >= 12.*"
|
||||
},
|
||||
"resolutions": {
|
||||
"serialize-javascript": ">= 2.1.2",
|
||||
"set-value": ">= 2.0.1"
|
||||
},
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"global": "npm install -g @angular/cli marked node-gyp nodemon node-nightly npm-check-updates npm-run-all rimraf typescript ts-node typedoc webpack webpack-bundle-analyzer pm2 rollup",
|
||||
"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",
|
||||
"start": "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",
|
||||
"build": "ng build",
|
||||
"build:prod": "yarn run build:ssr",
|
||||
"build:ssr": "yarn run build:client-and-server-bundles && yarn run compile:server",
|
||||
"build:client-and-server-bundles": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng build --prod && ng run dspace-angular:server:production --bundleDependencies all",
|
||||
"test:watch": "npm-run-all --parallel config:test:watch test",
|
||||
"test": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng test --sourceMap=true --watch=true",
|
||||
"test:headless": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng test --watch=false --sourceMap=true --browsers=ChromeHeadless --code-coverage",
|
||||
"lint": "ng lint",
|
||||
"e2e": "ng e2e",
|
||||
"e2e:ci": "ng e2e --protractor-config=./e2e/protractor-ci.conf.js",
|
||||
"compile:server": "webpack --config webpack.server.config.js --progress --colors",
|
||||
"serve:ssr": "node dist/server",
|
||||
"clean:coverage": "rimraf coverage",
|
||||
"clean:dist": "rimraf dist",
|
||||
"clean:doc": "rimraf doc",
|
||||
@@ -24,108 +40,68 @@
|
||||
"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:node",
|
||||
"prebuild": "yarn run clean:bld && yarn run clean:dist",
|
||||
"prebuild:ci": "yarn run prebuild",
|
||||
"prebuild:prod": "yarn run prebuild",
|
||||
"build": "node ./scripts/webpack.js --progress --mode development",
|
||||
"build:ci": "yarn run syncbuilddir && node ./scripts/webpack.js --env.aot --env.server --mode development && node ./scripts/webpack.js --env.aot --env.client --mode development",
|
||||
"build:prod": "yarn run syncbuilddir && node ./scripts/webpack.js --env.aot --env.server --mode production && node ./scripts/webpack.js --env.aot --env.client --mode production",
|
||||
"postbuild:prod": "yarn run rollup",
|
||||
"rollup": "rollup -c rollup.config.js",
|
||||
"prestart": "yarn run build:prod",
|
||||
"prestart:dev": "yarn run build",
|
||||
"start": "yarn run server",
|
||||
"start:dev": "yarn run server",
|
||||
"deploy": "pm2 start dist/server.js",
|
||||
"predeploy": "npm run build:prod",
|
||||
"preundeploy": "pm2 stop dist/server.js",
|
||||
"undeploy": "pm2 delete dist/server.js",
|
||||
"postundeploy": "npm run clean:dist",
|
||||
"server": "node dist/server.js",
|
||||
"server:watch": "nodemon dist/server.js",
|
||||
"server:watch:debug": "nodemon --debug dist/server.js",
|
||||
"syncbuilddir": "node ./scripts/sync-build-dir.js",
|
||||
"webpack:watch": "node ./scripts/webpack.js -w --mode development",
|
||||
"watch": "yarn run build && npm-run-all -p webpack:watch server:watch",
|
||||
"watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug",
|
||||
"predebug": "yarn run build",
|
||||
"predebug:server": "yarn run build",
|
||||
"debug": "node --debug-brk dist/server.js",
|
||||
"debug:server": "node-nightly --inspect --debug-brk dist/server.js",
|
||||
"debug:build": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --mode development",
|
||||
"debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --env.aot --env.client --env.server --mode production",
|
||||
"ci": "yarn run lint && yarn run build:ci && yarn run test:headless && npm-run-all -p -r server e2e",
|
||||
"protractor": "node node_modules/protractor/bin/protractor",
|
||||
"pree2e": "yarn run webdriver:update",
|
||||
"e2e": "yarn run protractor",
|
||||
"pretest": "yarn run clean:bld",
|
||||
"pretest:headless": "yarn run pretest",
|
||||
"pretest:watch": "yarn run pretest",
|
||||
"test": "karma start --single-run",
|
||||
"test:headless": "karma start --single-run --browsers ChromeHeadless",
|
||||
"test:watch": "karma start --no-single-run --auto-watch",
|
||||
"webdriver:start": "node node_modules/protractor/bin/webdriver-manager start --seleniumPort 4444",
|
||||
"webdriver:update": "node node_modules/protractor/bin/webdriver-manager update --standalone --gecko false",
|
||||
"lint": "tslint \"src/**/*.ts\" && tslint \"e2e/**/*.ts\"",
|
||||
"docs": "typedoc --options typedoc.json ./src/",
|
||||
"coverage": "http-server -c-1 -o -p 9875 ./coverage",
|
||||
"postinstall": "yarn run patch-protractor",
|
||||
"patch-protractor": "ncp node_modules/webdriver-manager node_modules/protractor/node_modules/webdriver-manager",
|
||||
"sync-i18n": "node ./scripts/sync-i18n-files.js"
|
||||
"clean": "yarn run clean:prod && yarn run clean:node && yarn run clean:env",
|
||||
"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"
|
||||
},
|
||||
"browser": {
|
||||
"fs": false,
|
||||
"path": false,
|
||||
"http": false,
|
||||
"https": false
|
||||
},
|
||||
"private": true,
|
||||
"resolutions": {
|
||||
"minimist": "^1.2.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@angular/animations": "^8.2.14",
|
||||
"@angular/animations": "~8.2.14",
|
||||
"@angular/cdk": "8.2.3",
|
||||
"@angular/cli": "^8.3.25",
|
||||
"@angular/common": "^8.2.14",
|
||||
"@angular/core": "^8.2.14",
|
||||
"@angular/forms": "^8.2.14",
|
||||
"@angular/platform-browser": "^8.2.14",
|
||||
"@angular/platform-browser-dynamic": "^8.2.14",
|
||||
"@angular/platform-server": "^8.2.14",
|
||||
"@angular/router": "^8.2.14",
|
||||
"@angular/common": "~8.2.14",
|
||||
"@angular/compiler": "~8.2.14",
|
||||
"@angular/core": "~8.2.14",
|
||||
"@angular/forms": "~8.2.14",
|
||||
"@angular/platform-browser": "~8.2.14",
|
||||
"@angular/platform-browser-dynamic": "~8.2.14",
|
||||
"@angular/platform-server": "~8.2.14",
|
||||
"@angular/router": "~8.2.14",
|
||||
"@angularclass/bootloader": "1.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^5.2.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "5.2.1",
|
||||
"@ng-dynamic-forms/core": "8.1.1",
|
||||
"@ng-dynamic-forms/ui-ng-bootstrap": "8.1.1",
|
||||
"@ngrx/effects": "^8.6.0",
|
||||
"@ngrx/router-store": "^8.6.0",
|
||||
"@ngrx/store": "^8.6.0",
|
||||
"@nguniversal/express-engine": "^8.2.6",
|
||||
"@nguniversal/express-engine": "8.2.6",
|
||||
"@nguniversal/module-map-ngfactory-loader": "v8.2.6",
|
||||
"@ngx-translate/core": "11.0.1",
|
||||
"@ngx-translate/http-loader": "4.0.0",
|
||||
"@nicky-lenaers/ngx-scroll-to": "^3.0.1",
|
||||
"angular-idle-preload": "3.0.0",
|
||||
"angular2-text-mask": "9.0.0",
|
||||
"angulartics2": "7.5.2",
|
||||
"body-parser": "1.18.2",
|
||||
"bootstrap": "4.3.1",
|
||||
"caniuse-lite": "^1.0.30000697",
|
||||
"cerialize": "0.1.18",
|
||||
"compression": "1.7.1",
|
||||
"cli-progress": "^3.8.0",
|
||||
"cookie-parser": "1.4.3",
|
||||
"core-js": "^3.6.4",
|
||||
"debug-loader": "^0.0.1",
|
||||
"deepmerge": "^4.2.2",
|
||||
"express": "4.16.2",
|
||||
"express-session": "1.15.6",
|
||||
"fast-json-patch": "^2.0.7",
|
||||
"file-saver": "^1.3.8",
|
||||
"filesize": "^6.1.0",
|
||||
"font-awesome": "4.7.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
||||
"hammerjs": "^2.0.8",
|
||||
"http-server": "0.11.1",
|
||||
"https": "1.0.0",
|
||||
"js-cookie": "2.2.0",
|
||||
"js.clone": "0.0.3",
|
||||
"json5": "^2.1.0",
|
||||
"jsonschema": "1.2.2",
|
||||
"jwt-decode": "^2.2.0",
|
||||
"methods": "1.1.2",
|
||||
"klaro": "^0.6.3",
|
||||
"moment": "^2.22.1",
|
||||
"moment-range": "^4.0.2",
|
||||
"morgan": "^1.9.1",
|
||||
"ng-mocks": "^8.1.0",
|
||||
"ng2-file-upload": "1.2.1",
|
||||
"ng2-file-upload": "1.4.0",
|
||||
"ng2-nouislider": "^1.8.2",
|
||||
"ngx-bootstrap": "^5.3.2",
|
||||
"ngx-infinite-scroll": "6.0.1",
|
||||
@@ -134,124 +110,75 @@
|
||||
"ngx-sortablejs": "^3.1.4",
|
||||
"nouislider": "^11.0.0",
|
||||
"pem": "1.13.2",
|
||||
"postcss-cli": "^6.0.0",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "6.5.4",
|
||||
"rxjs": "~6.4.0",
|
||||
"rxjs-spy": "^7.5.1",
|
||||
"sass-resources-loader": "^2.0.0",
|
||||
"sortablejs": "1.7.0",
|
||||
"text-mask-core": "5.0.1",
|
||||
"ts-loader": "^5.2.1",
|
||||
"ts-md5": "^1.2.4",
|
||||
"url-parse": "^1.4.7",
|
||||
"uuid": "^3.2.1",
|
||||
"tslib": "^1.10.0",
|
||||
"webfontloader": "1.6.28",
|
||||
"webpack-cli": "^3.2.0",
|
||||
"zone.js": "^0.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-devkit/build-angular": "^0.803.25",
|
||||
"@angular/compiler": "^8.2.14",
|
||||
"@angular/compiler-cli": "^8.2.14",
|
||||
"@angular-builders/custom-webpack": "8.4.1",
|
||||
"@angular-devkit/build-angular": "~0.803.25",
|
||||
"@angular/cli": "~8.3.25",
|
||||
"@angular/compiler-cli": "~8.2.14",
|
||||
"@angular/language-service": "~8.2.14",
|
||||
"@fortawesome/fontawesome-free": "^5.5.0",
|
||||
"@ngrx/entity": "^8.6.0",
|
||||
"@ngrx/schematics": "^8.6.0",
|
||||
"@ngrx/store-devtools": "^8.6.0",
|
||||
"@ngtools/webpack": "^8.3.25",
|
||||
"@schematics/angular": "^0.7.5",
|
||||
"@types/acorn": "^4.0.3",
|
||||
"@types/cookie-parser": "1.4.1",
|
||||
"@types/deep-freeze": "0.1.1",
|
||||
"@types/express": "^4.11.1",
|
||||
"@types/express-serve-static-core": "4.16.0",
|
||||
"@types/express": "^4.17.0",
|
||||
"@types/file-saver": "^1.3.0",
|
||||
"@types/hammerjs": "2.0.35",
|
||||
"@types/jasmine": "^3.3.9",
|
||||
"@types/jasminewd2": "~2.0.3",
|
||||
"@types/js-cookie": "2.1.0",
|
||||
"@types/json5": "^0.0.30",
|
||||
"@types/lodash": "^4.14.110",
|
||||
"@types/memory-cache": "0.2.0",
|
||||
"@types/mime": "2.0.0",
|
||||
"@types/node": "^11.11.2",
|
||||
"@types/serve-static": "1.13.2",
|
||||
"@types/uuid": "^3.4.3",
|
||||
"@types/webfontloader": "1.6.29",
|
||||
"@typescript-eslint/eslint-plugin": "^2.12.0",
|
||||
"@typescript-eslint/parser": "^2.12.0",
|
||||
"ajv": "^6.1.1",
|
||||
"ajv-keywords": "^3.1.0",
|
||||
"angular2-template-loader": "0.6.2",
|
||||
"autoprefixer": "^9.1.3",
|
||||
"caniuse-lite": "^1.0.30000697",
|
||||
"cli-progress": "^3.3.1",
|
||||
"codelyzer": "^5.1.0",
|
||||
"commander": "^3.0.2",
|
||||
"@types/node": "11.15.3",
|
||||
"codecov": "^3.7.2",
|
||||
"codelyzer": "^5.0.0",
|
||||
"compression-webpack-plugin": "^3.0.1",
|
||||
"copy-webpack-plugin": "^5.1.1",
|
||||
"copyfiles": "^2.1.1",
|
||||
"coveralls": "3.0.0",
|
||||
"css-loader": "3.4.0",
|
||||
"cssnano": "^4.1.10",
|
||||
"deep-freeze": "0.0.1",
|
||||
"eslint": "^6.7.2",
|
||||
"exports-loader": "^0.7.0",
|
||||
"html-webpack-plugin": "3.2.0",
|
||||
"imports-loader": "0.8.0",
|
||||
"istanbul-instrumenter-loader": "3.0.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"fork-ts-checker-webpack-plugin": "^0.4.10",
|
||||
"html-webpack-plugin": "^3.2.0",
|
||||
"http-proxy-middleware": "^1.0.5",
|
||||
"jasmine-core": "^3.3.0",
|
||||
"jasmine-marbles": "0.3.1",
|
||||
"jasmine-spec-reporter": "4.2.1",
|
||||
"karma": "4.0.1",
|
||||
"karma-chrome-launcher": "2.2.0",
|
||||
"karma-cli": "2.0.0",
|
||||
"karma-coverage": "1.1.2",
|
||||
"karma-istanbul-preprocessor": "0.0.2",
|
||||
"jasmine-spec-reporter": "~4.2.1",
|
||||
"karma": "^5.0.9",
|
||||
"karma-chrome-launcher": "~2.2.0",
|
||||
"karma-coverage-istanbul-reporter": "~2.0.1",
|
||||
"karma-jasmine": "2.0.1",
|
||||
"karma-jasmine-html-reporter": "^1.4.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"karma-phantomjs-launcher": "1.0.4",
|
||||
"karma-remap-coverage": "^0.1.5",
|
||||
"karma-remap-istanbul": "0.6.0",
|
||||
"karma-sourcemap-loader": "0.3.7",
|
||||
"karma-webdriver-launcher": "^1.0.7",
|
||||
"karma-webpack": "3.0.0",
|
||||
"ncp": "^2.0.0",
|
||||
"nodemon": "^1.15.0",
|
||||
"npm-run-all": "4.1.3",
|
||||
"nodemon": "^2.0.2",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"optimize-css-assets-webpack-plugin": "^5.0.1",
|
||||
"postcss": "^7.0.2",
|
||||
"postcss-apply": "0.11.0",
|
||||
"postcss-cli": "^6.0.0",
|
||||
"postcss-cssnext": "3.1.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-loader": "^3.0.0",
|
||||
"postcss-responsive-type": "1.0.0",
|
||||
"postcss-smart-import": "0.7.6",
|
||||
"protractor": "^5.4.2",
|
||||
"protractor": "^7.0.0",
|
||||
"protractor-istanbul-plugin": "2.0.0",
|
||||
"raw-loader": "0.5.1",
|
||||
"rimraf": "2.6.2",
|
||||
"rollup": "^0.65.0",
|
||||
"rollup-plugin-commonjs": "^9.1.6",
|
||||
"rollup-plugin-node-globals": "1.2.1",
|
||||
"rollup-plugin-node-resolve": "^3.0.3",
|
||||
"rollup-plugin-terser": "^2.0.2",
|
||||
"sass-loader": "7.3.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"script-ext-html-webpack-plugin": "2.1.4",
|
||||
"source-map": "0.7.3",
|
||||
"source-map-loader": "0.2.4",
|
||||
"string-replace-loader": "^2.1.1",
|
||||
"terser-webpack-plugin": "^2.3.1",
|
||||
"to-string-loader": "1.1.5",
|
||||
"ts-helpers": "1.1.2",
|
||||
"ts-node": "4.1.0",
|
||||
"tslint": "5.11.0",
|
||||
"typedoc": "^0.9.0",
|
||||
"typescript": "3.5.3",
|
||||
"webdriver-manager": "^12.1.7",
|
||||
"webpack": "^4.29.6",
|
||||
"ts-loader": "^5.2.0",
|
||||
"ts-node": "^8.8.1",
|
||||
"tslint": "~5.15.0",
|
||||
"typescript": "~3.5.3",
|
||||
"webpack": "^4.0.0",
|
||||
"webpack-bundle-analyzer": "^3.3.2",
|
||||
"webpack-dev-middleware": "3.2.0",
|
||||
"webpack-dev-server": "^3.1.11",
|
||||
"webpack-import-glob-loader": "^1.6.3",
|
||||
"webpack-merge": "4.1.4",
|
||||
"webpack-cli": "^3.1.0",
|
||||
"webpack-node-externals": "1.7.2"
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-smart-import')(),
|
||||
require('postcss-import')(),
|
||||
require('postcss-cssnext')(),
|
||||
require('postcss-apply')(),
|
||||
require('postcss-responsive-type')()
|
||||
|
@@ -1,21 +0,0 @@
|
||||
import nodeResolve from 'rollup-plugin-node-resolve'
|
||||
import commonjs from 'rollup-plugin-commonjs';
|
||||
import terser from 'rollup-plugin-terser'
|
||||
|
||||
export default {
|
||||
input: 'dist/client.js',
|
||||
output: {
|
||||
file: 'dist/client.js',
|
||||
format: 'iife',
|
||||
},
|
||||
plugins: [
|
||||
nodeResolve({
|
||||
jsnext: true,
|
||||
module: true
|
||||
}),
|
||||
commonjs({
|
||||
include: 'node_modules/rxjs/**'
|
||||
}),
|
||||
terser.terser()
|
||||
]
|
||||
}
|
11
scripts/serve.ts
Normal file
11
scripts/serve.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { environment } from '../src/environments/environment';
|
||||
|
||||
import * as child from 'child_process';
|
||||
|
||||
/**
|
||||
* Calls `ng serve` with the following arguments configured for the UI in the environment file: 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 }
|
||||
);
|
134
scripts/set-env.ts
Normal file
134
scripts/set-env.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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 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])))
|
||||
.catch(() => {
|
||||
console.log(colors.yellow.bold(`No specific environment file found for ` + environment));
|
||||
generateEnvironmentFile(merge(commonEnv, processEnv))
|
||||
});
|
||||
|
||||
function generateEnvironmentFile(file: GlobalConfig): void {
|
||||
file.production = production;
|
||||
buildBaseUrls(file);
|
||||
|
||||
// TODO remove workaround in beta 5
|
||||
if (file.rest.nameSpace.match("(.*)/api/?$") !== null) {
|
||||
file.rest.nameSpace = getNameSpace(file.rest.nameSpace);
|
||||
console.log(colors.white.bgMagenta.bold(`The rest.nameSpace property in your environment file or in your DSPACE_REST_NAMESPACE environment variable ends with '/api'.\nThis is deprecated. As '/api' isn't configurable on the rest side, it shouldn't be repeated in every environment file.\nPlease change the rest nameSpace to '${file.rest.nameSpace}'`));
|
||||
}
|
||||
|
||||
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 {
|
||||
// TODO remove workaround in beta 5
|
||||
const apiMatches = nameSpace.match("(.*)/api/?$");
|
||||
if (apiMatches != null) {
|
||||
let newValue = '/'
|
||||
if (hasValue(apiMatches[1])) {
|
||||
newValue = apiMatches[1];
|
||||
}
|
||||
return newValue;
|
||||
}
|
||||
else {
|
||||
return nameSpace ? nameSpace.charAt(0) === '/' ? nameSpace : '/' + nameSpace : '';
|
||||
}
|
||||
}
|
11
scripts/set-mock-env.ts
Normal file
11
scripts/set-mock-env.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
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);
|
||||
});
|
@@ -1,10 +1,9 @@
|
||||
#!/usr/bin/env node
|
||||
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 {projectRoot} = require('../webpack/helpers');
|
||||
|
||||
const program = new commander.Command();
|
||||
program.version('1.0.0', '-v, --version');
|
||||
@@ -13,8 +12,8 @@ const NEW_MESSAGE_TODO = '// TODO New key - Add a translation';
|
||||
const MESSAGE_CHANGED_TODO = '// TODO Source message changed - Revise the translation';
|
||||
const COMMENTS_CHANGED_TODO = '// TODO Source comments changed - Revise the translation';
|
||||
|
||||
const DEFAULT_SOURCE_FILE_LOCATION = 'resources/i18n/en.json5';
|
||||
const LANGUAGE_FILES_LOCATION = 'resources/i18n';
|
||||
const DEFAULT_SOURCE_FILE_LOCATION = 'src/assets/i18n/en.json5';
|
||||
const LANGUAGE_FILES_LOCATION = 'src/assets/i18n';
|
||||
|
||||
parseCliInput();
|
||||
|
226
server.ts
Normal file
226
server.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
/**
|
||||
* *** NOTE ON IMPORTING FROM ANGULAR AND NGUNIVERSAL IN THIS FILE ***
|
||||
*
|
||||
* If your application uses third-party dependencies, you'll need to
|
||||
* either use Webpack or the Angular CLI's `bundleDependencies` feature
|
||||
* in order to adequately package them for use on the server without a
|
||||
* node_modules directory.
|
||||
*
|
||||
* However, due to the nature of the CLI's `bundleDependencies`, importing
|
||||
* Angular in this file will create a different instance of Angular than
|
||||
* the version in the compiled application code. This leads to unavoidable
|
||||
* conflicts. Therefore, please do not explicitly import from @angular or
|
||||
* @nguniversal in this file. You can export any needed resources
|
||||
* from your application's main.server.ts file, as seen below with the
|
||||
* import for `ngExpressEngine`.
|
||||
*/
|
||||
|
||||
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 * as cookieParser from 'cookie-parser';
|
||||
import { join } from 'path';
|
||||
|
||||
import { enableProdMode, NgModuleFactory, Type } from '@angular/core';
|
||||
|
||||
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';
|
||||
|
||||
/*
|
||||
* Set path for the browser application's dist folder
|
||||
*/
|
||||
const DIST_FOLDER = join(process.cwd(), 'dist/browser');
|
||||
|
||||
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
|
||||
const { ServerAppModuleNgFactory, LAZY_MODULE_MAP, ngExpressEngine, provideModuleMap } = require('./dist/server/main');
|
||||
|
||||
/*
|
||||
* Create a new express application
|
||||
*/
|
||||
const app = express();
|
||||
|
||||
/*
|
||||
* If production mode is enabled in the environment file:
|
||||
* - Enable Angular's production mode
|
||||
* - Enable compression for response bodies. See [compression](https://github.com/expressjs/compression)
|
||||
*/
|
||||
if (environment.production) {
|
||||
enableProdMode();
|
||||
app.use(compression());
|
||||
}
|
||||
|
||||
/*
|
||||
* Enable request logging
|
||||
* See [morgan](https://github.com/expressjs/morgan)
|
||||
*/
|
||||
app.use(morgan('dev'));
|
||||
|
||||
/*
|
||||
* Add cookie parser middleware
|
||||
* See [morgan](https://github.com/expressjs/cookie-parser)
|
||||
*/
|
||||
app.use(cookieParser());
|
||||
|
||||
/*
|
||||
* Add parser for request bodies
|
||||
* See [morgan](https://github.com/expressjs/body-parser)
|
||||
*/
|
||||
app.use(bodyParser.json());
|
||||
|
||||
/*
|
||||
* Render html pages by running angular server side
|
||||
*/
|
||||
app.engine('html', (_, options, callback) =>
|
||||
ngExpressEngine({
|
||||
bootstrap: ServerAppModuleNgFactory,
|
||||
providers: [
|
||||
{
|
||||
provide: REQUEST,
|
||||
useValue: (options as any).req,
|
||||
},
|
||||
{
|
||||
provide: RESPONSE,
|
||||
useValue: (options as any).req.res,
|
||||
},
|
||||
provideModuleMap(LAZY_MODULE_MAP)
|
||||
],
|
||||
})(_, (options as any), callback)
|
||||
);
|
||||
|
||||
/*
|
||||
* Register the view engines for html and ejs
|
||||
*/
|
||||
app.set('view engine', 'html');
|
||||
|
||||
/*
|
||||
* Set views folder path to directory where template files are stored
|
||||
*/
|
||||
app.set('views', DIST_FOLDER);
|
||||
|
||||
/**
|
||||
* Proxy the sitemaps
|
||||
*/
|
||||
app.use('/sitemap**', createProxyMiddleware({ target: `${environment.rest.baseUrl}/sitemaps`, changeOrigin: true }));
|
||||
|
||||
/*
|
||||
* Adds a cache control header to the response
|
||||
* The cache control value can be configured in the environments file and defaults to max-age=60
|
||||
*/
|
||||
function cacheControl(req, res, next) {
|
||||
// instruct browser to revalidate
|
||||
res.header('Cache-Control', environment.cache.control || 'max-age=60');
|
||||
next();
|
||||
}
|
||||
|
||||
/*
|
||||
* Serve static resources (images, i18n messages, …)
|
||||
*/
|
||||
app.get('*.*', cacheControl, express.static(DIST_FOLDER, { index: false }));
|
||||
|
||||
/*
|
||||
* The callback function to serve server side angular
|
||||
*/
|
||||
function ngApp(req, res) {
|
||||
if (environment.universal.preboot) {
|
||||
res.render(DIST_FOLDER + '/index.html', {
|
||||
req,
|
||||
res,
|
||||
preboot: environment.universal.preboot,
|
||||
async: environment.universal.async,
|
||||
time: environment.universal.time,
|
||||
baseUrl: environment.ui.nameSpace,
|
||||
originUrl: environment.ui.baseUrl,
|
||||
requestUrl: req.originalUrl
|
||||
}, (err, data) => {
|
||||
if (hasNoValue(err) && hasValue(data)) {
|
||||
res.send(data);
|
||||
} else {
|
||||
console.warn('Error in SSR, serving for direct CSR.');
|
||||
if (hasValue(err)) {
|
||||
console.warn('Error details : ', err);
|
||||
}
|
||||
res.sendFile(DIST_FOLDER + '/index.html');
|
||||
}
|
||||
})
|
||||
} else {
|
||||
// If preboot is disabled, just serve the client
|
||||
console.log('Universal off, serving for direct CSR');
|
||||
res.sendFile(DIST_FOLDER + '/index.html');
|
||||
}
|
||||
}
|
||||
|
||||
// Register the ngApp callback function to handle incoming requests
|
||||
app.get('*', ngApp);
|
||||
|
||||
/*
|
||||
* Callback function for when the server has started
|
||||
*/
|
||||
function serverStarted() {
|
||||
console.log(`[${new Date().toTimeString()}] Listening at ${environment.ui.baseUrl}`);
|
||||
}
|
||||
|
||||
/*
|
||||
* Create an HTTPS server with the configured port and host
|
||||
* @param keys SSL credentials
|
||||
*/
|
||||
function createHttpsServer(keys) {
|
||||
https.createServer({
|
||||
key: keys.serviceKey,
|
||||
cert: keys.certificate
|
||||
}, app).listen(environment.ui.port, environment.ui.host, () => {
|
||||
serverStarted();
|
||||
});
|
||||
}
|
||||
|
||||
/*
|
||||
* 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) {
|
||||
let serviceKey;
|
||||
try {
|
||||
serviceKey = fs.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');
|
||||
} catch (e) {
|
||||
console.warn('Certificate not found at ./config/ssl/key.pem');
|
||||
}
|
||||
|
||||
if (serviceKey && certificate) {
|
||||
createHttpsServer({
|
||||
serviceKey: serviceKey,
|
||||
certificate: certificate
|
||||
});
|
||||
} else {
|
||||
|
||||
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0';
|
||||
|
||||
pem.createCertificate({
|
||||
days: 1,
|
||||
selfSigned: true
|
||||
}, (error, keys) => {
|
||||
createHttpsServer(keys);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
app.listen(environment.ui.port, environment.ui.host, () => {
|
||||
serverStarted();
|
||||
});
|
||||
}
|
@@ -1,48 +0,0 @@
|
||||
/**
|
||||
* @author: @AngularClass
|
||||
*/
|
||||
|
||||
/*
|
||||
* When testing with webpack and ES6, we have to do some extra
|
||||
* things to get testing to work right. Because we are gonna write tests
|
||||
* in ES6 too, we have to compile those as well. That's handled in
|
||||
* karma.conf.js with the karma-webpack plugin. This is the entry
|
||||
* file for webpack test. Just like webpack will create a bundle.js
|
||||
* file for our client, when we run test, it will compile and bundle them
|
||||
* all here! Crazy huh. So we need to do some setup
|
||||
*/
|
||||
Error.stackTraceLimit = Infinity;
|
||||
|
||||
require('core-js/es');
|
||||
require('core-js/features/reflect');
|
||||
|
||||
// Typescript emit helpers polyfill
|
||||
require('ts-helpers');
|
||||
|
||||
require('zone.js/dist/zone');
|
||||
require('zone.js/dist/long-stack-trace-zone');
|
||||
require('zone.js/dist/proxy'); // since zone.js 0.6.15
|
||||
require('zone.js/dist/sync-test');
|
||||
require('zone.js/dist/jasmine-patch'); // put here since zone.js 0.6.14
|
||||
require('zone.js/dist/async-test');
|
||||
require('zone.js/dist/fake-async-test');
|
||||
|
||||
// RxJS
|
||||
require('rxjs');
|
||||
|
||||
var testing = require('@angular/core/testing');
|
||||
var browser = require('@angular/platform-browser-dynamic/testing');
|
||||
|
||||
testing.TestBed.initTestEnvironment(
|
||||
browser.BrowserDynamicTestingModule,
|
||||
browser.platformBrowserDynamicTesting()
|
||||
);
|
||||
|
||||
var tests = require.context('./src', true, /\.spec\.ts$/);
|
||||
|
||||
tests.keys().forEach(tests);
|
||||
|
||||
// includes all modules into test coverage
|
||||
const modules = require.context('./src/app', true, /\.module\.ts$/);
|
||||
|
||||
modules.keys().forEach(modules);
|
@@ -0,0 +1,8 @@
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getAccessControlModuleRoute } from '../admin-routing-paths';
|
||||
|
||||
export const GROUP_EDIT_PATH = 'groups';
|
||||
|
||||
export function getGroupEditRoute(id: string) {
|
||||
return new URLCombiner(getAccessControlModuleRoute(), GROUP_EDIT_PATH, id).toString();
|
||||
}
|
@@ -3,14 +3,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getAccessControlModulePath } from '../admin-routing.module';
|
||||
|
||||
const GROUP_EDIT_PATH = 'groups';
|
||||
|
||||
export function getGroupEditPath(id: string) {
|
||||
return new URLCombiner(getAccessControlModulePath(), GROUP_EDIT_PATH, id).toString();
|
||||
}
|
||||
import { GROUP_EDIT_PATH } from './admin-access-control-routing-paths';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
|
@@ -4,90 +4,91 @@
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2">{{labelPrefix + 'head' | translate}}</h2>
|
||||
|
||||
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="forceUpdateEPeople()"
|
||||
<ds-eperson-form *ngIf="isEPersonFormShown" (submitForm)="reset()"
|
||||
(cancelForm)="isEPersonFormShown = false"></ds-eperson-form>
|
||||
|
||||
<div *ngIf="!isEPersonFormShown" class="button-row top d-flex pb-2">
|
||||
<button class="mr-auto btn btn-success addEPerson-button"
|
||||
(click)="isEPersonFormShown = true">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-primary float-right">{{labelPrefix + 'button.see-all' | translate}}</button>
|
||||
</h3>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
|
||||
<div class="col-12 col-sm-3">
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
||||
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
||||
</select>
|
||||
<div *ngIf="!isEPersonFormShown">
|
||||
<div class="button-row top d-flex pb-2">
|
||||
<button class="mr-auto btn btn-success addEPerson-button"
|
||||
(click)="isEPersonFormShown = true">
|
||||
<i class="fas fa-plus"></i>
|
||||
<span class="d-none d-sm-inline">{{labelPrefix + 'button.add' | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-sm-9 col-12">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit"
|
||||
class="search-button btn btn-secondary">{{ labelPrefix + 'search.button' | translate }}</button>
|
||||
</span>
|
||||
|
||||
<h3 id="search" class="border-bottom pb-2">{{labelPrefix + 'search.head' | translate}}
|
||||
<button (click)="clearFormAndResetResult();"
|
||||
class="btn btn-primary float-right">{{labelPrefix + 'button.see-all' | translate}}</button>
|
||||
</h3>
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="row">
|
||||
<div class="col-12 col-sm-3">
|
||||
<select name="scope" id="scope" formControlName="scope" class="form-control" aria-label="Search scope">
|
||||
<option value="metadata">{{labelPrefix + 'search.scope.metadata' | translate}}</option>
|
||||
<option value="email">{{labelPrefix + 'search.scope.email' | translate}}</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="col-sm-9 col-12">
|
||||
<div class="form-group input-group">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
<button type="submit"
|
||||
class="search-button btn btn-secondary">{{ labelPrefix + 'search.button' | translate }}</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(ePeopleDto$ | async)?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="pageInfoState$"
|
||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let epersonDto of (ePeopleDto$ | async)?.page"
|
||||
[ngClass]="{'table-primary' : isActive(epersonDto.eperson) | async}">
|
||||
<td>{{epersonDto.eperson.id}}</td>
|
||||
<td>{{epersonDto.eperson.name}}</td>
|
||||
<td>{{epersonDto.eperson.email}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button class="delete-button" (click)="toggleEditEPerson(epersonDto.eperson)"
|
||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: epersonDto.eperson.name} }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button [disabled]="!epersonDto.ableToDelete" (click)="deleteEPerson(epersonDto.eperson)"
|
||||
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: epersonDto.eperson.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(pageInfoState$ | async)?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{labelPrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(ePeople | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(ePeople | async)?.payload"
|
||||
[collectionSize]="(ePeople | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
|
||||
<div class="table-responsive">
|
||||
<table id="epeople" class="table table-striped table-hover table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{{labelPrefix + 'table.id' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.name' | translate}}</th>
|
||||
<th scope="col">{{labelPrefix + 'table.email' | translate}}</th>
|
||||
<th>{{labelPrefix + 'table.edit' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let eperson of (ePeople | async)?.payload?.page"
|
||||
[ngClass]="{'table-primary' : isActive(eperson) | async}">
|
||||
<td>{{eperson.id}}</td>
|
||||
<td>{{eperson.name}}</td>
|
||||
<td>{{eperson.email}}</td>
|
||||
<td>
|
||||
<div class="btn-group edit-field">
|
||||
<button (click)="toggleEditEPerson(eperson)"
|
||||
class="btn btn-outline-primary btn-sm access-control-editEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.edit' | translate: {name: eperson.name} }}">
|
||||
<i class="fas fa-edit fa-fw"></i>
|
||||
</button>
|
||||
<button (click)="deleteEPerson(eperson)"
|
||||
class="btn btn-outline-danger btn-sm access-control-deleteEPersonButton"
|
||||
title="{{labelPrefix + 'table.edit.buttons.remove' | translate: {name: eperson.name} }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(ePeople | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{labelPrefix + 'no-items' | translate}}
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -15,15 +15,17 @@ import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { FormBuilderService } from '../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
|
||||
import { getMockTranslateService } from '../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||
import { EPeopleRegistryComponent } from './epeople-registry.component';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { getMockFormBuilderService } from '../../../shared/mocks/form-builder-service.mock';
|
||||
import { getMockTranslateService } from '../../../shared/mocks/translate.service.mock';
|
||||
import { TranslateLoaderMock } from '../../../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
|
||||
describe('EPeopleRegistryComponent', () => {
|
||||
let component: EPeopleRegistryComponent;
|
||||
@@ -33,6 +35,8 @@ describe('EPeopleRegistryComponent', () => {
|
||||
|
||||
let mockEPeople;
|
||||
let ePersonDataServiceStub: any;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let modalService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
@@ -82,6 +86,9 @@ describe('EPeopleRegistryComponent', () => {
|
||||
return '/admin/access-control/epeople';
|
||||
}
|
||||
};
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
TestBed.configureTestingModule({
|
||||
@@ -89,16 +96,18 @@ describe('EPeopleRegistryComponent', () => {
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [EPeopleRegistryComponent],
|
||||
providers: [EPeopleRegistryComponent,
|
||||
providers: [
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: new RouterStub() },
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -107,12 +116,14 @@ describe('EPeopleRegistryComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EPeopleRegistryComponent);
|
||||
component = fixture.componentInstance;
|
||||
modalService = (component as any).modalService;
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create EPeopleRegistryComponent', inject([EPeopleRegistryComponent], (comp: EPeopleRegistryComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
it('should create EPeopleRegistryComponent', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
|
||||
it('should display list of ePeople', () => {
|
||||
const ePeopleIdsFound = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
@@ -175,7 +186,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
it('editEPerson form is toggled', () => {
|
||||
const ePeopleIds = fixture.debugElement.queryAll(By.css('#epeople tr td:first-child'));
|
||||
ePersonDataServiceStub.getActiveEPerson().subscribe((activeEPerson: EPerson) => {
|
||||
if (activeEPerson === ePeopleIds[0].nativeElement.textContent) {
|
||||
if (ePeopleIds[0] && activeEPerson === ePeopleIds[0].nativeElement.textContent) {
|
||||
expect(component.isEPersonFormShown).toEqual(false);
|
||||
} else {
|
||||
expect(component.isEPersonFormShown).toEqual(true);
|
||||
@@ -183,6 +194,10 @@ describe('EPeopleRegistryComponent', () => {
|
||||
|
||||
})
|
||||
});
|
||||
|
||||
it('EPerson search section is hidden', () => {
|
||||
expect(fixture.debugElement.query(By.css('#search'))).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -211,4 +226,20 @@ describe('EPeopleRegistryComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete EPerson button when the isAuthorized returns false', () => {
|
||||
let ePeopleDeleteButton;
|
||||
beforeEach(() => {
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(false)
|
||||
});
|
||||
});
|
||||
|
||||
it ('should be disabled', () => {
|
||||
ePeopleDeleteButton = fixture.debugElement.queryAll(By.css('#epeople tr td div button.delete-button'));
|
||||
ePeopleDeleteButton.forEach((deleteButton) => {
|
||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
||||
});
|
||||
|
||||
})
|
||||
})
|
||||
});
|
||||
|
@@ -2,9 +2,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FormBuilder } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { EPersonDataService } from '../../../core/eperson/eperson-data.service';
|
||||
@@ -12,6 +12,16 @@ import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { EpersonDtoModel } from '../../../core/eperson/models/eperson-dto.model';
|
||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
||||
import { ConfirmationModalComponent } from '../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { filter } from 'rxjs/internal/operators/filter';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-epeople-registry',
|
||||
@@ -28,7 +38,17 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* A list of all the current EPeople within the repository or the result of the search
|
||||
*/
|
||||
ePeople: Observable<RemoteData<PaginatedList<EPerson>>>;
|
||||
ePeople$: BehaviorSubject<RemoteData<PaginatedList<EPerson>>> = new BehaviorSubject<RemoteData<PaginatedList<EPerson>>>({} as any);
|
||||
/**
|
||||
* A BehaviorSubject with the list of EpersonDtoModel objects made from the EPeople in the repository or
|
||||
* as the result of the search
|
||||
*/
|
||||
ePeopleDto$: BehaviorSubject<PaginatedList<EpersonDtoModel>> = new BehaviorSubject<PaginatedList<EpersonDtoModel>>({} as any);
|
||||
|
||||
/**
|
||||
* An observable for the pageInfo, needed to pass to the pagination component
|
||||
*/
|
||||
pageInfoState$: BehaviorSubject<PageInfo> = new BehaviorSubject<PageInfo>(undefined);
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of epeople
|
||||
@@ -59,8 +79,11 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
constructor(private epersonService: EPersonDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private formBuilder: FormBuilder,
|
||||
private router: Router) {
|
||||
private router: Router,
|
||||
private modalService: NgbModal,
|
||||
public requestService: RequestService) {
|
||||
this.currentSearchQuery = '';
|
||||
this.currentSearchScope = 'metadata';
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
@@ -70,6 +93,13 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initialisePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will initialise the page
|
||||
*/
|
||||
initialisePage() {
|
||||
this.isEPersonFormShown = false;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery });
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
@@ -84,18 +114,10 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
* @param event
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery })
|
||||
}
|
||||
|
||||
/**
|
||||
* Force-update the list of EPeople by first clearing the cache related to EPeople, then performing
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateEPeople() {
|
||||
this.epersonService.clearEPersonRequests();
|
||||
this.isEPersonFormShown = false;
|
||||
this.search({ query: '', scope: 'metadata' })
|
||||
if (this.config.currentPage !== event) {
|
||||
this.config.currentPage = event;
|
||||
this.search({ scope: this.currentSearchScope, query: this.currentSearchQuery })
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,10 +137,33 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
this.currentSearchScope = scope;
|
||||
this.config.currentPage = 1;
|
||||
}
|
||||
this.ePeople = this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
this.subs.push(this.epersonService.searchByScope(this.currentSearchScope, this.currentSearchQuery, {
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize
|
||||
});
|
||||
}).subscribe((peopleRD) => {
|
||||
this.ePeople$.next(peopleRD)
|
||||
}
|
||||
));
|
||||
|
||||
this.subs.push(this.ePeople$.pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
switchMap((epeople) => {
|
||||
return combineLatest(...epeople.page.map((eperson) => {
|
||||
return this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined).pipe(
|
||||
map((authorized) => {
|
||||
const epersonDtoModel: EpersonDtoModel = new EpersonDtoModel();
|
||||
epersonDtoModel.ableToDelete = authorized;
|
||||
epersonDtoModel.eperson = eperson;
|
||||
return epersonDtoModel;
|
||||
})
|
||||
);
|
||||
})).pipe(map((dtos: EpersonDtoModel[]) => {
|
||||
return new PaginatedList(epeople.pageInfo, dtos);
|
||||
}))
|
||||
})).subscribe((value) => {
|
||||
this.ePeopleDto$.next(value);
|
||||
this.pageInfoState$.next(value.pageInfo);
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,16 +205,26 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
deleteEPerson(ePerson: EPerson) {
|
||||
if (hasValue(ePerson.id)) {
|
||||
this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name }));
|
||||
this.forceUpdateEPeople();
|
||||
} else {
|
||||
this.notificationsService.error(this.translateService.get(this.labelPrefix + 'notification.deleted.failure', { name: ePerson.name }));
|
||||
}
|
||||
this.epersonService.cancelEditEPerson();
|
||||
this.isEPersonFormShown = false;
|
||||
})
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.dso = ePerson;
|
||||
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
|
||||
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
|
||||
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
|
||||
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
|
||||
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
if (hasValue(ePerson.id)) {
|
||||
this.epersonService.deleteEPerson(ePerson).pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||
if (restResponse.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: ePerson.name }));
|
||||
this.reset();
|
||||
} else {
|
||||
const errorResponse = restResponse as ErrorResponse;
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + ePerson.id + ' with code: ' + errorResponse.statusCode + ' and message: ' + errorResponse.errorMessage);
|
||||
}
|
||||
})
|
||||
}}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,6 +232,10 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
* Unsub all subscriptions
|
||||
*/
|
||||
ngOnDestroy(): void {
|
||||
this.cleanupSubscribes();
|
||||
}
|
||||
|
||||
cleanupSubscribes() {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
@@ -199,4 +258,18 @@ export class EPeopleRegistryComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
this.search({ query: '' });
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
reset() {
|
||||
this.epersonService.getBrowseEndpoint().pipe(
|
||||
switchMap((href) => this.requestService.removeByHrefSubstring(href)),
|
||||
filter((isCached) => isCached),
|
||||
take(1)
|
||||
).subscribe(() => {
|
||||
this.cleanupSubscribes();
|
||||
this.initialisePage();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { EPersonMock } from '../../../shared/testing/eperson-mock';
|
||||
import { EPeopleRegistryCancelEPersonAction, EPeopleRegistryEditEPersonAction } from './epeople-registry.actions';
|
||||
import { ePeopleRegistryReducer, EPeopleRegistryState } from './epeople-registry.reducers';
|
||||
import { EPersonMock } from '../../../shared/testing/eperson.mock';
|
||||
|
||||
const initialState: EPeopleRegistryState = {
|
||||
editEPerson: null,
|
||||
|
@@ -14,6 +14,18 @@
|
||||
[formLayout]="formLayout"
|
||||
(cancel)="onCancel()"
|
||||
(submitForm)="onSubmit()">
|
||||
<button class="btn btn-light" [disabled]="!(canReset$ | async)">
|
||||
<i class="fa fa-key"></i> {{'admin.access-control.epeople.actions.reset' | translate}}
|
||||
</button>
|
||||
<button class="btn btn-light delete-button" [disabled]="!(canDelete$ | async)" (click)="delete()">
|
||||
<i class="fa fa-trash"></i> {{'admin.access-control.epeople.actions.delete' | translate}}
|
||||
</button>
|
||||
<button *ngIf="!isImpersonated" class="btn btn-light" [ngClass]="{'d-none' : !(canImpersonate$ | async)}" (click)="impersonate()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||
</button>
|
||||
<button *ngIf="isImpersonated" class="btn btn-light" (click)="stopImpersonating()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.stop-impersonating' | translate}}
|
||||
</button>
|
||||
</ds-form>
|
||||
|
||||
<div *ngIf="epersonService.getActiveEPerson() | async">
|
||||
|
@@ -1,45 +1,44 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { BrowserModule, By } from '@angular/platform-browser';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
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 { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { DSOChangeAnalyzer } from '../../../../core/data/dso-change-analyzer.service';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { FindListOptions } from '../../../../core/data/request.models';
|
||||
import { EPersonDataService } from '../../../../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../../../../core/eperson/models/eperson.model';
|
||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../../shared/mocks/mock-form-builder-service';
|
||||
import { getMockTranslateService } from '../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson-mock';
|
||||
import { MockTranslateLoader } from '../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||
import { EPeopleRegistryComponent } from '../epeople-registry.component';
|
||||
import { EPersonFormComponent } from './eperson-form.component';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../../shared/testing/eperson.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthServiceStub } from '../../../../shared/testing/auth-service.stub';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
|
||||
describe('EPersonFormComponent', () => {
|
||||
let component: EPersonFormComponent;
|
||||
let fixture: ComponentFixture<EPersonFormComponent>;
|
||||
let translateService: TranslateService;
|
||||
let builderService: FormBuilderService;
|
||||
|
||||
let mockEPeople;
|
||||
let ePersonDataServiceStub: any;
|
||||
let authService: AuthServiceStub;
|
||||
let authorizationService: AuthorizationDataService;
|
||||
let groupsDataService: GroupDataService;
|
||||
|
||||
beforeEach(async(() => {
|
||||
mockEPeople = [EPersonMock, EPersonMock2];
|
||||
@@ -103,29 +102,32 @@ describe('EPersonFormComponent', () => {
|
||||
}
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
authService = new AuthServiceStub();
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true)
|
||||
});
|
||||
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([])),
|
||||
getGroupRegistryRouterLink: ''
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
declarations: [EPeopleRegistryComponent, EPersonFormComponent],
|
||||
providers: [EPersonFormComponent,
|
||||
declarations: [EPersonFormComponent],
|
||||
providers: [
|
||||
{ provide: EPersonDataService, useValue: ePersonDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: GroupDataService, useValue: groupsDataService },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: HttpClient, useValue: {} },
|
||||
{ provide: ObjectCacheService, useValue: {} },
|
||||
{ provide: UUIDService, useValue: {} },
|
||||
{ provide: Store, useValue: {} },
|
||||
{ provide: RemoteDataBuildService, useValue: {} },
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
EPeopleRegistryComponent
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
||||
{ provide: RequestService, useValue: jasmine.createSpyObj('requestService', ['removeByHrefSubstring'])}
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -137,9 +139,9 @@ describe('EPersonFormComponent', () => {
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create EPersonFormComponent', inject([EPersonFormComponent], (comp: EPersonFormComponent) => {
|
||||
expect(comp).toBeDefined();
|
||||
}));
|
||||
it('should create EPersonFormComponent', () => {
|
||||
expect(component).toBeDefined();
|
||||
});
|
||||
|
||||
describe('when submitting the form', () => {
|
||||
let firstName;
|
||||
@@ -228,4 +230,89 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('impersonate', () => {
|
||||
let ePersonId;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'impersonate').and.callThrough();
|
||||
ePersonId = 'testEPersonId';
|
||||
component.epersonInitial = Object.assign(new EPerson(), {
|
||||
id: ePersonId
|
||||
});
|
||||
component.impersonate();
|
||||
});
|
||||
|
||||
it('should call authService.impersonate', () => {
|
||||
expect(authService.impersonate).toHaveBeenCalledWith(ePersonId);
|
||||
});
|
||||
|
||||
it('should set isImpersonated to true', () => {
|
||||
expect(component.isImpersonated).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopImpersonating', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'stopImpersonatingAndRefresh').and.callThrough();
|
||||
component.stopImpersonating();
|
||||
});
|
||||
|
||||
it('should call authService.stopImpersonatingAndRefresh', () => {
|
||||
expect(authService.stopImpersonatingAndRefresh).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should set isImpersonated to false', () => {
|
||||
expect(component.isImpersonated).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('delete', () => {
|
||||
|
||||
let ePersonId;
|
||||
let eperson: EPerson;
|
||||
let modalService;
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(authService, 'impersonate').and.callThrough();
|
||||
ePersonId = 'testEPersonId';
|
||||
eperson = EPersonMock;
|
||||
component.epersonInitial = eperson;
|
||||
component.canDelete$ = observableOf(true);
|
||||
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
||||
modalService = (component as any).modalService;
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
fixture.detectChanges()
|
||||
|
||||
});
|
||||
|
||||
it ('the delete button should be active if the eperson can be deleted', () => {
|
||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||
expect(deleteButton.nativeElement.disabled).toBe(false);
|
||||
});
|
||||
|
||||
it ('the delete button should be disabled if the eperson cannot be deleted', () => {
|
||||
component.canDelete$ = observableOf(false);
|
||||
fixture.detectChanges()
|
||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||
expect(deleteButton.nativeElement.disabled).toBe(true);
|
||||
});
|
||||
|
||||
it ('should call the epersonFormComponent delete when clicked on the button' , () => {
|
||||
spyOn(component, 'delete').and.stub();
|
||||
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(observableOf(new RestResponse(true, 204, 'No Content')));
|
||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||
deleteButton.triggerEventHandler('click', null);
|
||||
expect(component.delete).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it ('should call the epersonService delete when clicked on the button' , () => {
|
||||
// ePersonDataServiceStub.activeEPerson = eperson;
|
||||
spyOn(component.epersonService, 'deleteEPerson').and.returnValue(observableOf(new RestResponse(true, 204, 'No Content')));
|
||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||
expect(deleteButton.nativeElement.disabled).toBe(false);
|
||||
deleteButton.triggerEventHandler('click', null);
|
||||
fixture.detectChanges()
|
||||
expect(component.epersonService.deleteEPerson).toHaveBeenCalledWith(eperson);
|
||||
});
|
||||
})
|
||||
});
|
||||
|
@@ -7,9 +7,9 @@ import {
|
||||
DynamicInputModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Subscription, combineLatest } from 'rxjs';
|
||||
import { Subscription, combineLatest, of } from 'rxjs';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
@@ -22,6 +22,12 @@ import { hasValue } from '../../../../shared/empty.util';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||
import { AuthService } from '../../../../core/auth/auth.service';
|
||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||
import { ConfirmationModalComponent } from '../../../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RequestService } from '../../../../core/data/request.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-eperson-form',
|
||||
@@ -105,6 +111,22 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@Output() cancelForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
/**
|
||||
* Observable whether or not the admin is allowed to reset the EPerson's password
|
||||
* TODO: Initialize the observable once the REST API supports this (currently hardcoded to return false)
|
||||
*/
|
||||
canReset$: Observable<boolean> = of(false);
|
||||
|
||||
/**
|
||||
* Observable whether or not the admin is allowed to delete the EPerson
|
||||
*/
|
||||
canDelete$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* Observable whether or not the admin is allowed to impersonate the EPerson
|
||||
*/
|
||||
canImpersonate$: Observable<boolean>;
|
||||
|
||||
/**
|
||||
* List of subscriptions
|
||||
*/
|
||||
@@ -129,24 +151,43 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
epersonInitial: EPerson;
|
||||
|
||||
/**
|
||||
* Whether or not this EPerson is currently being impersonated
|
||||
*/
|
||||
isImpersonated = false;
|
||||
|
||||
constructor(public epersonService: EPersonDataService,
|
||||
public groupsDataService: GroupDataService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,) {
|
||||
private notificationsService: NotificationsService,
|
||||
private authService: AuthService,
|
||||
private authorizationService: AuthorizationDataService,
|
||||
private modalService: NgbModal,
|
||||
public requestService: RequestService) {
|
||||
this.subs.push(this.epersonService.getActiveEPerson().subscribe((eperson: EPerson) => {
|
||||
this.epersonInitial = eperson;
|
||||
if (hasValue(eperson)) {
|
||||
this.isImpersonated = this.authService.isImpersonatingUser(eperson.id);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.initialisePage();
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will initialise the page
|
||||
*/
|
||||
initialisePage() {
|
||||
combineLatest(
|
||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||
this.translateService.get(`${this.messagePrefix}.email`),
|
||||
this.translateService.get(`${this.messagePrefix}.canLogIn`),
|
||||
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
|
||||
this.translateService.get(`${this.messagePrefix}.emailHint`),
|
||||
this.translateService.get(`${this.messagePrefix}.firstName`),
|
||||
this.translateService.get(`${this.messagePrefix}.lastName`),
|
||||
this.translateService.get(`${this.messagePrefix}.email`),
|
||||
this.translateService.get(`${this.messagePrefix}.canLogIn`),
|
||||
this.translateService.get(`${this.messagePrefix}.requireCertificate`),
|
||||
this.translateService.get(`${this.messagePrefix}.emailHint`),
|
||||
).subscribe(([firstName, lastName, email, canLogIn, requireCertificate, emailHint]) => {
|
||||
this.firstName = new DynamicInputModel({
|
||||
id: 'firstName',
|
||||
@@ -178,19 +219,19 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
hint: emailHint
|
||||
});
|
||||
this.canLogIn = new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'canLogIn',
|
||||
label: canLogIn,
|
||||
name: 'canLogIn',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
|
||||
});
|
||||
{
|
||||
id: 'canLogIn',
|
||||
label: canLogIn,
|
||||
name: 'canLogIn',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.canLogIn : true)
|
||||
});
|
||||
this.requireCertificate = new DynamicCheckboxModel(
|
||||
{
|
||||
id: 'requireCertificate',
|
||||
label: requireCertificate,
|
||||
name: 'requireCertificate',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
|
||||
});
|
||||
{
|
||||
id: 'requireCertificate',
|
||||
label: requireCertificate,
|
||||
name: 'requireCertificate',
|
||||
value: (this.epersonInitial != null ? this.epersonInitial.requireCertificate : false)
|
||||
});
|
||||
this.formModel = [
|
||||
this.firstName,
|
||||
this.lastName,
|
||||
@@ -214,6 +255,12 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
requireCertificate: eperson != null ? eperson.requireCertificate : false
|
||||
});
|
||||
}));
|
||||
this.canImpersonate$ = this.epersonService.getActiveEPerson().pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, hasValue(eperson) ? eperson.self : undefined))
|
||||
);
|
||||
this.canDelete$ = this.epersonService.getActiveEPerson().pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -364,6 +411,51 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start impersonating the EPerson
|
||||
*/
|
||||
impersonate() {
|
||||
this.authService.impersonate(this.epersonInitial.id);
|
||||
this.isImpersonated = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the EPerson from the Repository. The EPerson will be the only that this form is showing.
|
||||
* It'll either show a success or error message depending on whether the delete was successful or not.
|
||||
*/
|
||||
delete() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.dso = eperson;
|
||||
modalRef.componentInstance.headerLabel = 'confirmation-modal.delete-eperson.header';
|
||||
modalRef.componentInstance.infoLabel = 'confirmation-modal.delete-eperson.info';
|
||||
modalRef.componentInstance.cancelLabel = 'confirmation-modal.delete-eperson.cancel';
|
||||
modalRef.componentInstance.confirmLabel = 'confirmation-modal.delete-eperson.confirm';
|
||||
modalRef.componentInstance.response.pipe(take(1)).subscribe((confirm: boolean) => {
|
||||
if (confirm) {
|
||||
if (hasValue(eperson.id)) {
|
||||
this.epersonService.deleteEPerson(eperson).pipe(take(1)).subscribe((restResponse: RestResponse) => {
|
||||
if (restResponse.isSuccessful) {
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + 'notification.deleted.success', { name: eperson.name }));
|
||||
this.reset();
|
||||
} else {
|
||||
this.notificationsService.error('Error occured when trying to delete EPerson with id: ' + eperson.id + ' with code: ' + restResponse.statusCode + ' and message: ' + restResponse.statusText);
|
||||
}
|
||||
this.cancelForm.emit();
|
||||
})
|
||||
}}
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop impersonating the EPerson
|
||||
*/
|
||||
stopImpersonating() {
|
||||
this.authService.stopImpersonatingAndRefresh();
|
||||
this.isImpersonated = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the current edit when component is destroyed & unsub all subscriptions
|
||||
*/
|
||||
@@ -371,4 +463,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
this.onCancel();
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* This method will ensure that the page gets reset and that the cache is cleared
|
||||
*/
|
||||
reset() {
|
||||
this.epersonService.getActiveEPerson().pipe(take(1)).subscribe((eperson: EPerson) => {
|
||||
this.requestService.removeByHrefSubstring(eperson.self);
|
||||
});
|
||||
this.initialisePage();
|
||||
}
|
||||
}
|
||||
|
@@ -23,15 +23,15 @@ import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service
|
||||
import { PageInfo } from '../../../../core/shared/page-info.model';
|
||||
import { UUIDService } from '../../../../core/shared/uuid.service';
|
||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockRouter } from '../../../../shared/mocks/mock-router';
|
||||
import { getMockTranslateService } from '../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { GroupMock, GroupMock2 } from '../../../../shared/testing/group-mock';
|
||||
import { MockTranslateLoader } from '../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
|
||||
import { GroupFormComponent } from './group-form.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||
import { getMockFormBuilderService } from '../../../../shared/mocks/form-builder-service.mock';
|
||||
import { getMockTranslateService } from '../../../../shared/mocks/translate.service.mock';
|
||||
import { TranslateLoaderMock } from '../../../../shared/testing/translate-loader.mock';
|
||||
import { RouterMock } from '../../../../shared/mocks/router.mock';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
|
||||
describe('GroupFormComponent', () => {
|
||||
let component: GroupFormComponent;
|
||||
@@ -90,13 +90,13 @@ describe('GroupFormComponent', () => {
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
router = new MockRouter();
|
||||
router = new RouterMock();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, NgbModule, FormsModule, ReactiveFormsModule, BrowserModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
@@ -16,17 +16,17 @@ import { EPerson } from '../../../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../../../core/eperson/models/group.model';
|
||||
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockRouter } from '../../../../../shared/mocks/mock-router';
|
||||
import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson-mock';
|
||||
import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock';
|
||||
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils';
|
||||
import { MembersListComponent } from './members-list.component';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../../../shared/testing/eperson.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
import { getMockTranslateService } from '../../../../../shared/mocks/translate.service.mock';
|
||||
import { getMockFormBuilderService } from '../../../../../shared/mocks/form-builder-service.mock';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub';
|
||||
import { RouterMock } from '../../../../../shared/mocks/router.mock';
|
||||
|
||||
describe('MembersListComponent', () => {
|
||||
let component: MembersListComponent;
|
||||
@@ -119,7 +119,7 @@ describe('MembersListComponent', () => {
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
@@ -129,7 +129,7 @@ describe('MembersListComponent', () => {
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: FormBuilderService, useValue: builderService },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -14,16 +14,16 @@ import { GroupDataService } from '../../../../../core/eperson/group-data.service
|
||||
import { Group } from '../../../../../core/eperson/models/group.model';
|
||||
import { PageInfo } from '../../../../../core/shared/page-info.model';
|
||||
import { FormBuilderService } from '../../../../../shared/form/builder/form-builder.service';
|
||||
import { getMockFormBuilderService } from '../../../../../shared/mocks/mock-form-builder-service';
|
||||
import { MockRouter } from '../../../../../shared/mocks/mock-router';
|
||||
import { getMockTranslateService } from '../../../../../shared/mocks/mock-translate.service';
|
||||
import { NotificationsService } from '../../../../../shared/notifications/notifications.service';
|
||||
import { GroupMock, GroupMock2 } from '../../../../../shared/testing/group-mock';
|
||||
import { MockTranslateLoader } from '../../../../../shared/testing/mock-translate-loader';
|
||||
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils';
|
||||
import { SubgroupsListComponent } from './subgroups-list.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
import { RouterMock } from '../../../../../shared/mocks/router.mock';
|
||||
import { getMockFormBuilderService } from '../../../../../shared/mocks/form-builder-service.mock';
|
||||
import { getMockTranslateService } from '../../../../../shared/mocks/translate.service.mock';
|
||||
import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock';
|
||||
import { NotificationsServiceStub } from '../../../../../shared/testing/notifications-service.stub';
|
||||
|
||||
describe('SubgroupsListComponent', () => {
|
||||
let component: SubgroupsListComponent;
|
||||
@@ -82,7 +82,7 @@ describe('SubgroupsListComponent', () => {
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
}
|
||||
};
|
||||
routerStub = new MockRouter();
|
||||
routerStub = new RouterMock();
|
||||
builderService = getMockFormBuilderService();
|
||||
translateService = getMockTranslateService();
|
||||
TestBed.configureTestingModule({
|
||||
@@ -90,7 +90,7 @@ describe('SubgroupsListComponent', () => {
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
|
@@ -15,15 +15,15 @@ import { EPerson } from '../../../core/eperson/models/eperson.model';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { MockRouter } from '../../../shared/mocks/mock-router';
|
||||
import { MockTranslateLoader } from '../../../shared/mocks/mock-translate-loader';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson-mock';
|
||||
import { GroupMock, GroupMock2 } from '../../../shared/testing/group-mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { routeServiceStub } from '../../../shared/testing/route-service-stub';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||
import { GroupsRegistryComponent } from './groups-registry.component';
|
||||
import { EPersonMock, EPersonMock2 } from '../../../shared/testing/eperson.mock';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { TranslateLoaderMock } from '../../../shared/testing/translate-loader.mock';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { routeServiceStub } from '../../../shared/testing/route-service.stub';
|
||||
import { RouterMock } from '../../../shared/mocks/router.mock';
|
||||
|
||||
describe('GroupRegistryComponent', () => {
|
||||
let component: GroupsRegistryComponent;
|
||||
@@ -82,7 +82,7 @@ describe('GroupRegistryComponent', () => {
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: MockTranslateLoader
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
],
|
||||
@@ -92,7 +92,7 @@ describe('GroupRegistryComponent', () => {
|
||||
{ provide: GroupDataService, useValue: groupsDataServiceStub },
|
||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||
{ provide: RouteService, useValue: routeServiceStub },
|
||||
{ provide: Router, useValue: new MockRouter() },
|
||||
{ provide: Router, useValue: new RouterMock() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
|
@@ -47,7 +47,7 @@ export class GroupsRegistryComponent implements OnInit {
|
||||
// Current search in groups registry
|
||||
currentSearchQuery: string;
|
||||
|
||||
constructor(private groupService: GroupDataService,
|
||||
constructor(public groupService: GroupDataService,
|
||||
private ePersonDataService: EPersonDataService,
|
||||
private translateService: TranslateService,
|
||||
private notificationsService: NotificationsService,
|
||||
|
@@ -0,0 +1,4 @@
|
||||
<div class="container">
|
||||
<h2>{{'admin.curation-tasks.header' |translate }}</h2>
|
||||
<ds-curation-form></ds-curation-form>
|
||||
</div>
|
@@ -0,0 +1,28 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks.component';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
|
||||
describe('AdminCurationTasksComponent', () => {
|
||||
let comp: AdminCurationTasksComponent;
|
||||
let fixture: ComponentFixture<AdminCurationTasksComponent>;
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [AdminCurationTasksComponent],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminCurationTasksComponent);
|
||||
comp = fixture.componentInstance;
|
||||
});
|
||||
describe('init', () => {
|
||||
it('should initialise the comp', () => {
|
||||
expect(comp).toBeDefined();
|
||||
expect(fixture.debugElement.nativeElement.innerHTML).toContain('ds-curation-form');
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,12 @@
|
||||
import { Component } from '@angular/core';
|
||||
|
||||
/**
|
||||
* Component responsible for rendering the system wide Curation Task UI
|
||||
*/
|
||||
@Component({
|
||||
selector: 'ds-admin-curation-task',
|
||||
templateUrl: './admin-curation-tasks.component.html',
|
||||
})
|
||||
export class AdminCurationTasksComponent {
|
||||
|
||||
}
|
@@ -0,0 +1,15 @@
|
||||
<div class="container">
|
||||
<h2 id="header">{{'admin.metadata-import.page.header' | translate}}</h2>
|
||||
<p>{{'admin.metadata-import.page.help' | translate}}</p>
|
||||
|
||||
<ds-file-dropzone-no-uploader
|
||||
(onFileAdded)="setFile($event)"
|
||||
[dropMessageLabel]="'admin.metadata-import.page.dropMsg'"
|
||||
[dropMessageLabelReplacement]="'admin.metadata-import.page.dropMsgReplace'">
|
||||
</ds-file-dropzone-no-uploader>
|
||||
|
||||
<button class="btn btn-secondary" id="backButton"
|
||||
(click)="this.onReturn();">{{'admin.metadata-import.page.button.return' | translate}}</button>
|
||||
<button class="btn btn-primary" id="proceedButton"
|
||||
(click)="this.importMetadata();">{{'admin.metadata-import.page.button.proceed' | translate}}</button>
|
||||
</div>
|
@@ -0,0 +1,151 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { async, ComponentFixture, fakeAsync, TestBed } from '@angular/core/testing';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs/internal/observable/of';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import {
|
||||
METADATA_IMPORT_SCRIPT_NAME,
|
||||
ScriptDataService
|
||||
} from '../../core/data/processes/script-data.service';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||
import { FileValueAccessorDirective } from '../../shared/utils/file-value-accessor.directive';
|
||||
import { FileValidator } from '../../shared/utils/require-file.validator';
|
||||
import { MetadataImportPageComponent } from './metadata-import-page.component';
|
||||
|
||||
describe('MetadataImportPageComponent', () => {
|
||||
let comp: MetadataImportPageComponent;
|
||||
let fixture: ComponentFixture<MetadataImportPageComponent>;
|
||||
|
||||
let user;
|
||||
|
||||
let notificationService: NotificationsServiceStub;
|
||||
let scriptService: any;
|
||||
let router;
|
||||
let authService;
|
||||
let locationStub;
|
||||
|
||||
function init() {
|
||||
notificationService = new NotificationsServiceStub();
|
||||
scriptService = jasmine.createSpyObj('scriptService',
|
||||
{
|
||||
invoke: observableOf({
|
||||
response:
|
||||
{
|
||||
isSuccessful: true,
|
||||
resourceSelfLinks: ['https://localhost:8080/api/core/processes/45']
|
||||
}
|
||||
})
|
||||
}
|
||||
);
|
||||
user = Object.assign(new EPerson(), {
|
||||
id: 'userId',
|
||||
email: 'user@test.com'
|
||||
});
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
getAuthenticatedUserFromStore: observableOf(user)
|
||||
});
|
||||
router = jasmine.createSpyObj('router', {
|
||||
navigateByUrl: jasmine.createSpy('navigateByUrl')
|
||||
});
|
||||
locationStub = jasmine.createSpyObj('location', {
|
||||
back: jasmine.createSpy('back')
|
||||
});
|
||||
}
|
||||
|
||||
beforeEach(async(() => {
|
||||
init();
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
FormsModule,
|
||||
TranslateModule.forRoot(),
|
||||
RouterTestingModule.withRoutes([])
|
||||
],
|
||||
declarations: [MetadataImportPageComponent, FileValueAccessorDirective, FileValidator],
|
||||
providers: [
|
||||
{ provide: NotificationsService, useValue: notificationService },
|
||||
{ provide: ScriptDataService, useValue: scriptService },
|
||||
{ provide: Router, useValue: router },
|
||||
{ provide: AuthService, useValue: authService },
|
||||
{ provide: Location, useValue: locationStub },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(MetadataImportPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(comp).toBeTruthy();
|
||||
});
|
||||
|
||||
describe('if back button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const proceed = fixture.debugElement.query(By.css('#backButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('should do location.back', () => {
|
||||
expect(locationStub.back).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('if file is set', () => {
|
||||
let fileMock: File;
|
||||
|
||||
beforeEach(() => {
|
||||
fileMock = new File([''], 'filename.txt', { type: 'text/plain' });
|
||||
comp.setFile(fileMock);
|
||||
});
|
||||
|
||||
describe('if proceed button is pressed', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('metadata-import script is invoked with its -e currentUserEmail, -f fileName and the mockFile', () => {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-e', value: user.email }),
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: 'filename.txt' }),
|
||||
];
|
||||
expect(scriptService.invoke).toHaveBeenCalledWith(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [fileMock]);
|
||||
});
|
||||
it('success notification is shown', () => {
|
||||
expect(notificationService.success).toHaveBeenCalled();
|
||||
});
|
||||
it('redirected to process page', () => {
|
||||
expect(router.navigateByUrl).toHaveBeenCalledWith('/processes/45');
|
||||
});
|
||||
});
|
||||
|
||||
describe('if proceed is pressed; but script invoke fails', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
jasmine.getEnv().allowRespy(true);
|
||||
spyOn(scriptService, 'invoke').and.returnValue(observableOf({
|
||||
response:
|
||||
{
|
||||
isSuccessful: false,
|
||||
}
|
||||
}));
|
||||
const proceed = fixture.debugElement.query(By.css('#proceedButton')).nativeElement;
|
||||
proceed.click();
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
it('error notification is shown', () => {
|
||||
expect(notificationService.error).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@@ -0,0 +1,106 @@
|
||||
import { Location } from '@angular/common';
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { METADATA_IMPORT_SCRIPT_NAME, ScriptDataService } from '../../core/data/processes/script-data.service';
|
||||
import { RequestEntry } from '../../core/data/request.reducer';
|
||||
import { EPerson } from '../../core/eperson/models/eperson.model';
|
||||
import { ProcessParameter } from '../../process-page/processes/process-parameter.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-import-page',
|
||||
templateUrl: './metadata-import-page.component.html'
|
||||
})
|
||||
|
||||
/**
|
||||
* Component that represents a metadata import page for administrators
|
||||
*/
|
||||
export class MetadataImportPageComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The current value of the file
|
||||
*/
|
||||
fileObject: File;
|
||||
|
||||
/**
|
||||
* The authenticated user's email
|
||||
*/
|
||||
private currentUserEmail$: Observable<string>;
|
||||
|
||||
public constructor(protected authService: AuthService,
|
||||
private location: Location,
|
||||
protected translate: TranslateService,
|
||||
protected notificationsService: NotificationsService,
|
||||
private scriptDataService: ScriptDataService,
|
||||
private router: Router) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Set file
|
||||
* @param file
|
||||
*/
|
||||
setFile(file) {
|
||||
this.fileObject = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method provided by Angular. Invoked after the constructor.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.currentUserEmail$ = this.authService.getAuthenticatedUserFromStore().pipe(
|
||||
map((user: EPerson) => user.email)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* When return button is pressed go to previous location
|
||||
*/
|
||||
public onReturn() {
|
||||
this.location.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts import-metadata script with -e currentUserEmail -f fileName (and the selected file)
|
||||
*/
|
||||
public importMetadata() {
|
||||
if (this.fileObject == null) {
|
||||
this.notificationsService.error(this.translate.get('admin.metadata-import.page.error.addFile'));
|
||||
} else {
|
||||
this.currentUserEmail$.pipe(
|
||||
switchMap((email: string) => {
|
||||
if (isNotEmpty(email)) {
|
||||
const parameterValues: ProcessParameter[] = [
|
||||
Object.assign(new ProcessParameter(), { name: '-e', value: email }),
|
||||
Object.assign(new ProcessParameter(), { name: '-f', value: this.fileObject.name }),
|
||||
];
|
||||
return this.scriptDataService.invoke(METADATA_IMPORT_SCRIPT_NAME, parameterValues, [this.fileObject])
|
||||
.pipe(
|
||||
take(1),
|
||||
map((requestEntry: RequestEntry) => {
|
||||
if (requestEntry.response.isSuccessful) {
|
||||
const title = this.translate.get('process.new.notification.success.title');
|
||||
const content = this.translate.get('process.new.notification.success.content');
|
||||
this.notificationsService.success(title, content);
|
||||
const response: any = requestEntry.response;
|
||||
if (isNotEmpty(response.resourceSelfLinks)) {
|
||||
const processNumber = response.resourceSelfLinks[0].split('/').pop();
|
||||
this.router.navigateByUrl('/processes/' + processNumber);
|
||||
}
|
||||
} else {
|
||||
const title = this.translate.get('process.new.notification.error.title');
|
||||
const content = this.translate.get('process.new.notification.error.content');
|
||||
this.notificationsService.error(title, content);
|
||||
}
|
||||
}));
|
||||
}
|
||||
}),
|
||||
take(1)
|
||||
).subscribe();
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,8 @@
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getRegistriesModuleRoute } from '../admin-routing-paths';
|
||||
|
||||
export const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
||||
|
||||
export function getBitstreamFormatsModuleRoute() {
|
||||
return new URLCombiner(getRegistriesModuleRoute(), BITSTREAMFORMATS_MODULE_PATH).toString();
|
||||
}
|
@@ -2,28 +2,34 @@ import { MetadataRegistryComponent } from './metadata-registry/metadata-registry
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NgModule } from '@angular/core';
|
||||
import { MetadataSchemaComponent } from './metadata-schema/metadata-schema.component';
|
||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||
import { getRegistriesModulePath } from '../admin-routing.module';
|
||||
|
||||
const BITSTREAMFORMATS_MODULE_PATH = 'bitstream-formats';
|
||||
|
||||
export function getBitstreamFormatsModulePath() {
|
||||
return new URLCombiner(getRegistriesModulePath(), BITSTREAMFORMATS_MODULE_PATH).toString();
|
||||
}
|
||||
import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { BITSTREAMFORMATS_MODULE_PATH } from './admin-registries-routing-paths';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
RouterModule.forChild([
|
||||
{path: 'metadata', component: MetadataRegistryComponent, data: {title: 'admin.registries.metadata.title'}},
|
||||
{
|
||||
path: 'metadata/:schemaName',
|
||||
component: MetadataSchemaComponent,
|
||||
data: {title: 'admin.registries.schema.title'}
|
||||
path: 'metadata',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
data: {title: 'admin.registries.metadata.title', breadcrumbKey: 'admin.registries.metadata'},
|
||||
children: [
|
||||
{
|
||||
path: '',
|
||||
component: MetadataRegistryComponent
|
||||
},
|
||||
{
|
||||
path: ':schemaName',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: MetadataSchemaComponent,
|
||||
data: {title: 'admin.registries.schema.title', breadcrumbKey: 'admin.registries.schema'}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMATS_MODULE_PATH,
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
loadChildren: './bitstream-formats/bitstream-formats.module#BitstreamFormatsModule',
|
||||
data: {title: 'admin.registries.bitstream-formats.title'}
|
||||
data: {title: 'admin.registries.bitstream-formats.title', breadcrumbKey: 'admin.registries.bitstream-formats'}
|
||||
},
|
||||
])
|
||||
]
|
||||
|
@@ -11,8 +11,8 @@ import { BitstreamFormatDataService } from '../../../../core/data/bitstream-form
|
||||
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { RouterStub } from '../../../../shared/testing/router.stub';
|
||||
import { AddBitstreamFormatComponent } from './add-bitstream-format.component';
|
||||
|
||||
describe('AddBitstreamFormatComponent', () => {
|
||||
|
@@ -5,8 +5,8 @@ import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
|
||||
|
||||
/**
|
||||
* This component renders the page to create a new bitstream format.
|
||||
@@ -37,7 +37,7 @@ export class AddBitstreamFormatComponent {
|
||||
if (response.isSuccessful) {
|
||||
this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.create.success.head'),
|
||||
this.translateService.get('admin.registries.bitstream-formats.create.success.content'));
|
||||
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||
this.router.navigate([getBitstreamFormatsModuleRoute()]);
|
||||
this.bitstreamFormatDataService.clearBitStreamFormatRequests().subscribe();
|
||||
} else {
|
||||
this.notificationService.error(this.translateService.get('admin.registries.bitstream-formats.create.failure.head'),
|
||||
|
@@ -4,6 +4,7 @@ import { BitstreamFormatsResolver } from './bitstream-formats.resolver';
|
||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format/edit-bitstream-format.component';
|
||||
import { BitstreamFormatsComponent } from './bitstream-formats.component';
|
||||
import { AddBitstreamFormatComponent } from './add-bitstream-format/add-bitstream-format.component';
|
||||
import { I18nBreadcrumbResolver } from '../../../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
|
||||
const BITSTREAMFORMAT_EDIT_PATH = ':id/edit';
|
||||
const BITSTREAMFORMAT_ADD_PATH = 'add';
|
||||
@@ -17,14 +18,18 @@ const BITSTREAMFORMAT_ADD_PATH = 'add';
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMAT_ADD_PATH,
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: AddBitstreamFormatComponent,
|
||||
data: {breadcrumbKey: 'admin.registries.bitstream-formats.create'}
|
||||
},
|
||||
{
|
||||
path: BITSTREAMFORMAT_EDIT_PATH,
|
||||
component: EditBitstreamFormatComponent,
|
||||
resolve: {
|
||||
bitstreamFormat: BitstreamFormatsResolver
|
||||
}
|
||||
bitstreamFormat: BitstreamFormatsResolver,
|
||||
breadcrumb: I18nBreadcrumbResolver
|
||||
},
|
||||
data: {breadcrumbKey: 'admin.registries.bitstream-formats.edit'}
|
||||
},
|
||||
])
|
||||
],
|
||||
|
@@ -11,10 +11,10 @@ import { PaginationComponent } from '../../../shared/pagination/pagination.compo
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { BitstreamFormat } from '../../../core/shared/bitstream-format.model';
|
||||
import { BitstreamFormatSupportLevel } from '../../../core/shared/bitstream-format-support-level';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
@@ -209,7 +209,7 @@ describe('BitstreamFormatsComponent', () => {
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: observableOf(true),
|
||||
delete: observableOf({ isSuccessful: true }),
|
||||
clearBitStreamFormatRequests: observableOf('cleared')
|
||||
});
|
||||
|
||||
|
@@ -11,6 +11,7 @@ import { hasValue } from '../../../shared/empty.util';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
|
||||
/**
|
||||
* This component renders a list of bitstream formats
|
||||
@@ -64,7 +65,7 @@ export class BitstreamFormatsComponent implements OnInit {
|
||||
const tasks$ = [];
|
||||
for (const format of formats) {
|
||||
if (hasValue(format.id)) {
|
||||
tasks$.push(this.bitstreamFormatService.delete(format.id));
|
||||
tasks$.push(this.bitstreamFormatService.delete(format.id).pipe(map((response: RestResponse) => response.isSuccessful)));
|
||||
}
|
||||
}
|
||||
zip(...tasks$).subscribe((results: boolean[]) => {
|
||||
|
@@ -12,8 +12,8 @@ import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { BitstreamFormatSupportLevel } from '../../../../core/shared/bitstream-format-support-level';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service-stub';
|
||||
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
||||
import { RouterStub } from '../../../../shared/testing/router.stub';
|
||||
import { EditBitstreamFormatComponent } from './edit-bitstream-format.component';
|
||||
|
||||
describe('EditBitstreamFormatComponent', () => {
|
||||
|
@@ -7,8 +7,8 @@ import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model'
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { RestResponse } from '../../../../core/cache/response.models';
|
||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
||||
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
|
||||
|
||||
/**
|
||||
* This component renders the edit page of a bitstream format.
|
||||
@@ -51,7 +51,7 @@ export class EditBitstreamFormatComponent implements OnInit {
|
||||
if (response.isSuccessful) {
|
||||
this.notificationService.success(this.translateService.get('admin.registries.bitstream-formats.edit.success.head'),
|
||||
this.translateService.get('admin.registries.bitstream-formats.edit.success.content'));
|
||||
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||
this.router.navigate([getBitstreamFormatsModuleRoute()]);
|
||||
} else {
|
||||
this.notificationService.error('admin.registries.bitstream-formats.edit.failure.head',
|
||||
'admin.registries.bitstream-formats.create.edit.content');
|
||||
|
@@ -3,7 +3,7 @@ import { CommonModule } from '@angular/common';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { RouterStub } from '../../../../shared/testing/router-stub';
|
||||
import { RouterStub } from '../../../../shared/testing/router.stub';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormatFormComponent } from './format-form.component';
|
||||
|
@@ -12,9 +12,9 @@ import {
|
||||
DynamicTextAreaModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { getBitstreamFormatsModulePath } from '../../admin-registries-routing.module';
|
||||
import { hasValue, isEmpty } from '../../../../shared/empty.util';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { getBitstreamFormatsModuleRoute } from '../../admin-registries-routing-paths';
|
||||
|
||||
/**
|
||||
* The component responsible for rendering the form to create/edit a bitstream format
|
||||
@@ -189,6 +189,6 @@ export class FormatFormComponent implements OnInit {
|
||||
* Cancels the edit/create action of the bitstream format and navigates back to the bitstream format registry
|
||||
*/
|
||||
onCancel() {
|
||||
this.router.navigate([getBitstreamFormatsModulePath()]);
|
||||
this.router.navigate([getBitstreamFormatsModuleRoute()]);
|
||||
}
|
||||
}
|
||||
|
@@ -11,7 +11,6 @@
|
||||
<ds-pagination
|
||||
*ngIf="(metadataSchemas | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataSchemas | async)?.payload"
|
||||
[collectionSize]="(metadataSchemas | async)?.payload?.totalElements"
|
||||
[hideGear]="true"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
|
@@ -10,14 +10,14 @@ import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||
import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
|
||||
describe('MetadataRegistryComponent', () => {
|
||||
let comp: MetadataRegistryComponent;
|
||||
|
@@ -4,7 +4,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { zip } from 'rxjs/internal/observable/zip';
|
||||
@@ -12,6 +12,8 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
import { Route, Router } from '@angular/router';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-registry',
|
||||
@@ -37,6 +39,11 @@ export class MetadataRegistryComponent {
|
||||
pageSize: 25
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether or not the list of MetadataSchemas needs an update
|
||||
*/
|
||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(private registryService: RegistryService,
|
||||
private notificationsService: NotificationsService,
|
||||
private router: Router,
|
||||
@@ -50,14 +57,17 @@ export class MetadataRegistryComponent {
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateSchemas();
|
||||
this.forceUpdateSchemas();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of schemas by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateSchemas() {
|
||||
this.metadataSchemas = this.registryService.getMetadataSchemas(this.config);
|
||||
this.metadataSchemas = this.needsUpdate$.pipe(
|
||||
filter((update) => update === true),
|
||||
switchMap(() => this.registryService.getMetadataSchemas(toFindListOptions(this.config)))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,8 +75,7 @@ export class MetadataRegistryComponent {
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateSchemas() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.updateSchemas();
|
||||
this.needsUpdate$.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -125,6 +134,7 @@ export class MetadataRegistryComponent {
|
||||
* Delete all the selected metadata schemas
|
||||
*/
|
||||
deleteSchemas() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.registryService.getSelectedMetadataSchemas().pipe(take(1)).subscribe(
|
||||
(schemas) => {
|
||||
const tasks$ = [];
|
||||
|
@@ -21,7 +21,8 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataSchema: () => observableOf(undefined),
|
||||
createOrUpdateMetadataSchema: (schema: MetadataSchema) => observableOf(schema),
|
||||
cancelEditMetadataSchema: () => {}
|
||||
cancelEditMetadataSchema: () => {},
|
||||
clearMetadataSchemaRequests: () => observableOf(undefined)
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
|
@@ -128,6 +128,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created schema using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.registryService.clearMetadataSchemaRequests().subscribe();
|
||||
this.registryService.getActiveMetadataSchema().pipe(take(1)).subscribe(
|
||||
(schema) => {
|
||||
const values = {
|
||||
@@ -139,7 +140,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
this.submitForm.emit(newSchema);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), {
|
||||
this.registryService.createOrUpdateMetadataSchema(Object.assign(new MetadataSchema(), schema, {
|
||||
id: schema.id,
|
||||
prefix: (values.prefix ? values.prefix : schema.prefix),
|
||||
namespace: (values.namespace ? values.namespace : schema.namespace)
|
||||
@@ -148,6 +149,7 @@ export class MetadataSchemaFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataSchema();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -27,9 +27,11 @@ describe('MetadataFieldFormComponent', () => {
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
createOrUpdateMetadataField: (field: MetadataField) => observableOf(field),
|
||||
createMetadataField: (field: MetadataField) => observableOf(field),
|
||||
updateMetadataField: (field: MetadataField) => observableOf(field),
|
||||
cancelEditMetadataField: () => {},
|
||||
cancelEditMetadataSchema: () => {},
|
||||
clearMetadataFieldRequests: () => observableOf(undefined)
|
||||
};
|
||||
const formBuilderServiceStub = {
|
||||
createFormGroup: () => {
|
||||
@@ -74,7 +76,6 @@ describe('MetadataFieldFormComponent', () => {
|
||||
const scopeNote = 'fakeScopeNote';
|
||||
|
||||
const expected = Object.assign(new MetadataField(), {
|
||||
schema: metadataSchema,
|
||||
element: element,
|
||||
qualifier: qualifier,
|
||||
scopeNote: scopeNote
|
||||
|
@@ -153,22 +153,21 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
* Emit the updated/created field using the EventEmitter submitForm
|
||||
*/
|
||||
onSubmit() {
|
||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||
this.registryService.getActiveMetadataField().pipe(take(1)).subscribe(
|
||||
(field) => {
|
||||
const values = {
|
||||
schema: this.metadataSchema,
|
||||
element: this.element.value,
|
||||
qualifier: this.qualifier.value,
|
||||
scopeNote: this.scopeNote.value
|
||||
};
|
||||
if (field == null) {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), values)).subscribe((newField) => {
|
||||
this.registryService.createMetadataField(Object.assign(new MetadataField(), values), this.metadataSchema).subscribe((newField) => {
|
||||
this.submitForm.emit(newField);
|
||||
});
|
||||
} else {
|
||||
this.registryService.createOrUpdateMetadataField(Object.assign(new MetadataField(), {
|
||||
this.registryService.updateMetadataField(Object.assign(new MetadataField(), field, {
|
||||
id: field.id,
|
||||
schema: this.metadataSchema,
|
||||
element: (values.element ? values.element : field.element),
|
||||
qualifier: (values.qualifier ? values.qualifier : field.qualifier),
|
||||
scopeNote: (values.scopeNote ? values.scopeNote : field.scopeNote)
|
||||
@@ -177,6 +176,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
this.clearFields();
|
||||
this.registryService.cancelEditMetadataField();
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@@ -1,36 +1,37 @@
|
||||
<div class="container">
|
||||
<div class="metadata-schema row">
|
||||
<div class="col-12">
|
||||
<div class="col-12" *ngVar="(metadataSchema$ | async) as schema">
|
||||
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{(metadataSchema | async)?.payload?.prefix}}"</h2>
|
||||
<h2 id="header" class="border-bottom pb-2">{{'admin.registries.schema.head' | translate}}: "{{schema?.prefix}}"</h2>
|
||||
|
||||
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:namespace }}</p>
|
||||
<p id="description" class="pb-2">{{'admin.registries.schema.description' | translate:{ namespace: schema?.namespace } }}</p>
|
||||
|
||||
<ds-metadata-field-form
|
||||
[metadataSchema]="(metadataSchema | async)?.payload"
|
||||
[metadataSchema]="schema"
|
||||
(submitForm)="forceUpdateFields()"></ds-metadata-field-form>
|
||||
|
||||
<h3>{{'admin.registries.schema.fields.head' | translate}}</h3>
|
||||
|
||||
<ds-pagination
|
||||
*ngIf="(metadataFields | async)?.payload?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="(metadataFields | async)?.payload"
|
||||
[collectionSize]="(metadataFields | async)?.payload?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-fields" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<ng-container *ngVar="(metadataFields$ | async)?.payload as fields">
|
||||
<ds-pagination
|
||||
*ngIf="fields?.totalElements > 0"
|
||||
[paginationOptions]="config"
|
||||
[pageInfoState]="fields"
|
||||
[collectionSize]="fields?.totalElements"
|
||||
[hideGear]="false"
|
||||
[hidePagerWhenSinglePage]="true"
|
||||
(pageChange)="onPageChange($event)">
|
||||
<div class="table-responsive">
|
||||
<table id="metadata-fields" class="table table-striped table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.field' | translate}}</th>
|
||||
<th scope="col">{{'admin.registries.schema.fields.table.scopenote' | translate}}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let field of (metadataFields | async)?.payload?.page"
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr *ngFor="let field of fields?.page"
|
||||
[ngClass]="{'table-primary' : isActive(field) | async}">
|
||||
<td>
|
||||
<label>
|
||||
@@ -39,22 +40,23 @@
|
||||
(change)="selectMetadataField(field, $event)">
|
||||
</label>
|
||||
</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{(metadataSchema | async)?.payload?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{schema?.prefix}}.{{field.element}}<label *ngIf="field.qualifier">.</label>{{field.qualifier}}</td>
|
||||
<td class="selectable-row" (click)="editField(field)">{{field.scopeNote}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="fields?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||
</div>
|
||||
</ds-pagination>
|
||||
|
||||
<div *ngIf="(metadataFields | async)?.payload?.totalElements == 0" class="alert alert-info w-100 mb-2" role="alert">
|
||||
{{'admin.registries.schema.fields.no-items' | translate}}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
||||
<button *ngIf="(metadataFields | async)?.payload?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||
</div>
|
||||
<div>
|
||||
<button [routerLink]="['/admin/registries/metadata']" class="btn btn-primary">{{'admin.registries.schema.return' | translate}}</button>
|
||||
<button *ngIf="fields?.page?.length > 0" type="submit" class="btn btn-danger float-right" (click)="deleteFields()">{{'admin.registries.schema.fields.table.delete' | translate}}</button>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -10,18 +10,19 @@ import { RegistryService } from '../../../core/registry/registry.service';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||
import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
|
||||
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
|
||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||
|
||||
describe('MetadataSchemaComponent', () => {
|
||||
let comp: MetadataSchemaComponent;
|
||||
@@ -60,7 +61,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'advisor',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0])
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
@@ -72,7 +73,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'author',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[0]
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[0])
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@@ -84,7 +85,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'editor',
|
||||
scopeNote: 'test scope note',
|
||||
schema: mockSchemasList[1]
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1])
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
@@ -96,15 +97,15 @@ describe('MetadataSchemaComponent', () => {
|
||||
element: 'contributor',
|
||||
qualifier: 'illustrator',
|
||||
scopeNote: null,
|
||||
schema: mockSchemasList[1]
|
||||
schema: createSuccessfulRemoteDataObject$(mockSchemasList[1])
|
||||
}
|
||||
];
|
||||
const mockSchemas = createSuccessfulRemoteDataObject$(new PaginatedList(null, mockSchemasList));
|
||||
/* tslint:disable:no-empty */
|
||||
const registryServiceStub = {
|
||||
getMetadataSchemas: () => mockSchemas,
|
||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema))),
|
||||
getMetadataSchemaByName: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
||||
getMetadataFieldsBySchema: (schema: MetadataSchema) => createSuccessfulRemoteDataObject$(new PaginatedList(null, mockFieldsList.filter((value) => value.id === 3 || value.id === 4))),
|
||||
getMetadataSchemaByPrefix: (schemaName: string) => createSuccessfulRemoteDataObject$(mockSchemasList.filter((value) => value.prefix === schemaName)[0]),
|
||||
getActiveMetadataField: () => observableOf(undefined),
|
||||
getSelectedMetadataFields: () => observableOf([]),
|
||||
editMetadataField: (schema) => {},
|
||||
@@ -124,7 +125,7 @@ describe('MetadataSchemaComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule],
|
||||
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe],
|
||||
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe, VarDirective],
|
||||
providers: [
|
||||
{ provide: RegistryService, useValue: registryServiceStub },
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
|
@@ -5,7 +5,7 @@ import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { map, take } from 'rxjs/operators';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../../shared/empty.util';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { zip } from 'rxjs/internal/observable/zip';
|
||||
@@ -13,6 +13,11 @@ import { NotificationsService } from '../../../shared/notifications/notification
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { MetadataField } from '../../../core/metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { toFindListOptions } from '../../../shared/pagination/pagination.utils';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { combineLatest } from 'rxjs/internal/observable/combineLatest';
|
||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-metadata-schema',
|
||||
@@ -24,21 +29,15 @@ import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
|
||||
* The admin can create, edit or delete metadata fields here.
|
||||
*/
|
||||
export class MetadataSchemaComponent implements OnInit {
|
||||
|
||||
/**
|
||||
* The namespace of the metadata schema
|
||||
*/
|
||||
namespace;
|
||||
|
||||
/**
|
||||
* The metadata schema
|
||||
*/
|
||||
metadataSchema: Observable<RemoteData<MetadataSchema>>;
|
||||
metadataSchema$: Observable<MetadataSchema>;
|
||||
|
||||
/**
|
||||
* A list of all the fields attached to this metadata schema
|
||||
*/
|
||||
metadataFields: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
||||
metadataFields$: Observable<RemoteData<PaginatedList<MetadataField>>>;
|
||||
|
||||
/**
|
||||
* Pagination config used to display the list of metadata fields
|
||||
@@ -49,6 +48,11 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
pageSizeOptions: [25, 50, 100, 200]
|
||||
});
|
||||
|
||||
/**
|
||||
* Whether or not the list of MetadataFields needs an update
|
||||
*/
|
||||
needsUpdate$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(true);
|
||||
|
||||
constructor(private registryService: RegistryService,
|
||||
private route: ActivatedRoute,
|
||||
private notificationsService: NotificationsService,
|
||||
@@ -68,7 +72,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
* @param params
|
||||
*/
|
||||
initialize(params) {
|
||||
this.metadataSchema = this.registryService.getMetadataSchemaByName(params.schemaName);
|
||||
this.metadataSchema$ = this.registryService.getMetadataSchemaByPrefix(params.schemaName).pipe(getFirstSucceededRemoteDataPayload());
|
||||
this.updateFields();
|
||||
}
|
||||
|
||||
@@ -78,18 +82,20 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
*/
|
||||
onPageChange(event) {
|
||||
this.config.currentPage = event;
|
||||
this.updateFields();
|
||||
this.forceUpdateFields();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of fields by fetching it from the rest api or cache
|
||||
*/
|
||||
private updateFields() {
|
||||
this.metadataSchema.subscribe((schemaData) => {
|
||||
const schema = schemaData.payload;
|
||||
this.metadataFields = this.registryService.getMetadataFieldsBySchema(schema, this.config);
|
||||
this.namespace = {namespace: schemaData.payload.namespace};
|
||||
});
|
||||
this.metadataFields$ = combineLatest(this.metadataSchema$, this.needsUpdate$).pipe(
|
||||
switchMap(([schema, update]: [MetadataSchema, boolean]) => {
|
||||
if (update) {
|
||||
return this.registryService.getMetadataFieldsBySchema(schema, toFindListOptions(this.config), followLink('schema'));
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -97,8 +103,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
* a new REST call
|
||||
*/
|
||||
public forceUpdateFields() {
|
||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||
this.updateFields();
|
||||
this.needsUpdate$.next(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -157,6 +162,7 @@ export class MetadataSchemaComponent implements OnInit {
|
||||
* Delete all the selected metadata fields
|
||||
*/
|
||||
deleteFields() {
|
||||
this.registryService.clearMetadataFieldRequests().subscribe();
|
||||
this.registryService.getSelectedMetadataFields().pipe(take(1)).subscribe(
|
||||
(fields) => {
|
||||
const tasks$ = [];
|
||||
|
13
src/app/+admin/admin-routing-paths.ts
Normal file
13
src/app/+admin/admin-routing-paths.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { getAdminModuleRoute } from '../app-routing-paths';
|
||||
|
||||
export const REGISTRIES_MODULE_PATH = 'registries';
|
||||
export const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
|
||||
export function getRegistriesModuleRoute() {
|
||||
return new URLCombiner(getAdminModuleRoute(), REGISTRIES_MODULE_PATH).toString();
|
||||
}
|
||||
|
||||
export function getAccessControlModuleRoute() {
|
||||
return new URLCombiner(getAdminModuleRoute(), ACCESS_CONTROL_MODULE_PATH).toString();
|
||||
}
|
@@ -1,20 +1,12 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { getAdminModulePath } from '../app-routing.module';
|
||||
import { MetadataImportPageComponent } from './admin-import-metadata-page/metadata-import-page.component';
|
||||
import { AdminSearchPageComponent } from './admin-search-page/admin-search-page.component';
|
||||
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
|
||||
const REGISTRIES_MODULE_PATH = 'registries';
|
||||
const ACCESS_CONTROL_MODULE_PATH = 'access-control';
|
||||
|
||||
export function getRegistriesModulePath() {
|
||||
return new URLCombiner(getAdminModulePath(), REGISTRIES_MODULE_PATH).toString();
|
||||
}
|
||||
|
||||
export function getAccessControlModulePath() {
|
||||
return new URLCombiner(getAdminModulePath(), ACCESS_CONTROL_MODULE_PATH).toString();
|
||||
}
|
||||
import { AdminWorkflowPageComponent } from './admin-workflow-page/admin-workflow-page.component';
|
||||
import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import { AdminCurationTasksComponent } from './admin-curation-tasks/admin-curation-tasks.component';
|
||||
import { ACCESS_CONTROL_MODULE_PATH, REGISTRIES_MODULE_PATH } from './admin-routing-paths';
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
@@ -32,8 +24,30 @@ export function getAccessControlModulePath() {
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: AdminSearchPageComponent,
|
||||
data: { title: 'admin.search.title', breadcrumbKey: 'admin.search' }
|
||||
}
|
||||
]),
|
||||
},
|
||||
{
|
||||
path: 'workflow',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: AdminWorkflowPageComponent,
|
||||
data: { title: 'admin.workflow.title', breadcrumbKey: 'admin.workflow' }
|
||||
},
|
||||
{
|
||||
path: 'curation-tasks',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: AdminCurationTasksComponent,
|
||||
data: { title: 'admin.curation-tasks.title', breadcrumbKey: 'admin.curation-tasks' }
|
||||
},
|
||||
{
|
||||
path: 'metadata-import',
|
||||
resolve: { breadcrumb: I18nBreadcrumbResolver },
|
||||
component: MetadataImportPageComponent,
|
||||
data: { title: 'admin.metadata-import.title', breadcrumbKey: 'admin.metadata-import' }
|
||||
},
|
||||
])
|
||||
],
|
||||
providers: [
|
||||
I18nBreadcrumbResolver,
|
||||
I18nBreadcrumbsService
|
||||
]
|
||||
})
|
||||
export class AdminRoutingModule {
|
||||
|
@@ -12,7 +12,7 @@ import { CollectionSearchResult } from '../../../../../shared/object-collection/
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
|
||||
|
||||
describe('CollectionAdminSearchResultGridElementComponent', () => {
|
||||
let component: CollectionAdminSearchResultGridElementComponent;
|
||||
@@ -61,6 +61,6 @@ describe('CollectionAdminSearchResultGridElementComponent', () => {
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.edit-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(getCollectionEditPath(id));
|
||||
expect(link).toContain(getCollectionEditRoute(id));
|
||||
})
|
||||
});
|
||||
|
@@ -4,8 +4,8 @@ import { listableObjectComponent } from '../../../../../shared/object-collection
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
|
||||
|
||||
@listableObjectComponent(CollectionSearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||
@Component({
|
||||
@@ -21,6 +21,6 @@ export class CollectionAdminSearchResultGridElementComponent extends SearchResul
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.editPath = getCollectionEditPath(this.dso.uuid);
|
||||
this.editPath = getCollectionEditRoute(this.dso.uuid);
|
||||
}
|
||||
}
|
||||
|
@@ -13,9 +13,9 @@ import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { CommunityAdminSearchResultGridElementComponent } from './community-admin-search-result-grid-element.component';
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { CommunityAdminSearchResultListElementComponent } from '../../admin-search-result-list-element/community-search-result/community-admin-search-result-list-element.component';
|
||||
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
|
||||
|
||||
describe('CommunityAdminSearchResultGridElementComponent', () => {
|
||||
let component: CommunityAdminSearchResultGridElementComponent;
|
||||
@@ -65,6 +65,6 @@ describe('CommunityAdminSearchResultGridElementComponent', () => {
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.edit-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(getCommunityEditPath(id));
|
||||
expect(link).toContain(getCommunityEditRoute(id));
|
||||
})
|
||||
});
|
||||
|
@@ -4,8 +4,8 @@ import { listableObjectComponent } from '../../../../../shared/object-collection
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
|
||||
|
||||
@listableObjectComponent(CommunitySearchResult, ViewMode.GridElement, Context.AdminSearch)
|
||||
@Component({
|
||||
@@ -21,6 +21,6 @@ export class CommunityAdminSearchResultGridElementComponent extends SearchResult
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.editPath = getCommunityEditPath(this.dso.uuid);
|
||||
this.editPath = getCommunityEditRoute(this.dso.uuid);
|
||||
}
|
||||
}
|
||||
|
@@ -10,7 +10,6 @@ import { Bitstream } from '../../../../../core/shared/bitstream.model';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import { mockTruncatableService } from '../../../../../shared/mocks/mock-trucatable.service';
|
||||
import { SharedModule } from '../../../../../shared/shared.module';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { CollectionElementLinkType } from '../../../../../shared/object-collection/collection-element-link.type';
|
||||
import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
@@ -18,6 +17,7 @@ import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { ItemAdminSearchResultGridElementComponent } from './item-admin-search-result-grid-element.component';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
|
||||
describe('ItemAdminSearchResultGridElementComponent', () => {
|
||||
let component: ItemAdminSearchResultGridElementComponent;
|
||||
|
@@ -4,16 +4,6 @@ import { ViewMode } from '../../../../../core/shared/view-mode.model';
|
||||
import { getListableObjectComponent, listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
|
||||
import { Context } from '../../../../../core/shared/context.model';
|
||||
import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model';
|
||||
import { getItemEditPath } from '../../../../../+item-page/item-page-routing.module';
|
||||
import { URLCombiner } from '../../../../../core/url-combiner/url-combiner';
|
||||
import {
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} from '../../../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||
import { SearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/search-result-grid-element.component';
|
||||
import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service';
|
||||
import { BitstreamDataService } from '../../../../../core/data/bitstream-data.service';
|
||||
|
@@ -10,7 +10,7 @@ import { CollectionSearchResult } from '../../../../../shared/object-collection/
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
|
||||
|
||||
describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||
let component: CollectionAdminSearchResultListElementComponent;
|
||||
@@ -55,6 +55,6 @@ describe('CollectionAdminSearchResultListElementComponent', () => {
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(getCollectionEditPath(id));
|
||||
expect(link).toContain(getCollectionEditRoute(id));
|
||||
})
|
||||
});
|
||||
|
@@ -5,7 +5,7 @@ import { Context } from '../../../../../core/shared/context.model';
|
||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||
import { CollectionSearchResult } from '../../../../../shared/object-collection/shared/collection-search-result.model';
|
||||
import { Collection } from '../../../../../core/shared/collection.model';
|
||||
import { getCollectionEditPath } from '../../../../../+collection-page/collection-page-routing.module';
|
||||
import { getCollectionEditRoute } from '../../../../../+collection-page/collection-page-routing-paths';
|
||||
|
||||
@listableObjectComponent(CollectionSearchResult, ViewMode.ListElement, Context.AdminSearch)
|
||||
@Component({
|
||||
@@ -21,6 +21,6 @@ export class CollectionAdminSearchResultListElementComponent extends SearchResul
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.editPath = getCollectionEditPath(this.dso.uuid);
|
||||
this.editPath = getCollectionEditRoute(this.dso.uuid);
|
||||
}
|
||||
}
|
||||
|
@@ -9,8 +9,8 @@ import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { CommunityAdminSearchResultListElementComponent } from './community-admin-search-result-list-element.component';
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
|
||||
|
||||
describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||
let component: CommunityAdminSearchResultListElementComponent;
|
||||
@@ -55,6 +55,6 @@ describe('CommunityAdminSearchResultListElementComponent', () => {
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(getCommunityEditPath(id));
|
||||
expect(link).toContain(getCommunityEditRoute(id));
|
||||
})
|
||||
});
|
||||
|
@@ -5,7 +5,7 @@ import { Context } from '../../../../../core/shared/context.model';
|
||||
import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component';
|
||||
import { CommunitySearchResult } from '../../../../../shared/object-collection/shared/community-search-result.model';
|
||||
import { Community } from '../../../../../core/shared/community.model';
|
||||
import { getCommunityEditPath } from '../../../../../+community-page/community-page-routing.module';
|
||||
import { getCommunityEditRoute } from '../../../../../+community-page/community-page-routing-paths';
|
||||
|
||||
@listableObjectComponent(CommunitySearchResult, ViewMode.ListElement, Context.AdminSearch)
|
||||
@Component({
|
||||
@@ -21,6 +21,6 @@ export class CommunityAdminSearchResultListElementComponent extends SearchResult
|
||||
|
||||
ngOnInit() {
|
||||
super.ngOnInit();
|
||||
this.editPath = getCommunityEditPath(this.dso.uuid);
|
||||
this.editPath = getCommunityEditRoute(this.dso.uuid);
|
||||
}
|
||||
}
|
||||
|
@@ -1,27 +1,27 @@
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 edit-link" [routerLink]="[getEditPath()]" [title]="'admin.search.item.edit' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 edit-link" [routerLink]="[getEditRoute()]" [title]="'admin.search.item.edit' | translate">
|
||||
<i class="fa fa-edit"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.edit" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-light my-1 withdraw-link" [routerLink]="[getWithdrawPath()]" [title]="'admin.search.item.withdraw' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isWithdrawn" class="btn btn-light my-1 withdraw-link" [routerLink]="[getWithdrawRoute()]" [title]="'admin.search.item.withdraw' | translate">
|
||||
<i class="fa fa-ban"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.withdraw" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-light my-1 reinstate-link" [routerLink]="[getReinstatePath()]" [title]="'admin.search.item.reinstate' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isWithdrawn" class="btn btn-light my-1 reinstate-link" [routerLink]="[getReinstateRoute()]" [title]="'admin.search.item.reinstate' | translate">
|
||||
<i class="fa fa-undo"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.reinstate" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-light my-1 private-link" [routerLink]="[getPrivatePath()]" [title]="'admin.search.item.make-private' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && item.isDiscoverable" class="btn btn-light my-1 private-link" [routerLink]="[getPrivateRoute()]" [title]="'admin.search.item.make-private' | translate">
|
||||
<i class="fa fa-eye-slash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-private" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-light my-1 public-link" [routerLink]="[getPublicPath()]" [title]="'admin.search.item.make-public' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" *ngIf="item && !item.isDiscoverable" class="btn btn-light my-1 public-link" [routerLink]="[getPublicRoute()]" [title]="'admin.search.item.make-public' | translate">
|
||||
<i class="fa fa-eye"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.make-public" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeletePath()]" [title]="'admin.search.item.delete' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 delete-link" [routerLink]="[getDeleteRoute()]" [title]="'admin.search.item.delete' | translate">
|
||||
<i class="fa fa-trash"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.delete" | translate}}</span>
|
||||
</a>
|
||||
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 move-link" [routerLink]="[getMovePath()]" [title]="'admin.search.item.move' | translate">
|
||||
<a [ngClass]="{'btn-sm': small}" class="btn btn-light my-1 move-link" [routerLink]="[getMoveRoute()]" [title]="'admin.search.item.move' | translate">
|
||||
<i class="fa fa-arrow-circle-right"></i><span *ngIf="!small" class="d-none d-sm-inline"> {{"admin.search.item.move" | translate}}</span>
|
||||
</a>
|
||||
|
@@ -6,16 +6,16 @@ import { By } from '@angular/platform-browser';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { ItemAdminSearchResultActionsComponent } from './item-admin-search-result-actions.component';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
import { getItemEditRoute } from '../../../+item-page/item-page-routing-paths';
|
||||
import {
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} from '../../../+item-page/edit-item-page/edit-item-page.routing.module';
|
||||
import { getItemEditPath } from '../../../+item-page/item-page-routing.module';
|
||||
import { URLCombiner } from '../../../core/url-combiner/url-combiner';
|
||||
} from '../../../+item-page/edit-item-page/edit-item-page.routing-paths';
|
||||
|
||||
describe('ItemAdminSearchResultActionsComponent', () => {
|
||||
let component: ItemAdminSearchResultActionsComponent;
|
||||
@@ -55,19 +55,19 @@ describe('ItemAdminSearchResultActionsComponent', () => {
|
||||
it('should render an edit button with the correct link', () => {
|
||||
const button = fixture.debugElement.query(By.css('a.edit-link'));
|
||||
const link = button.nativeElement.href;
|
||||
expect(link).toContain(getItemEditPath(id));
|
||||
expect(link).toContain(getItemEditRoute(id));
|
||||
});
|
||||
|
||||
it('should render a delete button with the correct link', () => {
|
||||
const button = fixture.debugElement.query(By.css('a.delete-link'));
|
||||
const link = button.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_DELETE_PATH).toString());
|
||||
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_DELETE_PATH).toString());
|
||||
});
|
||||
|
||||
it('should render a move button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.move-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_MOVE_PATH).toString());
|
||||
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_MOVE_PATH).toString());
|
||||
});
|
||||
|
||||
describe('when the item is not withdrawn', () => {
|
||||
@@ -79,7 +79,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
|
||||
it('should render a withdraw button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.withdraw-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_WITHDRAW_PATH).toString());
|
||||
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_WITHDRAW_PATH).toString());
|
||||
});
|
||||
|
||||
it('should not render a reinstate button with the correct link', () => {
|
||||
@@ -102,7 +102,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
|
||||
it('should render a reinstate button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.reinstate-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_REINSTATE_PATH).toString());
|
||||
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_REINSTATE_PATH).toString());
|
||||
});
|
||||
});
|
||||
|
||||
@@ -115,7 +115,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
|
||||
it('should render a make private button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.private-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PRIVATE_PATH).toString());
|
||||
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_PRIVATE_PATH).toString());
|
||||
});
|
||||
|
||||
it('should not render a make public button with the correct link', () => {
|
||||
@@ -138,7 +138,7 @@ describe('ItemAdminSearchResultActionsComponent', () => {
|
||||
it('should render a make private button with the correct link', () => {
|
||||
const a = fixture.debugElement.query(By.css('a.public-link'));
|
||||
const link = a.nativeElement.href;
|
||||
expect(link).toContain(new URLCombiner(getItemEditPath(id), ITEM_EDIT_PUBLIC_PATH).toString());
|
||||
expect(link).toContain(new URLCombiner(getItemEditRoute(id), ITEM_EDIT_PUBLIC_PATH).toString());
|
||||
});
|
||||
})
|
||||
});
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user