mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Compare commits
738 Commits
dspace-8.1
...
dspace-7.6
Author | SHA1 | Date | |
---|---|---|---|
![]() |
9497decf07 | ||
![]() |
d4edbf2e25 | ||
![]() |
f80992f1fc | ||
![]() |
bb3af14b6c | ||
![]() |
3a5f4565a5 | ||
![]() |
fc43e2473d | ||
![]() |
16da9855fc | ||
![]() |
821f16ea1d | ||
![]() |
f9fb6d06f1 | ||
![]() |
4fa71b801c | ||
![]() |
7a7e468492 | ||
![]() |
34fc08766d | ||
![]() |
13d6acaf76 | ||
![]() |
d9c0401bdd | ||
![]() |
5a2702c797 | ||
![]() |
dabc3b400b | ||
![]() |
5b19ebe48c | ||
![]() |
32511b0f50 | ||
![]() |
17e58ae5e3 | ||
![]() |
0d87a72f41 | ||
![]() |
a17d58acc5 | ||
![]() |
44f2ad7f4f | ||
![]() |
c7f8ed1f42 | ||
![]() |
c6ef2467bc | ||
![]() |
c07bc3c379 | ||
![]() |
b44f74a20a | ||
![]() |
50757247ab | ||
![]() |
1883d93198 | ||
![]() |
d9ab4bca65 | ||
![]() |
edeb2d2e3a | ||
![]() |
c2257ebc81 | ||
![]() |
cf2407ba90 | ||
![]() |
e0af01ff43 | ||
![]() |
7afe49d2b0 | ||
![]() |
c89ad9f22a | ||
![]() |
391e87a11f | ||
![]() |
a864260d00 | ||
![]() |
abfdf63482 | ||
![]() |
7f3b3370d9 | ||
![]() |
d56119793f | ||
![]() |
7677f2941d | ||
![]() |
7ef44c5e35 | ||
![]() |
a77f98e29e | ||
![]() |
5c66bb9d2b | ||
![]() |
7cc17a5544 | ||
![]() |
b7b1e368d6 | ||
![]() |
5e8dd3480f | ||
![]() |
f2131ec757 | ||
![]() |
c52c7e778a | ||
![]() |
b870c54c83 | ||
![]() |
f58ab77d5d | ||
![]() |
406b46ba3f | ||
![]() |
b6448a7caf | ||
![]() |
8a1daa5cf9 | ||
![]() |
fa9ddefb59 | ||
![]() |
8a1ca84d91 | ||
![]() |
06e30050a4 | ||
![]() |
ffda0d6521 | ||
![]() |
a692a6762d | ||
![]() |
2687ded0df | ||
![]() |
e387be1e10 | ||
![]() |
b0e56c4129 | ||
![]() |
9a11618d11 | ||
![]() |
5b47170b6d | ||
![]() |
983551aea8 | ||
![]() |
1727acab33 | ||
![]() |
bf585e77c3 | ||
![]() |
a226502c72 | ||
![]() |
4f79574a76 | ||
![]() |
cb0ac0c9e5 | ||
![]() |
d3876c7c0a | ||
![]() |
db119e4118 | ||
![]() |
0c9f8c02f6 | ||
![]() |
e42d4228ed | ||
![]() |
38812a3e04 | ||
![]() |
1aaa20ec73 | ||
![]() |
e7709b8091 | ||
![]() |
6971ac98c2 | ||
![]() |
280866a58f | ||
![]() |
c8b375d238 | ||
![]() |
37383acb6b | ||
![]() |
277eca837e | ||
![]() |
160f33f98a | ||
![]() |
ca92b4cf0e | ||
![]() |
46b58c369e | ||
![]() |
a0ebf4af63 | ||
![]() |
c58b398e43 | ||
![]() |
df2d18b5f1 | ||
![]() |
1e8c55faf4 | ||
![]() |
9273c83179 | ||
![]() |
e06caab25c | ||
![]() |
a21db5b887 | ||
![]() |
cae435a5c7 | ||
![]() |
974e3bf3f3 | ||
![]() |
f0ce4b2170 | ||
![]() |
830be1f15a | ||
![]() |
8ff943b084 | ||
![]() |
206df7781e | ||
![]() |
545b2ff8a7 | ||
![]() |
163cc75437 | ||
![]() |
e9940f4005 | ||
![]() |
2301a8fd52 | ||
![]() |
f9aa721ec8 | ||
![]() |
d47baab3a1 | ||
![]() |
c0e71a0e68 | ||
![]() |
21ac024423 | ||
![]() |
ce0f6153b9 | ||
![]() |
cafcee110e | ||
![]() |
a8fcad7b6a | ||
![]() |
5c446d18f7 | ||
![]() |
cd0aa134f8 | ||
![]() |
9aed649160 | ||
![]() |
fbbf16f387 | ||
![]() |
283b345cdc | ||
![]() |
da6ace1882 | ||
![]() |
aaa4b910e2 | ||
![]() |
926abe6241 | ||
![]() |
97fd42a2e1 | ||
![]() |
3cff971297 | ||
![]() |
49b27b5bc0 | ||
![]() |
f9f3d186c5 | ||
![]() |
05e1fc3505 | ||
![]() |
3635bf40fe | ||
![]() |
70d1e499c1 | ||
![]() |
e40a44c5ac | ||
![]() |
3d32715d25 | ||
![]() |
498fad9e76 | ||
![]() |
e9061a46b6 | ||
![]() |
a1d391576e | ||
![]() |
d985bfa091 | ||
![]() |
4eb827c089 | ||
![]() |
43db1731ec | ||
![]() |
e2b72201b2 | ||
![]() |
34248186e8 | ||
![]() |
9dca838f25 | ||
![]() |
ae23e68d12 | ||
![]() |
dc8b10593c | ||
![]() |
501ccfea1b | ||
![]() |
02be3e0ad5 | ||
![]() |
d34dafd838 | ||
![]() |
85610d4eb6 | ||
![]() |
c1bd65e8c6 | ||
![]() |
0dabc8ed8f | ||
![]() |
da12147043 | ||
![]() |
99e8c1044c | ||
![]() |
287d35cb26 | ||
![]() |
15a8008869 | ||
![]() |
e35adee7f8 | ||
![]() |
ee3154c069 | ||
![]() |
b491ed3c27 | ||
![]() |
b184db7a99 | ||
![]() |
eac787f1f4 | ||
![]() |
8dbdb27c67 | ||
![]() |
02f30ab331 | ||
![]() |
523c13ff3d | ||
![]() |
1ddd248d0e | ||
![]() |
11f251755b | ||
![]() |
7b25b1d63c | ||
![]() |
d43730a542 | ||
![]() |
1c1b129068 | ||
![]() |
ae563442e7 | ||
![]() |
08d56407ff | ||
![]() |
8bc2c8768f | ||
![]() |
9a0e4ec107 | ||
![]() |
3a04ea8f79 | ||
![]() |
c272e51aeb | ||
![]() |
c6a1401f34 | ||
![]() |
19c680ebf0 | ||
![]() |
9a025f6610 | ||
![]() |
ac940c1bb2 | ||
![]() |
1c4f650ef9 | ||
![]() |
6ef781822d | ||
![]() |
0115edaa07 | ||
![]() |
faebbbe680 | ||
![]() |
0ff0b5298a | ||
![]() |
87bb0d4e5a | ||
![]() |
eee9897312 | ||
![]() |
fdc23a3350 | ||
![]() |
c4104be75b | ||
![]() |
08a707477c | ||
![]() |
d4efd6a8ef | ||
![]() |
04516fad5a | ||
![]() |
28eb709ef5 | ||
![]() |
cc202b7adb | ||
![]() |
f5df726a18 | ||
![]() |
077467efa2 | ||
![]() |
dc3e842747 | ||
![]() |
828648aa7e | ||
![]() |
7063c49072 | ||
![]() |
036fd1c871 | ||
![]() |
22db6745b8 | ||
![]() |
4f42c1e95f | ||
![]() |
b37a7a1456 | ||
![]() |
33146603df | ||
![]() |
d3a97d9d15 | ||
![]() |
2a1c762e73 | ||
![]() |
c94279edc0 | ||
![]() |
878b2dba4a | ||
![]() |
2eca99379f | ||
![]() |
8a4405c473 | ||
![]() |
0d2e49b12c | ||
![]() |
edd5496a4d | ||
![]() |
01becae7d0 | ||
![]() |
eef6bbe134 | ||
![]() |
ff5f23017a | ||
![]() |
7dd6ab79ff | ||
![]() |
ecc03343b7 | ||
![]() |
fa6b8cc21d | ||
![]() |
6c11621d91 | ||
![]() |
d65ff20777 | ||
![]() |
c2528a9c1f | ||
![]() |
3ad99804d1 | ||
![]() |
122d31b11b | ||
![]() |
7ab598d571 | ||
![]() |
bd06eded8c | ||
![]() |
99840ba3bc | ||
![]() |
204556de20 | ||
![]() |
8d7b4cd101 | ||
![]() |
ac5c5c8a14 | ||
![]() |
008fe0151b | ||
![]() |
f8bcce43b5 | ||
![]() |
e75052bbcf | ||
![]() |
3340192382 | ||
![]() |
dcd32c48c3 | ||
![]() |
cd825ac0da | ||
![]() |
ca80812d0a | ||
![]() |
4a74a3ac99 | ||
![]() |
62904747fc | ||
![]() |
4c30e2d0be | ||
![]() |
d05096c015 | ||
![]() |
b837f63b7c | ||
![]() |
e838873235 | ||
![]() |
e9fa2812a0 | ||
![]() |
885c52ed34 | ||
![]() |
9b0669f76c | ||
![]() |
3daa110116 | ||
![]() |
d9c6856d9c | ||
![]() |
3372110839 | ||
![]() |
ddd7c323ab | ||
![]() |
961bb11b59 | ||
![]() |
f82426b1dc | ||
![]() |
8b90d999ac | ||
![]() |
80948d98f0 | ||
![]() |
6446a52e46 | ||
![]() |
e58a66f849 | ||
![]() |
1167f8a414 | ||
![]() |
eee851f001 | ||
![]() |
b18ff86894 | ||
![]() |
e811083339 | ||
![]() |
e0590109c1 | ||
![]() |
ed1aa37100 | ||
![]() |
601850cb6a | ||
![]() |
ebe26a7eb0 | ||
![]() |
6eb5eec050 | ||
![]() |
1956e25a83 | ||
![]() |
59cb19ae80 | ||
![]() |
2e28a51b02 | ||
![]() |
7fab9633ea | ||
![]() |
7584b9e1c3 | ||
![]() |
7660dd8d95 | ||
![]() |
6f4c3860a3 | ||
![]() |
d73885137a | ||
![]() |
290c24b448 | ||
![]() |
21dac408ee | ||
![]() |
d118496075 | ||
![]() |
cb5deb644e | ||
![]() |
f9d8e4e0b7 | ||
![]() |
a3ffda3bf8 | ||
![]() |
8ca668159e | ||
![]() |
95064122d0 | ||
![]() |
d92424e2f4 | ||
![]() |
45cc177cab | ||
![]() |
59b39f12d6 | ||
![]() |
636fba40f7 | ||
![]() |
f8123fbbfd | ||
![]() |
8313b9a929 | ||
![]() |
51aa5fb61c | ||
![]() |
a5378e0cf5 | ||
![]() |
0abbf80c1d | ||
![]() |
d4536ffbf4 | ||
![]() |
6b73c75205 | ||
![]() |
f01e49f05f | ||
![]() |
073b2b0acd | ||
![]() |
d155d778ee | ||
![]() |
50e440bcf5 | ||
![]() |
af39526d77 | ||
![]() |
f77ce869f8 | ||
![]() |
bc06839926 | ||
![]() |
a3ebfc0f36 | ||
![]() |
362364136d | ||
![]() |
b892b13529 | ||
![]() |
b220e20053 | ||
![]() |
eaff0b1397 | ||
![]() |
0108f525ba | ||
![]() |
3f5de295a7 | ||
![]() |
62924cc5e0 | ||
![]() |
8453b667cb | ||
![]() |
3922130434 | ||
![]() |
c3b265d213 | ||
![]() |
2b58377830 | ||
![]() |
4536d9c74d | ||
![]() |
a3b7125ef9 | ||
![]() |
6f9c55d250 | ||
![]() |
9c6fb1794f | ||
![]() |
0f27ae1125 | ||
![]() |
73285284ce | ||
![]() |
53186988f5 | ||
![]() |
8f9236f06d | ||
![]() |
470ad80741 | ||
![]() |
c3f17b9754 | ||
![]() |
e296295df5 | ||
![]() |
795f300158 | ||
![]() |
38d5f87d3e | ||
![]() |
5a5c1f18e3 | ||
![]() |
bb3d2bb483 | ||
![]() |
8c92364d43 | ||
![]() |
1cc4756ee2 | ||
![]() |
08bda53a2e | ||
![]() |
cd2f8abd2d | ||
![]() |
8a4f24fc31 | ||
![]() |
868b021118 | ||
![]() |
c7d8949c82 | ||
![]() |
7f1ae419af | ||
![]() |
30d57174e6 | ||
![]() |
c155a60a7e | ||
![]() |
5d6bf0bb33 | ||
![]() |
147564b114 | ||
![]() |
ecd2faea4a | ||
![]() |
990f00b129 | ||
![]() |
483b97d9c1 | ||
![]() |
171a971572 | ||
![]() |
8a4e8f6045 | ||
![]() |
32364e4f68 | ||
![]() |
311f648c3f | ||
![]() |
2c28f75263 | ||
![]() |
53335658e0 | ||
![]() |
9467838066 | ||
![]() |
99656f1357 | ||
![]() |
8ee05f4352 | ||
![]() |
6756a78ba0 | ||
![]() |
b482b58d53 | ||
![]() |
312a2a7f58 | ||
![]() |
d6f0f02c1a | ||
![]() |
61eb14783e | ||
![]() |
126f3c71f4 | ||
![]() |
6cd092671f | ||
![]() |
39c5de6e18 | ||
![]() |
5868a3198f | ||
![]() |
c5fd4426cd | ||
![]() |
ea715ba9f5 | ||
![]() |
f097652594 | ||
![]() |
cecf87364e | ||
![]() |
afa0a9017f | ||
![]() |
7638511762 | ||
![]() |
31336efad2 | ||
![]() |
fc2d3f0a23 | ||
![]() |
6cb3e8bbfb | ||
![]() |
00ff0cc7e4 | ||
![]() |
81fa0ba446 | ||
![]() |
5342653b99 | ||
![]() |
d4b0cf21d3 | ||
![]() |
88fb275017 | ||
![]() |
d9614b9a1b | ||
![]() |
b55a318676 | ||
![]() |
b839252ed7 | ||
![]() |
e3f13f8c54 | ||
![]() |
2bd53d822a | ||
![]() |
b11543c3a5 | ||
![]() |
6035d9e925 | ||
![]() |
74d6dbb454 | ||
![]() |
d0b6571954 | ||
![]() |
39ddc5cbcd | ||
![]() |
9bc65b99a6 | ||
![]() |
9741ce3e69 | ||
![]() |
7472be0098 | ||
![]() |
f3df3cc96c | ||
![]() |
8ef53178db | ||
![]() |
395a5b2cb1 | ||
![]() |
ce5ab70fd4 | ||
![]() |
2abdf122b4 | ||
![]() |
31c9b854c6 | ||
![]() |
75e45d4c15 | ||
![]() |
d88c8ef986 | ||
![]() |
f07a62b8b6 | ||
![]() |
2b96983f9d | ||
![]() |
c8b2642f57 | ||
![]() |
fd166fe79a | ||
![]() |
6446911441 | ||
![]() |
9e1ce916f6 | ||
![]() |
ecceaf9ee1 | ||
![]() |
fcf3370d66 | ||
![]() |
813e965938 | ||
![]() |
3c16537fe4 | ||
![]() |
a57fd1a069 | ||
![]() |
6f5864d106 | ||
![]() |
fbe4ce5829 | ||
![]() |
112bed787a | ||
![]() |
05d5a0816d | ||
![]() |
58e9a60812 | ||
![]() |
76539dd8a1 | ||
![]() |
d0a8448de6 | ||
![]() |
9fce33f514 | ||
![]() |
04123f92aa | ||
![]() |
7538f49a36 | ||
![]() |
60586f6c33 | ||
![]() |
86a6897861 | ||
![]() |
68372b2cb8 | ||
![]() |
cefeb6a1d6 | ||
![]() |
9c859b1fdc | ||
![]() |
fbaa5068af | ||
![]() |
8515de3d10 | ||
![]() |
96cdab8d6a | ||
![]() |
ebc7799404 | ||
![]() |
d3d86f4f2d | ||
![]() |
a078932857 | ||
![]() |
a1b7c8d9c5 | ||
![]() |
894448e3dc | ||
![]() |
b25294c872 | ||
![]() |
ecf3298345 | ||
![]() |
18819b76a8 | ||
![]() |
4ffde928d4 | ||
![]() |
9406f7b085 | ||
![]() |
33fdee554e | ||
![]() |
cee9d0422b | ||
![]() |
5c8eabddab | ||
![]() |
a18225aa4d | ||
![]() |
d578222f8f | ||
![]() |
abc3c41c63 | ||
![]() |
a8e658a399 | ||
![]() |
81522586dd | ||
![]() |
a8ff6a41e0 | ||
![]() |
75260b00d9 | ||
![]() |
14680b013e | ||
![]() |
b601405e56 | ||
![]() |
c6ef2f1bd0 | ||
![]() |
2a1ef02d75 | ||
![]() |
25ab69ff21 | ||
![]() |
eb3db9ceb6 | ||
![]() |
84c0371aab | ||
![]() |
4bf4e18389 | ||
![]() |
2ff5350302 | ||
![]() |
36dd4762ed | ||
![]() |
655558538c | ||
![]() |
12fc5aff71 | ||
![]() |
cd500007b6 | ||
![]() |
686b61915a | ||
![]() |
996c877412 | ||
![]() |
c9d6c95563 | ||
![]() |
bee8bde87f | ||
![]() |
442b4ea284 | ||
![]() |
c71c6667e0 | ||
![]() |
010b2f9693 | ||
![]() |
297fc01892 | ||
![]() |
f9e67dc513 | ||
![]() |
830ada37f5 | ||
![]() |
b5a8b56473 | ||
![]() |
2a72fac646 | ||
![]() |
6988df519e | ||
![]() |
ce17d23c08 | ||
![]() |
91acc957b1 | ||
![]() |
987ea5104a | ||
![]() |
135c085024 | ||
![]() |
80e938b1f2 | ||
![]() |
c9df52cba2 | ||
![]() |
e975585350 | ||
![]() |
7eeeab4c26 | ||
![]() |
a53df4fed4 | ||
![]() |
cdec4880d2 | ||
![]() |
c743387a26 | ||
![]() |
c38352ed22 | ||
![]() |
deb4a63c88 | ||
![]() |
ec016e80fb | ||
![]() |
ced163a25f | ||
![]() |
5a28e66b2f | ||
![]() |
35a25b372d | ||
![]() |
62b31ea8c5 | ||
![]() |
9ac92e0196 | ||
![]() |
287d028331 | ||
![]() |
cae13942e2 | ||
![]() |
b16cec631d | ||
![]() |
dc8a699e94 | ||
![]() |
b54e54da30 | ||
![]() |
fdfa6e2c06 | ||
![]() |
ecb00a95a0 | ||
![]() |
fe90d39943 | ||
![]() |
e2615a798e | ||
![]() |
ff42b65fc9 | ||
![]() |
1e5eff918d | ||
![]() |
e59674b5ac | ||
![]() |
5a1522deac | ||
![]() |
a5dff6df87 | ||
![]() |
790fa26953 | ||
![]() |
b2f9743f9f | ||
![]() |
66dedc5b2e | ||
![]() |
6330516630 | ||
![]() |
bd638f0356 | ||
![]() |
b11efbbf0c | ||
![]() |
68b6cc9bd4 | ||
![]() |
d586a8450d | ||
![]() |
33bc8ba1a3 | ||
![]() |
865268e820 | ||
![]() |
e5ea435cbe | ||
![]() |
6b32d04aec | ||
![]() |
06783364c4 | ||
![]() |
a812e6ae27 | ||
![]() |
841caec3e6 | ||
![]() |
9715eaba36 | ||
![]() |
bc50efec5e | ||
![]() |
25fd1a9d76 | ||
![]() |
64628f7e0d | ||
![]() |
4353c57cd0 | ||
![]() |
ce010725ac | ||
![]() |
7b4d5f9aad | ||
![]() |
9f879d3850 | ||
![]() |
87f5b50201 | ||
![]() |
bb7f0cd3a5 | ||
![]() |
3c92acb770 | ||
![]() |
04515591e2 | ||
![]() |
37455a8b6c | ||
![]() |
82fd9539b7 | ||
![]() |
52eabec70d | ||
![]() |
cad086c945 | ||
![]() |
6a49df59af | ||
![]() |
d224a2c47d | ||
![]() |
b72ce73931 | ||
![]() |
8f708d0e28 | ||
![]() |
38c7988274 | ||
![]() |
9c3363d465 | ||
![]() |
33262795ba | ||
![]() |
2aaa32ae5f | ||
![]() |
1b77530a31 | ||
![]() |
2eb120909b | ||
![]() |
26f4d1d329 | ||
![]() |
9d4eeb9c9f | ||
![]() |
1011468273 | ||
![]() |
1fad3cffc7 | ||
![]() |
c330e83095 | ||
![]() |
3b3fa4f643 | ||
![]() |
a6b45f777c | ||
![]() |
585bbec5d5 | ||
![]() |
0a005c2d95 | ||
![]() |
dda9c6ec09 | ||
![]() |
5b8cc3c9da | ||
![]() |
45fb5aa611 | ||
![]() |
6f360a6a2d | ||
![]() |
4a394a2a38 | ||
![]() |
da9e856369 | ||
![]() |
eedaa2c120 | ||
![]() |
d4e6aa8660 | ||
![]() |
4d365936f2 | ||
![]() |
6655dec1ec | ||
![]() |
99d421caec | ||
![]() |
8d33d52174 | ||
![]() |
6a7c19ae4b | ||
![]() |
fc9eb1157b | ||
![]() |
c83b6ff036 | ||
![]() |
e93e66d3e4 | ||
![]() |
ef11340280 | ||
![]() |
d4bb79ca18 | ||
![]() |
40e805f90f | ||
![]() |
a719a2337a | ||
![]() |
0ed8c05e1c | ||
![]() |
3deef34f5b | ||
![]() |
425078dc4e | ||
![]() |
5c877f56e9 | ||
![]() |
9486ab5fa1 | ||
![]() |
7c6ecf8e9e | ||
![]() |
ee36bab60b | ||
![]() |
ccf6f36ade | ||
![]() |
5cb22bb1d0 | ||
![]() |
b237c7f5cb | ||
![]() |
30c3e3db0c | ||
![]() |
39cdd1692a | ||
![]() |
603e46283f | ||
![]() |
c17adef242 | ||
![]() |
bacd484832 | ||
![]() |
7eb0a47fca | ||
![]() |
cfa6d50583 | ||
![]() |
47db669191 | ||
![]() |
ab60ad2072 | ||
![]() |
081c573653 | ||
![]() |
e22ed77527 | ||
![]() |
4eca7c062c | ||
![]() |
bef3d37802 | ||
![]() |
2ab52dad8e | ||
![]() |
6bd66910a9 | ||
![]() |
cfecf0ff62 | ||
![]() |
1bea5f8e6c | ||
![]() |
7ee4fd5b06 | ||
![]() |
b90f8e1b49 | ||
![]() |
2a92215d82 | ||
![]() |
d03b3c3a48 | ||
![]() |
5ac126250e | ||
![]() |
a1919a7a1f | ||
![]() |
dfae913dea | ||
![]() |
627d8097ac | ||
![]() |
8dc777fc46 | ||
![]() |
e7430b9c1a | ||
![]() |
4543285a9d | ||
![]() |
86ea2dc5cb | ||
![]() |
43d4122bc5 | ||
![]() |
e87da70191 | ||
![]() |
4f24f2d12f | ||
![]() |
44fdc49e26 | ||
![]() |
610bd32869 | ||
![]() |
059da40c64 | ||
![]() |
6f7c64e8ca | ||
![]() |
1b99ffbf58 | ||
![]() |
36195a9ed1 | ||
![]() |
d784a28ddb | ||
![]() |
e1619e6a6b | ||
![]() |
e3be0cd794 | ||
![]() |
ac3bccc67d | ||
![]() |
b2d46190cb | ||
![]() |
532da9d21f | ||
![]() |
dc73f85458 | ||
![]() |
ac1adefeec | ||
![]() |
6eb407ff3b | ||
![]() |
2c152e632b | ||
![]() |
047b4629bf | ||
![]() |
1244f23522 | ||
![]() |
5275f8d13f | ||
![]() |
c3eaf8bd6f | ||
![]() |
53c6e89486 | ||
![]() |
1bfd68bd34 | ||
![]() |
9410114be3 | ||
![]() |
5a3b0c6d78 | ||
![]() |
3459bf68cb | ||
![]() |
ab7bded221 | ||
![]() |
14a6894540 | ||
![]() |
946a3afde8 | ||
![]() |
b0900d5aa9 | ||
![]() |
e181e854cc | ||
![]() |
ad8dd8b648 | ||
![]() |
dfe2d4580e | ||
![]() |
7f1e267b2d | ||
![]() |
8c88f7052a | ||
![]() |
f749ade52c | ||
![]() |
533da6624f | ||
![]() |
f7063d4eaf | ||
![]() |
4de3c58e06 | ||
![]() |
e74dc22077 | ||
![]() |
7884de94e0 | ||
![]() |
169ae41d49 | ||
![]() |
6388613948 | ||
![]() |
34a4f5b597 | ||
![]() |
7cf7749367 | ||
![]() |
adcc6f564d | ||
![]() |
ba2570b83b | ||
![]() |
7ac14daf46 | ||
![]() |
caed26d191 | ||
![]() |
5c0c559685 | ||
![]() |
9079a968f0 | ||
![]() |
3df66e0257 | ||
![]() |
be36175bb8 | ||
![]() |
d0265eb3f6 | ||
![]() |
5479cc6274 | ||
![]() |
db891f2f16 | ||
![]() |
543cc42bf3 | ||
![]() |
2ad9a44b99 | ||
![]() |
ecd2275e1b | ||
![]() |
f904c0f67d | ||
![]() |
64855c9cf8 | ||
![]() |
9a7b242827 | ||
![]() |
e46c801554 | ||
![]() |
7be3f1fada | ||
![]() |
144b455eae | ||
![]() |
bd8659ac11 | ||
![]() |
5636db84a8 | ||
![]() |
5621b84284 | ||
![]() |
35cc7a8096 | ||
![]() |
a4e2c47c51 | ||
![]() |
7a60cd9aaf | ||
![]() |
2f94f49c06 | ||
![]() |
3306d0d699 | ||
![]() |
bf9590c1e6 | ||
![]() |
e2a84b2145 | ||
![]() |
3e01912eb2 | ||
![]() |
4bd6071071 | ||
![]() |
876d94e124 | ||
![]() |
a8e5b26744 | ||
![]() |
6c0514ab48 | ||
![]() |
bc84bc867b | ||
![]() |
98c49231b4 | ||
![]() |
cf5ac335ca | ||
![]() |
fe7d2a8a7e | ||
![]() |
5ffd405ba6 | ||
![]() |
edc738ae39 | ||
![]() |
46b241eda5 | ||
![]() |
f244d388be | ||
![]() |
59fd32ac68 | ||
![]() |
48ec0cdc34 | ||
![]() |
aed97c9dcc | ||
![]() |
26dcb6b8b0 | ||
![]() |
29c1b51073 | ||
![]() |
35946dcf7c | ||
![]() |
f49bdcd8c8 | ||
![]() |
cd51baa5f1 | ||
![]() |
4a7ebeea16 | ||
![]() |
f65d6c5775 | ||
![]() |
685e6d83c5 | ||
![]() |
593e67fe72 | ||
![]() |
f7706760cd | ||
![]() |
350cbea6d5 | ||
![]() |
98d9f639f7 | ||
![]() |
36f07604ae | ||
![]() |
4ee1c83073 | ||
![]() |
e73139d977 | ||
![]() |
fa95f56be0 | ||
![]() |
ef4496fd21 | ||
![]() |
dc3bf37521 | ||
![]() |
b00470aca0 | ||
![]() |
c9ecc560ee | ||
![]() |
d697d0f7a7 | ||
![]() |
0945021378 | ||
![]() |
b5d301e53c | ||
![]() |
fd2bc90ae2 | ||
![]() |
288c75851e | ||
![]() |
d5a375ee65 | ||
![]() |
5101677d9a | ||
![]() |
9135c6318d | ||
![]() |
d31e17894c | ||
![]() |
11fad8d3ce | ||
![]() |
f7f279c5ed | ||
![]() |
35b83cc168 | ||
![]() |
73b5d776da | ||
![]() |
b08b6cca8c | ||
![]() |
e2c886c9a4 | ||
![]() |
b00ce65e8b | ||
![]() |
bdea9a6d71 | ||
![]() |
bb770ba65b | ||
![]() |
1d8d3b3c27 | ||
![]() |
a5595015e6 | ||
![]() |
a23cdfbc2b | ||
![]() |
087c203f72 | ||
![]() |
970b19bf01 | ||
![]() |
99384a7060 | ||
![]() |
36c95db7bf |
108
.eslintrc.json
108
.eslintrc.json
@@ -8,16 +8,7 @@
|
|||||||
"eslint-plugin-deprecation",
|
"eslint-plugin-deprecation",
|
||||||
"unused-imports",
|
"unused-imports",
|
||||||
"eslint-plugin-lodash",
|
"eslint-plugin-lodash",
|
||||||
"eslint-plugin-jsonc",
|
"eslint-plugin-jsonc"
|
||||||
"eslint-plugin-rxjs",
|
|
||||||
"eslint-plugin-simple-import-sort",
|
|
||||||
"eslint-plugin-import-newlines",
|
|
||||||
"eslint-plugin-jsonc",
|
|
||||||
"dspace-angular-ts",
|
|
||||||
"dspace-angular-html"
|
|
||||||
],
|
|
||||||
"ignorePatterns": [
|
|
||||||
"lint/test/fixture"
|
|
||||||
],
|
],
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
@@ -27,8 +18,7 @@
|
|||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
"project": [
|
"project": [
|
||||||
"./tsconfig.json",
|
"./tsconfig.json",
|
||||||
"./cypress/tsconfig.json",
|
"./cypress/tsconfig.json"
|
||||||
"./lint/tsconfig.json"
|
|
||||||
],
|
],
|
||||||
"createDefaultProgram": true
|
"createDefaultProgram": true
|
||||||
},
|
},
|
||||||
@@ -37,32 +27,17 @@
|
|||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
"plugin:@typescript-eslint/recommended-requiring-type-checking",
|
||||||
"plugin:@angular-eslint/recommended",
|
"plugin:@angular-eslint/recommended",
|
||||||
"plugin:@angular-eslint/template/process-inline-templates",
|
"plugin:@angular-eslint/template/process-inline-templates"
|
||||||
"plugin:rxjs/recommended"
|
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"indent": [
|
|
||||||
"error",
|
|
||||||
2,
|
|
||||||
{
|
|
||||||
"SwitchCase": 1,
|
|
||||||
"ignoredNodes": [
|
|
||||||
"ClassBody.body > PropertyDefinition[decorators.length > 0] > .key"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"max-classes-per-file": [
|
"max-classes-per-file": [
|
||||||
"error",
|
"error",
|
||||||
1
|
1
|
||||||
],
|
],
|
||||||
"comma-dangle": [
|
"comma-dangle": [
|
||||||
"error",
|
"off",
|
||||||
"always-multiline"
|
"always-multiline"
|
||||||
],
|
],
|
||||||
"object-curly-spacing": [
|
|
||||||
"error",
|
|
||||||
"always"
|
|
||||||
],
|
|
||||||
"eol-last": [
|
"eol-last": [
|
||||||
"error",
|
"error",
|
||||||
"always"
|
"always"
|
||||||
@@ -129,13 +104,15 @@
|
|||||||
"allowTernary": true
|
"allowTernary": true
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"prefer-const": "error",
|
"prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint)
|
||||||
"no-case-declarations": "error",
|
|
||||||
"no-extra-boolean-cast": "error",
|
|
||||||
"prefer-spread": "off",
|
"prefer-spread": "off",
|
||||||
"no-underscore-dangle": "off",
|
"no-underscore-dangle": "off",
|
||||||
|
|
||||||
|
// todo: disabled rules from eslint:recommended, consider re-enabling & fixing
|
||||||
"no-prototype-builtins": "off",
|
"no-prototype-builtins": "off",
|
||||||
"no-useless-escape": "off",
|
"no-useless-escape": "off",
|
||||||
|
"no-case-declarations": "off",
|
||||||
|
"no-extra-boolean-cast": "off",
|
||||||
|
|
||||||
"@angular-eslint/directive-selector": [
|
"@angular-eslint/directive-selector": [
|
||||||
"error",
|
"error",
|
||||||
@@ -162,10 +139,10 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@angular-eslint/no-attribute-decorator": "error",
|
"@angular-eslint/no-attribute-decorator": "error",
|
||||||
|
"@angular-eslint/no-forward-ref": "error",
|
||||||
"@angular-eslint/no-output-native": "warn",
|
"@angular-eslint/no-output-native": "warn",
|
||||||
"@angular-eslint/no-output-on-prefix": "warn",
|
"@angular-eslint/no-output-on-prefix": "warn",
|
||||||
"@angular-eslint/no-conflicting-lifecycle": "warn",
|
"@angular-eslint/no-conflicting-lifecycle": "warn",
|
||||||
"@angular-eslint/use-lifecycle-interface": "error",
|
|
||||||
|
|
||||||
"@typescript-eslint/no-inferrable-types":[
|
"@typescript-eslint/no-inferrable-types":[
|
||||||
"error",
|
"error",
|
||||||
@@ -206,7 +183,7 @@
|
|||||||
],
|
],
|
||||||
"@typescript-eslint/type-annotation-spacing": "error",
|
"@typescript-eslint/type-annotation-spacing": "error",
|
||||||
"@typescript-eslint/unified-signatures": "error",
|
"@typescript-eslint/unified-signatures": "error",
|
||||||
"@typescript-eslint/ban-types": "error",
|
"@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable
|
||||||
"@typescript-eslint/no-floating-promises": "warn",
|
"@typescript-eslint/no-floating-promises": "warn",
|
||||||
"@typescript-eslint/no-misused-promises": "warn",
|
"@typescript-eslint/no-misused-promises": "warn",
|
||||||
"@typescript-eslint/restrict-plus-operands": "warn",
|
"@typescript-eslint/restrict-plus-operands": "warn",
|
||||||
@@ -223,65 +200,17 @@
|
|||||||
"@typescript-eslint/no-unsafe-return": "off",
|
"@typescript-eslint/no-unsafe-return": "off",
|
||||||
"@typescript-eslint/restrict-template-expressions": "off",
|
"@typescript-eslint/restrict-template-expressions": "off",
|
||||||
"@typescript-eslint/require-await": "off",
|
"@typescript-eslint/require-await": "off",
|
||||||
"@typescript-eslint/no-base-to-string": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"ignoredTypeNames": [
|
|
||||||
"ResourceType",
|
|
||||||
"Error"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"deprecation/deprecation": "warn",
|
"deprecation/deprecation": "warn",
|
||||||
|
|
||||||
"simple-import-sort/imports": "error",
|
|
||||||
"simple-import-sort/exports": "error",
|
|
||||||
"import/order": "off",
|
"import/order": "off",
|
||||||
"import/first": "error",
|
|
||||||
"import/newline-after-import": "error",
|
|
||||||
"import/no-duplicates": "error",
|
|
||||||
"import/no-deprecated": "warn",
|
"import/no-deprecated": "warn",
|
||||||
"import/no-namespace": "error",
|
"import/no-namespace": "error",
|
||||||
"import-newlines/enforce": [
|
|
||||||
"error",
|
|
||||||
{
|
|
||||||
"items": 1,
|
|
||||||
"semi": true,
|
|
||||||
"forceSingleLine": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
"unused-imports/no-unused-imports": "error",
|
"unused-imports/no-unused-imports": "error",
|
||||||
"lodash/import-scope": [
|
"lodash/import-scope": [
|
||||||
"error",
|
"error",
|
||||||
"method"
|
"method"
|
||||||
],
|
]
|
||||||
|
|
||||||
"rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases
|
|
||||||
|
|
||||||
// Custom DSpace Angular rules
|
|
||||||
"dspace-angular-ts/themed-component-classes": "error",
|
|
||||||
"dspace-angular-ts/themed-component-selectors": "error",
|
|
||||||
"dspace-angular-ts/themed-component-usages": "error"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"files": [
|
|
||||||
"*.spec.ts"
|
|
||||||
],
|
|
||||||
"parserOptions": {
|
|
||||||
"project": [
|
|
||||||
"./tsconfig.json",
|
|
||||||
"./cypress/tsconfig.json"
|
|
||||||
],
|
|
||||||
"createDefaultProgram": true
|
|
||||||
},
|
|
||||||
"rules": {
|
|
||||||
"prefer-const": "off",
|
|
||||||
|
|
||||||
// Custom DSpace Angular rules
|
|
||||||
"dspace-angular-ts/themed-component-usages": "error"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -292,9 +221,9 @@
|
|||||||
"plugin:@angular-eslint/template/recommended"
|
"plugin:@angular-eslint/template/recommended"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
// Custom DSpace Angular rules
|
// todo: re-enable & fix errors
|
||||||
"dspace-angular-html/themed-component-usages": "error",
|
"@angular-eslint/template/no-negated-async": "off",
|
||||||
"dspace-angular-html/no-disabled-attribute-on-button": "error"
|
"@angular-eslint/template/eqeqeq": "off"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -302,10 +231,13 @@
|
|||||||
"*.json5"
|
"*.json5"
|
||||||
],
|
],
|
||||||
"extends": [
|
"extends": [
|
||||||
"plugin:jsonc/recommended-with-jsonc"
|
"plugin:jsonc/recommended-with-json5"
|
||||||
],
|
],
|
||||||
"rules": {
|
"rules": {
|
||||||
"no-irregular-whitespace": "error",
|
// The ESLint core no-irregular-whitespace rule doesn't work well in JSON
|
||||||
|
// See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html
|
||||||
|
"no-irregular-whitespace": "off",
|
||||||
|
"jsonc/no-irregular-whitespace": "error",
|
||||||
"no-trailing-spaces": "error",
|
"no-trailing-spaces": "error",
|
||||||
"jsonc/comma-dangle": [
|
"jsonc/comma-dangle": [
|
||||||
"error",
|
"error",
|
||||||
|
3
.gitattributes
vendored
3
.gitattributes
vendored
@@ -14,6 +14,3 @@
|
|||||||
*.scss eol=lf
|
*.scss eol=lf
|
||||||
*.html eol=lf
|
*.html eol=lf
|
||||||
*.svg eol=lf
|
*.svg eol=lf
|
||||||
|
|
||||||
# Generated documentation should have LF line endings to reduce git noise
|
|
||||||
docs/lint/**/*.md eol=lf
|
|
115
.github/workflows/build.yml
vendored
115
.github/workflows/build.yml
vendored
@@ -29,6 +29,8 @@ jobs:
|
|||||||
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
||||||
# Tell Cypress to run e2e tests using the same UI URL
|
# Tell Cypress to run e2e tests using the same UI URL
|
||||||
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
CYPRESS_BASE_URL: http://127.0.0.1:4000
|
||||||
|
# Disable the cookie consent banner in e2e tests to avoid errors because of elements hidden by it
|
||||||
|
DSPACE_INFO_ENABLECOOKIECONSENTPOPUP: false
|
||||||
# When Chrome version is specified, we pin to a specific version of Chrome
|
# When Chrome version is specified, we pin to a specific version of Chrome
|
||||||
# Comment this out to use the latest release
|
# Comment this out to use the latest release
|
||||||
#CHROME_VERSION: "90.0.4430.212-1"
|
#CHROME_VERSION: "90.0.4430.212-1"
|
||||||
@@ -89,14 +91,8 @@ jobs:
|
|||||||
- name: Install Yarn dependencies
|
- name: Install Yarn dependencies
|
||||||
run: yarn install --frozen-lockfile
|
run: yarn install --frozen-lockfile
|
||||||
|
|
||||||
- name: Build lint plugins
|
|
||||||
run: yarn run build:lint
|
|
||||||
|
|
||||||
- name: Run lint plugin tests
|
|
||||||
run: yarn run test:lint:nobuild
|
|
||||||
|
|
||||||
- name: Run lint
|
- name: Run lint
|
||||||
run: yarn run lint:nobuild --quiet
|
run: yarn run lint --quiet
|
||||||
|
|
||||||
- name: Check for circular dependencies
|
- name: Check for circular dependencies
|
||||||
run: yarn run check-circ-deps
|
run: yarn run check-circ-deps
|
||||||
@@ -190,12 +186,115 @@ jobs:
|
|||||||
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
|
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
|
||||||
# If it does, then SSR is working, as this tag is created by our MetadataService.
|
# If it does, then SSR is working, as this tag is created by our MetadataService.
|
||||||
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
||||||
- name: Verify SSR (server-side rendering)
|
- name: Verify SSR (server-side rendering) on Homepage
|
||||||
run: |
|
run: |
|
||||||
result=$(wget -O- -q http://127.0.0.1:4000/home)
|
result=$(wget -O- -q http://127.0.0.1:4000/home)
|
||||||
echo "$result"
|
echo "$result"
|
||||||
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep DSpace
|
||||||
|
|
||||||
|
# Get a specific community in our test data and verify that the "<h1>" tag includes "Publications" (the community name).
|
||||||
|
# If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Community page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/communities/0958c910-2037-42a9-81c7-dca80e3892b4)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Publications
|
||||||
|
|
||||||
|
# Get a specific collection in our test data and verify that the "<h1>" tag includes "Articles" (the collection name).
|
||||||
|
# If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Collection page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/collections/282164f5-d325-4740-8dd1-fa4d6d3e7200)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<h1 [^>]*>[^><]*</h1>" | grep Articles
|
||||||
|
|
||||||
|
# Get a specific publication in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the title of this publication. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Publication page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/publication/6160810f-1e53-40db-81ef-f6621a727398)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "An Economic Model of Mortality Salience"
|
||||||
|
|
||||||
|
# Get a specific person in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the person. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Person page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/person/b1b2c768-bda1-448a-a073-fc541e8b24d9)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Simmons, Cameron"
|
||||||
|
|
||||||
|
# Get a specific project in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the project. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Project page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/project/46ccb608-a74c-4bf6-bc7a-e29cc7defea9)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "University Research Fellowship"
|
||||||
|
|
||||||
|
# Get a specific orgunit in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the orgunit. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on an OrgUnit page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/orgunit/9851674d-bd9a-467b-8d84-068deb568ccf)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Law and Development"
|
||||||
|
|
||||||
|
# Get a specific journal in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the journal. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Journal page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/journal/d4af6c3e-53d0-4757-81eb-566f3b45d63a)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology"
|
||||||
|
|
||||||
|
# Get a specific journal volume in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the volume. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Journal Volume page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalvolume/07c6249f-4bf7-494d-9ce3-6ffdb2aed538)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology Volume 28 (2017)"
|
||||||
|
|
||||||
|
# Get a specific journal issue in our test data and verify that the <meta name="title"> tag includes
|
||||||
|
# the name of the issue. If it does, then SSR is working.
|
||||||
|
- name: Verify SSR on a Journal Issue page
|
||||||
|
run: |
|
||||||
|
result=$(wget -O- -q http://127.0.0.1:4000/entities/journalissue/44c29473-5de2-48fa-b005-e5029aa1a50b)
|
||||||
|
echo "$result"
|
||||||
|
echo "$result" | grep -oE "<meta name=\"title\" [^>]*>" | grep "Environmental & Architectural Phenomenology Vol. 28, No. 1"
|
||||||
|
|
||||||
|
# Verify 301 Handle redirect behavior
|
||||||
|
# Note: /handle/123456789/260 is the same test Publication used by our e2e tests
|
||||||
|
- name: Verify 301 redirect from '/handle' URLs
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/handle/123456789/260 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "301" ]]
|
||||||
|
|
||||||
|
# Verify 403 error code behavior
|
||||||
|
- name: Verify 403 error code from '/403'
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/403 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "403" ]]
|
||||||
|
|
||||||
|
# Verify 404 error code behavior
|
||||||
|
- name: Verify 404 error code from '/404' and on invalid pages
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/404 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
result2=$(wget --server-response --quiet http://127.0.0.1:4000/invalidurl 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result2"
|
||||||
|
[[ "$result" -eq "404" && "$result2" -eq "404" ]]
|
||||||
|
|
||||||
|
# Verify 500 error code behavior
|
||||||
|
- name: Verify 500 error code from '/500'
|
||||||
|
run: |
|
||||||
|
result=$(wget --server-response --quiet http://127.0.0.1:4000/500 2>&1 | head -1 | awk '{print $2}')
|
||||||
|
echo "$result"
|
||||||
|
[[ "$result" -eq "500" ]]
|
||||||
|
|
||||||
- name: Stop running app
|
- name: Stop running app
|
||||||
run: kill -9 $(lsof -t -i:4000)
|
run: kill -9 $(lsof -t -i:4000)
|
||||||
|
|
||||||
|
6
.github/workflows/docker.yml
vendored
6
.github/workflows/docker.yml
vendored
@@ -4,7 +4,7 @@ name: Docker images
|
|||||||
# Run this Build for all pushes to 'main' or maintenance branches, or tagged releases.
|
# Run this Build for all pushes to 'main' or maintenance branches, or tagged releases.
|
||||||
# Also run for PRs to ensure PR doesn't break Docker build process
|
# Also run for PRs to ensure PR doesn't break Docker build process
|
||||||
# NOTE: uses "reusable-docker-build.yml" in DSpace/DSpace to actually build each of the Docker images
|
# NOTE: uses "reusable-docker-build.yml" in DSpace/DSpace to actually build each of the Docker images
|
||||||
# https://github.com/DSpace/DSpace/blob/main/.github/workflows/reusable-docker-build.yml
|
# https://github.com/DSpace/DSpace/blob/dspace-7_x/.github/workflows/reusable-docker-build.yml
|
||||||
#
|
#
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
||||||
if: github.repository == 'dspace/dspace-angular'
|
if: github.repository == 'dspace/dspace-angular'
|
||||||
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
|
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
|
||||||
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
|
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@dspace-7_x
|
||||||
with:
|
with:
|
||||||
build_id: dspace-angular-dev
|
build_id: dspace-angular-dev
|
||||||
image_name: dspace/dspace-angular
|
image_name: dspace/dspace-angular
|
||||||
@@ -43,7 +43,7 @@ jobs:
|
|||||||
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
|
||||||
if: github.repository == 'dspace/dspace-angular'
|
if: github.repository == 'dspace/dspace-angular'
|
||||||
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
|
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
|
||||||
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
|
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@dspace-7_x
|
||||||
with:
|
with:
|
||||||
build_id: dspace-angular-dist
|
build_id: dspace-angular-dist
|
||||||
image_name: dspace/dspace-angular
|
image_name: dspace/dspace-angular
|
||||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,5 +1,4 @@
|
|||||||
/.angular/cache
|
/.angular/cache
|
||||||
/.nx
|
|
||||||
/__build__
|
/__build__
|
||||||
/__server_build__
|
/__server_build__
|
||||||
/node_modules
|
/node_modules
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
|
||||||
|
|
||||||
# Test build:
|
# Test build:
|
||||||
# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-8_x-dist .
|
# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
|
||||||
|
|
||||||
FROM docker.io/node:18-alpine AS build
|
FROM docker.io/node:18-alpine AS build
|
||||||
|
|
||||||
|
@@ -35,7 +35,7 @@ https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
|
|||||||
Quick start
|
Quick start
|
||||||
-----------
|
-----------
|
||||||
|
|
||||||
**Ensure you're running [Node](https://nodejs.org) `v18.x` or `v20.x`, [npm](https://www.npmjs.com/) >= `v10.x` and [yarn](https://yarnpkg.com) == `v1.x`**
|
**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# clone the repo
|
# clone the repo
|
||||||
@@ -90,7 +90,7 @@ Requirements
|
|||||||
------------
|
------------
|
||||||
|
|
||||||
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
|
||||||
- Ensure you're running node `v18.x` or `v20.x` and yarn == `v1.x`
|
- Ensure you're running node `v16.x` or `v18.x` and yarn == `v1.x`
|
||||||
|
|
||||||
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
|
||||||
|
|
||||||
|
20
angular.json
20
angular.json
@@ -108,22 +108,22 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "dspace-angular:build",
|
"browserTarget": "dspace-angular:build",
|
||||||
"port": 4000
|
"port": 4000
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
"buildTarget": "dspace-angular:build:development"
|
"browserTarget": "dspace-angular:build:development"
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "dspace-angular:build:production"
|
"browserTarget": "dspace-angular:build:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "dspace-angular:build"
|
"browserTarget": "dspace-angular:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
@@ -216,23 +216,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve-ssr": {
|
"serve-ssr": {
|
||||||
"builder": "@angular-devkit/build-angular:ssr-dev-server",
|
"builder": "@nguniversal/builders:ssr-dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "dspace-angular:build",
|
"browserTarget": "dspace-angular:build",
|
||||||
"serverTarget": "dspace-angular:server",
|
"serverTarget": "dspace-angular:server",
|
||||||
"port": 4000
|
"port": 4000
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"buildTarget": "dspace-angular:build:production",
|
"browserTarget": "dspace-angular:build:production",
|
||||||
"serverTarget": "dspace-angular:server:production"
|
"serverTarget": "dspace-angular:server:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prerender": {
|
"prerender": {
|
||||||
"builder": "@angular-devkit/build-angular:prerender",
|
"builder": "@nguniversal/builders:prerender",
|
||||||
"options": {
|
"options": {
|
||||||
"buildTarget": "dspace-angular:build:production",
|
"browserTarget": "dspace-angular:build:production",
|
||||||
"serverTarget": "dspace-angular:server:production",
|
"serverTarget": "dspace-angular:server:production",
|
||||||
"routes": [
|
"routes": [
|
||||||
"/"
|
"/"
|
||||||
@@ -265,8 +265,6 @@
|
|||||||
"options": {
|
"options": {
|
||||||
"lintFilePatterns": [
|
"lintFilePatterns": [
|
||||||
"src/**/*.ts",
|
"src/**/*.ts",
|
||||||
"cypress/**/*.ts",
|
|
||||||
"lint/**/*.ts",
|
|
||||||
"src/**/*.html",
|
"src/**/*.html",
|
||||||
"src/**/*.json5"
|
"src/**/*.json5"
|
||||||
]
|
]
|
||||||
|
@@ -17,16 +17,30 @@ ui:
|
|||||||
# Trust X-FORWARDED-* headers from proxies (default = true)
|
# Trust X-FORWARDED-* headers from proxies (default = true)
|
||||||
useProxies: true
|
useProxies: true
|
||||||
|
|
||||||
# Angular Server Side Rendering (SSR) settings
|
# Angular Universal / Server Side Rendering (SSR) settings
|
||||||
ssr:
|
universal:
|
||||||
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
|
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
|
||||||
# Determining which styles are critical is a relatively expensive operation; this option is
|
# Determining which styles are critical is a relatively expensive operation; this option is
|
||||||
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
# disabled (false) by default to boost server performance at the expense of loading smoothness.
|
||||||
inlineCriticalCss: false
|
inlineCriticalCss: false
|
||||||
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
|
# Patterns to be run as regexes against the path of the page to check if SSR is allowed.
|
||||||
# NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures
|
# If the path match any of the regexes it will be served directly in CSR.
|
||||||
# hard refreshes (e.g. after login) trigger SSR while fully reloading the page.
|
# By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools.
|
||||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ]
|
excludePathPatterns:
|
||||||
|
- pattern: "^/communities/[a-f0-9-]{36}/browse(/.*)?$"
|
||||||
|
flag: "i"
|
||||||
|
- pattern: "^/collections/[a-f0-9-]{36}/browse(/.*)?$"
|
||||||
|
flag: "i"
|
||||||
|
- pattern: "^/browse/"
|
||||||
|
- pattern: "^/search$"
|
||||||
|
- pattern: "^/community-list$"
|
||||||
|
- pattern: "^/admin/"
|
||||||
|
- pattern: "^/processes/?"
|
||||||
|
- pattern: "^/notifications/"
|
||||||
|
- pattern: "^/statistics/?"
|
||||||
|
- pattern: "^/access-control/"
|
||||||
|
- pattern: "^/health$"
|
||||||
|
|
||||||
# Whether to enable rendering of Search component on SSR.
|
# Whether to enable rendering of Search component on SSR.
|
||||||
# If set to true the component will be included in the HTML returned from the server side rendering.
|
# If set to true the component will be included in the HTML returned from the server side rendering.
|
||||||
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
# If set to false the component will not be included in the HTML returned from the server side rendering.
|
||||||
@@ -45,16 +59,13 @@ ssr:
|
|||||||
# REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
|
# REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
|
||||||
# Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
|
# Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
|
||||||
replaceRestUrl: true
|
replaceRestUrl: true
|
||||||
# Enable request performance profiling data collection and printing the results in the server console.
|
|
||||||
# Defaults to false. Enabling in production is NOT recommended
|
|
||||||
#enablePerformanceProfiler: false
|
|
||||||
|
|
||||||
# The REST API server settings
|
# The REST API server settings
|
||||||
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
# NOTE: these settings define which (publicly available) REST API to use. They are usually
|
||||||
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
|
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
|
||||||
rest:
|
rest:
|
||||||
ssl: true
|
ssl: true
|
||||||
host: sandbox.dspace.org
|
host: demo.dspace.org
|
||||||
port: 443
|
port: 443
|
||||||
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
nameSpace: /server
|
nameSpace: /server
|
||||||
@@ -166,16 +177,12 @@ submission:
|
|||||||
# NOTE: after how many time (milliseconds) submission is saved automatically
|
# NOTE: after how many time (milliseconds) submission is saved automatically
|
||||||
# eg. timer: 5 * (1000 * 60); // 5 minutes
|
# eg. timer: 5 * (1000 * 60); // 5 minutes
|
||||||
timer: 0
|
timer: 0
|
||||||
# Always show the duplicate detection section if enabled, even if there are no potential duplicates detected
|
|
||||||
# (a message will be displayed to indicate no matches were found)
|
|
||||||
duplicateDetection:
|
|
||||||
alwaysShowSection: false
|
|
||||||
icons:
|
icons:
|
||||||
metadata:
|
metadata:
|
||||||
# NOTE: example of configuration
|
# NOTE: example of configuration
|
||||||
# # NOTE: metadata name
|
# # NOTE: metadata name
|
||||||
# - name: dc.author
|
# - name: dc.author
|
||||||
# # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used
|
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
||||||
# style: fas fa-user
|
# style: fas fa-user
|
||||||
- name: dc.author
|
- name: dc.author
|
||||||
style: fas fa-user
|
style: fas fa-user
|
||||||
@@ -186,40 +193,18 @@ submission:
|
|||||||
confidence:
|
confidence:
|
||||||
# NOTE: example of configuration
|
# NOTE: example of configuration
|
||||||
# # NOTE: confidence value
|
# # NOTE: confidence value
|
||||||
# - value: 600
|
# - name: dc.author
|
||||||
# # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used
|
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
|
||||||
# style: text-success
|
# style: fa-user
|
||||||
# icon: fa-circle-check
|
|
||||||
# # NOTE: the class configured in property style is used by default, the icon property could be used in component
|
|
||||||
# configured to use a 'icon mode' display (mainly in edit-item page)
|
|
||||||
- value: 600
|
- value: 600
|
||||||
style: text-success
|
style: text-success
|
||||||
icon: fa-circle-check
|
|
||||||
- value: 500
|
- value: 500
|
||||||
style: text-info
|
style: text-info
|
||||||
icon: fa-gear
|
|
||||||
- value: 400
|
- value: 400
|
||||||
style: text-warning
|
style: text-warning
|
||||||
icon: fa-circle-question
|
|
||||||
- value: 300
|
|
||||||
style: text-muted
|
|
||||||
icon: fa-thumbs-down
|
|
||||||
- value: 200
|
|
||||||
style: text-muted
|
|
||||||
icon: fa-circle-exclamation
|
|
||||||
- value: 100
|
|
||||||
style: text-muted
|
|
||||||
icon: fa-circle-stop
|
|
||||||
- value: 0
|
|
||||||
style: text-muted
|
|
||||||
icon: fa-ban
|
|
||||||
- value: -1
|
|
||||||
style: text-muted
|
|
||||||
icon: fa-circle-xmark
|
|
||||||
# default configuration
|
# default configuration
|
||||||
- value: default
|
- value: default
|
||||||
style: text-muted
|
style: text-muted
|
||||||
icon: fa-circle-xmark
|
|
||||||
|
|
||||||
# Default Language in which the UI will be rendered if the user's browser language is not an active language
|
# Default Language in which the UI will be rendered if the user's browser language is not an active language
|
||||||
defaultLanguage: en
|
defaultLanguage: en
|
||||||
@@ -336,8 +321,6 @@ homePage:
|
|||||||
# No. of communities to list per page on the home page
|
# No. of communities to list per page on the home page
|
||||||
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
|
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
|
||||||
pageSize: 5
|
pageSize: 5
|
||||||
# Enable or disable the Discover filters on the homepage
|
|
||||||
showDiscoverFilters: false
|
|
||||||
|
|
||||||
# Item Config
|
# Item Config
|
||||||
item:
|
item:
|
||||||
@@ -351,17 +334,8 @@ item:
|
|||||||
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
|
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
|
||||||
pageSize: 5
|
pageSize: 5
|
||||||
|
|
||||||
# Community Page Config
|
|
||||||
community:
|
|
||||||
# Search tab config
|
|
||||||
searchSection:
|
|
||||||
showSidebar: true
|
|
||||||
|
|
||||||
# Collection Page Config
|
# Collection Page Config
|
||||||
collection:
|
collection:
|
||||||
# Search tab config
|
|
||||||
searchSection:
|
|
||||||
showSidebar: true
|
|
||||||
edit:
|
edit:
|
||||||
undoTimeout: 10000 # 10 seconds
|
undoTimeout: 10000 # 10 seconds
|
||||||
|
|
||||||
@@ -438,11 +412,10 @@ mediaViewer:
|
|||||||
|
|
||||||
# Whether the end user agreement is required before users use the repository.
|
# Whether the end user agreement is required before users use the repository.
|
||||||
# If enabled, the user will be required to accept the agreement before they can use the repository.
|
# If enabled, the user will be required to accept the agreement before they can use the repository.
|
||||||
# And whether the privacy statement/COAR notify support page should exist or not.
|
# And whether the privacy statement should exist or not.
|
||||||
info:
|
info:
|
||||||
enableEndUserAgreement: true
|
enableEndUserAgreement: true
|
||||||
enablePrivacyStatement: true
|
enablePrivacyStatement: true
|
||||||
enableCOARNotifySupport: true
|
|
||||||
|
|
||||||
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||||
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
||||||
@@ -463,80 +436,6 @@ comcolSelectionSort:
|
|||||||
sortField: 'dc.title'
|
sortField: 'dc.title'
|
||||||
sortDirection: 'ASC'
|
sortDirection: 'ASC'
|
||||||
|
|
||||||
# Example of fallback collection for suggestions import
|
|
||||||
# suggestion:
|
|
||||||
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
|
|
||||||
# source: "openaire"
|
|
||||||
|
|
||||||
|
|
||||||
# Search settings
|
|
||||||
search:
|
|
||||||
# Settings to enable/disable or configure advanced search filters.
|
|
||||||
advancedFilters:
|
|
||||||
enabled: false
|
|
||||||
# List of filters to enable in "Advanced Search" dropdown
|
|
||||||
filter: [ 'title', 'author', 'subject', 'entityType' ]
|
|
||||||
#
|
|
||||||
# Number used to render n UI elements called loading skeletons that act as placeholders.
|
|
||||||
# These elements indicate that some content will be loaded in their stead.
|
|
||||||
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
|
|
||||||
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
|
|
||||||
defaultFiltersCount: 5
|
|
||||||
|
|
||||||
|
|
||||||
# Notify metrics
|
|
||||||
# Configuration for Notify Admin Dashboard for metrics visualization
|
|
||||||
notifyMetrics:
|
|
||||||
# Configuration for received messages
|
|
||||||
- title: 'admin-notify-dashboard.received-ldn'
|
|
||||||
boxes:
|
|
||||||
- color: '#B8DAFF'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.incoming.accepted'
|
|
||||||
config: 'NOTIFY.incoming.accepted'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.incoming.accepted.description'
|
|
||||||
- color: '#D4EDDA'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.incoming.processed'
|
|
||||||
config: 'NOTIFY.incoming.processed'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.incoming.processed.description'
|
|
||||||
- color: '#FDBBC7'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.incoming.failure'
|
|
||||||
config: 'NOTIFY.incoming.failure'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.incoming.failure.description'
|
|
||||||
- color: '#FDBBC7'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.incoming.untrusted'
|
|
||||||
config: 'NOTIFY.incoming.untrusted'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.incoming.untrusted.description'
|
|
||||||
- color: '#43515F'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems'
|
|
||||||
textColor: '#fff'
|
|
||||||
config: 'NOTIFY.incoming.involvedItems'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems.description'
|
|
||||||
# Configuration for outgoing messages
|
|
||||||
- title: 'admin-notify-dashboard.generated-ldn'
|
|
||||||
boxes:
|
|
||||||
- color: '#B8DAFF'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.queued'
|
|
||||||
config: 'NOTIFY.outgoing.queued'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.queued.description'
|
|
||||||
- color: '#FDEEBB'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry'
|
|
||||||
config: 'NOTIFY.outgoing.queued_for_retry'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry.description'
|
|
||||||
- color: '#FDBBC7'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.failure'
|
|
||||||
config: 'NOTIFY.outgoing.failure'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.failure.description'
|
|
||||||
- color: '#43515F'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems'
|
|
||||||
textColor: '#fff'
|
|
||||||
config: 'NOTIFY.outgoing.involvedItems'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems.description'
|
|
||||||
- color: '#D4EDDA'
|
|
||||||
title: 'admin-notify-dashboard.NOTIFY.outgoing.delivered'
|
|
||||||
config: 'NOTIFY.outgoing.delivered'
|
|
||||||
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
|
|
||||||
|
|
||||||
|
|
||||||
# Live Region configuration
|
# Live Region configuration
|
||||||
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
|
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
|
||||||
# Live regions are perceivable regions of a web page that are typically updated as a
|
# Live regions are perceivable regions of a web page that are typically updated as a
|
||||||
@@ -550,3 +449,17 @@ liveRegion:
|
|||||||
messageTimeOutDurationMs: 30000
|
messageTimeOutDurationMs: 30000
|
||||||
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
|
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
|
||||||
isVisible: false
|
isVisible: false
|
||||||
|
|
||||||
|
|
||||||
|
# Search settings
|
||||||
|
search:
|
||||||
|
# Number used to render n UI elements called loading skeletons that act as placeholders.
|
||||||
|
# These elements indicate that some content will be loaded in their stead.
|
||||||
|
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
|
||||||
|
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
|
||||||
|
defaultFiltersCount: 5
|
||||||
|
|
||||||
|
# Configuration for storing accessibility settings, used by the AccessibilitySettingsService
|
||||||
|
accessibility:
|
||||||
|
# The duration in days after which the accessibility settings cookie expires
|
||||||
|
cookieExpirationDuration: 7
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
rest:
|
rest:
|
||||||
ssl: true
|
ssl: true
|
||||||
host: sandbox.dspace.org
|
host: demo.dspace.org
|
||||||
port: 443
|
port: 443
|
||||||
nameSpace: /server
|
nameSpace: /server
|
||||||
|
@@ -34,6 +34,6 @@ describe('Admin Export Modals', () => {
|
|||||||
cy.get('a[data-test="menu.section.export_batch"]').click();
|
cy.get('a[data-test="menu.section.export_batch"]').click();
|
||||||
|
|
||||||
// Analyze <ds-export-batch-selector> for accessibility
|
// Analyze <ds-export-batch-selector> for accessibility
|
||||||
testA11y('ds-export-batch-selector');
|
testA11y('ds-export-metadata-selector');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,17 +0,0 @@
|
|||||||
import { testA11y } from 'cypress/support/utils';
|
|
||||||
|
|
||||||
describe('Admin Notifications Publication Claim Page', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Must login as an Admin to see the page
|
|
||||||
cy.visit('/admin/notifications/publication-claim');
|
|
||||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
|
||||||
|
|
||||||
//Page must first be visible
|
|
||||||
cy.get('ds-admin-notifications-publication-claim-page').should('be.visible');
|
|
||||||
// Analyze <ds-admin-notifications-publication-claim-page> for accessibility issues
|
|
||||||
testA11y('ds-admin-notifications-publication-claim-page');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,5 +1,5 @@
|
|||||||
import { testA11y } from 'cypress/support/utils';
|
|
||||||
import { Options } from 'cypress-axe';
|
import { Options } from 'cypress-axe';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Admin Sidebar', () => {
|
describe('Admin Sidebar', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -13,7 +13,7 @@ describe('Admin Sidebar', () => {
|
|||||||
cy.get('#sidebar-collapse-toggle').click();
|
cy.get('#sidebar-collapse-toggle').click();
|
||||||
|
|
||||||
// Click on every expandable section to open all menus
|
// Click on every expandable section to open all menus
|
||||||
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
|
cy.get('ds-expandable-admin-sidebar-section').click({multiple: true});
|
||||||
|
|
||||||
// Analyze <ds-admin-sidebar> for accessibility
|
// Analyze <ds-admin-sidebar> for accessibility
|
||||||
testA11y('ds-admin-sidebar',
|
testA11y('ds-admin-sidebar',
|
||||||
@@ -22,7 +22,7 @@ describe('Admin Sidebar', () => {
|
|||||||
// Currently all expandable sections have nested interactive elements
|
// Currently all expandable sections have nested interactive elements
|
||||||
// See https://github.com/DSpace/dspace-angular/issues/2178
|
// See https://github.com/DSpace/dspace-angular/issues/2178
|
||||||
'nested-interactive': { enabled: false },
|
'nested-interactive': { enabled: false },
|
||||||
},
|
}
|
||||||
} as Options);
|
} as Options);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -5,9 +5,9 @@ describe('Browse By Author', () => {
|
|||||||
cy.visit('/browse/author');
|
cy.visit('/browse/author');
|
||||||
|
|
||||||
// Wait for <ds-browse-by-metadata-page> to be visible
|
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||||
cy.get('ds-browse-by-metadata').should('be.visible');
|
cy.get('ds-browse-by-metadata-page').should('be.visible');
|
||||||
|
|
||||||
// Analyze <ds-browse-by-metadata-page> for accessibility
|
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||||
testA11y('ds-browse-by-metadata');
|
testA11y('ds-browse-by-metadata-page');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -5,9 +5,9 @@ describe('Browse By Date Issued', () => {
|
|||||||
cy.visit('/browse/dateissued');
|
cy.visit('/browse/dateissued');
|
||||||
|
|
||||||
// Wait for <ds-browse-by-date-page> to be visible
|
// Wait for <ds-browse-by-date-page> to be visible
|
||||||
cy.get('ds-browse-by-date').should('be.visible');
|
cy.get('ds-browse-by-date-page').should('be.visible');
|
||||||
|
|
||||||
// Analyze <ds-browse-by-date-page> for accessibility
|
// Analyze <ds-browse-by-date-page> for accessibility
|
||||||
testA11y('ds-browse-by-date');
|
testA11y('ds-browse-by-date-page');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -5,9 +5,9 @@ describe('Browse By Subject', () => {
|
|||||||
cy.visit('/browse/subject');
|
cy.visit('/browse/subject');
|
||||||
|
|
||||||
// Wait for <ds-browse-by-metadata-page> to be visible
|
// Wait for <ds-browse-by-metadata-page> to be visible
|
||||||
cy.get('ds-browse-by-metadata').should('be.visible');
|
cy.get('ds-browse-by-metadata-page').should('be.visible');
|
||||||
|
|
||||||
// Analyze <ds-browse-by-metadata-page> for accessibility
|
// Analyze <ds-browse-by-metadata-page> for accessibility
|
||||||
testA11y('ds-browse-by-metadata');
|
testA11y('ds-browse-by-metadata-page');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -5,9 +5,9 @@ describe('Browse By Title', () => {
|
|||||||
cy.visit('/browse/title');
|
cy.visit('/browse/title');
|
||||||
|
|
||||||
// Wait for <ds-browse-by-title-page> to be visible
|
// Wait for <ds-browse-by-title-page> to be visible
|
||||||
cy.get('ds-browse-by-title').should('be.visible');
|
cy.get('ds-browse-by-title-page').should('be.visible');
|
||||||
|
|
||||||
// Analyze <ds-browse-by-title-page> for accessibility
|
// Analyze <ds-browse-by-title-page> for accessibility
|
||||||
testA11y('ds-browse-by-title');
|
testA11y('ds-browse-by-title-page');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
beforeEach(() => {
|
|
||||||
cy.visit('/collections/create?parent='.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
|
||||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show loading component while saving', () => {
|
|
||||||
const title = 'Test Collection Title';
|
|
||||||
cy.get('#title').type(title);
|
|
||||||
|
|
||||||
cy.get('button[type="submit"]').click();
|
|
||||||
|
|
||||||
cy.get('ds-loading').should('be.visible');
|
|
||||||
});
|
|
@@ -3,8 +3,15 @@ import { testA11y } from 'cypress/support/utils';
|
|||||||
describe('Collection Page', () => {
|
describe('Collection Page', () => {
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
|
||||||
|
|
||||||
|
// Visit Collections page
|
||||||
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
|
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
|
||||||
|
|
||||||
|
// Wait for the "viewevent" to trigger on the Collection page.
|
||||||
|
// This ensures our <ds-collection-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
|
||||||
|
cy.wait('@viewevent');
|
||||||
|
|
||||||
// <ds-collection-page> tag must be loaded
|
// <ds-collection-page> tag must be loaded
|
||||||
cy.get('ds-collection-page').should('be.visible');
|
cy.get('ds-collection-page').should('be.visible');
|
||||||
|
|
||||||
|
@@ -1,13 +0,0 @@
|
|||||||
beforeEach(() => {
|
|
||||||
cy.visit('/communities/create');
|
|
||||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show loading component while saving', () => {
|
|
||||||
const title = 'Test Community Title';
|
|
||||||
cy.get('#title').type(title);
|
|
||||||
|
|
||||||
cy.get('button[type="submit"]').click();
|
|
||||||
|
|
||||||
cy.get('ds-loading').should('be.visible');
|
|
||||||
});
|
|
@@ -6,7 +6,7 @@ describe('Community Statistics Page', () => {
|
|||||||
|
|
||||||
it('should load if you click on "Statistics" from a Community page', () => {
|
it('should load if you click on "Statistics" from a Community page', () => {
|
||||||
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
|
||||||
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -15,24 +15,24 @@ describe('Header', () => {
|
|||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
|
|
||||||
// Click the language switcher (globe) in header
|
// Click the language switcher (globe) in header
|
||||||
cy.get('a[data-test="lang-switch"]').click();
|
cy.get('button[data-test="lang-switch"]').click();
|
||||||
// Click on the "Deusch" language in dropdown
|
// Click on the "Deusch" language in dropdown
|
||||||
cy.get('#language-menu-list li').contains('Deutsch').click();
|
cy.get('#language-menu-list div[role="option"]').contains('Deutsch').click();
|
||||||
|
|
||||||
// HTML "lang" attribute should switch to "de"
|
// HTML "lang" attribute should switch to "de"
|
||||||
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
|
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
|
||||||
|
|
||||||
// Login menu should now be in German
|
// Login menu should now be in German
|
||||||
cy.get('a[data-test="login-menu"]').contains('Anmelden');
|
cy.get('[data-test="login-menu"]').contains('Anmelden');
|
||||||
|
|
||||||
// Change back to English from language switcher
|
// Change back to English from language switcher
|
||||||
cy.get('a[data-test="lang-switch"]').click();
|
cy.get('button[data-test="lang-switch"]').click();
|
||||||
cy.get('#language-menu-list li').contains('English').click();
|
cy.get('#language-menu-list div[role="option"]').contains('English').click();
|
||||||
|
|
||||||
// HTML "lang" attribute should switch to "en"
|
// HTML "lang" attribute should switch to "en"
|
||||||
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
|
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
|
||||||
|
|
||||||
// Login menu should now be in English
|
// Login menu should now be in English
|
||||||
cy.get('a[data-test="login-menu"]').contains('Log In');
|
cy.get('[data-test="login-menu"]').contains('Log In');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,12 +1,11 @@
|
|||||||
import '../support/commands';
|
|
||||||
|
|
||||||
import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
|
import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
|
||||||
import { testA11y } from 'cypress/support/utils';
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
import '../support/commands';
|
||||||
|
|
||||||
describe('Site Statistics Page', () => {
|
describe('Site Statistics Page', () => {
|
||||||
it('should load if you click on "Statistics" from homepage', () => {
|
it('should load if you click on "Statistics" from homepage', () => {
|
||||||
cy.visit('/');
|
cy.visit('/');
|
||||||
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', '/statistics');
|
cy.location('pathname').should('eq', '/statistics');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -7,12 +7,19 @@ describe('Item Page', () => {
|
|||||||
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
|
||||||
it('should redirect to the entity page when navigating to an item page', () => {
|
it('should redirect to the entity page when navigating to an item page', () => {
|
||||||
cy.visit(ITEMPAGE);
|
cy.visit(ITEMPAGE);
|
||||||
|
cy.wait(1000);
|
||||||
cy.location('pathname').should('eq', ENTITYPAGE);
|
cy.location('pathname').should('eq', ENTITYPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
|
||||||
|
|
||||||
cy.visit(ENTITYPAGE);
|
cy.visit(ENTITYPAGE);
|
||||||
|
|
||||||
|
// Wait for the "viewevent" to trigger on the Item page.
|
||||||
|
// This ensures our <ds-item-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
|
||||||
|
cy.wait('@viewevent');
|
||||||
|
|
||||||
// <ds-item-page> tag must be loaded
|
// <ds-item-page> tag must be loaded
|
||||||
cy.get('ds-item-page').should('be.visible');
|
cy.get('ds-item-page').should('be.visible');
|
||||||
|
|
||||||
@@ -21,8 +28,14 @@ describe('Item Page', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests on full item page', () => {
|
it('should pass accessibility tests on full item page', () => {
|
||||||
|
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
|
||||||
|
|
||||||
cy.visit(ENTITYPAGE + '/full');
|
cy.visit(ENTITYPAGE + '/full');
|
||||||
|
|
||||||
|
// Wait for the "viewevent" to trigger on the Item page.
|
||||||
|
// This ensures our <ds-item-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
|
||||||
|
cy.wait('@viewevent');
|
||||||
|
|
||||||
// <ds-full-item-page> tag must be loaded
|
// <ds-full-item-page> tag must be loaded
|
||||||
cy.get('ds-full-item-page').should('be.visible');
|
cy.get('ds-full-item-page').should('be.visible');
|
||||||
|
|
||||||
|
@@ -6,7 +6,7 @@ describe('Item Statistics Page', () => {
|
|||||||
|
|
||||||
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
it('should load if you click on "Statistics" from an Item/Entity page', () => {
|
||||||
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
|
||||||
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
|
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
|
||||||
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -23,8 +23,7 @@ describe('Item Statistics Page', () => {
|
|||||||
|
|
||||||
it('should contain a "Total visits per month" section', () => {
|
it('should contain a "Total visits per month" section', () => {
|
||||||
cy.visit(ITEMSTATISTICSPAGE);
|
cy.visit(ITEMSTATISTICSPAGE);
|
||||||
// Check just for existence because this table is empty in CI environment as it's historical data
|
cy.get('table[data-test="TotalVisitsPerMonth"]').should('be.visible');
|
||||||
cy.get('.'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('_TotalVisitsPerMonth')).should('exist');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
it('should pass accessibility tests', () => {
|
||||||
|
@@ -1,15 +0,0 @@
|
|||||||
const ADD_TEMPLATE_ITEM_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/itemtemplate');
|
|
||||||
|
|
||||||
describe('Item Template', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
cy.visit(ADD_TEMPLATE_ITEM_PAGE);
|
|
||||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should load properly', () => {
|
|
||||||
cy.contains('.ds-header-row .lbl-cell', 'Field', { timeout: 10000 }).should('exist').should('be.visible');
|
|
||||||
cy.contains('.ds-header-row b', 'Value', { timeout: 10000 }).should('exist').should('be.visible');
|
|
||||||
cy.contains('.ds-header-row b', 'Lang', { timeout: 10000 }).should('exist').should('be.visible');
|
|
||||||
cy.contains('.ds-header-row b', 'Edit', { timeout: 10000 }).should('exist').should('be.visible');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -102,10 +102,10 @@ describe('Login Modal', () => {
|
|||||||
page.openLoginMenu();
|
page.openLoginMenu();
|
||||||
|
|
||||||
// Registration link should be visible
|
// Registration link should be visible
|
||||||
cy.get('ds-header [data-test="register"]').should('be.visible');
|
cy.get('ds-themed-header [data-test="register"]').should('be.visible');
|
||||||
|
|
||||||
// Click registration link & you should go to registration page
|
// Click registration link & you should go to registration page
|
||||||
cy.get('ds-header [data-test="register"]').click();
|
cy.get('ds-themed-header [data-test="register"]').click();
|
||||||
cy.location('pathname').should('eq', '/register');
|
cy.location('pathname').should('eq', '/register');
|
||||||
cy.get('ds-register-email').should('exist');
|
cy.get('ds-register-email').should('exist');
|
||||||
|
|
||||||
@@ -119,10 +119,10 @@ describe('Login Modal', () => {
|
|||||||
page.openLoginMenu();
|
page.openLoginMenu();
|
||||||
|
|
||||||
// Forgot password link should be visible
|
// Forgot password link should be visible
|
||||||
cy.get('ds-header [data-test="forgot"]').should('be.visible');
|
cy.get('ds-themed-header [data-test="forgot"]').should('be.visible');
|
||||||
|
|
||||||
// Click link & you should go to Forgot Password page
|
// Click link & you should go to Forgot Password page
|
||||||
cy.get('ds-header [data-test="forgot"]').click();
|
cy.get('ds-themed-header [data-test="forgot"]').click();
|
||||||
cy.location('pathname').should('eq', '/forgot');
|
cy.location('pathname').should('eq', '/forgot');
|
||||||
cy.get('ds-forgot-email').should('exist');
|
cy.get('ds-forgot-email').should('exist');
|
||||||
|
|
||||||
|
@@ -84,7 +84,7 @@ describe('My DSpace page', () => {
|
|||||||
cy.url().should('include', '/mydspace');
|
cy.url().should('include', '/mydspace');
|
||||||
|
|
||||||
// Close any open notifications, to make sure they don't get in the way of next steps
|
// Close any open notifications, to make sure they don't get in the way of next steps
|
||||||
cy.get('[data-dismiss="alert"]').click({ multiple: true });
|
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||||
|
|
||||||
// This is the GET command that will actually run the search
|
// This is the GET command that will actually run the search
|
||||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||||
|
@@ -1,16 +0,0 @@
|
|||||||
import { testA11y } from 'cypress/support/utils';
|
|
||||||
|
|
||||||
describe('Quality Assurance Source Page', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
// Must login as an Admin to see the page
|
|
||||||
cy.visit('/notifications/quality-assurance');
|
|
||||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should pass accessibility tests', () => {
|
|
||||||
// Source page must first be visible
|
|
||||||
cy.get('ds-quality-assurance-source-page-component').should('be.visible');
|
|
||||||
// Analyze <ds-quality-assurance-source-page-component> for accessibility issues
|
|
||||||
testA11y('ds-quality-assurance-source-page-component');
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,16 +1,16 @@
|
|||||||
const page = {
|
const page = {
|
||||||
fillOutQueryInNavBar(query) {
|
fillOutQueryInNavBar(query) {
|
||||||
// Click the magnifying glass
|
// Click the magnifying glass
|
||||||
cy.get('ds-header [data-test="header-search-icon"]').click();
|
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
|
||||||
// Fill out a query in input that appears
|
// Fill out a query in input that appears
|
||||||
cy.get('ds-header [data-test="header-search-box"]').type(query);
|
cy.get('ds-themed-header [data-test="header-search-box"]').type(query);
|
||||||
},
|
},
|
||||||
submitQueryByPressingEnter() {
|
submitQueryByPressingEnter() {
|
||||||
cy.get('ds-header [data-test="header-search-box"]').type('{enter}');
|
cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}');
|
||||||
},
|
},
|
||||||
submitQueryByPressingIcon() {
|
submitQueryByPressingIcon() {
|
||||||
cy.get('ds-header [data-test="header-search-icon"]').click();
|
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
|
||||||
},
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('Search from Navigation Bar', () => {
|
describe('Search from Navigation Bar', () => {
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
import { testA11y } from 'cypress/support/utils';
|
|
||||||
import { Options } from 'cypress-axe';
|
import { Options } from 'cypress-axe';
|
||||||
|
import { testA11y } from 'cypress/support/utils';
|
||||||
|
|
||||||
describe('Search Page', () => {
|
describe('Search Page', () => {
|
||||||
// NOTE: these tests currently assume this query will return results!
|
// NOTE: these tests currently assume this query will return results!
|
||||||
@@ -49,9 +49,9 @@ describe('Search Page', () => {
|
|||||||
{
|
{
|
||||||
rules: {
|
rules: {
|
||||||
// Card titles fail this test currently
|
// Card titles fail this test currently
|
||||||
'heading-order': { enabled: false },
|
'heading-order': { enabled: false }
|
||||||
},
|
}
|
||||||
} as Options,
|
} as Options
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -41,9 +41,9 @@ describe('New Submission page', () => {
|
|||||||
// All select boxes fail to have a name / aria-label.
|
// All select boxes fail to have a name / aria-label.
|
||||||
// This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216
|
// This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
'select-name': { enabled: false },
|
'select-name': { enabled: false },
|
||||||
},
|
}
|
||||||
|
|
||||||
} as Options,
|
} as Options
|
||||||
);
|
);
|
||||||
|
|
||||||
// Discard button should work
|
// Discard button should work
|
||||||
@@ -95,7 +95,7 @@ describe('New Submission page', () => {
|
|||||||
// A success alert should be visible
|
// A success alert should be visible
|
||||||
cy.get('ds-notification div.alert-success').should('be.visible');
|
cy.get('ds-notification div.alert-success').should('be.visible');
|
||||||
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
|
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
|
||||||
cy.get('[data-dismiss="alert"]').click({ multiple: true });
|
cy.get('[data-dismiss="alert"]').click({multiple: true});
|
||||||
|
|
||||||
// This is the GET command that will actually run the search
|
// This is the GET command that will actually run the search
|
||||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||||
@@ -125,7 +125,7 @@ describe('New Submission page', () => {
|
|||||||
|
|
||||||
// Confirm the required license by checking checkbox
|
// Confirm the required license by checking checkbox
|
||||||
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
|
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
|
||||||
cy.get('input#granted').check( { force: true } );
|
cy.get('input#granted').check( {force: true} );
|
||||||
|
|
||||||
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
|
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
|
||||||
// This ensures our UI displays the dropzone that covers the entire submission page.
|
// This ensures our UI displays the dropzone that covers the entire submission page.
|
||||||
@@ -138,7 +138,7 @@ describe('New Submission page', () => {
|
|||||||
// Upload our DSpace logo via drag & drop onto submission form
|
// Upload our DSpace logo via drag & drop onto submission form
|
||||||
// cy.get('div#section_upload')
|
// cy.get('div#section_upload')
|
||||||
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', {
|
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', {
|
||||||
action: 'drag-drop',
|
action: 'drag-drop'
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for upload to complete before proceeding
|
// Wait for upload to complete before proceeding
|
||||||
@@ -196,9 +196,9 @@ describe('New Submission page', () => {
|
|||||||
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
|
||||||
'aria-required-children': { enabled: false },
|
'aria-required-children': { enabled: false },
|
||||||
'nested-interactive': { enabled: false },
|
'nested-interactive': { enabled: false },
|
||||||
},
|
}
|
||||||
|
|
||||||
} as Options,
|
} as Options
|
||||||
);
|
);
|
||||||
|
|
||||||
// Click the lookup button next to "Publication" field
|
// Click the lookup button next to "Publication" field
|
||||||
@@ -212,7 +212,7 @@ describe('New Submission page', () => {
|
|||||||
testA11y({
|
testA11y({
|
||||||
include: ['ds-dynamic-lookup-relation-modal'],
|
include: ['ds-dynamic-lookup-relation-modal'],
|
||||||
exclude: [
|
exclude: [
|
||||||
['ul.nav-tabs'], // Tabs at top of model have several issues which seem to be caused by ng-bootstrap
|
['ul.nav-tabs'] // Tabs at top of model have several issues which seem to be caused by ng-bootstrap
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -54,6 +54,6 @@ module.exports = (on, config) => {
|
|||||||
// Retrieve currently saved value of REST Domain
|
// Retrieve currently saved value of REST Domain
|
||||||
getRestBaseDomain() {
|
getRestBaseDomain() {
|
||||||
return REST_DOMAIN ;
|
return REST_DOMAIN ;
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@@ -3,14 +3,8 @@
|
|||||||
// See docs at https://docs.cypress.io/api/cypress-api/custom-commands
|
// See docs at https://docs.cypress.io/api/cypress-api/custom-commands
|
||||||
// ***********************************************
|
// ***********************************************
|
||||||
|
|
||||||
import {
|
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
|
||||||
AuthTokenInfo,
|
import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants';
|
||||||
TOKENITEM,
|
|
||||||
} from 'src/app/core/auth/models/auth-token-info.model';
|
|
||||||
import {
|
|
||||||
DSPACE_XSRF_COOKIE,
|
|
||||||
XSRF_REQUEST_HEADER,
|
|
||||||
} from 'src/app/core/xsrf/xsrf.constants';
|
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
|
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
|
||||||
@@ -71,9 +65,9 @@ function login(email: string, password: string): void {
|
|||||||
cy.request({
|
cy.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
url: baseRestUrl + '/api/authn/login',
|
url: baseRestUrl + '/api/authn/login',
|
||||||
headers: { [XSRF_REQUEST_HEADER]: csrfToken },
|
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
|
||||||
form: true, // indicates the body should be form urlencoded
|
form: true, // indicates the body should be form urlencoded
|
||||||
body: { user: email, password: password },
|
body: { user: email, password: password }
|
||||||
}).then((resp) => {
|
}).then((resp) => {
|
||||||
// We expect a successful login
|
// We expect a successful login
|
||||||
expect(resp.status).to.eq(200);
|
expect(resp.status).to.eq(200);
|
||||||
|
@@ -15,12 +15,13 @@
|
|||||||
|
|
||||||
// Import all custom Commands (from commands.ts) for all tests
|
// Import all custom Commands (from commands.ts) for all tests
|
||||||
import './commands';
|
import './commands';
|
||||||
|
|
||||||
// Import Cypress Axe tools for all tests
|
// Import Cypress Axe tools for all tests
|
||||||
// https://github.com/component-driven/cypress-axe
|
// https://github.com/component-driven/cypress-axe
|
||||||
import 'cypress-axe';
|
import 'cypress-axe';
|
||||||
|
|
||||||
import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants';
|
import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants';
|
||||||
|
|
||||||
|
|
||||||
// Runs once before all tests
|
// Runs once before all tests
|
||||||
before(() => {
|
before(() => {
|
||||||
// Cypress doesn't have access to the running application in Node.js.
|
// Cypress doesn't have access to the running application in Node.js.
|
||||||
|
@@ -7,7 +7,7 @@ import { Options } from 'cypress-axe';
|
|||||||
function terminalLog(violations: Result[]) {
|
function terminalLog(violations: Result[]) {
|
||||||
cy.task(
|
cy.task(
|
||||||
'log',
|
'log',
|
||||||
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`,
|
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`
|
||||||
);
|
);
|
||||||
// pluck specific keys to keep the table readable
|
// pluck specific keys to keep the table readable
|
||||||
const violationData = violations.map(
|
const violationData = violations.map(
|
||||||
@@ -17,8 +17,8 @@ function terminalLog(violations: Result[]) {
|
|||||||
description,
|
description,
|
||||||
helpUrl,
|
helpUrl,
|
||||||
nodes: nodes.length,
|
nodes: nodes.length,
|
||||||
html: nodes.map(node => node.html),
|
html: nodes.map(node => node.html)
|
||||||
}),
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Print violations as an array, since 'node.html' above often breaks table alignment
|
// Print violations as an array, since 'node.html' above often breaks table alignment
|
||||||
@@ -38,7 +38,7 @@ export const testA11y = (context?: any, options?: Options) => {
|
|||||||
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
|
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
|
||||||
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
|
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
|
||||||
{ id: 'color-contrast', enabled: false },
|
{ id: 'color-contrast', enabled: false },
|
||||||
],
|
]
|
||||||
});
|
});
|
||||||
cy.checkA11y(context, options, terminalLog);
|
cy.checkA11y(context, options, terminalLog);
|
||||||
};
|
};
|
||||||
|
@@ -4,7 +4,6 @@
|
|||||||
"**/*.ts"
|
"**/*.ts"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"sourceMap": false,
|
|
||||||
"types": [
|
"types": [
|
||||||
"cypress",
|
"cypress",
|
||||||
"cypress-axe",
|
"cypress-axe",
|
||||||
|
@@ -20,17 +20,17 @@ the Docker compose scripts in this 'docker' folder.
|
|||||||
|
|
||||||
### Dockerfile
|
### Dockerfile
|
||||||
|
|
||||||
This Dockerfile is used to build a *development* DSpace Angular UI image, published as 'dspace/dspace-angular'
|
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
|
||||||
|
|
||||||
```
|
```
|
||||||
docker build -t dspace/dspace-angular:dspace-8_x .
|
docker build -t dspace/dspace-angular:dspace-7_x .
|
||||||
```
|
```
|
||||||
|
|
||||||
This image is built *automatically* after each commit is made to the `main` branch.
|
This image is built *automatically* after each commit is made to the `main` branch.
|
||||||
|
|
||||||
Admins to our DockerHub repo can manually publish with the following command.
|
Admins to our DockerHub repo can manually publish with the following command.
|
||||||
```
|
```
|
||||||
docker push dspace/dspace-angular:dspace-8_x
|
docker push dspace/dspace-angular:dspace-7_x
|
||||||
```
|
```
|
||||||
|
|
||||||
### Dockerfile.dist
|
### Dockerfile.dist
|
||||||
@@ -39,18 +39,18 @@ The `Dockerfile.dist` is used to generate a *production* build and runtime envir
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# build the latest image
|
# build the latest image
|
||||||
docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-8_x-dist .
|
docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
|
||||||
```
|
```
|
||||||
|
|
||||||
A default/demo version of this image is built *automatically*.
|
A default/demo version of this image is built *automatically*.
|
||||||
|
|
||||||
## 'docker' directory
|
## 'docker' directory
|
||||||
- docker-compose.yml
|
- docker-compose.yml
|
||||||
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace REST instance will also be started in Docker.
|
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
||||||
- docker-compose-rest.yml
|
- docker-compose-rest.yml
|
||||||
- Runs a published instance of the DSpace REST API - persists data in Docker volumes
|
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
|
||||||
- docker-compose-ci.yml
|
- docker-compose-ci.yml
|
||||||
- Runs a published instance of the DSpace REST API for CI testing. The database is re-populated from a SQL dump on each startup.
|
- Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
|
||||||
- cli.yml
|
- cli.yml
|
||||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||||
- cli.assetstore.yml
|
- cli.assetstore.yml
|
||||||
@@ -59,19 +59,19 @@ A default/demo version of this image is built *automatically*.
|
|||||||
|
|
||||||
## To refresh / pull DSpace images from Dockerhub
|
## To refresh / pull DSpace images from Dockerhub
|
||||||
```
|
```
|
||||||
docker compose -f docker/docker-compose.yml pull
|
docker-compose -f docker/docker-compose.yml pull
|
||||||
```
|
```
|
||||||
|
|
||||||
## To build DSpace images using code in your branch
|
## To build DSpace images using code in your branch
|
||||||
```
|
```
|
||||||
docker compose -f docker/docker-compose.yml build
|
docker-compose -f docker/docker-compose.yml build
|
||||||
```
|
```
|
||||||
|
|
||||||
## To start DSpace (REST and Angular) from your branch
|
## To start DSpace (REST and Angular) from your branch
|
||||||
|
|
||||||
This command provides a quick way to start both the frontend & backend from this single codebase
|
This command provides a quick way to start both the frontend & backend from this single codebase
|
||||||
```
|
```
|
||||||
docker compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
|
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section.
|
Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section.
|
||||||
@@ -86,14 +86,14 @@ _The system will be started in 2 steps. Each step shares the same docker network
|
|||||||
|
|
||||||
From 'DSpace/DSpace' clone (build first as needed):
|
From 'DSpace/DSpace' clone (build first as needed):
|
||||||
```
|
```
|
||||||
docker compose -p d8 up -d
|
docker-compose -p d7 up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md).
|
NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md).
|
||||||
|
|
||||||
From 'DSpace/dspace-angular' clone (build first as needed)
|
From 'DSpace/dspace-angular' clone (build first as needed)
|
||||||
```
|
```
|
||||||
docker compose -p d8 -f docker/docker-compose.yml up -d
|
docker-compose -p d7 -f docker/docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
At this point, you should be able to access the UI from http://localhost:4000,
|
At this point, you should be able to access the UI from http://localhost:4000,
|
||||||
@@ -105,21 +105,21 @@ This allows you to run the Angular UI in *production* mode, pointing it at the d
|
|||||||
(https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/).
|
(https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/).
|
||||||
|
|
||||||
```
|
```
|
||||||
docker compose -f docker/docker-compose-dist.yml pull
|
docker-compose -f docker/docker-compose-dist.yml pull
|
||||||
docker compose -f docker/docker-compose-dist.yml build
|
docker-compose -f docker/docker-compose-dist.yml build
|
||||||
docker compose -p d8 -f docker/docker-compose-dist.yml up -d
|
docker-compose -p d7 -f docker/docker-compose-dist.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ingest test data from AIPDIR
|
## Ingest test data from AIPDIR
|
||||||
|
|
||||||
Create an administrator
|
Create an administrator
|
||||||
```
|
```
|
||||||
docker compose -p d8 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
|
docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
|
||||||
```
|
```
|
||||||
|
|
||||||
Load content from AIP files
|
Load content from AIP files
|
||||||
```
|
```
|
||||||
docker compose -p d8 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## Alternative Ingest - Use Entities dataset
|
## Alternative Ingest - Use Entities dataset
|
||||||
@@ -127,12 +127,12 @@ _Delete your docker volumes or use a unique project (-p) name_
|
|||||||
|
|
||||||
Start DSpace with Database Content from a database dump
|
Start DSpace with Database Content from a database dump
|
||||||
```
|
```
|
||||||
docker compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
|
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Load assetstore content and trigger a re-index of the repository
|
Load assetstore content and trigger a re-index of the repository
|
||||||
```
|
```
|
||||||
docker compose -p d8 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
|
docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## End to end testing of the REST API (runs in GitHub Actions CI).
|
## End to end testing of the REST API (runs in GitHub Actions CI).
|
||||||
@@ -140,5 +140,5 @@ _In this instance, only the REST api runs in Docker using the Entities dataset.
|
|||||||
|
|
||||||
This command is only really useful for testing our Continuous Integration process.
|
This command is only really useful for testing our Continuous Integration process.
|
||||||
```
|
```
|
||||||
docker compose -p d8ci -f docker/docker-compose-ci.yml up -d
|
docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d
|
||||||
```
|
```
|
||||||
|
@@ -32,7 +32,5 @@ services:
|
|||||||
|
|
||||||
/dspace/bin/dspace packager -r -a -t AIP -e $${ADMIN_EMAIL} -f -u SITE*.zip
|
/dspace/bin/dspace packager -r -a -t AIP -e $${ADMIN_EMAIL} -f -u SITE*.zip
|
||||||
/dspace/bin/dspace database update-sequences
|
/dspace/bin/dspace database update-sequences
|
||||||
touch /dspace/solr/search/conf/reindex.flag
|
|
||||||
|
|
||||||
/dspace/bin/dspace oai import
|
/dspace/bin/dspace index-discovery
|
||||||
/dspace/bin/dspace oai clean-cache
|
|
||||||
|
@@ -14,14 +14,14 @@
|
|||||||
# Therefore, it should be kept in sync with that file
|
# Therefore, it should be kept in sync with that file
|
||||||
networks:
|
networks:
|
||||||
# Default to using network named 'dspacenet' from docker-compose-rest.yml.
|
# Default to using network named 'dspacenet' from docker-compose-rest.yml.
|
||||||
# Its full name will be prepended with the project name (e.g. "-p d8" means it will be named "d8_dspacenet")
|
# Its full name will be prepended with the project name (e.g. "-p d7" means it will be named "d7_dspacenet")
|
||||||
# If COMPOSITE_PROJECT_NAME is missing, default value will be "docker" (name of folder this file is in)
|
# If COMPOSITE_PROJECT_NAME is missing, default value will be "docker" (name of folder this file is in)
|
||||||
default:
|
default:
|
||||||
name: ${COMPOSE_PROJECT_NAME:-docker}_dspacenet
|
name: ${COMPOSE_PROJECT_NAME:-docker}_dspacenet
|
||||||
external: true
|
external: true
|
||||||
services:
|
services:
|
||||||
dspace-cli:
|
dspace-cli:
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-8_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
||||||
container_name: dspace-cli
|
container_name: dspace-cli
|
||||||
environment:
|
environment:
|
||||||
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.
|
||||||
|
@@ -14,11 +14,10 @@
|
|||||||
# # Therefore, it should be kept in sync with that file
|
# # Therefore, it should be kept in sync with that file
|
||||||
services:
|
services:
|
||||||
dspacedb:
|
dspacedb:
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql"
|
image: ${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
|
||||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
# NOTE: currently there is no dspace8 version
|
|
||||||
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
- LOADSQL=https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
||||||
dspace:
|
dspace:
|
||||||
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
### OVERRIDE default 'entrypoint' in 'docker-compose-rest.yml' ####
|
||||||
@@ -28,11 +27,23 @@ services:
|
|||||||
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
|
||||||
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
|
||||||
# 4. Finally, start DSpace
|
# 4. Finally, start Tomcat
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate ignored
|
/dspace/bin/dspace database migrate ignored
|
||||||
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
|
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
|
||||||
|
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
|
||||||
|
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
|
||||||
|
<name-map collection-handle="123456789/5" submission-name="Publication"/> \
|
||||||
|
<name-map collection-handle="123456789/8" submission-name="OrgUnit"/> \
|
||||||
|
<name-map collection-handle="123456789/6" submission-name="Person"/> \
|
||||||
|
<name-map collection-handle="123456789/279" submission-name="Person"/> \
|
||||||
|
<name-map collection-handle="123456789/7" submission-name="Project"/> \
|
||||||
|
<name-map collection-handle="123456789/280" submission-name="Project"/> \
|
||||||
|
<name-map collection-handle="123456789/28" submission-name="Journal"/> \
|
||||||
|
<name-map collection-handle="123456789/29" submission-name="JournalVolume"/> \
|
||||||
|
<name-map collection-handle="123456789/30" submission-name="JournalIssue"/>' /dspace/config/item-submission.xml
|
||||||
|
catalina.sh run
|
@@ -33,7 +33,7 @@ services:
|
|||||||
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
||||||
solr__D__statistics__P__autoCommit: 'false'
|
solr__D__statistics__P__autoCommit: 'false'
|
||||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-8_x-test}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -48,24 +48,23 @@ services:
|
|||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
|
||||||
# 3. Finally, start DSpace
|
# 3. Finally, start Tomcat
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate ignored
|
/dspace/bin/dspace database migrate ignored
|
||||||
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
|
catalina.sh run
|
||||||
# DSpace database container
|
# DSpace database container
|
||||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}-loadsql"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql"
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the LOADSQL in
|
# This LOADSQL should be kept in sync with the LOADSQL in
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
||||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
# NOTE: currently there is no dspace8 version
|
|
||||||
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
||||||
PGDATA: /pgdata
|
PGDATA: /pgdata
|
||||||
POSTGRES_PASSWORD: dspace
|
POSTGRES_PASSWORD: dspace
|
||||||
@@ -82,7 +81,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-8_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
@@ -108,10 +107,6 @@ services:
|
|||||||
cp -r /opt/solr/server/solr/configsets/search/* search
|
cp -r /opt/solr/server/solr/configsets/search/* search
|
||||||
precreate-core statistics /opt/solr/server/solr/configsets/statistics
|
precreate-core statistics /opt/solr/server/solr/configsets/statistics
|
||||||
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
||||||
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
|
|
||||||
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
|
||||||
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
|
|
||||||
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
|
|
||||||
exec solr -f
|
exec solr -f
|
||||||
volumes:
|
volumes:
|
||||||
assetstore:
|
assetstore:
|
||||||
|
@@ -23,10 +23,10 @@ services:
|
|||||||
# This is because Server Side Rendering (SSR) currently requires a public URL,
|
# This is because Server Side Rendering (SSR) currently requires a public URL,
|
||||||
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
|
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
|
||||||
DSPACE_REST_SSL: 'true'
|
DSPACE_REST_SSL: 'true'
|
||||||
DSPACE_REST_HOST: sandbox.dspace.org
|
DSPACE_REST_HOST: demo.dspace.org
|
||||||
DSPACE_REST_PORT: 443
|
DSPACE_REST_PORT: 443
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x}-dist"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}-dist"
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile.dist
|
dockerfile: Dockerfile.dist
|
||||||
|
@@ -40,7 +40,7 @@ services:
|
|||||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||||
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-8_x-test}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
networks:
|
networks:
|
||||||
@@ -51,24 +51,23 @@ services:
|
|||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
# Keep DSpace assetstore directory between reboots
|
|
||||||
- assetstore:/dspace/assetstore
|
- assetstore:/dspace/assetstore
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
# 2. Then, run database migration to init database tables
|
# 2. Then, run database migration to init database tables
|
||||||
# 3. Finally, start DSpace
|
# 3. Finally, start Tomcat
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
- |
|
- |
|
||||||
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
|
||||||
/dspace/bin/dspace database migrate
|
/dspace/bin/dspace database migrate
|
||||||
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
|
catalina.sh run
|
||||||
# DSpace database container
|
# DSpace database container
|
||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
# Uses a custom Postgres image with pgcrypto installed
|
# Uses a custom Postgres image with pgcrypto installed
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-8_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}"
|
||||||
environment:
|
environment:
|
||||||
PGDATA: /pgdata
|
PGDATA: /pgdata
|
||||||
POSTGRES_PASSWORD: dspace
|
POSTGRES_PASSWORD: dspace
|
||||||
@@ -85,7 +84,7 @@ services:
|
|||||||
# DSpace Solr container
|
# DSpace Solr container
|
||||||
dspacesolr:
|
dspacesolr:
|
||||||
container_name: dspacesolr
|
container_name: dspacesolr
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-8_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
@@ -101,7 +100,7 @@ services:
|
|||||||
# * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op
|
# * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op
|
||||||
# * Second, copy configsets to this core:
|
# * Second, copy configsets to this core:
|
||||||
# Updates to Solr configs require the container to be rebuilt/restarted:
|
# Updates to Solr configs require the container to be rebuilt/restarted:
|
||||||
# `docker compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr`
|
# `docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr`
|
||||||
entrypoint:
|
entrypoint:
|
||||||
- /bin/bash
|
- /bin/bash
|
||||||
- '-c'
|
- '-c'
|
||||||
@@ -115,10 +114,6 @@ services:
|
|||||||
cp -r /opt/solr/server/solr/configsets/search/* search
|
cp -r /opt/solr/server/solr/configsets/search/* search
|
||||||
precreate-core statistics /opt/solr/server/solr/configsets/statistics
|
precreate-core statistics /opt/solr/server/solr/configsets/statistics
|
||||||
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
||||||
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
|
|
||||||
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
|
||||||
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
|
|
||||||
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
|
|
||||||
exec solr -f
|
exec solr -f
|
||||||
volumes:
|
volumes:
|
||||||
assetstore:
|
assetstore:
|
||||||
|
@@ -23,7 +23,7 @@ services:
|
|||||||
DSPACE_REST_HOST: localhost
|
DSPACE_REST_HOST: localhost
|
||||||
DSPACE_REST_PORT: 8080
|
DSPACE_REST_PORT: 8080
|
||||||
DSPACE_REST_NAMESPACE: /server
|
DSPACE_REST_NAMESPACE: /server
|
||||||
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-8_x}"
|
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}"
|
||||||
build:
|
build:
|
||||||
context: ..
|
context: ..
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
@@ -1,5 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../lint/README.md) > HTML rules
|
|
||||||
_______
|
|
||||||
|
|
||||||
- [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class
|
|
||||||
- [`dspace-angular-html/no-disabled-attribute-on-button`](./rules/no-disabled-attribute-on-button.md): Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
|
|
@@ -1,78 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/no-disabled-attribute-on-button`
|
|
||||||
_______
|
|
||||||
|
|
||||||
Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
|
|
||||||
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
|
|
||||||
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.
|
|
||||||
|
|
||||||
_______
|
|
||||||
|
|
||||||
[Source code](../../../../lint/src/rules/html/no-disabled-attribute-on-button.ts)
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
|
|
||||||
#### Valid code
|
|
||||||
|
|
||||||
##### should use [dsBtnDisabled] in HTML templates
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button [dsBtnDisabled]="true">Submit</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
##### disabled attribute is still valid on non-button elements
|
|
||||||
|
|
||||||
```html
|
|
||||||
<input disabled>
|
|
||||||
```
|
|
||||||
|
|
||||||
##### [disabled] attribute is still valid on non-button elements
|
|
||||||
|
|
||||||
```html
|
|
||||||
<input [disabled]="true">
|
|
||||||
```
|
|
||||||
|
|
||||||
##### angular dynamic attributes that use disabled are still valid
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button [class.disabled]="isDisabled">Submit</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Invalid code & automatic fixes
|
|
||||||
|
|
||||||
##### should not use disabled attribute in HTML templates
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button disabled>Submit</button>
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```html
|
|
||||||
<button [dsBtnDisabled]="true">Submit</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### should not use [disabled] attribute in HTML templates
|
|
||||||
|
|
||||||
```html
|
|
||||||
<button [disabled]="true">Submit</button>
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```html
|
|
||||||
<button [dsBtnDisabled]="true">Submit</button>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -1,110 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/themed-component-usages`
|
|
||||||
_______
|
|
||||||
|
|
||||||
Themeable components should be used via the selector of their `ThemedComponent` wrapper class
|
|
||||||
|
|
||||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
|
||||||
The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple.
|
|
||||||
|
|
||||||
|
|
||||||
_______
|
|
||||||
|
|
||||||
[Source code](../../../../lint/src/rules/html/themed-component-usages.ts)
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
|
|
||||||
#### Valid code
|
|
||||||
|
|
||||||
##### use no-prefix selectors in HTML templates
|
|
||||||
|
|
||||||
```html
|
|
||||||
<ds-test-themeable/>
|
|
||||||
<ds-test-themeable></ds-test-themeable>
|
|
||||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
|
||||||
```
|
|
||||||
|
|
||||||
##### use no-prefix selectors in TypeScript templates
|
|
||||||
|
|
||||||
```html
|
|
||||||
@Component({
|
|
||||||
template: '<ds-test-themeable></ds-test-themeable>'
|
|
||||||
})
|
|
||||||
class Test {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### use no-prefix selectors in TypeScript test templates
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/test.spec.ts`
|
|
||||||
|
|
||||||
```html
|
|
||||||
@Component({
|
|
||||||
template: '<ds-test-themeable></ds-test-themeable>'
|
|
||||||
})
|
|
||||||
class Test {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### base selectors are also allowed in TypeScript test templates
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/test.spec.ts`
|
|
||||||
|
|
||||||
```html
|
|
||||||
@Component({
|
|
||||||
template: '<ds-base-test-themeable></ds-base-test-themeable>'
|
|
||||||
})
|
|
||||||
class Test {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Invalid code & automatic fixes
|
|
||||||
|
|
||||||
##### themed override selectors are not allowed in HTML templates
|
|
||||||
|
|
||||||
```html
|
|
||||||
<ds-themed-test-themeable/>
|
|
||||||
<ds-themed-test-themeable></ds-themed-test-themeable>
|
|
||||||
<ds-themed-test-themeable [test]="something"></ds-themed-test-themeable>
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```html
|
|
||||||
<ds-test-themeable/>
|
|
||||||
<ds-test-themeable></ds-test-themeable>
|
|
||||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### base selectors are not allowed in HTML templates
|
|
||||||
|
|
||||||
```html
|
|
||||||
<ds-base-test-themeable/>
|
|
||||||
<ds-base-test-themeable></ds-base-test-themeable>
|
|
||||||
<ds-base-test-themeable [test]="something"></ds-base-test-themeable>
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper's selector
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```html
|
|
||||||
<ds-test-themeable/>
|
|
||||||
<ds-test-themeable></ds-test-themeable>
|
|
||||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -1,6 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../lint/README.md) > TypeScript rules
|
|
||||||
_______
|
|
||||||
|
|
||||||
- [`dspace-angular-ts/themed-component-classes`](./rules/themed-component-classes.md): Formatting rules for themeable component classes
|
|
||||||
- [`dspace-angular-ts/themed-component-selectors`](./rules/themed-component-selectors.md): Themeable component selectors should follow the DSpace convention
|
|
||||||
- [`dspace-angular-ts/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via their `ThemedComponent` wrapper class
|
|
@@ -1,257 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-classes`
|
|
||||||
_______
|
|
||||||
|
|
||||||
Formatting rules for themeable component classes
|
|
||||||
|
|
||||||
- All themeable components must be standalone.
|
|
||||||
- The base component must always be imported in the `ThemedComponent` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component.
|
|
||||||
|
|
||||||
|
|
||||||
_______
|
|
||||||
|
|
||||||
[Source code](../../../../lint/src/rules/ts/themed-component-classes.ts)
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
|
|
||||||
#### Valid code
|
|
||||||
|
|
||||||
##### Regular non-themeable component
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class Something {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Base component
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class TestThemeableTomponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Wrapper component
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
TestThemeableComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableTomponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Override component
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class Override extends BaseComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Invalid code & automatic fixes
|
|
||||||
|
|
||||||
##### Base component must be standalone
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-test-themable',
|
|
||||||
})
|
|
||||||
class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components must be standalone
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### Wrapper component must be standalone and import base component
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable component wrapper classes must be standalone and import the base class
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### Wrapper component must import base component (array present but empty)
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themed component wrapper classes must only import the base class
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### Wrapper component must import base component (array is wrong)
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { SomethingElse } from './somewhere-else';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
SomethingElse,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themed component wrapper classes must only import the base class
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
import { SomethingElse } from './somewhere-else';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### Wrapper component must import base component (array is wrong)
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Something, SomethingElse } from './somewhere-else';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
SomethingElse,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themed component wrapper classes must only import the base class
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
import { Something, SomethingElse } from './somewhere-else';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### Override component must be standalone
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-test-themable',
|
|
||||||
})
|
|
||||||
class Override extends BaseComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components must be standalone
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class Override extends BaseComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -1,156 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-selectors`
|
|
||||||
_______
|
|
||||||
|
|
||||||
Themeable component selectors should follow the DSpace convention
|
|
||||||
|
|
||||||
Each themeable component is comprised of a base component, a wrapper component and any number of themed components
|
|
||||||
- Base components should have a selector starting with `ds-base-`
|
|
||||||
- Themed components should have a selector starting with `ds-themed-`
|
|
||||||
- Wrapper components should have a selector starting with `ds-`, but not `ds-base-` or `ds-themed-`
|
|
||||||
- This is the regular DSpace selector prefix
|
|
||||||
- **When making a regular component themeable, its selector prefix should be changed to `ds-base-`, and the new wrapper's component should reuse the previous selector**
|
|
||||||
|
|
||||||
Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source.
|
|
||||||
|
|
||||||
|
|
||||||
_______
|
|
||||||
|
|
||||||
[Source code](../../../../lint/src/rules/ts/themed-component-selectors.ts)
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
|
|
||||||
#### Valid code
|
|
||||||
|
|
||||||
##### Regular non-themeable component selector
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class Something {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Themeable component selector should replace the original version, unthemed version should be changed to ds-base-
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-something',
|
|
||||||
})
|
|
||||||
class Something {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class ThemedSomething extends ThemedComponent<Something> {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-something',
|
|
||||||
})
|
|
||||||
class OverrideSomething extends Something {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### Other themed component wrappers should not interfere
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class Something {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something-else',
|
|
||||||
})
|
|
||||||
class ThemedSomethingElse extends ThemedComponent<SomethingElse> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Invalid code & automatic fixes
|
|
||||||
|
|
||||||
##### Wrong selector for base component
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Unthemed version of themeable component should have a selector starting with 'ds-base-'
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-something',
|
|
||||||
})
|
|
||||||
class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### Wrong selector for wrapper component
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-something',
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themed component wrapper of themeable component shouldn't have a selector starting with 'ds-themed-'
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### Wrong selector for theme override
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class TestThememeableComponent extends BaseComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Theme override of themeable component should have a selector starting with 'ds-themed-'
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-something',
|
|
||||||
})
|
|
||||||
class TestThememeableComponent extends BaseComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -1,332 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-usages`
|
|
||||||
_______
|
|
||||||
|
|
||||||
Themeable components should be used via their `ThemedComponent` wrapper class
|
|
||||||
|
|
||||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
|
||||||
There are a few exceptions where the base class can still be used:
|
|
||||||
- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place)
|
|
||||||
- Angular modules (except for routing modules)
|
|
||||||
- Angular `@ViewChild` decorators
|
|
||||||
- Type annotations
|
|
||||||
|
|
||||||
|
|
||||||
_______
|
|
||||||
|
|
||||||
[Source code](../../../../lint/src/rules/ts/themed-component-usages.ts)
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
|
|
||||||
#### Valid code
|
|
||||||
|
|
||||||
##### allow wrapper class usages
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: ThemedTestThemeableComponent,
|
|
||||||
b: ChipsComponent,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### allow base class in class declaration
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### allow inheriting from base class
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
|
|
||||||
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### allow base class in ViewChild
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
|
|
||||||
export class Something {
|
|
||||||
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
##### allow wrapper selectors in test queries
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
By.css('ds-themeable');
|
|
||||||
By.css('#test > ds-themeable > #nest');
|
|
||||||
```
|
|
||||||
|
|
||||||
##### allow wrapper selectors in cypress queries
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
By.css('ds-themeable');
|
|
||||||
By.css('#test > ds-themeable > #nest');
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
#### Invalid code & automatic fixes
|
|
||||||
|
|
||||||
##### disallow direct usages of base class
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
import { TestComponent } from './app/test/test.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: TestThemeableComponent,
|
|
||||||
b: TestComponent,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
|
||||||
import { TestComponent } from './app/test/test.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: ThemedTestThemeableComponent,
|
|
||||||
b: TestComponent,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### disallow direct usages of base class, keep other imports
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Something, TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
import { TestComponent } from './app/test/test.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: TestThemeableComponent,
|
|
||||||
b: TestComponent,
|
|
||||||
c: Something,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
import { Something } from './app/test/test-themeable.component';
|
|
||||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
|
||||||
import { TestComponent } from './app/test/test.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: ThemedTestThemeableComponent,
|
|
||||||
b: TestComponent,
|
|
||||||
c: Something,
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### handle array replacements correctly
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const DECLARATIONS = [
|
|
||||||
Something,
|
|
||||||
TestThemeableComponent,
|
|
||||||
Something,
|
|
||||||
ThemedTestThemeableComponent,
|
|
||||||
];
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
const DECLARATIONS = [
|
|
||||||
Something,
|
|
||||||
Something,
|
|
||||||
ThemedTestThemeableComponent,
|
|
||||||
];
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### disallow override selector in test queries
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
By.css('ds-themed-themeable');
|
|
||||||
By.css('#test > ds-themed-themeable > #nest');
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
By.css('ds-themeable');
|
|
||||||
By.css('#test > ds-themeable > #nest');
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### disallow base selector in test queries
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
By.css('ds-base-themeable');
|
|
||||||
By.css('#test > ds-base-themeable > #nest');
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
By.css('ds-themeable');
|
|
||||||
By.css('#test > ds-themeable > #nest');
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### disallow override selector in cypress queries
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
cy.get('ds-themed-themeable');
|
|
||||||
cy.get('#test > ds-themed-themeable > #nest');
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
cy.get('ds-themeable');
|
|
||||||
cy.get('#test > ds-themeable > #nest');
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### disallow base selector in cypress queries
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
cy.get('ds-base-themeable');
|
|
||||||
cy.get('#test > ds-base-themeable > #nest');
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
cy.get('ds-themeable');
|
|
||||||
cy.get('#test > ds-themeable > #nest');
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### edge case: unable to find usage node through usage token, but import is still flagged and fixed
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Context } from './app/core/shared/context.model';
|
|
||||||
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class UsageComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Context } from './app/core/shared/context.model';
|
|
||||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
imports: [ThemedTestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class UsageComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
##### edge case edge case: both are imported, only wrapper is retained
|
|
||||||
|
|
||||||
Filename: `lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts`
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Context } from './app/core/shared/context.model';
|
|
||||||
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
|
|
||||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent, ThemedTestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class UsageComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
Themeable components should be used via their ThemedComponent wrapper
|
|
||||||
```
|
|
||||||
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```typescript
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Context } from './app/core/shared/context.model';
|
|
||||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
imports: [ThemedTestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class UsageComponent {
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@@ -15,10 +15,7 @@ module.exports = function (config) {
|
|||||||
],
|
],
|
||||||
client: {
|
client: {
|
||||||
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
clearContext: false, // leave Jasmine Spec Runner output visible in browser
|
||||||
captureConsole: false,
|
captureConsole: false
|
||||||
jasmine: {
|
|
||||||
failSpecWithNoExpectations: true
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
coverageIstanbulReporter: {
|
coverageIstanbulReporter: {
|
||||||
dir: require('path').join(__dirname, './coverage/dspace-angular'),
|
dir: require('path').join(__dirname, './coverage/dspace-angular'),
|
||||||
|
3
lint/.gitignore
vendored
3
lint/.gitignore
vendored
@@ -1,3 +0,0 @@
|
|||||||
/dist/
|
|
||||||
/coverage/
|
|
||||||
/node-modules/
|
|
@@ -1,50 +0,0 @@
|
|||||||
# DSpace ESLint plugins
|
|
||||||
|
|
||||||
Custom ESLint rules for DSpace Angular peculiarities.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
These plugins are included with the rest of our ESLint configuration in [.eslintc.json](../.eslintrc.json). Individual rules can be configured or disabled there, like usual.
|
|
||||||
- In order for the new rules to be picked up by your IDE, you should first run `yarn build:lint` to build the plugins.
|
|
||||||
- This will also happen automatically each time `yarn lint` is run.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
The rules are split up into plugins by language:
|
|
||||||
- [TypeScript rules](../docs/lint/ts/index.md)
|
|
||||||
- [HTML rules](../docs/lint/html/index.md)
|
|
||||||
|
|
||||||
> Run `yarn docs:lint` to generate this documentation!
|
|
||||||
|
|
||||||
## Developing
|
|
||||||
|
|
||||||
### Overview
|
|
||||||
|
|
||||||
- All rules are written in TypeScript and compiled into [`dist`](./dist)
|
|
||||||
- The plugins are linked into the main project dependencies from here
|
|
||||||
- These directories already contain the necessary `package.json` files to mark them as ESLint plugins
|
|
||||||
- Rule source files are structured, so they can be imported all in one go
|
|
||||||
- Each rule must export the following:
|
|
||||||
- `Messages`: an Enum of error message IDs
|
|
||||||
- `info`: metadata about this rule (name, description, messages, options, ...)
|
|
||||||
- `rule`: the implementation of the rule
|
|
||||||
- `tests`: the tests for this rule, as a set of valid/invalid code snippets. These snippets are used as example in the documentation.
|
|
||||||
- New rules should be added to their plugin's `index.ts`
|
|
||||||
- Some useful links
|
|
||||||
- [Developing ESLint plugins](https://eslint.org/docs/latest/extend/plugins)
|
|
||||||
- [Custom rules in typescript-eslint](https://typescript-eslint.io/developers/custom-rules)
|
|
||||||
- [Angular ESLint](https://github.com/angular-eslint/angular-eslint)
|
|
||||||
|
|
||||||
### Parsing project metadata in advance ~ TypeScript AST
|
|
||||||
|
|
||||||
While it is possible to retain persistent state between files during the linting process, it becomes quite complicated if the content of one file determines how we want to lint another file.
|
|
||||||
Because the two files may be linted out of order, we may not know whether the first file is wrong before we pass by the second. This means that we cannot report or fix the issue, because the first file is already detached from the linting context.
|
|
||||||
|
|
||||||
For example, we cannot consistently determine which components are themeable (i.e. have a `ThemedComponent` wrapper) while linting.
|
|
||||||
To work around this issue, we construct a registry of themeable components _before_ linting anything.
|
|
||||||
- We don't have a good way to hook into the ESLint parser at this time
|
|
||||||
- Instead, we leverage the actual TypeScript AST parser
|
|
||||||
- Retrieve all `ThemedComponent` wrapper files by the pattern of their path (`themed-*.component.ts`)
|
|
||||||
- Determine the themed component they're linked to (by the actual type annotation/import path, since filenames are prone to errors)
|
|
||||||
- Store metadata describing these component pairs in a global registry that can be shared between rules
|
|
||||||
- This only needs to happen once, and only takes a fraction of a second (for ~100 themeable components)
|
|
6
lint/dist/src/rules/html/package.json
vendored
6
lint/dist/src/rules/html/package.json
vendored
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "eslint-plugin-dspace-angular-html",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"main": "./index.js",
|
|
||||||
"private": true
|
|
||||||
}
|
|
6
lint/dist/src/rules/ts/package.json
vendored
6
lint/dist/src/rules/ts/package.json
vendored
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "eslint-plugin-dspace-angular-ts",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"main": "./index.js",
|
|
||||||
"private": true
|
|
||||||
}
|
|
@@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import {
|
|
||||||
existsSync,
|
|
||||||
mkdirSync,
|
|
||||||
readFileSync,
|
|
||||||
rmSync,
|
|
||||||
writeFileSync,
|
|
||||||
} from 'fs';
|
|
||||||
import { join } from 'path';
|
|
||||||
|
|
||||||
import { default as htmlPlugin } from './src/rules/html';
|
|
||||||
import { default as tsPlugin } from './src/rules/ts';
|
|
||||||
|
|
||||||
const templates = new Map();
|
|
||||||
|
|
||||||
function lazyEJS(path: string, data: object): string {
|
|
||||||
if (!templates.has(path)) {
|
|
||||||
templates.set(path, require('ejs').compile(readFileSync(path).toString()));
|
|
||||||
}
|
|
||||||
|
|
||||||
return templates.get(path)(data).replace(/\r\n/g, '\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
const docsDir = join('docs', 'lint');
|
|
||||||
const tsDir = join(docsDir, 'ts');
|
|
||||||
const htmlDir = join(docsDir, 'html');
|
|
||||||
|
|
||||||
if (existsSync(docsDir)) {
|
|
||||||
rmSync(docsDir, { recursive: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
mkdirSync(join(tsDir, 'rules'), { recursive: true });
|
|
||||||
mkdirSync(join(htmlDir, 'rules'), { recursive: true });
|
|
||||||
|
|
||||||
function template(name: string): string {
|
|
||||||
return join('lint', 'src', 'util', 'templates', name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TypeScript docs
|
|
||||||
writeFileSync(
|
|
||||||
join(tsDir, 'index.md'),
|
|
||||||
lazyEJS(template('index.ejs'), {
|
|
||||||
plugin: tsPlugin,
|
|
||||||
rules: tsPlugin.index.map(rule => rule.info),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const rule of tsPlugin.index) {
|
|
||||||
writeFileSync(
|
|
||||||
join(tsDir, 'rules', rule.info.name + '.md'),
|
|
||||||
lazyEJS(template('rule.ejs'), {
|
|
||||||
plugin: tsPlugin,
|
|
||||||
rule: rule.info,
|
|
||||||
tests: rule.tests,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// HTML docs
|
|
||||||
writeFileSync(
|
|
||||||
join(htmlDir, 'index.md'),
|
|
||||||
lazyEJS(template('index.ejs'), {
|
|
||||||
plugin: htmlPlugin,
|
|
||||||
rules: htmlPlugin.index.map(rule => rule.info),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
for (const rule of htmlPlugin.index) {
|
|
||||||
writeFileSync(
|
|
||||||
join(htmlDir, 'rules', rule.info.name + '.md'),
|
|
||||||
lazyEJS(template('rule.ejs'), {
|
|
||||||
plugin: htmlPlugin,
|
|
||||||
rule: rule.info,
|
|
||||||
tests: rule.tests,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"spec_files": ["**/*.spec.js"],
|
|
||||||
"spec_dir": "lint/dist/test",
|
|
||||||
"helpers": [
|
|
||||||
"./test/helpers.js"
|
|
||||||
]
|
|
||||||
}
|
|
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
/* eslint-disable import/no-namespace */
|
|
||||||
import {
|
|
||||||
bundle,
|
|
||||||
RuleExports,
|
|
||||||
} from '../../util/structure';
|
|
||||||
import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
|
|
||||||
import * as themedComponentUsages from './themed-component-usages';
|
|
||||||
|
|
||||||
const index = [
|
|
||||||
themedComponentUsages,
|
|
||||||
noDisabledAttributeOnButton,
|
|
||||||
|
|
||||||
] as unknown as RuleExports[];
|
|
||||||
|
|
||||||
export = {
|
|
||||||
parser: require('@angular-eslint/template-parser'),
|
|
||||||
...bundle('dspace-angular-html', 'HTML', index),
|
|
||||||
};
|
|
@@ -1,147 +0,0 @@
|
|||||||
import {
|
|
||||||
TmplAstBoundAttribute,
|
|
||||||
TmplAstTextAttribute,
|
|
||||||
} from '@angular-eslint/bundled-angular-compiler';
|
|
||||||
import { TemplateParserServices } from '@angular-eslint/utils';
|
|
||||||
import {
|
|
||||||
ESLintUtils,
|
|
||||||
TSESLint,
|
|
||||||
} from '@typescript-eslint/utils';
|
|
||||||
|
|
||||||
import {
|
|
||||||
DSpaceESLintRuleInfo,
|
|
||||||
NamedTests,
|
|
||||||
} from '../../util/structure';
|
|
||||||
import { getSourceCode } from '../../util/typescript';
|
|
||||||
|
|
||||||
export enum Message {
|
|
||||||
USE_DSBTN_DISABLED = 'mustUseDsBtnDisabled',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const info = {
|
|
||||||
name: 'no-disabled-attribute-on-button',
|
|
||||||
meta: {
|
|
||||||
docs: {
|
|
||||||
description: `Buttons should use the \`dsBtnDisabled\` directive instead of the HTML \`disabled\` attribute.
|
|
||||||
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
|
|
||||||
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.`,
|
|
||||||
},
|
|
||||||
type: 'problem',
|
|
||||||
fixable: 'code',
|
|
||||||
schema: [],
|
|
||||||
messages: {
|
|
||||||
[Message.USE_DSBTN_DISABLED]: 'Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultOptions: [],
|
|
||||||
} as DSpaceESLintRuleInfo;
|
|
||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
|
||||||
...info,
|
|
||||||
create(context: TSESLint.RuleContext<Message, unknown[]>) {
|
|
||||||
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Some dynamic angular inputs will have disabled as name because of how Angular handles this internally (e.g [class.disabled]="isDisabled")
|
|
||||||
* But these aren't actually the disabled attribute we're looking for, we can determine this by checking the details of the keySpan
|
|
||||||
*/
|
|
||||||
function isOtherAttributeDisabled(node: TmplAstBoundAttribute | TmplAstTextAttribute): boolean {
|
|
||||||
// if the details are not null, and the details are not 'disabled', then it's not the disabled attribute we're looking for
|
|
||||||
return node.keySpan?.details !== null && node.keySpan?.details !== 'disabled';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Replace the disabled text with [dsBtnDisabled] in the template
|
|
||||||
*/
|
|
||||||
function replaceDisabledText(text: string ): string {
|
|
||||||
const hasBrackets = text.includes('[') && text.includes(']');
|
|
||||||
const newDisabledText = hasBrackets ? 'dsBtnDisabled' : '[dsBtnDisabled]="true"';
|
|
||||||
return text.replace('disabled', newDisabledText);
|
|
||||||
}
|
|
||||||
|
|
||||||
function inputIsChildOfButton(node: any): boolean {
|
|
||||||
return (node.parent?.tagName === 'button' || node.parent?.name === 'button');
|
|
||||||
}
|
|
||||||
|
|
||||||
function reportAndFix(node: TmplAstBoundAttribute | TmplAstTextAttribute) {
|
|
||||||
if (!inputIsChildOfButton(node) || isOtherAttributeDisabled(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const sourceSpan = node.sourceSpan;
|
|
||||||
context.report({
|
|
||||||
messageId: Message.USE_DSBTN_DISABLED,
|
|
||||||
loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
|
|
||||||
fix(fixer) {
|
|
||||||
const templateText = sourceSpan.start.file.content;
|
|
||||||
const disabledText = templateText.slice(sourceSpan.start.offset, sourceSpan.end.offset);
|
|
||||||
const newText = replaceDisabledText(disabledText);
|
|
||||||
return fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], newText);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'BoundAttribute[name="disabled"]'(node: TmplAstBoundAttribute) {
|
|
||||||
reportAndFix(node);
|
|
||||||
},
|
|
||||||
'TextAttribute[name="disabled"]'(node: TmplAstTextAttribute) {
|
|
||||||
reportAndFix(node);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tests = {
|
|
||||||
plugin: info.name,
|
|
||||||
valid: [
|
|
||||||
{
|
|
||||||
name: 'should use [dsBtnDisabled] in HTML templates',
|
|
||||||
code: `
|
|
||||||
<button [dsBtnDisabled]="true">Submit</button>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'disabled attribute is still valid on non-button elements',
|
|
||||||
code: `
|
|
||||||
<input disabled>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '[disabled] attribute is still valid on non-button elements',
|
|
||||||
code: `
|
|
||||||
<input [disabled]="true">
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'angular dynamic attributes that use disabled are still valid',
|
|
||||||
code: `
|
|
||||||
<button [class.disabled]="isDisabled">Submit</button>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
invalid: [
|
|
||||||
{
|
|
||||||
name: 'should not use disabled attribute in HTML templates',
|
|
||||||
code: `
|
|
||||||
<button disabled>Submit</button>
|
|
||||||
`,
|
|
||||||
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
|
|
||||||
output: `
|
|
||||||
<button [dsBtnDisabled]="true">Submit</button>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'should not use [disabled] attribute in HTML templates',
|
|
||||||
code: `
|
|
||||||
<button [disabled]="true">Submit</button>
|
|
||||||
`,
|
|
||||||
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
|
|
||||||
output: `
|
|
||||||
<button [dsBtnDisabled]="true">Submit</button>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as NamedTests;
|
|
||||||
|
|
||||||
export default rule;
|
|
@@ -1,189 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { TmplAstElement } from '@angular-eslint/bundled-angular-compiler';
|
|
||||||
import { TemplateParserServices } from '@angular-eslint/utils';
|
|
||||||
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
||||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
|
||||||
|
|
||||||
import { fixture } from '../../../test/fixture';
|
|
||||||
import {
|
|
||||||
DSpaceESLintRuleInfo,
|
|
||||||
NamedTests,
|
|
||||||
} from '../../util/structure';
|
|
||||||
import {
|
|
||||||
DISALLOWED_THEME_SELECTORS,
|
|
||||||
fixSelectors,
|
|
||||||
} from '../../util/theme-support';
|
|
||||||
import {
|
|
||||||
getFilename,
|
|
||||||
getSourceCode,
|
|
||||||
} from '../../util/typescript';
|
|
||||||
|
|
||||||
export enum Message {
|
|
||||||
WRONG_SELECTOR = 'mustUseThemedWrapperSelector',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const info = {
|
|
||||||
name: 'themed-component-usages',
|
|
||||||
meta: {
|
|
||||||
docs: {
|
|
||||||
description: `Themeable components should be used via the selector of their \`ThemedComponent\` wrapper class
|
|
||||||
|
|
||||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
|
||||||
The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple.
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
type: 'problem',
|
|
||||||
fixable: 'code',
|
|
||||||
schema: [],
|
|
||||||
messages: {
|
|
||||||
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper\'s selector',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultOptions: [],
|
|
||||||
} as DSpaceESLintRuleInfo;
|
|
||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
|
||||||
...info,
|
|
||||||
create(context: RuleContext<Message, unknown[]>) {
|
|
||||||
if (getFilename(context).includes('.spec.ts')) {
|
|
||||||
// skip inline templates in unit tests
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
|
|
||||||
|
|
||||||
return {
|
|
||||||
[`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: TmplAstElement) {
|
|
||||||
const { startSourceSpan, endSourceSpan } = node;
|
|
||||||
const openStart = startSourceSpan.start.offset as number;
|
|
||||||
|
|
||||||
context.report({
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
loc: parserServices.convertNodeSourceSpanToLoc(startSourceSpan),
|
|
||||||
fix(fixer) {
|
|
||||||
const oldSelector = node.name;
|
|
||||||
const newSelector = fixSelectors(oldSelector);
|
|
||||||
|
|
||||||
const ops = [
|
|
||||||
fixer.replaceTextRange([openStart + 1, openStart + 1 + oldSelector.length], newSelector),
|
|
||||||
];
|
|
||||||
|
|
||||||
// make sure we don't mangle self-closing tags
|
|
||||||
if (endSourceSpan !== null && startSourceSpan.end.offset !== endSourceSpan.end.offset) {
|
|
||||||
const closeStart = endSourceSpan.start.offset as number;
|
|
||||||
const closeEnd = endSourceSpan.end.offset as number;
|
|
||||||
|
|
||||||
ops.push(fixer.replaceTextRange([closeStart + 2, closeEnd - 1], newSelector));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ops;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tests = {
|
|
||||||
plugin: info.name,
|
|
||||||
valid: [
|
|
||||||
{
|
|
||||||
name: 'use no-prefix selectors in HTML templates',
|
|
||||||
code: `
|
|
||||||
<ds-test-themeable/>
|
|
||||||
<ds-test-themeable></ds-test-themeable>
|
|
||||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'use no-prefix selectors in TypeScript templates',
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
template: '<ds-test-themeable></ds-test-themeable>'
|
|
||||||
})
|
|
||||||
class Test {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'use no-prefix selectors in TypeScript test templates',
|
|
||||||
filename: fixture('src/test.spec.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
template: '<ds-test-themeable></ds-test-themeable>'
|
|
||||||
})
|
|
||||||
class Test {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'base selectors are also allowed in TypeScript test templates',
|
|
||||||
filename: fixture('src/test.spec.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
template: '<ds-base-test-themeable></ds-base-test-themeable>'
|
|
||||||
})
|
|
||||||
class Test {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
invalid: [
|
|
||||||
{
|
|
||||||
name: 'themed override selectors are not allowed in HTML templates',
|
|
||||||
code: `
|
|
||||||
<ds-themed-test-themeable/>
|
|
||||||
<ds-themed-test-themeable></ds-themed-test-themeable>
|
|
||||||
<ds-themed-test-themeable [test]="something"></ds-themed-test-themeable>
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
<ds-test-themeable/>
|
|
||||||
<ds-test-themeable></ds-test-themeable>
|
|
||||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'base selectors are not allowed in HTML templates',
|
|
||||||
code: `
|
|
||||||
<ds-base-test-themeable/>
|
|
||||||
<ds-base-test-themeable></ds-base-test-themeable>
|
|
||||||
<ds-base-test-themeable [test]="something"></ds-base-test-themeable>
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
<ds-test-themeable/>
|
|
||||||
<ds-test-themeable></ds-test-themeable>
|
|
||||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
} as NamedTests;
|
|
||||||
|
|
||||||
export default rule;
|
|
@@ -1,25 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
bundle,
|
|
||||||
RuleExports,
|
|
||||||
} from '../../util/structure';
|
|
||||||
/* eslint-disable import/no-namespace */
|
|
||||||
import * as themedComponentClasses from './themed-component-classes';
|
|
||||||
import * as themedComponentSelectors from './themed-component-selectors';
|
|
||||||
import * as themedComponentUsages from './themed-component-usages';
|
|
||||||
|
|
||||||
const index = [
|
|
||||||
themedComponentClasses,
|
|
||||||
themedComponentSelectors,
|
|
||||||
themedComponentUsages,
|
|
||||||
] as unknown as RuleExports[];
|
|
||||||
|
|
||||||
export = {
|
|
||||||
...bundle('dspace-angular-ts', 'TypeScript', index),
|
|
||||||
};
|
|
@@ -1,382 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
ESLintUtils,
|
|
||||||
TSESTree,
|
|
||||||
} from '@typescript-eslint/utils';
|
|
||||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
|
||||||
|
|
||||||
import { fixture } from '../../../test/fixture';
|
|
||||||
import {
|
|
||||||
getComponentImportNode,
|
|
||||||
getComponentInitializer,
|
|
||||||
getComponentStandaloneNode,
|
|
||||||
} from '../../util/angular';
|
|
||||||
import { appendObjectProperties } from '../../util/fix';
|
|
||||||
import { DSpaceESLintRuleInfo } from '../../util/structure';
|
|
||||||
import {
|
|
||||||
getBaseComponentClassName,
|
|
||||||
inThemedComponentOverrideFile,
|
|
||||||
isThemeableComponent,
|
|
||||||
isThemedComponentWrapper,
|
|
||||||
} from '../../util/theme-support';
|
|
||||||
import { getFilename } from '../../util/typescript';
|
|
||||||
|
|
||||||
export enum Message {
|
|
||||||
NOT_STANDALONE = 'mustBeStandalone',
|
|
||||||
NOT_STANDALONE_IMPORTS_BASE = 'mustBeStandaloneAndImportBase',
|
|
||||||
WRAPPER_IMPORTS_BASE = 'wrapperShouldImportBase',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const info = {
|
|
||||||
name: 'themed-component-classes',
|
|
||||||
meta: {
|
|
||||||
docs: {
|
|
||||||
description: `Formatting rules for themeable component classes
|
|
||||||
|
|
||||||
- All themeable components must be standalone.
|
|
||||||
- The base component must always be imported in the \`ThemedComponent\` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component.
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
type: 'problem',
|
|
||||||
fixable: 'code',
|
|
||||||
schema: [],
|
|
||||||
messages: {
|
|
||||||
[Message.NOT_STANDALONE]: 'Themeable components must be standalone',
|
|
||||||
[Message.NOT_STANDALONE_IMPORTS_BASE]: 'Themeable component wrapper classes must be standalone and import the base class',
|
|
||||||
[Message.WRAPPER_IMPORTS_BASE]: 'Themed component wrapper classes must only import the base class',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultOptions: [],
|
|
||||||
} as DSpaceESLintRuleInfo;
|
|
||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
|
||||||
...info,
|
|
||||||
create(context: RuleContext<Message, unknown[]>) {
|
|
||||||
const filename = getFilename(context);
|
|
||||||
|
|
||||||
if (filename.endsWith('.spec.ts')) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function enforceStandalone(decoratorNode: TSESTree.Decorator, withBaseImport = false) {
|
|
||||||
const standaloneNode = getComponentStandaloneNode(decoratorNode);
|
|
||||||
|
|
||||||
if (standaloneNode === undefined) {
|
|
||||||
// We may need to add these properties in one go
|
|
||||||
if (!withBaseImport) {
|
|
||||||
context.report({
|
|
||||||
messageId: Message.NOT_STANDALONE,
|
|
||||||
node: decoratorNode,
|
|
||||||
fix(fixer) {
|
|
||||||
const initializer = getComponentInitializer(decoratorNode);
|
|
||||||
return appendObjectProperties(context, fixer, initializer, ['standalone: true']);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else if (!standaloneNode.value) {
|
|
||||||
context.report({
|
|
||||||
messageId: Message.NOT_STANDALONE,
|
|
||||||
node: standaloneNode,
|
|
||||||
fix(fixer) {
|
|
||||||
return fixer.replaceText(standaloneNode, 'true');
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (withBaseImport) {
|
|
||||||
const baseClass = getBaseComponentClassName(decoratorNode);
|
|
||||||
|
|
||||||
if (baseClass === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const importsNode = getComponentImportNode(decoratorNode);
|
|
||||||
|
|
||||||
if (importsNode === undefined) {
|
|
||||||
if (standaloneNode === undefined) {
|
|
||||||
context.report({
|
|
||||||
messageId: Message.NOT_STANDALONE_IMPORTS_BASE,
|
|
||||||
node: decoratorNode,
|
|
||||||
fix(fixer) {
|
|
||||||
const initializer = getComponentInitializer(decoratorNode);
|
|
||||||
return appendObjectProperties(context, fixer, initializer, ['standalone: true', `imports: [${baseClass}]`]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
context.report({
|
|
||||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
|
||||||
node: decoratorNode,
|
|
||||||
fix(fixer) {
|
|
||||||
const initializer = getComponentInitializer(decoratorNode);
|
|
||||||
return appendObjectProperties(context, fixer, initializer, [`imports: [${baseClass}]`]);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If we have an imports node, standalone: true will be enforced by another rule
|
|
||||||
|
|
||||||
const imports = importsNode.elements.map(e => (e as TSESTree.Identifier).name);
|
|
||||||
|
|
||||||
if (!imports.includes(baseClass) || imports.length > 1) {
|
|
||||||
// The wrapper should _only_ import the base component
|
|
||||||
context.report({
|
|
||||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
|
||||||
node: importsNode,
|
|
||||||
fix(fixer) {
|
|
||||||
// todo: this may leave unused imports, but that's better than mangling things
|
|
||||||
return fixer.replaceText(importsNode, `[${baseClass}]`);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) {
|
|
||||||
const classNode = node.parent as TSESTree.ClassDeclaration;
|
|
||||||
const className = classNode.id?.name;
|
|
||||||
|
|
||||||
if (className === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isThemedComponentWrapper(node)) {
|
|
||||||
enforceStandalone(node, true);
|
|
||||||
} else if (inThemedComponentOverrideFile(filename)) {
|
|
||||||
enforceStandalone(node);
|
|
||||||
} else if (isThemeableComponent(className)) {
|
|
||||||
enforceStandalone(node);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tests = {
|
|
||||||
plugin: info.name,
|
|
||||||
valid: [
|
|
||||||
{
|
|
||||||
name: 'Regular non-themeable component',
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class Something {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Base component',
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class TestThemeableTomponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Wrapper component',
|
|
||||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
TestThemeableComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableTomponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Override component',
|
|
||||||
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class Override extends BaseComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
invalid: [
|
|
||||||
{
|
|
||||||
name: 'Base component must be standalone',
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-test-themable',
|
|
||||||
})
|
|
||||||
class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors:[
|
|
||||||
{
|
|
||||||
messageId: Message.NOT_STANDALONE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Wrapper component must be standalone and import base component',
|
|
||||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors:[
|
|
||||||
{
|
|
||||||
messageId: Message.NOT_STANDALONE_IMPORTS_BASE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'Wrapper component must import base component (array present but empty)',
|
|
||||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors:[
|
|
||||||
{
|
|
||||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Wrapper component must import base component (array is wrong)',
|
|
||||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
import { SomethingElse } from './somewhere-else';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
SomethingElse,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors:[
|
|
||||||
{
|
|
||||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
import { SomethingElse } from './somewhere-else';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
}, {
|
|
||||||
name: 'Wrapper component must import base component (array is wrong)',
|
|
||||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
import { Something, SomethingElse } from './somewhere-else';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
SomethingElse,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors:[
|
|
||||||
{
|
|
||||||
messageId: Message.WRAPPER_IMPORTS_BASE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
import { Something, SomethingElse } from './somewhere-else';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Override component must be standalone',
|
|
||||||
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-test-themable',
|
|
||||||
})
|
|
||||||
class Override extends BaseComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors:[
|
|
||||||
{
|
|
||||||
messageId: Message.NOT_STANDALONE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-test-themable',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
class Override extends BaseComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
@@ -1,257 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
ESLintUtils,
|
|
||||||
TSESTree,
|
|
||||||
} from '@typescript-eslint/utils';
|
|
||||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
|
||||||
|
|
||||||
import { fixture } from '../../../test/fixture';
|
|
||||||
import { getComponentSelectorNode } from '../../util/angular';
|
|
||||||
import { stringLiteral } from '../../util/misc';
|
|
||||||
import { DSpaceESLintRuleInfo } from '../../util/structure';
|
|
||||||
import {
|
|
||||||
inThemedComponentOverrideFile,
|
|
||||||
isThemeableComponent,
|
|
||||||
isThemedComponentWrapper,
|
|
||||||
} from '../../util/theme-support';
|
|
||||||
import { getFilename } from '../../util/typescript';
|
|
||||||
|
|
||||||
export enum Message {
|
|
||||||
BASE = 'wrongSelectorUnthemedComponent',
|
|
||||||
WRAPPER = 'wrongSelectorThemedComponentWrapper',
|
|
||||||
THEMED = 'wrongSelectorThemedComponentOverride',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const info = {
|
|
||||||
name: 'themed-component-selectors',
|
|
||||||
meta: {
|
|
||||||
docs: {
|
|
||||||
description: `Themeable component selectors should follow the DSpace convention
|
|
||||||
|
|
||||||
Each themeable component is comprised of a base component, a wrapper component and any number of themed components
|
|
||||||
- Base components should have a selector starting with \`ds-base-\`
|
|
||||||
- Themed components should have a selector starting with \`ds-themed-\`
|
|
||||||
- Wrapper components should have a selector starting with \`ds-\`, but not \`ds-base-\` or \`ds-themed-\`
|
|
||||||
- This is the regular DSpace selector prefix
|
|
||||||
- **When making a regular component themeable, its selector prefix should be changed to \`ds-base-\`, and the new wrapper's component should reuse the previous selector**
|
|
||||||
|
|
||||||
Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source.
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
type: 'problem',
|
|
||||||
schema: [],
|
|
||||||
fixable: 'code',
|
|
||||||
messages: {
|
|
||||||
[Message.BASE]: 'Unthemed version of themeable component should have a selector starting with \'ds-base-\'',
|
|
||||||
[Message.WRAPPER]: 'Themed component wrapper of themeable component shouldn\'t have a selector starting with \'ds-themed-\'',
|
|
||||||
[Message.THEMED]: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultOptions: [],
|
|
||||||
} as DSpaceESLintRuleInfo;
|
|
||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
|
||||||
...info,
|
|
||||||
create(context: RuleContext<Message, unknown[]>) {
|
|
||||||
const filename = getFilename(context);
|
|
||||||
|
|
||||||
if (filename.endsWith('.spec.ts')) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
function enforceWrapperSelector(selectorNode: TSESTree.StringLiteral) {
|
|
||||||
if (selectorNode?.value.startsWith('ds-themed-')) {
|
|
||||||
context.report({
|
|
||||||
messageId: Message.WRAPPER,
|
|
||||||
node: selectorNode,
|
|
||||||
fix(fixer) {
|
|
||||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-')));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function enforceBaseSelector(selectorNode: TSESTree.StringLiteral) {
|
|
||||||
if (!selectorNode?.value.startsWith('ds-base-')) {
|
|
||||||
context.report({
|
|
||||||
messageId: Message.BASE,
|
|
||||||
node: selectorNode,
|
|
||||||
fix(fixer) {
|
|
||||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-')));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function enforceThemedSelector(selectorNode: TSESTree.StringLiteral) {
|
|
||||||
if (!selectorNode?.value.startsWith('ds-themed-')) {
|
|
||||||
context.report({
|
|
||||||
messageId: Message.THEMED,
|
|
||||||
node: selectorNode,
|
|
||||||
fix(fixer) {
|
|
||||||
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-')));
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) {
|
|
||||||
const selectorNode = getComponentSelectorNode(node);
|
|
||||||
|
|
||||||
if (selectorNode === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const selector = selectorNode?.value;
|
|
||||||
const classNode = node.parent as TSESTree.ClassDeclaration;
|
|
||||||
const className = classNode.id?.name;
|
|
||||||
|
|
||||||
if (selector === undefined || className === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isThemedComponentWrapper(node)) {
|
|
||||||
enforceWrapperSelector(selectorNode);
|
|
||||||
} else if (inThemedComponentOverrideFile(filename)) {
|
|
||||||
enforceThemedSelector(selectorNode);
|
|
||||||
} else if (isThemeableComponent(className)) {
|
|
||||||
enforceBaseSelector(selectorNode);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tests = {
|
|
||||||
plugin: info.name,
|
|
||||||
valid: [
|
|
||||||
{
|
|
||||||
name: 'Regular non-themeable component selector',
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class Something {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Themeable component selector should replace the original version, unthemed version should be changed to ds-base-',
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-something',
|
|
||||||
})
|
|
||||||
class Something {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class ThemedSomething extends ThemedComponent<Something> {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-something',
|
|
||||||
})
|
|
||||||
class OverrideSomething extends Something {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Other themed component wrappers should not interfere',
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class Something {
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something-else',
|
|
||||||
})
|
|
||||||
class ThemedSomethingElse extends ThemedComponent<SomethingElse> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
invalid: [
|
|
||||||
{
|
|
||||||
name: 'Wrong selector for base component',
|
|
||||||
filename: fixture('src/app/test/test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.BASE,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-something',
|
|
||||||
})
|
|
||||||
class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Wrong selector for wrapper component',
|
|
||||||
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-something',
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRAPPER,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Wrong selector for theme override',
|
|
||||||
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-something',
|
|
||||||
})
|
|
||||||
class TestThememeableComponent extends BaseComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.THEMED,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-something',
|
|
||||||
})
|
|
||||||
class TestThememeableComponent extends BaseComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default rule;
|
|
@@ -1,502 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
ESLintUtils,
|
|
||||||
TSESTree,
|
|
||||||
} from '@typescript-eslint/utils';
|
|
||||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
|
||||||
|
|
||||||
import { fixture } from '../../../test/fixture';
|
|
||||||
import {
|
|
||||||
removeWithCommas,
|
|
||||||
replaceOrRemoveArrayIdentifier,
|
|
||||||
} from '../../util/fix';
|
|
||||||
import { DSpaceESLintRuleInfo } from '../../util/structure';
|
|
||||||
import {
|
|
||||||
allThemeableComponents,
|
|
||||||
DISALLOWED_THEME_SELECTORS,
|
|
||||||
fixSelectors,
|
|
||||||
getThemeableComponentByBaseClass,
|
|
||||||
isAllowedUnthemedUsage,
|
|
||||||
} from '../../util/theme-support';
|
|
||||||
import {
|
|
||||||
findImportSpecifier,
|
|
||||||
findUsages,
|
|
||||||
findUsagesByName,
|
|
||||||
getFilename,
|
|
||||||
relativePath,
|
|
||||||
} from '../../util/typescript';
|
|
||||||
|
|
||||||
export enum Message {
|
|
||||||
WRONG_CLASS = 'mustUseThemedWrapperClass',
|
|
||||||
WRONG_IMPORT = 'mustImportThemedWrapper',
|
|
||||||
WRONG_SELECTOR = 'mustUseThemedWrapperSelector',
|
|
||||||
BASE_IN_MODULE = 'baseComponentNotNeededInModule',
|
|
||||||
}
|
|
||||||
|
|
||||||
export const info = {
|
|
||||||
name: 'themed-component-usages',
|
|
||||||
meta: {
|
|
||||||
docs: {
|
|
||||||
description: `Themeable components should be used via their \`ThemedComponent\` wrapper class
|
|
||||||
|
|
||||||
This ensures that custom themes can correctly override _all_ instances of this component.
|
|
||||||
There are a few exceptions where the base class can still be used:
|
|
||||||
- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place)
|
|
||||||
- Angular modules (except for routing modules)
|
|
||||||
- Angular \`@ViewChild\` decorators
|
|
||||||
- Type annotations
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
type: 'problem',
|
|
||||||
schema: [],
|
|
||||||
fixable: 'code',
|
|
||||||
messages: {
|
|
||||||
[Message.WRONG_CLASS]: 'Themeable components should be used via their ThemedComponent wrapper',
|
|
||||||
[Message.WRONG_IMPORT]: 'Themeable components should be used via their ThemedComponent wrapper',
|
|
||||||
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper',
|
|
||||||
[Message.BASE_IN_MODULE]: 'Base themeable components shouldn\'t be declared in modules',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
defaultOptions: [],
|
|
||||||
} as DSpaceESLintRuleInfo;
|
|
||||||
|
|
||||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
|
||||||
...info,
|
|
||||||
create(context: RuleContext<Message, unknown[]>) {
|
|
||||||
const filename = getFilename(context);
|
|
||||||
|
|
||||||
function handleUnthemedUsagesInTypescript(node: TSESTree.Identifier) {
|
|
||||||
if (isAllowedUnthemedUsage(node)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const entry = getThemeableComponentByBaseClass(node.name);
|
|
||||||
|
|
||||||
if (entry === undefined) {
|
|
||||||
// this should never happen
|
|
||||||
throw new Error(`No such themeable component in registry: '${node.name}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.report({
|
|
||||||
messageId: Message.WRONG_CLASS,
|
|
||||||
node: node,
|
|
||||||
fix(fixer) {
|
|
||||||
if (node.parent.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
|
|
||||||
return replaceOrRemoveArrayIdentifier(context, fixer, node, entry.wrapperClass);
|
|
||||||
} else {
|
|
||||||
return fixer.replaceText(node, entry.wrapperClass);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleThemedSelectorQueriesInTests(node: TSESTree.Literal) {
|
|
||||||
context.report({
|
|
||||||
node,
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
fix(fixer){
|
|
||||||
const newSelector = fixSelectors(node.raw);
|
|
||||||
return fixer.replaceText(node, newSelector);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUnthemedImportsInTypescript(specifierNode: TSESTree.ImportSpecifier) {
|
|
||||||
const allUsages = findUsages(context, specifierNode.local);
|
|
||||||
const badUsages = allUsages.filter(usage => !isAllowedUnthemedUsage(usage));
|
|
||||||
|
|
||||||
if (badUsages.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const importedNode = specifierNode.imported;
|
|
||||||
const declarationNode = specifierNode.parent as TSESTree.ImportDeclaration;
|
|
||||||
|
|
||||||
const entry = getThemeableComponentByBaseClass(importedNode.name);
|
|
||||||
if (entry === undefined) {
|
|
||||||
// this should never happen
|
|
||||||
throw new Error(`No such themeable component in registry: '${importedNode.name}'`);
|
|
||||||
}
|
|
||||||
|
|
||||||
context.report({
|
|
||||||
messageId: Message.WRONG_IMPORT,
|
|
||||||
node: importedNode,
|
|
||||||
fix(fixer) {
|
|
||||||
const ops = [];
|
|
||||||
|
|
||||||
const wrapperImport = findImportSpecifier(context, entry.wrapperClass);
|
|
||||||
|
|
||||||
if (findUsagesByName(context, entry.wrapperClass).length === 0) {
|
|
||||||
// Wrapper is not present in this file, safe to add import
|
|
||||||
|
|
||||||
const newImportLine = `import { ${entry.wrapperClass} } from '${relativePath(filename, entry.wrapperPath)}';`;
|
|
||||||
|
|
||||||
if (declarationNode.specifiers.length === 1) {
|
|
||||||
if (allUsages.length === badUsages.length) {
|
|
||||||
ops.push(fixer.replaceText(declarationNode, newImportLine));
|
|
||||||
} else if (wrapperImport === undefined) {
|
|
||||||
ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine));
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ops.push(...removeWithCommas(context, fixer, specifierNode));
|
|
||||||
if (wrapperImport === undefined) {
|
|
||||||
ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Wrapper already present in the file, remove import instead
|
|
||||||
|
|
||||||
if (allUsages.length === badUsages.length) {
|
|
||||||
if (declarationNode.specifiers.length === 1) {
|
|
||||||
// Make sure we remove the newline as well
|
|
||||||
ops.push(fixer.removeRange([declarationNode.range[0], declarationNode.range[1] + 1]));
|
|
||||||
} else {
|
|
||||||
ops.push(...removeWithCommas(context, fixer, specifierNode));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return ops;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ignore tests and non-routing modules
|
|
||||||
if (filename.endsWith('.spec.ts')) {
|
|
||||||
return {
|
|
||||||
[`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests,
|
|
||||||
};
|
|
||||||
} else if (filename.endsWith('.cy.ts')) {
|
|
||||||
return {
|
|
||||||
[`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests,
|
|
||||||
};
|
|
||||||
} else if (
|
|
||||||
filename.match(/(?!src\/themes\/).*(?!routing).module.ts$/)
|
|
||||||
|| filename.match(/themed-.+\.component\.ts$/)
|
|
||||||
) {
|
|
||||||
// do nothing
|
|
||||||
return {};
|
|
||||||
} else {
|
|
||||||
return allThemeableComponents().reduce(
|
|
||||||
(rules, entry) => {
|
|
||||||
return {
|
|
||||||
...rules,
|
|
||||||
[`:not(:matches(ClassDeclaration, ImportSpecifier)) > Identifier[name = "${entry.baseClass}"]`]: handleUnthemedUsagesInTypescript,
|
|
||||||
[`ImportSpecifier[imported.name = "${entry.baseClass}"]`]: handleUnthemedImportsInTypescript,
|
|
||||||
};
|
|
||||||
}, {},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const tests = {
|
|
||||||
plugin: info.name,
|
|
||||||
valid: [
|
|
||||||
{
|
|
||||||
name: 'allow wrapper class usages',
|
|
||||||
code: `
|
|
||||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: ThemedTestThemeableComponent,
|
|
||||||
b: ChipsComponent,
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allow base class in class declaration',
|
|
||||||
code: `
|
|
||||||
export class TestThemeableComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allow inheriting from base class',
|
|
||||||
code: `
|
|
||||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
|
|
||||||
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allow base class in ViewChild',
|
|
||||||
code: `
|
|
||||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
|
|
||||||
export class Something {
|
|
||||||
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allow wrapper selectors in test queries',
|
|
||||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
|
||||||
code: `
|
|
||||||
By.css('ds-themeable');
|
|
||||||
By.css('#test > ds-themeable > #nest');
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'allow wrapper selectors in cypress queries',
|
|
||||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
|
||||||
code: `
|
|
||||||
By.css('ds-themeable');
|
|
||||||
By.css('#test > ds-themeable > #nest');
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
invalid: [
|
|
||||||
{
|
|
||||||
name: 'disallow direct usages of base class',
|
|
||||||
code: `
|
|
||||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
import { TestComponent } from './app/test/test.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: TestThemeableComponent,
|
|
||||||
b: TestComponent,
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_IMPORT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_CLASS,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
|
||||||
import { TestComponent } from './app/test/test.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: ThemedTestThemeableComponent,
|
|
||||||
b: TestComponent,
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'disallow direct usages of base class, keep other imports',
|
|
||||||
code: `
|
|
||||||
import { Something, TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
import { TestComponent } from './app/test/test.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: TestThemeableComponent,
|
|
||||||
b: TestComponent,
|
|
||||||
c: Something,
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_IMPORT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_CLASS,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
import { Something } from './app/test/test-themeable.component';
|
|
||||||
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
|
|
||||||
import { TestComponent } from './app/test/test.component';
|
|
||||||
|
|
||||||
const config = {
|
|
||||||
a: ThemedTestThemeableComponent,
|
|
||||||
b: TestComponent,
|
|
||||||
c: Something,
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'handle array replacements correctly',
|
|
||||||
code: `
|
|
||||||
const DECLARATIONS = [
|
|
||||||
Something,
|
|
||||||
TestThemeableComponent,
|
|
||||||
Something,
|
|
||||||
ThemedTestThemeableComponent,
|
|
||||||
];
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_CLASS,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
const DECLARATIONS = [
|
|
||||||
Something,
|
|
||||||
Something,
|
|
||||||
ThemedTestThemeableComponent,
|
|
||||||
];
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'disallow override selector in test queries',
|
|
||||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
|
||||||
code: `
|
|
||||||
By.css('ds-themed-themeable');
|
|
||||||
By.css('#test > ds-themed-themeable > #nest');
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
By.css('ds-themeable');
|
|
||||||
By.css('#test > ds-themeable > #nest');
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'disallow base selector in test queries',
|
|
||||||
filename: fixture('src/app/test/test.component.spec.ts'),
|
|
||||||
code: `
|
|
||||||
By.css('ds-base-themeable');
|
|
||||||
By.css('#test > ds-base-themeable > #nest');
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
By.css('ds-themeable');
|
|
||||||
By.css('#test > ds-themeable > #nest');
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'disallow override selector in cypress queries',
|
|
||||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
|
||||||
code: `
|
|
||||||
cy.get('ds-themed-themeable');
|
|
||||||
cy.get('#test > ds-themed-themeable > #nest');
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
cy.get('ds-themeable');
|
|
||||||
cy.get('#test > ds-themeable > #nest');
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'disallow base selector in cypress queries',
|
|
||||||
filename: fixture('src/app/test/test.component.cy.ts'),
|
|
||||||
code: `
|
|
||||||
cy.get('ds-base-themeable');
|
|
||||||
cy.get('#test > ds-base-themeable > #nest');
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_SELECTOR,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
cy.get('ds-themeable');
|
|
||||||
cy.get('#test > ds-themeable > #nest');
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'edge case: unable to find usage node through usage token, but import is still flagged and fixed',
|
|
||||||
filename: fixture('src/themes/test/app/test/other-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Context } from './app/core/shared/context.model';
|
|
||||||
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class UsageComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_IMPORT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_CLASS,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Context } from './app/core/shared/context.model';
|
|
||||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
imports: [ThemedTestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class UsageComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'edge case edge case: both are imported, only wrapper is retained',
|
|
||||||
filename: fixture('src/themes/test/app/test/other-themeable.component.ts'),
|
|
||||||
code: `
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Context } from './app/core/shared/context.model';
|
|
||||||
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
|
|
||||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent, ThemedTestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class UsageComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
errors: [
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_IMPORT,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
messageId: Message.WRONG_CLASS,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
output: `
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { Context } from './app/core/shared/context.model';
|
|
||||||
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
standalone: true,
|
|
||||||
imports: [ThemedTestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class UsageComponent {
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
export default rule;
|
|
@@ -1,83 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { TSESTree } from '@typescript-eslint/utils';
|
|
||||||
|
|
||||||
import { getObjectPropertyNodeByName } from './typescript';
|
|
||||||
|
|
||||||
export function getComponentSelectorNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.StringLiteral | undefined {
|
|
||||||
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'selector');
|
|
||||||
|
|
||||||
if (property !== undefined) {
|
|
||||||
// todo: support template literals as well
|
|
||||||
if (property.type === TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'string') {
|
|
||||||
return property as TSESTree.StringLiteral;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getComponentStandaloneNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.BooleanLiteral | undefined {
|
|
||||||
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'standalone');
|
|
||||||
|
|
||||||
if (property !== undefined) {
|
|
||||||
if (property.type === TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'boolean') {
|
|
||||||
return property as TSESTree.BooleanLiteral;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
export function getComponentImportNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.ArrayExpression | undefined {
|
|
||||||
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'imports');
|
|
||||||
|
|
||||||
if (property !== undefined) {
|
|
||||||
if (property.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
|
|
||||||
return property as TSESTree.ArrayExpression;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined {
|
|
||||||
if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decoratorNode.parent.id?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return decoratorNode.parent.id.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getComponentSuperClassName(decoratorNode: TSESTree.Decorator): string | undefined {
|
|
||||||
if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return decoratorNode.parent.superClass.name;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getComponentInitializer(componentDecoratorNode: TSESTree.Decorator): TSESTree.ObjectExpression {
|
|
||||||
return (componentDecoratorNode.expression as TSESTree.CallExpression).arguments[0] as TSESTree.ObjectExpression;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getComponentInitializerNodeByName(componentDecoratorNode: TSESTree.Decorator, name: string): TSESTree.Node | undefined {
|
|
||||||
const initializer = getComponentInitializer(componentDecoratorNode);
|
|
||||||
return getObjectPropertyNodeByName(initializer, name);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPartOfViewChild(node: TSESTree.Identifier): boolean {
|
|
||||||
return (node.parent as any)?.callee?.name === 'ViewChild';
|
|
||||||
}
|
|
@@ -1,125 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { TSESTree } from '@typescript-eslint/utils';
|
|
||||||
import {
|
|
||||||
RuleContext,
|
|
||||||
RuleFix,
|
|
||||||
RuleFixer,
|
|
||||||
} from '@typescript-eslint/utils/ts-eslint';
|
|
||||||
|
|
||||||
import { getSourceCode } from './typescript';
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function appendObjectProperties(context: RuleContext<any, any>, fixer: RuleFixer, objectNode: TSESTree.ObjectExpression, properties: string[]): RuleFix {
|
|
||||||
// todo: may not handle empty objects too well
|
|
||||||
const lastProperty = objectNode.properties[objectNode.properties.length - 1];
|
|
||||||
const source = getSourceCode(context);
|
|
||||||
const nextToken = source.getTokenAfter(lastProperty);
|
|
||||||
|
|
||||||
// todo: newline & indentation are hardcoded for @Component({})
|
|
||||||
// todo: we're assuming that we need trailing commas, what if we don't?
|
|
||||||
const newPart = '\n' + properties.map(p => ` ${p},`).join('\n');
|
|
||||||
|
|
||||||
if (nextToken !== null && nextToken.value === ',') {
|
|
||||||
return fixer.insertTextAfter(nextToken, newPart);
|
|
||||||
} else {
|
|
||||||
return fixer.insertTextAfter(lastProperty, ',' + newPart);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function appendArrayElement(context: RuleContext<any, any>, fixer: RuleFixer, arrayNode: TSESTree.ArrayExpression, value: string): RuleFix {
|
|
||||||
const source = getSourceCode(context);
|
|
||||||
|
|
||||||
if (arrayNode.elements.length === 0) {
|
|
||||||
// This is the first element
|
|
||||||
const openArray = source.getTokenByRangeStart(arrayNode.range[0]);
|
|
||||||
|
|
||||||
if (openArray == null) {
|
|
||||||
throw new Error('Unexpected null token for opening square bracket');
|
|
||||||
}
|
|
||||||
|
|
||||||
// safe to assume the list is single-line
|
|
||||||
return fixer.insertTextAfter(openArray, `${value}`);
|
|
||||||
} else {
|
|
||||||
const lastElement = arrayNode.elements[arrayNode.elements.length - 1];
|
|
||||||
|
|
||||||
if (lastElement == null) {
|
|
||||||
throw new Error('Unexpected null node in array');
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextToken = source.getTokenAfter(lastElement);
|
|
||||||
|
|
||||||
// todo: we don't know if the list is chopped or not, so we can't make any assumptions -- may produce output that will be flagged by other rules on the next run!
|
|
||||||
// todo: we're assuming that we need trailing commas, what if we don't?
|
|
||||||
if (nextToken !== null && nextToken.value === ',') {
|
|
||||||
return fixer.insertTextAfter(nextToken, ` ${value},`);
|
|
||||||
} else {
|
|
||||||
return fixer.insertTextAfter(lastElement, `, ${value},`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isLast(elementNode: TSESTree.Node): boolean {
|
|
||||||
if (!elementNode.parent) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
let siblingNodes: (TSESTree.Node | null)[] = [null];
|
|
||||||
if (elementNode.parent.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
|
|
||||||
siblingNodes = elementNode.parent.elements;
|
|
||||||
} else if (elementNode.parent.type === TSESTree.AST_NODE_TYPES.ImportDeclaration) {
|
|
||||||
siblingNodes = elementNode.parent.specifiers;
|
|
||||||
}
|
|
||||||
|
|
||||||
return elementNode === siblingNodes[siblingNodes.length - 1];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function removeWithCommas(context: RuleContext<any, any>, fixer: RuleFixer, elementNode: TSESTree.Node): RuleFix[] {
|
|
||||||
const ops = [];
|
|
||||||
|
|
||||||
const source = getSourceCode(context);
|
|
||||||
let nextToken = source.getTokenAfter(elementNode);
|
|
||||||
let prevToken = source.getTokenBefore(elementNode);
|
|
||||||
|
|
||||||
if (nextToken !== null && prevToken !== null) {
|
|
||||||
if (nextToken.value === ',') {
|
|
||||||
nextToken = source.getTokenAfter(nextToken);
|
|
||||||
if (nextToken !== null) {
|
|
||||||
ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (isLast(elementNode) && prevToken.value === ',') {
|
|
||||||
prevToken = source.getTokenBefore(prevToken);
|
|
||||||
if (prevToken !== null) {
|
|
||||||
ops.push(fixer.removeRange([prevToken.range[1], elementNode.range[1]]));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (nextToken !== null) {
|
|
||||||
ops.push(fixer.removeRange([elementNode.range[0], nextToken.range[0]]));
|
|
||||||
}
|
|
||||||
|
|
||||||
return ops;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function replaceOrRemoveArrayIdentifier(context: RuleContext<any, any>, fixer: RuleFixer, identifierNode: TSESTree.Identifier, newValue: string): RuleFix[] {
|
|
||||||
if (identifierNode.parent.type !== TSESTree.AST_NODE_TYPES.ArrayExpression) {
|
|
||||||
throw new Error('Parent node is not an array expression!');
|
|
||||||
}
|
|
||||||
|
|
||||||
const array = identifierNode.parent as TSESTree.ArrayExpression;
|
|
||||||
|
|
||||||
for (const element of array.elements) {
|
|
||||||
if (element !== null && element.type === TSESTree.AST_NODE_TYPES.Identifier && element.name === newValue) {
|
|
||||||
return removeWithCommas(context, fixer, identifierNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [fixer.replaceText(identifierNode, newValue)];
|
|
||||||
}
|
|
@@ -1,28 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
||||||
export function match(rangeA: number[], rangeB: number[]) {
|
|
||||||
return rangeA[0] === rangeB[0] && rangeA[1] === rangeB[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function stringLiteral(value: string): string {
|
|
||||||
return `'${value}'`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transform Windows-style paths into Unix-style paths
|
|
||||||
*/
|
|
||||||
export function toUnixStylePath(path: string): string {
|
|
||||||
// note: we're assuming that none of the directory/file names contain '\' or '/' characters.
|
|
||||||
// using these characters in paths is very bad practice in general, so this should be a safe assumption.
|
|
||||||
if (path.includes('\\')) {
|
|
||||||
return path.replace(/^[A-Z]:\\/, '/').replaceAll('\\', '/');
|
|
||||||
}
|
|
||||||
return path;
|
|
||||||
}
|
|
@@ -1,61 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import {
|
|
||||||
InvalidTestCase,
|
|
||||||
RuleMetaData,
|
|
||||||
RuleModule,
|
|
||||||
ValidTestCase,
|
|
||||||
} from '@typescript-eslint/utils/ts-eslint';
|
|
||||||
import { EnumType } from 'typescript';
|
|
||||||
|
|
||||||
export type Meta = RuleMetaData<string, unknown[]>;
|
|
||||||
export type Valid = ValidTestCase<unknown[]>;
|
|
||||||
export type Invalid = InvalidTestCase<string, unknown[]>;
|
|
||||||
|
|
||||||
export interface DSpaceESLintRuleInfo {
|
|
||||||
name: string;
|
|
||||||
meta: Meta,
|
|
||||||
defaultOptions: unknown[],
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NamedTests {
|
|
||||||
plugin: string;
|
|
||||||
valid: Valid[];
|
|
||||||
invalid: Invalid[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RuleExports {
|
|
||||||
Message: EnumType,
|
|
||||||
info: DSpaceESLintRuleInfo,
|
|
||||||
rule: RuleModule<string>,
|
|
||||||
tests: NamedTests,
|
|
||||||
default: unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PluginExports {
|
|
||||||
name: string,
|
|
||||||
language: string,
|
|
||||||
rules: Record<string, unknown>,
|
|
||||||
index: RuleExports[],
|
|
||||||
}
|
|
||||||
|
|
||||||
export function bundle(
|
|
||||||
name: string,
|
|
||||||
language: string,
|
|
||||||
index: RuleExports[],
|
|
||||||
): PluginExports {
|
|
||||||
return index.reduce((o: PluginExports, i: RuleExports) => {
|
|
||||||
o.rules[i.info.name] = i.rule;
|
|
||||||
return o;
|
|
||||||
}, {
|
|
||||||
name,
|
|
||||||
language,
|
|
||||||
rules: {},
|
|
||||||
index,
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,5 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../lint/README.md) > <%= plugin.language %> rules
|
|
||||||
_______
|
|
||||||
<% rules.forEach(rule => { %>
|
|
||||||
- [`<%= plugin.name %>/<%= rule.name %>`](./rules/<%= rule.name %>.md)<% if (rule.meta?.docs?.description) {%>: <%= rule.meta.docs.description.split('\n')[0].trim() -%><% }-%>
|
|
||||||
<% }) %>
|
|
@@ -1,48 +0,0 @@
|
|||||||
[DSpace ESLint plugins](../../../../lint/README.md) > [<%= plugin.language %> rules](../index.md) > `<%= plugin.name %>/<%= rule.name %>`
|
|
||||||
_______
|
|
||||||
|
|
||||||
<%- rule.meta.docs?.description %>
|
|
||||||
|
|
||||||
_______
|
|
||||||
|
|
||||||
[Source code](../../../../lint/src/rules/<%- plugin.name.replace('dspace-angular-', '') %>/<%- rule.name %>.ts)
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
<% if (tests.valid) {%>
|
|
||||||
#### Valid code
|
|
||||||
<% tests.valid.forEach(test => { %>
|
|
||||||
##### <%= test.name !== undefined ? test.name : 'UNNAMED' %>
|
|
||||||
<% if (test.filename) { %>
|
|
||||||
Filename: `<%- test.filename %>`
|
|
||||||
<% } %>
|
|
||||||
```<%- plugin.language.toLowerCase() %>
|
|
||||||
<%- test.code.trim() %>
|
|
||||||
```
|
|
||||||
<% }) %>
|
|
||||||
<% } %>
|
|
||||||
|
|
||||||
<% if (tests.invalid) {%>
|
|
||||||
#### Invalid code <%= rule.meta.fixable ? ' & automatic fixes' : '' %>
|
|
||||||
<% tests.invalid.forEach(test => { %>
|
|
||||||
##### <%= test.name !== undefined ? test.name : 'UNNAMED' %>
|
|
||||||
<% if (test.filename) { %>
|
|
||||||
Filename: `<%- test.filename %>`
|
|
||||||
<% } %>
|
|
||||||
```<%- plugin.language.toLowerCase() %>
|
|
||||||
<%- test.code.trim() %>
|
|
||||||
```
|
|
||||||
Will produce the following error(s):
|
|
||||||
```
|
|
||||||
<% for (const error of test.errors) { -%>
|
|
||||||
<%- rule.meta.messages[error.messageId] %>
|
|
||||||
<% } -%>
|
|
||||||
```
|
|
||||||
<% if (test.output) { %>
|
|
||||||
Result of `yarn lint --fix`:
|
|
||||||
```<%- plugin.language.toLowerCase() %>
|
|
||||||
<%- test.output.trim() %>
|
|
||||||
```
|
|
||||||
<% } %>
|
|
||||||
<% }) %>
|
|
||||||
<% } %>
|
|
@@ -1,265 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { TSESTree } from '@typescript-eslint/utils';
|
|
||||||
import { readFileSync } from 'fs';
|
|
||||||
import { basename } from 'path';
|
|
||||||
import ts, { Identifier } from 'typescript';
|
|
||||||
|
|
||||||
import {
|
|
||||||
getComponentClassName,
|
|
||||||
isPartOfViewChild,
|
|
||||||
} from './angular';
|
|
||||||
import {
|
|
||||||
isPartOfClassDeclaration,
|
|
||||||
isPartOfTypeExpression,
|
|
||||||
} from './typescript';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Couples a themeable Component to its ThemedComponent wrapper
|
|
||||||
*/
|
|
||||||
export interface ThemeableComponentRegistryEntry {
|
|
||||||
basePath: string;
|
|
||||||
baseFileName: string,
|
|
||||||
baseClass: string;
|
|
||||||
|
|
||||||
wrapperPath: string;
|
|
||||||
wrapperFileName: string,
|
|
||||||
wrapperClass: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isAngularComponentDecorator(node: ts.Node) {
|
|
||||||
if (node.kind === ts.SyntaxKind.Decorator && node.parent.kind === ts.SyntaxKind.ClassDeclaration) {
|
|
||||||
const decorator = node as ts.Decorator;
|
|
||||||
|
|
||||||
if (decorator.expression.kind === ts.SyntaxKind.CallExpression) {
|
|
||||||
const method = decorator.expression as ts.CallExpression;
|
|
||||||
|
|
||||||
if (method.expression.kind === ts.SyntaxKind.Identifier) {
|
|
||||||
return (method.expression as Identifier).text === 'Component';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findImportDeclaration(source: ts.SourceFile, identifierName: string): ts.ImportDeclaration | undefined {
|
|
||||||
return ts.forEachChild(source, (topNode: ts.Node) => {
|
|
||||||
if (topNode.kind === ts.SyntaxKind.ImportDeclaration) {
|
|
||||||
const importDeclaration = topNode as ts.ImportDeclaration;
|
|
||||||
|
|
||||||
if (importDeclaration.importClause?.namedBindings?.kind === ts.SyntaxKind.NamedImports) {
|
|
||||||
const namedImports = importDeclaration.importClause?.namedBindings as ts.NamedImports;
|
|
||||||
|
|
||||||
for (const element of namedImports.elements) {
|
|
||||||
if (element.name.text === identifierName) {
|
|
||||||
return importDeclaration;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listing of all themeable Components
|
|
||||||
*/
|
|
||||||
class ThemeableComponentRegistry {
|
|
||||||
public readonly entries: Set<ThemeableComponentRegistryEntry>;
|
|
||||||
public readonly byBaseClass: Map<string, ThemeableComponentRegistryEntry>;
|
|
||||||
public readonly byWrapperClass: Map<string, ThemeableComponentRegistryEntry>;
|
|
||||||
public readonly byBasePath: Map<string, ThemeableComponentRegistryEntry>;
|
|
||||||
public readonly byWrapperPath: Map<string, ThemeableComponentRegistryEntry>;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this.entries = new Set();
|
|
||||||
this.byBaseClass = new Map();
|
|
||||||
this.byWrapperClass = new Map();
|
|
||||||
this.byBasePath = new Map();
|
|
||||||
this.byWrapperPath = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
public initialize(prefix = '') {
|
|
||||||
if (this.entries.size > 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
function registerWrapper(path: string) {
|
|
||||||
const source = getSource(path);
|
|
||||||
|
|
||||||
function traverse(node: ts.Node) {
|
|
||||||
if (node.parent !== undefined && isAngularComponentDecorator(node)) {
|
|
||||||
const classNode = node.parent as ts.ClassDeclaration;
|
|
||||||
|
|
||||||
if (classNode.name === undefined || classNode.heritageClauses === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const wrapperClass = classNode.name?.escapedText as string;
|
|
||||||
|
|
||||||
for (const heritageClause of classNode.heritageClauses) {
|
|
||||||
for (const type of heritageClause.types) {
|
|
||||||
if ((type as any).expression.escapedText === 'ThemedComponent') {
|
|
||||||
if (type.kind !== ts.SyntaxKind.ExpressionWithTypeArguments || type.typeArguments === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const firstTypeArg = type.typeArguments[0] as ts.TypeReferenceNode;
|
|
||||||
const baseClass = (firstTypeArg.typeName as ts.Identifier)?.escapedText;
|
|
||||||
|
|
||||||
if (baseClass === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const importDeclaration = findImportDeclaration(source, baseClass);
|
|
||||||
|
|
||||||
if (importDeclaration === undefined) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const basePath = resolveLocalPath((importDeclaration.moduleSpecifier as ts.StringLiteral).text, path);
|
|
||||||
|
|
||||||
themeableComponents.add({
|
|
||||||
baseClass,
|
|
||||||
basePath: basePath.replace(new RegExp(`^${prefix}`), ''),
|
|
||||||
baseFileName: basename(basePath).replace(/\.ts$/, ''),
|
|
||||||
wrapperClass,
|
|
||||||
wrapperPath: path.replace(new RegExp(`^${prefix}`), ''),
|
|
||||||
wrapperFileName: basename(path).replace(/\.ts$/, ''),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
ts.forEachChild(node, traverse);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
traverse(source);
|
|
||||||
}
|
|
||||||
|
|
||||||
const glob = require('glob');
|
|
||||||
|
|
||||||
// note: this outputs Unix-style paths on Windows
|
|
||||||
const wrappers: string[] = glob.GlobSync(prefix + 'src/app/**/themed-*.component.ts', { ignore: 'node_modules/**' }).found;
|
|
||||||
|
|
||||||
for (const wrapper of wrappers) {
|
|
||||||
registerWrapper(wrapper);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private add(entry: ThemeableComponentRegistryEntry) {
|
|
||||||
this.entries.add(entry);
|
|
||||||
this.byBaseClass.set(entry.baseClass, entry);
|
|
||||||
this.byWrapperClass.set(entry.wrapperClass, entry);
|
|
||||||
this.byBasePath.set(entry.basePath, entry);
|
|
||||||
this.byWrapperPath.set(entry.wrapperPath, entry);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const themeableComponents = new ThemeableComponentRegistry();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Construct the AST of a TypeScript source file
|
|
||||||
* @param file
|
|
||||||
*/
|
|
||||||
function getSource(file: string): ts.SourceFile {
|
|
||||||
return ts.createSourceFile(
|
|
||||||
file,
|
|
||||||
readFileSync(file).toString(),
|
|
||||||
ts.ScriptTarget.ES2020, // todo: actually use tsconfig.json?
|
|
||||||
/*setParentNodes */ true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve a possibly relative local path into an absolute path starting from the root directory of the project
|
|
||||||
*/
|
|
||||||
function resolveLocalPath(path: string, relativeTo: string) {
|
|
||||||
if (path.startsWith('src/')) {
|
|
||||||
return path;
|
|
||||||
} else if (path.startsWith('./')) {
|
|
||||||
const parts = relativeTo.split('/');
|
|
||||||
return [
|
|
||||||
...parts.slice(0, parts.length - 1),
|
|
||||||
path.replace(/^.\//, ''),
|
|
||||||
].join('/') + '.ts';
|
|
||||||
} else {
|
|
||||||
throw new Error(`Unsupported local path: ${path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isThemedComponentWrapper(decoratorNode: TSESTree.Decorator): boolean {
|
|
||||||
if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (decoratorNode.parent.superClass as any)?.name === 'ThemedComponent';
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getBaseComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined {
|
|
||||||
const wrapperClass = getComponentClassName(decoratorNode);
|
|
||||||
|
|
||||||
if (wrapperClass === undefined) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
themeableComponents.initialize();
|
|
||||||
const entry = themeableComponents.byWrapperClass.get(wrapperClass);
|
|
||||||
|
|
||||||
if (entry === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return entry.baseClass;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isThemeableComponent(className: string): boolean {
|
|
||||||
themeableComponents.initialize();
|
|
||||||
return themeableComponents.byBaseClass.has(className);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function inThemedComponentOverrideFile(filename: string): boolean {
|
|
||||||
const match = filename.match(/src\/themes\/[^\/]+\/(app\/.*)/);
|
|
||||||
|
|
||||||
if (!match) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
themeableComponents.initialize();
|
|
||||||
// todo: this is fragile!
|
|
||||||
return themeableComponents.byBasePath.has(`src/${match[1]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function allThemeableComponents(): ThemeableComponentRegistryEntry[] {
|
|
||||||
themeableComponents.initialize();
|
|
||||||
return [...themeableComponents.entries];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getThemeableComponentByBaseClass(baseClass: string): ThemeableComponentRegistryEntry | undefined {
|
|
||||||
themeableComponents.initialize();
|
|
||||||
return themeableComponents.byBaseClass.get(baseClass);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isAllowedUnthemedUsage(usageNode: TSESTree.Identifier) {
|
|
||||||
return isPartOfClassDeclaration(usageNode) || isPartOfTypeExpression(usageNode) || isPartOfViewChild(usageNode);
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-';
|
|
||||||
|
|
||||||
export function fixSelectors(text: string): string {
|
|
||||||
return text.replaceAll(/ds-(base|themed)-/g, 'ds-');
|
|
||||||
}
|
|
@@ -1,155 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { TSESTree } from '@typescript-eslint/utils';
|
|
||||||
import {
|
|
||||||
RuleContext,
|
|
||||||
SourceCode,
|
|
||||||
} from '@typescript-eslint/utils/ts-eslint';
|
|
||||||
|
|
||||||
import {
|
|
||||||
match,
|
|
||||||
toUnixStylePath,
|
|
||||||
} from './misc';
|
|
||||||
|
|
||||||
export type AnyRuleContext = RuleContext<string, unknown[]>;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return the current filename based on the ESLint rule context as a Unix-style path.
|
|
||||||
* This is easier for regex and comparisons to glob paths.
|
|
||||||
*/
|
|
||||||
export function getFilename(context: AnyRuleContext): string {
|
|
||||||
// TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?)
|
|
||||||
// eslint-disable-next-line deprecation/deprecation
|
|
||||||
return toUnixStylePath(context.getFilename());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getSourceCode(context: AnyRuleContext): SourceCode {
|
|
||||||
// TSESLint claims this is deprecated, but the suggested alternative is undefined (could be a version mismatch between ESLint and TSESlint?)
|
|
||||||
// eslint-disable-next-line deprecation/deprecation
|
|
||||||
return context.getSourceCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getObjectPropertyNodeByName(objectNode: TSESTree.ObjectExpression, propertyName: string): TSESTree.Node | undefined {
|
|
||||||
for (const propertyNode of objectNode.properties) {
|
|
||||||
if (
|
|
||||||
propertyNode.type === TSESTree.AST_NODE_TYPES.Property
|
|
||||||
&& (
|
|
||||||
(
|
|
||||||
propertyNode.key?.type === TSESTree.AST_NODE_TYPES.Identifier
|
|
||||||
&& propertyNode.key?.name === propertyName
|
|
||||||
) || (
|
|
||||||
propertyNode.key?.type === TSESTree.AST_NODE_TYPES.Literal
|
|
||||||
&& propertyNode.key?.value === propertyName
|
|
||||||
)
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return propertyNode.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findUsages(context: AnyRuleContext, localNode: TSESTree.Identifier): TSESTree.Identifier[] {
|
|
||||||
const source = getSourceCode(context);
|
|
||||||
|
|
||||||
const usages: TSESTree.Identifier[] = [];
|
|
||||||
|
|
||||||
for (const token of source.ast.tokens) {
|
|
||||||
if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === localNode.name && !match(token.range, localNode.range)) {
|
|
||||||
const node = source.getNodeByRangeIndex(token.range[0]);
|
|
||||||
// todo: in some cases, the resulting node can actually be the whole program (!)
|
|
||||||
if (node !== null) {
|
|
||||||
usages.push(node as TSESTree.Identifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return usages;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function findUsagesByName(context: AnyRuleContext, identifier: string): TSESTree.Identifier[] {
|
|
||||||
const source = getSourceCode(context);
|
|
||||||
|
|
||||||
const usages: TSESTree.Identifier[] = [];
|
|
||||||
|
|
||||||
for (const token of source.ast.tokens) {
|
|
||||||
if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === identifier) {
|
|
||||||
const node = source.getNodeByRangeIndex(token.range[0]);
|
|
||||||
// todo: in some cases, the resulting node can actually be the whole program (!)
|
|
||||||
if (node !== null) {
|
|
||||||
usages.push(node as TSESTree.Identifier);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return usages;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPartOfTypeExpression(node: TSESTree.Identifier): boolean {
|
|
||||||
return node.parent?.type?.valueOf().startsWith('TSType');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isPartOfClassDeclaration(node: TSESTree.Identifier): boolean {
|
|
||||||
return node.parent?.type === TSESTree.AST_NODE_TYPES.ClassDeclaration;
|
|
||||||
}
|
|
||||||
|
|
||||||
function fromSrc(path: string): string {
|
|
||||||
const m = path.match(/^.*(src\/.+)(\.(ts|json|js)?)$/);
|
|
||||||
|
|
||||||
if (m) {
|
|
||||||
return m[1];
|
|
||||||
} else {
|
|
||||||
throw new Error(`Can't infer project-absolute TS/resource path from: ${path}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function relativePath(thisFile: string, importFile: string): string {
|
|
||||||
const fromParts = fromSrc(thisFile).split('/');
|
|
||||||
const toParts = fromSrc(importFile).split('/');
|
|
||||||
|
|
||||||
let lastCommon = 0;
|
|
||||||
for (let i = 0; i < fromParts.length - 1; i++) {
|
|
||||||
if (fromParts[i] === toParts[i]) {
|
|
||||||
lastCommon++;
|
|
||||||
} else {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const path = toParts.slice(lastCommon, toParts.length).join('/');
|
|
||||||
const backtrack = fromParts.length - lastCommon - 1;
|
|
||||||
|
|
||||||
let prefix: string;
|
|
||||||
if (backtrack > 0) {
|
|
||||||
prefix = '../'.repeat(backtrack);
|
|
||||||
} else {
|
|
||||||
prefix = './';
|
|
||||||
}
|
|
||||||
|
|
||||||
return prefix + path;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function findImportSpecifier(context: AnyRuleContext, identifier: string): TSESTree.ImportSpecifier | undefined {
|
|
||||||
const source = getSourceCode(context);
|
|
||||||
|
|
||||||
const usages: TSESTree.Identifier[] = [];
|
|
||||||
|
|
||||||
for (const token of source.ast.tokens) {
|
|
||||||
if (token.type === TSESTree.AST_TOKEN_TYPES.Identifier && token.value === identifier) {
|
|
||||||
const node = source.getNodeByRangeIndex(token.range[0]);
|
|
||||||
// todo: in some cases, the resulting node can actually be the whole program (!)
|
|
||||||
if (node && node.parent && node.parent.type === TSESTree.AST_NODE_TYPES.ImportSpecifier) {
|
|
||||||
return node.parent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return undefined;
|
|
||||||
}
|
|
@@ -1,9 +0,0 @@
|
|||||||
# ESLint testing fixtures
|
|
||||||
|
|
||||||
The files in this directory are used for the ESLint testing environment
|
|
||||||
- Some rules rely on registries that must be built up _before_ the rule is run
|
|
||||||
- In order to test these registries, the fixture sources contain a few dummy components
|
|
||||||
- The TypeScript ESLint test runner requires at least one dummy file to exist to run any tests
|
|
||||||
- By default, [`test.ts`](./src/test.ts) is used. Note that this file is empty; it's only there for the TypeScript configuration, the actual content is injected from the `code` property in the tests.
|
|
||||||
- To test rules that make assertions based on the path of the file, you'll need to include the `filename` property in the test configuration. Note that it must point to an existing file too!
|
|
||||||
- The `filename` must be provided as `fixture('src/something.ts')`
|
|
@@ -1,13 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
||||||
export const FIXTURE = 'lint/test/fixture/';
|
|
||||||
|
|
||||||
export function fixture(path: string): string {
|
|
||||||
return FIXTURE + path;
|
|
||||||
}
|
|
@@ -1,14 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { ThemedTestThemeableComponent } from './themed-test-themeable.component';
|
|
||||||
|
|
||||||
export const ROUTES = [
|
|
||||||
{
|
|
||||||
component: ThemedTestThemeableComponent,
|
|
||||||
},
|
|
||||||
];
|
|
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-base-test-themeable',
|
|
||||||
template: '',
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class TestThemeableComponent {
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
@@ -1,8 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
@@ -1,15 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test',
|
|
||||||
template: '',
|
|
||||||
})
|
|
||||||
export class TestComponent {
|
|
||||||
}
|
|
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
// @ts-ignore
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
|
|
||||||
import { TestComponent } from './test.component';
|
|
||||||
import { TestThemeableComponent } from './test-themeable.component';
|
|
||||||
import { ThemedTestThemeableComponent } from './themed-test-themeable.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
TestComponent,
|
|
||||||
TestThemeableComponent,
|
|
||||||
ThemedTestThemeableComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class TestModule {
|
|
||||||
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { ThemedComponent } from '../../../../../../src/app/shared/theme-support/themed.component';
|
|
||||||
import { TestThemeableComponent } from './test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-test-themeable',
|
|
||||||
template: '',
|
|
||||||
standalone: true,
|
|
||||||
imports: [TestThemeableComponent],
|
|
||||||
})
|
|
||||||
export class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
|
||||||
protected getComponentName(): string {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected importThemedComponent(themeName: string): Promise<any> {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected importUnthemedComponent(): Promise<any> {
|
|
||||||
return Promise.resolve(undefined);
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,16 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-test-themeable',
|
|
||||||
template: '',
|
|
||||||
})
|
|
||||||
export class OtherThemeableComponent {
|
|
||||||
|
|
||||||
}
|
|
@@ -1,18 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
import { Component } from '@angular/core';
|
|
||||||
|
|
||||||
import { TestThemeableComponent as BaseComponent } from '../../../../app/test/test-themeable.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-themed-test-themeable',
|
|
||||||
template: '',
|
|
||||||
})
|
|
||||||
export class TestThemeableComponent extends BaseComponent {
|
|
||||||
|
|
||||||
}
|
|
@@ -1,22 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
// @ts-ignore
|
|
||||||
import { NgModule } from '@angular/core';
|
|
||||||
|
|
||||||
import { OtherThemeableComponent } from './app/test/other-themeable.component';
|
|
||||||
import { TestThemeableComponent } from './app/test/test-themeable.component';
|
|
||||||
|
|
||||||
@NgModule({
|
|
||||||
declarations: [
|
|
||||||
TestThemeableComponent,
|
|
||||||
OtherThemeableComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class TestModule {
|
|
||||||
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"extends": "../../tsconfig.json",
|
|
||||||
"include": [
|
|
||||||
"src/**/*.ts"
|
|
||||||
],
|
|
||||||
"exclude": []
|
|
||||||
}
|
|
@@ -1,13 +0,0 @@
|
|||||||
const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
|
|
||||||
const StacktraceOption = require('jasmine-spec-reporter').StacktraceOption;
|
|
||||||
|
|
||||||
jasmine.getEnv().clearReporters(); // Clear default console reporter for those instead
|
|
||||||
jasmine.getEnv().addReporter(new SpecReporter({
|
|
||||||
spec: {
|
|
||||||
displayErrorMessages: false,
|
|
||||||
},
|
|
||||||
summary: {
|
|
||||||
displayFailed: true,
|
|
||||||
displayStacktrace: StacktraceOption.PRETTY,
|
|
||||||
},
|
|
||||||
}));
|
|
@@ -1,26 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { default as htmlPlugin } from '../src/rules/html';
|
|
||||||
import { default as tsPlugin } from '../src/rules/ts';
|
|
||||||
import {
|
|
||||||
htmlRuleTester,
|
|
||||||
tsRuleTester,
|
|
||||||
} from './testing';
|
|
||||||
|
|
||||||
describe('TypeScript rules', () => {
|
|
||||||
for (const { info, rule, tests } of tsPlugin.index) {
|
|
||||||
tsRuleTester.run(info.name, rule, tests as any);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('HTML rules', () => {
|
|
||||||
for (const { info, rule, tests } of htmlPlugin.index) {
|
|
||||||
htmlRuleTester.run(info.name, rule, tests);
|
|
||||||
}
|
|
||||||
});
|
|
@@ -1,76 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { default as html } from '../src/rules/html';
|
|
||||||
import { default as ts } from '../src/rules/ts';
|
|
||||||
|
|
||||||
describe('plugin structure', () => {
|
|
||||||
for (const pluginExports of [ts, html]) {
|
|
||||||
const pluginName = pluginExports.name ?? 'UNNAMED PLUGIN';
|
|
||||||
|
|
||||||
describe(pluginName, () => {
|
|
||||||
it('should have a name', () => {
|
|
||||||
expect(pluginExports.name).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have rules', () => {
|
|
||||||
expect(pluginExports.index).toBeTruthy();
|
|
||||||
expect(pluginExports.rules).toBeTruthy();
|
|
||||||
expect(pluginExports.index.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
for (const ruleExports of pluginExports.index) {
|
|
||||||
const ruleName = ruleExports.info.name ?? 'UNNAMED RULE';
|
|
||||||
|
|
||||||
describe(ruleName, () => {
|
|
||||||
it('should have a name', () => {
|
|
||||||
expect(ruleExports.info.name).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be included under the right name in the plugin', () => {
|
|
||||||
expect(pluginExports.rules[ruleExports.info.name]).toBe(ruleExports.rule);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain metadata', () => {
|
|
||||||
expect(ruleExports.info).toBeTruthy();
|
|
||||||
expect(ruleExports.info.name).toBeTruthy();
|
|
||||||
expect(ruleExports.info.meta).toBeTruthy();
|
|
||||||
expect(ruleExports.info.defaultOptions).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain messages', () => {
|
|
||||||
expect(ruleExports.Message).toBeTruthy();
|
|
||||||
expect(ruleExports.info.meta.messages).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('messages', () => {
|
|
||||||
for (const member of Object.keys(ruleExports.Message)) {
|
|
||||||
describe(member, () => {
|
|
||||||
const id = (ruleExports.Message as any)[member];
|
|
||||||
|
|
||||||
it('should have a valid ID', () => {
|
|
||||||
expect(id).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have valid metadata', () => {
|
|
||||||
expect(ruleExports.info.meta.messages[id]).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain tests', () => {
|
|
||||||
expect(ruleExports.tests).toBeTruthy();
|
|
||||||
expect(ruleExports.tests.valid.length).toBeGreaterThan(0);
|
|
||||||
expect(ruleExports.tests.invalid.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
@@ -1,53 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { RuleTester as TypeScriptRuleTester } from '@typescript-eslint/rule-tester';
|
|
||||||
import { RuleTester } from '@typescript-eslint/utils/ts-eslint';
|
|
||||||
|
|
||||||
import { themeableComponents } from '../src/util/theme-support';
|
|
||||||
import {
|
|
||||||
FIXTURE,
|
|
||||||
fixture,
|
|
||||||
} from './fixture';
|
|
||||||
|
|
||||||
|
|
||||||
// Register themed components from test fixture
|
|
||||||
themeableComponents.initialize(FIXTURE);
|
|
||||||
|
|
||||||
TypeScriptRuleTester.itOnly = fit;
|
|
||||||
TypeScriptRuleTester.itSkip = xit;
|
|
||||||
|
|
||||||
export const tsRuleTester = new TypeScriptRuleTester({
|
|
||||||
parser: '@typescript-eslint/parser',
|
|
||||||
defaultFilenames: {
|
|
||||||
ts: fixture('src/test.ts'),
|
|
||||||
tsx: 'n/a',
|
|
||||||
},
|
|
||||||
parserOptions: {
|
|
||||||
project: fixture('tsconfig.json'),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
class HtmlRuleTester extends RuleTester {
|
|
||||||
run(name: string, rule: any, tests: { valid: any[], invalid: any[] }) {
|
|
||||||
super.run(name, rule, {
|
|
||||||
valid: tests.valid.map((test) => ({
|
|
||||||
filename: fixture('test.html'),
|
|
||||||
...test,
|
|
||||||
})),
|
|
||||||
invalid: tests.invalid.map((test) => ({
|
|
||||||
filename: fixture('test.html'),
|
|
||||||
...test,
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const htmlRuleTester = new HtmlRuleTester({
|
|
||||||
parser: require.resolve('@angular-eslint/template-parser'),
|
|
||||||
});
|
|
@@ -1,24 +0,0 @@
|
|||||||
/**
|
|
||||||
* The contents of this file are subject to the license and copyright
|
|
||||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
|
||||||
* tree and available online at
|
|
||||||
*
|
|
||||||
* http://www.dspace.org/license/
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { themeableComponents } from '../src/util/theme-support';
|
|
||||||
|
|
||||||
describe('theme-support', () => {
|
|
||||||
describe('themeable component registry', () => {
|
|
||||||
it('should contain all themeable components from the fixture', () => {
|
|
||||||
expect(themeableComponents.entries.size).toBe(1);
|
|
||||||
expect(themeableComponents.byBasePath.size).toBe(1);
|
|
||||||
expect(themeableComponents.byWrapperPath.size).toBe(1);
|
|
||||||
expect(themeableComponents.byBaseClass.size).toBe(1);
|
|
||||||
|
|
||||||
expect(themeableComponents.byBaseClass.get('TestThemeableComponent')).toBeTruthy();
|
|
||||||
expect(themeableComponents.byBasePath.get('src/app/test/test-themeable.component.ts')).toBeTruthy();
|
|
||||||
expect(themeableComponents.byWrapperPath.get('src/app/test/themed-test-themeable.component.ts')).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,27 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2021",
|
|
||||||
"lib": [
|
|
||||||
"es2021"
|
|
||||||
],
|
|
||||||
"module": "nodenext",
|
|
||||||
"moduleResolution": "nodenext",
|
|
||||||
"noImplicitReturns": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"strict": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"sourceMap": true,
|
|
||||||
"allowSyntheticDefaultImports": true,
|
|
||||||
"types": [
|
|
||||||
"jasmine",
|
|
||||||
"node"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"include": [
|
|
||||||
"**/*.ts",
|
|
||||||
],
|
|
||||||
"exclude": [
|
|
||||||
"dist",
|
|
||||||
"test/fixture"
|
|
||||||
]
|
|
||||||
}
|
|
162
package.json
162
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dspace-angular",
|
"name": "dspace-angular",
|
||||||
"version": "8.1.0",
|
"version": "7.6.5",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"config:watch": "nodemon",
|
"config:watch": "nodemon",
|
||||||
@@ -16,16 +16,11 @@
|
|||||||
"build:stats": "ng build --stats-json",
|
"build:stats": "ng build --stats-json",
|
||||||
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
|
"build:prod": "cross-env NODE_ENV=production yarn run build:ssr",
|
||||||
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
"build:ssr": "ng build --configuration production && ng run dspace-angular:server:production",
|
||||||
"build:lint": "rimraf 'lint/dist/**/*.js' 'lint/dist/**/*.js.map' && tsc -b lint/tsconfig.json",
|
|
||||||
"test": "ng test --source-map=true --watch=false --configuration test",
|
"test": "ng test --source-map=true --watch=false --configuration test",
|
||||||
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
"test:watch": "nodemon --exec \"ng test --source-map=true --watch=true --configuration test\"",
|
||||||
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
"test:headless": "ng test --source-map=true --watch=false --configuration test --browsers=ChromeHeadless --code-coverage",
|
||||||
"test:lint": "yarn build:lint && yarn test:lint:nobuild",
|
"lint": "ng lint",
|
||||||
"test:lint:nobuild": "jasmine --config=lint/jasmine.json",
|
"lint-fix": "ng lint --fix=true",
|
||||||
"lint": "yarn build:lint && yarn lint:nobuild",
|
|
||||||
"lint:nobuild": "ng lint",
|
|
||||||
"lint-fix": "yarn build:lint && ng lint --fix=true",
|
|
||||||
"docs:lint": "ts-node --project ./lint/tsconfig.json ./lint/generate-docs.ts",
|
|
||||||
"e2e": "cross-env NODE_ENV=production ng e2e",
|
"e2e": "cross-env NODE_ENV=production ng e2e",
|
||||||
"clean:dev:config": "rimraf src/assets/config.json",
|
"clean:dev:config": "rimraf src/assets/config.json",
|
||||||
"clean:coverage": "rimraf coverage",
|
"clean:coverage": "rimraf coverage",
|
||||||
@@ -44,8 +39,7 @@
|
|||||||
"cypress:run": "cypress run",
|
"cypress:run": "cypress run",
|
||||||
"env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts",
|
"env:yaml": "ts-node --project ./tsconfig.ts-node.json scripts/env-to-yaml.ts",
|
||||||
"base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts",
|
"base-href": "ts-node --project ./tsconfig.ts-node.json scripts/base-href.ts",
|
||||||
"check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./",
|
"check-circ-deps": "npx madge --exclude '(bitstream|bundle|collection|config-submission-form|eperson|item|version)\\.model\\.ts$' --circular --extensions ts ./"
|
||||||
"postinstall": "yarn build:lint || echo 'Skipped DSpace ESLint plugins.'"
|
|
||||||
},
|
},
|
||||||
"browser": {
|
"browser": {
|
||||||
"fs": false,
|
"fs": false,
|
||||||
@@ -55,38 +49,39 @@
|
|||||||
},
|
},
|
||||||
"private": true,
|
"private": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^17.3.11",
|
"@angular/animations": "^15.2.10",
|
||||||
"@angular/cdk": "^17.3.10",
|
"@angular/cdk": "^15.2.9",
|
||||||
"@angular/common": "^17.3.11",
|
"@angular/common": "^15.2.10",
|
||||||
"@angular/compiler": "^17.3.11",
|
"@angular/compiler": "^15.2.10",
|
||||||
"@angular/core": "^17.3.11",
|
"@angular/core": "^15.2.10",
|
||||||
"@angular/forms": "^17.3.11",
|
"@angular/forms": "^15.2.10",
|
||||||
"@angular/localize": "17.3.12",
|
"@angular/localize": "15.2.10",
|
||||||
"@angular/platform-browser": "^17.3.11",
|
"@angular/platform-browser": "^15.2.10",
|
||||||
"@angular/platform-browser-dynamic": "^17.3.11",
|
"@angular/platform-browser-dynamic": "^15.2.10",
|
||||||
"@angular/platform-server": "^17.3.11",
|
"@angular/platform-server": "^15.2.10",
|
||||||
"@angular/router": "^17.3.11",
|
"@angular/router": "^15.2.10",
|
||||||
"@angular/ssr": "^17.3.11",
|
"@babel/runtime": "7.27.6",
|
||||||
"@babel/runtime": "7.26.0",
|
|
||||||
"@kolkov/ngx-gallery": "^2.0.1",
|
"@kolkov/ngx-gallery": "^2.0.1",
|
||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@ng-dynamic-forms/core": "^16.0.0",
|
"@ng-dynamic-forms/core": "^15.0.0",
|
||||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
"@ng-dynamic-forms/ui-ng-bootstrap": "^15.0.0",
|
||||||
"@ngrx/effects": "^17.1.1",
|
"@ngrx/effects": "^15.4.0",
|
||||||
"@ngrx/router-store": "^17.1.1",
|
"@ngrx/router-store": "^15.4.0",
|
||||||
"@ngrx/store": "^17.1.1",
|
"@ngrx/store": "^15.4.0",
|
||||||
|
"@nguniversal/express-engine": "^15.2.1",
|
||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
||||||
"angulartics2": "^12.2.0",
|
"angular-idle-preload": "3.0.0",
|
||||||
"axios": "^1.7.9",
|
"angulartics2": "^12.2.1",
|
||||||
|
"axios": "^1.10.0",
|
||||||
"bootstrap": "^4.6.1",
|
"bootstrap": "^4.6.1",
|
||||||
"cerialize": "0.1.18",
|
"cerialize": "0.1.18",
|
||||||
"cli-progress": "^3.12.0",
|
"cli-progress": "^3.12.0",
|
||||||
"colors": "^1.4.0",
|
"colors": "^1.4.0",
|
||||||
"compression": "^1.7.5",
|
"compression": "^1.8.1",
|
||||||
"cookie-parser": "1.4.7",
|
"cookie-parser": "1.4.7",
|
||||||
"core-js": "^3.40.0",
|
"core-js": "^3.42.0",
|
||||||
"date-fns": "^2.29.3",
|
"date-fns": "^2.30.0",
|
||||||
"date-fns-tz": "^1.3.7",
|
"date-fns-tz": "^1.3.7",
|
||||||
"deepmerge": "^4.3.1",
|
"deepmerge": "^4.3.1",
|
||||||
"ejs": "^3.1.10",
|
"ejs": "^3.1.10",
|
||||||
@@ -94,97 +89,94 @@
|
|||||||
"express-rate-limit": "^5.1.3",
|
"express-rate-limit": "^5.1.3",
|
||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"filesize": "^6.1.0",
|
"filesize": "^6.1.0",
|
||||||
"http-proxy-middleware": "^2.0.7",
|
"http-proxy-middleware": "^2.0.9",
|
||||||
"http-terminator": "^3.2.0",
|
"http-terminator": "^3.2.0",
|
||||||
"isbot": "^5.1.21",
|
"isbot": "^5.1.28",
|
||||||
"js-cookie": "2.2.1",
|
"js-cookie": "2.2.1",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"jsonschema": "1.5.0",
|
"jsonschema": "1.5.0",
|
||||||
"jwt-decode": "^3.1.2",
|
"jwt-decode": "^3.1.2",
|
||||||
"klaro": "^0.7.18",
|
"klaro": "^0.7.21",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"lru-cache": "^7.14.1",
|
"lru-cache": "^7.14.1",
|
||||||
"markdown-it": "^13.0.1",
|
"markdown-it": "^13.0.2",
|
||||||
|
"markdown-it-mathjax3": "^4.3.2",
|
||||||
"mirador": "^3.4.3",
|
"mirador": "^3.4.3",
|
||||||
"mirador-dl-plugin": "^0.13.0",
|
"mirador-dl-plugin": "^0.13.0",
|
||||||
"mirador-share-plugin": "^0.16.0",
|
"mirador-share-plugin": "^0.16.0",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.1",
|
||||||
"ng2-file-upload": "5.0.0",
|
"ng2-file-upload": "1.4.0",
|
||||||
"ng2-nouislider": "^2.0.0",
|
"ng2-nouislider": "^2.0.0",
|
||||||
"ngx-infinite-scroll": "^16.0.0",
|
"ngx-infinite-scroll": "^15.0.0",
|
||||||
"ngx-pagination": "6.0.3",
|
"ngx-pagination": "6.0.3",
|
||||||
"ngx-skeleton-loader": "^9.0.0",
|
"ngx-skeleton-loader": "^7.0.0",
|
||||||
|
"ngx-sortablejs": "^11.1.0",
|
||||||
"ngx-ui-switch": "^14.1.0",
|
"ngx-ui-switch": "^14.1.0",
|
||||||
"nouislider": "^15.7.1",
|
"nouislider": "^15.8.1",
|
||||||
"pem": "1.14.8",
|
"pem": "1.14.8",
|
||||||
"reflect-metadata": "^0.2.2",
|
"reflect-metadata": "^0.2.2",
|
||||||
"rxjs": "^7.8.0",
|
"rxjs": "^7.8.2",
|
||||||
|
"sanitize-html": "^2.17.0",
|
||||||
|
"sortablejs": "1.15.6",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"zone.js": "~0.15.0"
|
"zone.js": "~0.13.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "~17.0.2",
|
"@angular-builders/custom-webpack": "~15.0.0",
|
||||||
"@angular-devkit/build-angular": "^17.3.11",
|
"@angular-devkit/build-angular": "^15.2.11",
|
||||||
"@angular-eslint/builder": "17.5.3",
|
"@angular-eslint/builder": "15.2.1",
|
||||||
"@angular-eslint/bundled-angular-compiler": "17.5.3",
|
"@angular-eslint/eslint-plugin": "15.2.1",
|
||||||
"@angular-eslint/eslint-plugin": "17.5.3",
|
"@angular-eslint/eslint-plugin-template": "15.2.1",
|
||||||
"@angular-eslint/eslint-plugin-template": "17.5.3",
|
"@angular-eslint/schematics": "15.2.1",
|
||||||
"@angular-eslint/schematics": "17.5.3",
|
"@angular-eslint/template-parser": "15.2.1",
|
||||||
"@angular-eslint/template-parser": "17.5.3",
|
"@angular/cli": "^16.2.16",
|
||||||
"@angular/cli": "^17.3.11",
|
"@angular/compiler-cli": "^15.2.10",
|
||||||
"@angular/compiler-cli": "^17.3.11",
|
"@angular/language-service": "^15.2.10",
|
||||||
"@angular/language-service": "^17.3.11",
|
|
||||||
"@cypress/schematic": "^1.5.0",
|
"@cypress/schematic": "^1.5.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||||
"@material-ui/core": "^4.12.4",
|
"@material-ui/core": "^4.12.4",
|
||||||
"@material-ui/icons": "^4.11.3",
|
"@material-ui/icons": "^4.11.3",
|
||||||
"@ngrx/store-devtools": "^17.1.1",
|
"@ngrx/store-devtools": "^15.4.0",
|
||||||
"@ngtools/webpack": "^16.2.16",
|
"@ngtools/webpack": "^15.2.6",
|
||||||
|
"@nguniversal/builders": "^15.2.1",
|
||||||
"@types/deep-freeze": "0.1.5",
|
"@types/deep-freeze": "0.1.5",
|
||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.5",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.21",
|
||||||
"@types/grecaptcha": "^3.0.9",
|
"@types/grecaptcha": "^3.0.9",
|
||||||
"@types/jasmine": "~3.6.0",
|
"@types/jasmine": "~3.6.0",
|
||||||
"@types/js-cookie": "2.2.6",
|
"@types/js-cookie": "2.2.6",
|
||||||
"@types/lodash": "^4.17.14",
|
"@types/lodash": "^4.17.17",
|
||||||
"@types/node": "^14.14.9",
|
"@types/node": "^14.18.63",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.2.0",
|
"@types/sanitize-html": "^2.16.0",
|
||||||
"@typescript-eslint/parser": "^7.2.0",
|
"@typescript-eslint/eslint-plugin": "^5.62.0",
|
||||||
"@typescript-eslint/rule-tester": "^7.2.0",
|
"@typescript-eslint/parser": "^5.62.0",
|
||||||
"@typescript-eslint/utils": "^7.2.0",
|
"axe-core": "^4.10.3",
|
||||||
"axe-core": "^4.10.2",
|
|
||||||
"compression-webpack-plugin": "^9.2.0",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"csstype": "^3.1.3",
|
"csstype": "^3.1.3",
|
||||||
"cypress": "^13.17.0",
|
"cypress": "^13.17.0",
|
||||||
"cypress-axe": "^1.5.0",
|
"cypress-axe": "^1.6.0",
|
||||||
"deep-freeze": "0.0.1",
|
"deep-freeze": "0.0.1",
|
||||||
"eslint": "^8.39.0",
|
"eslint": "^8.39.0",
|
||||||
"eslint-plugin-deprecation": "^1.4.1",
|
"eslint-plugin-deprecation": "^1.5.0",
|
||||||
"eslint-plugin-dspace-angular-html": "link:./lint/dist/src/rules/html",
|
"eslint-plugin-import": "^2.32.0",
|
||||||
"eslint-plugin-dspace-angular-ts": "link:./lint/dist/src/rules/ts",
|
|
||||||
"eslint-plugin-import": "^2.31.0",
|
|
||||||
"eslint-plugin-import-newlines": "^1.3.1",
|
|
||||||
"eslint-plugin-jsdoc": "^45.0.0",
|
"eslint-plugin-jsdoc": "^45.0.0",
|
||||||
"eslint-plugin-jsonc": "^2.18.2",
|
"eslint-plugin-jsonc": "^2.20.1",
|
||||||
"eslint-plugin-lodash": "^7.4.0",
|
"eslint-plugin-lodash": "^7.4.0",
|
||||||
"eslint-plugin-rxjs": "^5.0.3",
|
"eslint-plugin-unused-imports": "^2.0.0",
|
||||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
|
||||||
"eslint-plugin-unused-imports": "^3.2.0",
|
|
||||||
"express-static-gzip": "^2.2.0",
|
"express-static-gzip": "^2.2.0",
|
||||||
"jasmine": "^3.8.0",
|
|
||||||
"jasmine-core": "^3.8.0",
|
"jasmine-core": "^3.8.0",
|
||||||
"jasmine-marbles": "0.9.2",
|
"jasmine-marbles": "0.9.2",
|
||||||
"karma": "^6.4.2",
|
"karma": "^6.4.4",
|
||||||
"karma-chrome-launcher": "~3.2.0",
|
"karma-chrome-launcher": "~3.2.0",
|
||||||
"karma-coverage-istanbul-reporter": "~3.0.3",
|
"karma-coverage-istanbul-reporter": "~3.0.3",
|
||||||
"karma-jasmine": "~4.0.0",
|
"karma-jasmine": "~4.0.0",
|
||||||
"karma-jasmine-html-reporter": "^1.5.0",
|
"karma-jasmine-html-reporter": "^1.5.0",
|
||||||
"karma-mocha-reporter": "2.2.5",
|
"karma-mocha-reporter": "2.2.5",
|
||||||
"ng-mocks": "^14.13.2",
|
"ng-mocks": "^14.13.5",
|
||||||
"ngx-mask": "14.2.4",
|
"ngx-mask": "^13.1.7",
|
||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"postcss": "^8.5",
|
"postcss": "^8.5",
|
||||||
"postcss-import": "^14.0.0",
|
"postcss-import": "^14.0.0",
|
||||||
@@ -195,13 +187,13 @@
|
|||||||
"react-copy-to-clipboard": "^5.1.0",
|
"react-copy-to-clipboard": "^5.1.0",
|
||||||
"react-dom": "^16.14.0",
|
"react-dom": "^16.14.0",
|
||||||
"rimraf": "^3.0.2",
|
"rimraf": "^3.0.2",
|
||||||
"sass": "~1.83.4",
|
"sass": "~1.89.2",
|
||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"sass-resources-loader": "^2.2.5",
|
"sass-resources-loader": "^2.2.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typescript": "~5.4.5",
|
"typescript": "~4.8.4",
|
||||||
"webpack": "5.97.1",
|
"webpack": "5.76.1",
|
||||||
"webpack-cli": "^5.1.4",
|
"webpack-cli": "^4.2.0",
|
||||||
"webpack-dev-server": "^4.15.1"
|
"webpack-dev-server": "^4.15.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -275,11 +275,9 @@ function readFileIfExists(pathToFile) {
|
|||||||
try {
|
try {
|
||||||
return fs.readFileSync(pathToFile, 'utf8');
|
return fs.readFileSync(pathToFile, 'utf8');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof Error) {
|
|
||||||
console.error('Error:', e.stack);
|
console.error('Error:', e.stack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
162
server.ts
162
server.ts
@@ -17,6 +17,7 @@
|
|||||||
|
|
||||||
import 'zone.js/node';
|
import 'zone.js/node';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
|
import 'rxjs';
|
||||||
|
|
||||||
/* eslint-disable import/no-namespace */
|
/* eslint-disable import/no-namespace */
|
||||||
import * as morgan from 'morgan';
|
import * as morgan from 'morgan';
|
||||||
@@ -38,26 +39,24 @@ import { join } from 'path';
|
|||||||
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
|
|
||||||
|
import { ngExpressEngine } from '@nguniversal/express-engine';
|
||||||
|
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
||||||
|
|
||||||
import { environment } from './src/environments/environment';
|
import { environment } from './src/environments/environment';
|
||||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
import { hasValue } from './src/app/shared/empty.util';
|
import { hasNoValue, hasValue } from './src/app/shared/empty.util';
|
||||||
|
|
||||||
import { UIServerConfig } from './src/config/ui-server-config.interface';
|
import { UIServerConfig } from './src/config/ui-server-config.interface';
|
||||||
import bootstrap from './src/main.server';
|
|
||||||
|
import { ServerAppModule } from './src/main.server';
|
||||||
|
|
||||||
import { buildAppConfig } from './src/config/config.server';
|
import { buildAppConfig } from './src/config/config.server';
|
||||||
import {
|
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
||||||
APP_CONFIG,
|
|
||||||
AppConfig,
|
|
||||||
} from './src/config/app-config.interface';
|
|
||||||
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||||
import { logStartupMessage } from './startup-message';
|
import { logStartupMessage } from './startup-message';
|
||||||
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
|
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
|
||||||
import { CommonEngine } from '@angular/ssr';
|
import { SsrExcludePatterns } from './src/config/universal-config.interface';
|
||||||
import { APP_BASE_HREF } from '@angular/common';
|
|
||||||
import {
|
|
||||||
REQUEST,
|
|
||||||
RESPONSE,
|
|
||||||
} from './src/express.tokens';
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
@@ -132,6 +131,28 @@ export function app() {
|
|||||||
*/
|
*/
|
||||||
server.use(json());
|
server.use(json());
|
||||||
|
|
||||||
|
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
||||||
|
server.engine('html', (_, options, callback) =>
|
||||||
|
ngExpressEngine({
|
||||||
|
bootstrap: ServerAppModule,
|
||||||
|
inlineCriticalCss: environment.universal.inlineCriticalCss,
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: REQUEST,
|
||||||
|
useValue: (options as any).req,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: RESPONSE,
|
||||||
|
useValue: (options as any).req.res,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
provide: APP_CONFIG,
|
||||||
|
useValue: environment
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})(_, (options as any), callback)
|
||||||
|
);
|
||||||
|
|
||||||
server.engine('ejs', ejs.renderFile);
|
server.engine('ejs', ejs.renderFile);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -146,7 +167,7 @@ export function app() {
|
|||||||
server.get('/robots.txt', (req, res) => {
|
server.get('/robots.txt', (req, res) => {
|
||||||
res.setHeader('content-type', 'text/plain');
|
res.setHeader('content-type', 'text/plain');
|
||||||
res.render('assets/robots.txt.ejs', {
|
res.render('assets/robots.txt.ejs', {
|
||||||
'origin': req.protocol + '://' + req.headers.host,
|
'origin': req.protocol + '://' + req.headers.host
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -161,7 +182,7 @@ export function app() {
|
|||||||
router.use('/sitemap**', createProxyMiddleware({
|
router.use('/sitemap**', createProxyMiddleware({
|
||||||
target: `${REST_BASE_URL}/sitemaps`,
|
target: `${REST_BASE_URL}/sitemaps`,
|
||||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||||
changeOrigin: true,
|
changeOrigin: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -170,7 +191,7 @@ export function app() {
|
|||||||
router.use('/signposting**', createProxyMiddleware({
|
router.use('/signposting**', createProxyMiddleware({
|
||||||
target: `${REST_BASE_URL}`,
|
target: `${REST_BASE_URL}`,
|
||||||
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
pathRewrite: path => path.replace(environment.ui.nameSpace, '/'),
|
||||||
changeOrigin: true,
|
changeOrigin: true
|
||||||
}));
|
}));
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,7 +202,7 @@ export function app() {
|
|||||||
const RateLimit = require('express-rate-limit');
|
const RateLimit = require('express-rate-limit');
|
||||||
const limiter = new RateLimit({
|
const limiter = new RateLimit({
|
||||||
windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs,
|
windowMs: (environment.ui as UIServerConfig).rateLimiter.windowMs,
|
||||||
max: (environment.ui as UIServerConfig).rateLimiter.max,
|
max: (environment.ui as UIServerConfig).rateLimiter.max
|
||||||
});
|
});
|
||||||
server.use(limiter);
|
server.use(limiter);
|
||||||
}
|
}
|
||||||
@@ -220,10 +241,10 @@ export function app() {
|
|||||||
/*
|
/*
|
||||||
* The callback function to serve server side angular
|
* The callback function to serve server side angular
|
||||||
*/
|
*/
|
||||||
function ngApp(req, res, next) {
|
function ngApp(req, res) {
|
||||||
if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || environment.ssr.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) {
|
if (environment.universal.preboot && req.method === 'GET' && (req.path === '/' || !isExcludedFromSsr(req.path, environment.universal.excludePathPatterns))) {
|
||||||
// Render the page to user via SSR (server side rendering)
|
// Render the page to user via SSR (server side rendering)
|
||||||
serverSideRender(req, res, next);
|
serverSideRender(req, res);
|
||||||
} else {
|
} else {
|
||||||
// If preboot is disabled, just serve the client
|
// If preboot is disabled, just serve the client
|
||||||
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
||||||
@@ -236,55 +257,35 @@ function ngApp(req, res, next) {
|
|||||||
* returned to the user.
|
* returned to the user.
|
||||||
* @param req current request
|
* @param req current request
|
||||||
* @param res current response
|
* @param res current response
|
||||||
* @param next the next function
|
|
||||||
* @param sendToUser if true (default), send the rendered content to the user.
|
* @param sendToUser if true (default), send the rendered content to the user.
|
||||||
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
||||||
*/
|
*/
|
||||||
function serverSideRender(req, res, next, sendToUser: boolean = true) {
|
function serverSideRender(req, res, sendToUser: boolean = true) {
|
||||||
const { protocol, originalUrl, baseUrl, headers } = req;
|
|
||||||
const commonEngine = new CommonEngine({ enablePerformanceProfiler: environment.ssr.enablePerformanceProfiler });
|
|
||||||
// Render the page via SSR (server side rendering)
|
// Render the page via SSR (server side rendering)
|
||||||
commonEngine
|
res.render(indexHtml, {
|
||||||
.render({
|
req,
|
||||||
bootstrap,
|
res,
|
||||||
documentFilePath: indexHtml,
|
preboot: environment.universal.preboot,
|
||||||
inlineCriticalCss: environment.ssr.inlineCriticalCss,
|
async: environment.universal.async,
|
||||||
url: `${protocol}://${headers.host}${originalUrl}`,
|
time: environment.universal.time,
|
||||||
publicPath: DIST_FOLDER,
|
baseUrl: environment.ui.nameSpace,
|
||||||
providers: [
|
originUrl: environment.ui.baseUrl,
|
||||||
{ provide: APP_BASE_HREF, useValue: baseUrl },
|
requestUrl: req.originalUrl,
|
||||||
{
|
}, (err, data) => {
|
||||||
provide: REQUEST,
|
if (hasNoValue(err) && hasValue(data)) {
|
||||||
useValue: req,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: RESPONSE,
|
|
||||||
useValue: res,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: APP_CONFIG,
|
|
||||||
useValue: environment,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})
|
|
||||||
.then((html) => {
|
|
||||||
if (hasValue(html)) {
|
|
||||||
// Replace REST URL with UI URL
|
// Replace REST URL with UI URL
|
||||||
if (environment.ssr.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
|
if (environment.universal.replaceRestUrl && REST_BASE_URL !== environment.rest.baseUrl) {
|
||||||
html = html.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
|
data = data.replace(new RegExp(REST_BASE_URL, 'g'), environment.rest.baseUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
// save server side rendered page to cache (if any are enabled)
|
// save server side rendered page to cache (if any are enabled)
|
||||||
saveToCache(req, html);
|
saveToCache(req, data);
|
||||||
if (sendToUser) {
|
if (sendToUser) {
|
||||||
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
||||||
// send rendered page to user
|
// send rendered page to user
|
||||||
res.send(html);
|
res.send(data);
|
||||||
}
|
}
|
||||||
}
|
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
|
||||||
// When this error occurs we can't fall back to CSR because the response has already been
|
// When this error occurs we can't fall back to CSR because the response has already been
|
||||||
// sent. These errors occur for various reasons in universal, not all of which are in our
|
// sent. These errors occur for various reasons in universal, not all of which are in our
|
||||||
// control to solve.
|
// control to solve.
|
||||||
@@ -299,7 +300,6 @@ function serverSideRender(req, res, next, sendToUser: boolean = true) {
|
|||||||
clientSideRender(req, res);
|
clientSideRender(req, res);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
next(err);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,7 +335,7 @@ function initCache() {
|
|||||||
botCache = new LRU( {
|
botCache = new LRU( {
|
||||||
max: environment.cache.serverSide.botCache.max,
|
max: environment.cache.serverSide.botCache.max,
|
||||||
ttl: environment.cache.serverSide.botCache.timeToLive,
|
ttl: environment.cache.serverSide.botCache.timeToLive,
|
||||||
allowStale: environment.cache.serverSide.botCache.allowStale,
|
allowStale: environment.cache.serverSide.botCache.allowStale
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,7 +347,7 @@ function initCache() {
|
|||||||
anonymousCache = new LRU( {
|
anonymousCache = new LRU( {
|
||||||
max: environment.cache.serverSide.anonymousCache.max,
|
max: environment.cache.serverSide.anonymousCache.max,
|
||||||
ttl: environment.cache.serverSide.anonymousCache.timeToLive,
|
ttl: environment.cache.serverSide.anonymousCache.timeToLive,
|
||||||
allowStale: environment.cache.serverSide.anonymousCache.allowStale,
|
allowStale: environment.cache.serverSide.anonymousCache.allowStale
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,7 +358,7 @@ function initCache() {
|
|||||||
function botCacheEnabled(): boolean {
|
function botCacheEnabled(): boolean {
|
||||||
// Caching is only enabled if SSR is enabled AND
|
// Caching is only enabled if SSR is enabled AND
|
||||||
// "max" pages to cache is greater than zero
|
// "max" pages to cache is greater than zero
|
||||||
return environment.ssr.enabled && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
|
return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,7 +367,7 @@ function botCacheEnabled(): boolean {
|
|||||||
function anonymousCacheEnabled(): boolean {
|
function anonymousCacheEnabled(): boolean {
|
||||||
// Caching is only enabled if SSR is enabled AND
|
// Caching is only enabled if SSR is enabled AND
|
||||||
// "max" pages to cache is greater than zero
|
// "max" pages to cache is greater than zero
|
||||||
return environment.ssr.enabled && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
|
return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -380,9 +380,9 @@ function cacheCheck(req, res, next) {
|
|||||||
|
|
||||||
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
|
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
|
||||||
if (botCacheEnabled() && isbot(req.get('user-agent'))) {
|
if (botCacheEnabled() && isbot(req.get('user-agent'))) {
|
||||||
cachedCopy = checkCacheForRequest('bot', botCache, req, res, next);
|
cachedCopy = checkCacheForRequest('bot', botCache, req, res);
|
||||||
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
|
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
|
||||||
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res, next);
|
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If cached copy exists, return it to the user.
|
// If cached copy exists, return it to the user.
|
||||||
@@ -418,15 +418,14 @@ function cacheCheck(req, res, next) {
|
|||||||
* @param cache LRU cache to check
|
* @param cache LRU cache to check
|
||||||
* @param req current request to look for in the cache
|
* @param req current request to look for in the cache
|
||||||
* @param res current response
|
* @param res current response
|
||||||
* @param next the next function
|
|
||||||
* @returns cached copy (if found) or undefined (if not found)
|
* @returns cached copy (if found) or undefined (if not found)
|
||||||
*/
|
*/
|
||||||
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res, next): any {
|
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res): any {
|
||||||
// Get the cache key for this request
|
// Get the cache key for this request
|
||||||
const key = getCacheKey(req);
|
const key = getCacheKey(req);
|
||||||
|
|
||||||
// Check if this page is in our cache
|
// Check if this page is in our cache
|
||||||
const cachedCopy = cache.get(key);
|
let cachedCopy = cache.get(key);
|
||||||
if (cachedCopy) {
|
if (cachedCopy) {
|
||||||
if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); }
|
if (environment.cache.serverSide.debug) { console.log(`CACHE HIT FOR ${key} in ${cacheName} cache`); }
|
||||||
|
|
||||||
@@ -437,7 +436,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, r
|
|||||||
// Update cached copy by rerendering server-side
|
// Update cached copy by rerendering server-side
|
||||||
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
||||||
// This re-render is peformed behind the scenes to update cached copy for next user.
|
// This re-render is peformed behind the scenes to update cached copy for next user.
|
||||||
serverSideRender(req, res, next, false);
|
serverSideRender(req, res, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
|
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
|
||||||
@@ -540,13 +539,13 @@ function serverStarted() {
|
|||||||
function createHttpsServer(keys) {
|
function createHttpsServer(keys) {
|
||||||
const listener = createServer({
|
const listener = createServer({
|
||||||
key: keys.serviceKey,
|
key: keys.serviceKey,
|
||||||
cert: keys.certificate,
|
cert: keys.certificate
|
||||||
}, app()).listen(environment.ui.port, environment.ui.host, () => {
|
}, app).listen(environment.ui.port, environment.ui.host, () => {
|
||||||
serverStarted();
|
serverStarted();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Graceful shutdown when signalled
|
// Graceful shutdown when signalled
|
||||||
const terminator = createHttpTerminator({ server: listener });
|
const terminator = createHttpTerminator({server: listener});
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
void (async ()=> {
|
void (async ()=> {
|
||||||
console.debug('Closing HTTPS server on signal');
|
console.debug('Closing HTTPS server on signal');
|
||||||
@@ -570,7 +569,7 @@ function run() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Graceful shutdown when signalled
|
// Graceful shutdown when signalled
|
||||||
const terminator = createHttpTerminator({ server: listener });
|
const terminator = createHttpTerminator({server: listener});
|
||||||
process.on('SIGINT', () => {
|
process.on('SIGINT', () => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
console.debug('Closing HTTP server on signal');
|
console.debug('Closing HTTP server on signal');
|
||||||
@@ -608,7 +607,7 @@ function start() {
|
|||||||
if (serviceKey && certificate) {
|
if (serviceKey && certificate) {
|
||||||
createHttpsServer({
|
createHttpsServer({
|
||||||
serviceKey: serviceKey,
|
serviceKey: serviceKey,
|
||||||
certificate: certificate,
|
certificate: certificate
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.');
|
console.warn('Disabling certificate validation and proceeding with a self-signed certificate. If this is a production server, it is recommended that you configure a valid certificate instead.');
|
||||||
@@ -617,7 +616,7 @@ function start() {
|
|||||||
|
|
||||||
createCertificate({
|
createCertificate({
|
||||||
days: 1,
|
days: 1,
|
||||||
selfSigned: true,
|
selfSigned: true
|
||||||
}, (error, keys) => {
|
}, (error, keys) => {
|
||||||
createHttpsServer(keys);
|
createHttpsServer(keys);
|
||||||
});
|
});
|
||||||
@@ -627,6 +626,21 @@ function start() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if SSR should be skipped for path
|
||||||
|
*
|
||||||
|
* @param path
|
||||||
|
* @param excludePathPattern
|
||||||
|
*/
|
||||||
|
function isExcludedFromSsr(path: string, excludePathPattern: SsrExcludePatterns[]): boolean {
|
||||||
|
const patterns = excludePathPattern.map(p =>
|
||||||
|
new RegExp(p.pattern, p.flag || '')
|
||||||
|
);
|
||||||
|
return patterns.some((regex) => {
|
||||||
|
return regex.test(path)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* The callback function to serve health check requests
|
* The callback function to serve health check requests
|
||||||
*/
|
*/
|
||||||
@@ -638,7 +652,7 @@ function healthCheck(req, res) {
|
|||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
res.status(error.response.status).send({
|
res.status(error.response.status).send({
|
||||||
error: error.message,
|
error: error.message
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -1,114 +0,0 @@
|
|||||||
import { AbstractControl } from '@angular/forms';
|
|
||||||
import { Route } from '@angular/router';
|
|
||||||
import {
|
|
||||||
DYNAMIC_ERROR_MESSAGES_MATCHER,
|
|
||||||
DynamicErrorMessagesMatcher,
|
|
||||||
} from '@ng-dynamic-forms/core';
|
|
||||||
|
|
||||||
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
|
||||||
import { groupAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
|
||||||
import { siteAdministratorGuard } from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
|
||||||
import {
|
|
||||||
EPERSON_PATH,
|
|
||||||
GROUP_PATH,
|
|
||||||
} from './access-control-routing-paths';
|
|
||||||
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
|
||||||
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
|
||||||
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
|
||||||
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
|
|
||||||
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
|
||||||
import { groupPageGuard } from './group-registry/group-page.guard';
|
|
||||||
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Condition for displaying error messages on email form field
|
|
||||||
*/
|
|
||||||
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
|
||||||
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
|
||||||
return ( control.touched && !hasFocus ) || ( control.errors?.emailTaken && hasFocus );
|
|
||||||
};
|
|
||||||
|
|
||||||
const providers = [
|
|
||||||
{
|
|
||||||
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
|
||||||
useValue: ValidateEmailErrorStateMatcher,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
export const ROUTES: Route[] = [
|
|
||||||
{
|
|
||||||
path: EPERSON_PATH,
|
|
||||||
component: EPeopleRegistryComponent,
|
|
||||||
resolve: {
|
|
||||||
breadcrumb: i18nBreadcrumbResolver,
|
|
||||||
},
|
|
||||||
providers,
|
|
||||||
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
|
|
||||||
canActivate: [siteAdministratorGuard],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${EPERSON_PATH}/create`,
|
|
||||||
component: EPersonFormComponent,
|
|
||||||
resolve: {
|
|
||||||
breadcrumb: i18nBreadcrumbResolver,
|
|
||||||
},
|
|
||||||
providers,
|
|
||||||
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
|
|
||||||
canActivate: [siteAdministratorGuard],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${EPERSON_PATH}/:id/edit`,
|
|
||||||
component: EPersonFormComponent,
|
|
||||||
resolve: {
|
|
||||||
breadcrumb: i18nBreadcrumbResolver,
|
|
||||||
ePerson: EPersonResolver,
|
|
||||||
},
|
|
||||||
providers,
|
|
||||||
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
|
|
||||||
canActivate: [siteAdministratorGuard],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: GROUP_PATH,
|
|
||||||
component: GroupsRegistryComponent,
|
|
||||||
resolve: {
|
|
||||||
breadcrumb: i18nBreadcrumbResolver,
|
|
||||||
},
|
|
||||||
providers,
|
|
||||||
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
|
|
||||||
canActivate: [groupAdministratorGuard],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${GROUP_PATH}/create`,
|
|
||||||
component: GroupFormComponent,
|
|
||||||
resolve: {
|
|
||||||
breadcrumb: i18nBreadcrumbResolver,
|
|
||||||
},
|
|
||||||
providers,
|
|
||||||
data: {
|
|
||||||
title: 'admin.access-control.groups.title.addGroup',
|
|
||||||
breadcrumbKey: 'admin.access-control.groups.addGroup',
|
|
||||||
},
|
|
||||||
canActivate: [groupAdministratorGuard],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: `${GROUP_PATH}/:groupId/edit`,
|
|
||||||
component: GroupFormComponent,
|
|
||||||
resolve: {
|
|
||||||
breadcrumb: i18nBreadcrumbResolver,
|
|
||||||
},
|
|
||||||
providers,
|
|
||||||
data: {
|
|
||||||
title: 'admin.access-control.groups.title.singleGroup',
|
|
||||||
breadcrumbKey: 'admin.access-control.groups.singleGroup',
|
|
||||||
},
|
|
||||||
canActivate: [groupPageGuard],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
path: 'bulk-access',
|
|
||||||
component: BulkAccessComponent,
|
|
||||||
resolve: {
|
|
||||||
breadcrumb: i18nBreadcrumbResolver,
|
|
||||||
},
|
|
||||||
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
|
|
||||||
canActivate: [siteAdministratorGuard],
|
|
||||||
},
|
|
||||||
];
|
|
@@ -1,5 +1,5 @@
|
|||||||
import { getAccessControlModuleRoute } from '../app-routing-paths';
|
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
|
import { getAccessControlModuleRoute } from '../app-routing-paths';
|
||||||
|
|
||||||
export const EPERSON_PATH = 'epeople';
|
export const EPERSON_PATH = 'epeople';
|
||||||
|
|
||||||
|
94
src/app/access-control/access-control-routing.module.ts
Normal file
94
src/app/access-control/access-control-routing.module.ts
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||||
|
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||||
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
|
import { EPERSON_PATH, GROUP_PATH } from './access-control-routing-paths';
|
||||||
|
import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
|
import { GroupPageGuard } from './group-registry/group-page.guard';
|
||||||
|
import {
|
||||||
|
GroupAdministratorGuard
|
||||||
|
} from '../core/data/feature-authorization/feature-authorization-guard/group-administrator.guard';
|
||||||
|
import {
|
||||||
|
SiteAdministratorGuard
|
||||||
|
} from '../core/data/feature-authorization/feature-authorization-guard/site-administrator.guard';
|
||||||
|
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
||||||
|
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
||||||
|
import { EPersonResolver } from './epeople-registry/eperson-resolver.service';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
RouterModule.forChild([
|
||||||
|
{
|
||||||
|
path: EPERSON_PATH,
|
||||||
|
component: EPeopleRegistryComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.epeople.title', breadcrumbKey: 'admin.access-control.epeople' },
|
||||||
|
canActivate: [SiteAdministratorGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${EPERSON_PATH}/create`,
|
||||||
|
component: EPersonFormComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.epeople.add.title', breadcrumbKey: 'admin.access-control.epeople.add' },
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${EPERSON_PATH}/:id/edit`,
|
||||||
|
component: EPersonFormComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver,
|
||||||
|
ePerson: EPersonResolver,
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.epeople.edit.title', breadcrumbKey: 'admin.access-control.epeople.edit' },
|
||||||
|
canActivate: [SiteAdministratorGuard],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: GROUP_PATH,
|
||||||
|
component: GroupsRegistryComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.groups.title', breadcrumbKey: 'admin.access-control.groups' },
|
||||||
|
canActivate: [GroupAdministratorGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${GROUP_PATH}/create`,
|
||||||
|
component: GroupFormComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.groups.title.addGroup', breadcrumbKey: 'admin.access-control.groups.addGroup' },
|
||||||
|
canActivate: [GroupAdministratorGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: `${GROUP_PATH}/:groupId/edit`,
|
||||||
|
component: GroupFormComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.groups.title.singleGroup', breadcrumbKey: 'admin.access-control.groups.singleGroup' },
|
||||||
|
canActivate: [GroupPageGuard]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: 'bulk-access',
|
||||||
|
component: BulkAccessComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: I18nBreadcrumbResolver
|
||||||
|
},
|
||||||
|
data: { title: 'admin.access-control.bulk-access.title', breadcrumbKey: 'admin.access-control.bulk-access' },
|
||||||
|
canActivate: [SiteAdministratorGuard]
|
||||||
|
},
|
||||||
|
])
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Routing module for the AccessControl section of the admin sidebar
|
||||||
|
*/
|
||||||
|
export class AccessControlRoutingModule {
|
||||||
|
|
||||||
|
}
|
67
src/app/access-control/access-control.module.ts
Normal file
67
src/app/access-control/access-control.module.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NgModule } from '@angular/core';
|
||||||
|
import { RouterModule } from '@angular/router';
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { AccessControlRoutingModule } from './access-control-routing.module';
|
||||||
|
import { EPeopleRegistryComponent } from './epeople-registry/epeople-registry.component';
|
||||||
|
import { EPersonFormComponent } from './epeople-registry/eperson-form/eperson-form.component';
|
||||||
|
import { GroupFormComponent } from './group-registry/group-form/group-form.component';
|
||||||
|
import { MembersListComponent } from './group-registry/group-form/members-list/members-list.component';
|
||||||
|
import { SubgroupsListComponent } from './group-registry/group-form/subgroup-list/subgroups-list.component';
|
||||||
|
import { GroupsRegistryComponent } from './group-registry/groups-registry.component';
|
||||||
|
import { FormModule } from '../shared/form/form.module';
|
||||||
|
import { DYNAMIC_ERROR_MESSAGES_MATCHER, DynamicErrorMessagesMatcher } from '@ng-dynamic-forms/core';
|
||||||
|
import { AbstractControl } from '@angular/forms';
|
||||||
|
import { BulkAccessComponent } from './bulk-access/bulk-access.component';
|
||||||
|
import { NgbAccordionModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { BulkAccessBrowseComponent } from './bulk-access/browse/bulk-access-browse.component';
|
||||||
|
import { BulkAccessSettingsComponent } from './bulk-access/settings/bulk-access-settings.component';
|
||||||
|
import { SearchModule } from '../shared/search/search.module';
|
||||||
|
import { AccessControlFormModule } from '../shared/access-control-form-container/access-control-form.module';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Condition for displaying error messages on email form field
|
||||||
|
*/
|
||||||
|
export const ValidateEmailErrorStateMatcher: DynamicErrorMessagesMatcher =
|
||||||
|
(control: AbstractControl, model: any, hasFocus: boolean) => {
|
||||||
|
return (control.touched && !hasFocus) || (control.errors?.emailTaken && hasFocus);
|
||||||
|
};
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
SharedModule,
|
||||||
|
RouterModule,
|
||||||
|
AccessControlRoutingModule,
|
||||||
|
FormModule,
|
||||||
|
NgbAccordionModule,
|
||||||
|
SearchModule,
|
||||||
|
AccessControlFormModule,
|
||||||
|
],
|
||||||
|
exports: [
|
||||||
|
MembersListComponent,
|
||||||
|
],
|
||||||
|
declarations: [
|
||||||
|
EPeopleRegistryComponent,
|
||||||
|
EPersonFormComponent,
|
||||||
|
GroupsRegistryComponent,
|
||||||
|
GroupFormComponent,
|
||||||
|
SubgroupsListComponent,
|
||||||
|
MembersListComponent,
|
||||||
|
BulkAccessComponent,
|
||||||
|
BulkAccessBrowseComponent,
|
||||||
|
BulkAccessSettingsComponent,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: DYNAMIC_ERROR_MESSAGES_MATCHER,
|
||||||
|
useValue: ValidateEmailErrorStateMatcher
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* This module handles all components related to the access control pages
|
||||||
|
*/
|
||||||
|
export class AccessControlModule {
|
||||||
|
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user