mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Compare commits
417 Commits
dspace-9.0
...
dspace-9.1
Author | SHA1 | Date | |
---|---|---|---|
![]() |
2bf9e11a7c | ||
![]() |
f0cba80262 | ||
![]() |
ad51dc884d | ||
![]() |
28a914a335 | ||
![]() |
3cd4959d8f | ||
![]() |
5888277ca7 | ||
![]() |
e258fb7af7 | ||
![]() |
cbb01c514d | ||
![]() |
1bc47d06b7 | ||
![]() |
d0f61c1e50 | ||
![]() |
44c7e4b524 | ||
![]() |
598f5884e4 | ||
![]() |
e3b5405698 | ||
![]() |
b4d694e0c0 | ||
![]() |
36c19ef976 | ||
![]() |
8685e1398b | ||
![]() |
4956d26f69 | ||
![]() |
5c419bd975 | ||
![]() |
c253cff188 | ||
![]() |
9be1562083 | ||
![]() |
45934e31f2 | ||
![]() |
ff6d0ddc9f | ||
![]() |
e017f99125 | ||
![]() |
945f1bcec3 | ||
![]() |
cecb8f6c69 | ||
![]() |
b26b14b764 | ||
![]() |
2ed43035eb | ||
![]() |
d3b098b407 | ||
![]() |
f506be3dea | ||
![]() |
29e8c29ade | ||
![]() |
3acdb3124e | ||
![]() |
d0464c4886 | ||
![]() |
036dba942d | ||
![]() |
6c3bbf617c | ||
![]() |
c147354ee1 | ||
![]() |
343718d07b | ||
![]() |
f8c6a80fbe | ||
![]() |
a009f3549a | ||
![]() |
85128a4cfd | ||
![]() |
c9e6a65eec | ||
![]() |
790e7d580b | ||
![]() |
eb8f35f9f1 | ||
![]() |
f798b30c06 | ||
![]() |
8fb625ed73 | ||
![]() |
ba3c696c47 | ||
![]() |
d7d598e972 | ||
![]() |
0a6550986b | ||
![]() |
36878b119a | ||
![]() |
e7b85eee5b | ||
![]() |
c81ddf1520 | ||
![]() |
f626e0baf9 | ||
![]() |
fc2c32e1c6 | ||
![]() |
e359fb9169 | ||
![]() |
c947f49820 | ||
![]() |
6ac6ac8998 | ||
![]() |
b726688b47 | ||
![]() |
d87b83f9e9 | ||
![]() |
bcc437d029 | ||
![]() |
269404cb4f | ||
![]() |
120817f5f4 | ||
![]() |
c8a136f0d5 | ||
![]() |
bcd2081ee3 | ||
![]() |
e623b68d91 | ||
![]() |
814b411192 | ||
![]() |
f77dfa37da | ||
![]() |
04964787fa | ||
![]() |
20748d2f64 | ||
![]() |
b5a29fa400 | ||
![]() |
2fffda3a28 | ||
![]() |
537fb4bfda | ||
![]() |
4ab895f821 | ||
![]() |
880b9e4130 | ||
![]() |
d965968fc9 | ||
![]() |
3f02846fed | ||
![]() |
a36e085920 | ||
![]() |
443809f3b2 | ||
![]() |
e1f3cc09cb | ||
![]() |
f14cd51678 | ||
![]() |
f0a00aca95 | ||
![]() |
aed0460cfe | ||
![]() |
6017537107 | ||
![]() |
c6657b5f9b | ||
![]() |
2d8fada3c4 | ||
![]() |
eb0572640b | ||
![]() |
598471913e | ||
![]() |
8ff5a23c40 | ||
![]() |
01c8c60624 | ||
![]() |
4200357100 | ||
![]() |
39fec7ce64 | ||
![]() |
f10447b8d3 | ||
![]() |
abb03799e0 | ||
![]() |
d4aedbbb51 | ||
![]() |
d1beb28de0 | ||
![]() |
e3152c94e0 | ||
![]() |
e9b8b25da5 | ||
![]() |
5cdc72ddbf | ||
![]() |
be3dbd604b | ||
![]() |
f277832dc1 | ||
![]() |
ba3c43ac9a | ||
![]() |
520831bd33 | ||
![]() |
6703a07207 | ||
![]() |
056351ddfe | ||
![]() |
87e4a33621 | ||
![]() |
9be8a87a57 | ||
![]() |
15f96f3c82 | ||
![]() |
98da22047a | ||
![]() |
887bf0d266 | ||
![]() |
f1a1aebe33 | ||
![]() |
ebd69c04e8 | ||
![]() |
aba3a9439d | ||
![]() |
63c2ac0a96 | ||
![]() |
96d2b66068 | ||
![]() |
d32681bbe9 | ||
![]() |
dfc1ecef25 | ||
![]() |
cef7cae573 | ||
![]() |
be85947fb6 | ||
![]() |
830be1f15a | ||
![]() |
8ff943b084 | ||
![]() |
545b2ff8a7 | ||
![]() |
163cc75437 | ||
![]() |
e9940f4005 | ||
![]() |
8c8ff6189b | ||
![]() |
788f173066 | ||
![]() |
daaa8109d6 | ||
![]() |
7ec20edbc0 | ||
![]() |
80c38f0c3d | ||
![]() |
6a138eccc6 | ||
![]() |
1fe9065fca | ||
![]() |
c442d35505 | ||
![]() |
7da8f92b15 | ||
![]() |
de66aa294f | ||
![]() |
5f53a7c966 | ||
![]() |
6115e673cd | ||
![]() |
5b18304e1c | ||
![]() |
a51cc5485c | ||
![]() |
512d2e215f | ||
![]() |
3adb81e2c6 | ||
![]() |
0ae73c7131 | ||
![]() |
9926121912 | ||
![]() |
c1524c0713 | ||
![]() |
dd4aa5ac80 | ||
![]() |
a9dff09300 | ||
![]() |
37cef0fbae | ||
![]() |
e28d212789 | ||
![]() |
d5c9e694c3 | ||
![]() |
73bd0d67ea | ||
![]() |
c0a5420e8b | ||
![]() |
bec91c5b8d | ||
![]() |
484befafc3 | ||
![]() |
7a49802444 | ||
![]() |
12a3b4f846 | ||
![]() |
9e8c0dc262 | ||
![]() |
da6ace1882 | ||
![]() |
dec05e5cfe | ||
![]() |
4c9638150a | ||
![]() |
b1191117fc | ||
![]() |
fd23044005 | ||
![]() |
c2af23b3df | ||
![]() |
8757712905 | ||
![]() |
149feee93a | ||
![]() |
b15e9d74d6 | ||
![]() |
f5ce6301e0 | ||
![]() |
c222c446b5 | ||
![]() |
a84365996b | ||
![]() |
a7bcddf597 | ||
![]() |
81fb2e1cbf | ||
![]() |
3d32715d25 | ||
![]() |
25d6809e1c | ||
![]() |
bcd9bfd607 | ||
![]() |
3a419b733a | ||
![]() |
ea265075c8 | ||
![]() |
d423f82324 | ||
![]() |
79d03d0433 | ||
![]() |
bc3d3e4129 | ||
![]() |
9d3b298f1a | ||
![]() |
0180b633e2 | ||
![]() |
f1444b6edb | ||
![]() |
e9061a46b6 | ||
![]() |
052e1c39ab | ||
![]() |
19d438040b | ||
![]() |
4d74d2c6dc | ||
![]() |
e40a697dbd | ||
![]() |
c3ba7ef51a | ||
![]() |
6d7d3572a2 | ||
![]() |
e7359a3fd2 | ||
![]() |
46a89533f0 | ||
![]() |
f7b77a1c3f | ||
![]() |
2bdb996e1a | ||
![]() |
56e45a9f08 | ||
![]() |
62f15668b6 | ||
![]() |
6c1212de4e | ||
![]() |
b1c5460bbb | ||
![]() |
c68e5a181d | ||
![]() |
0574c8ed98 | ||
![]() |
70c6af3630 | ||
![]() |
4b0ab8161f | ||
![]() |
9a24fb2b64 | ||
![]() |
df9c950f1f | ||
![]() |
af3d68ff71 | ||
![]() |
8c1f50b4d2 | ||
![]() |
ca22bf327a | ||
![]() |
5eda4db743 | ||
![]() |
a41a2441df | ||
![]() |
f44599a3c9 | ||
![]() |
3370bda789 | ||
![]() |
19078a18fc | ||
![]() |
f9b58ecd06 | ||
![]() |
dc8b10593c | ||
![]() |
7b9cd73ee0 | ||
![]() |
41afcf9cf6 | ||
![]() |
d3b48f4ea4 | ||
![]() |
0aea4a7666 | ||
![]() |
4c26359e29 | ||
![]() |
5719f06dd1 | ||
![]() |
67e91e370a | ||
![]() |
aacedd2db6 | ||
![]() |
ea6f640820 | ||
![]() |
00143d29e7 | ||
![]() |
f16a03e226 | ||
![]() |
ad7aa36f88 | ||
![]() |
6232d4e9cf | ||
![]() |
a6c14e50c2 | ||
![]() |
c1bd65e8c6 | ||
![]() |
3c1d514807 | ||
![]() |
6cd88ec57b | ||
![]() |
28f865eb2e | ||
![]() |
f107573bc1 | ||
![]() |
557879941b | ||
![]() |
0dabc8ed8f | ||
![]() |
62822ade31 | ||
![]() |
a16f1e8248 | ||
![]() |
b123e86760 | ||
![]() |
d664f1f94a | ||
![]() |
0b02dbbb67 | ||
![]() |
16e4ae5583 | ||
![]() |
cace253d8b | ||
![]() |
97a4c3c0c0 | ||
![]() |
72a050c7ae | ||
![]() |
9dc96e7b33 | ||
![]() |
99e8c1044c | ||
![]() |
33d75cf217 | ||
![]() |
287d35cb26 | ||
![]() |
c5d203bbd6 | ||
![]() |
e4d53eddcc | ||
![]() |
25f474386b | ||
![]() |
3d8951db18 | ||
![]() |
d6875117b0 | ||
![]() |
b64f69105e | ||
![]() |
353d660f23 | ||
![]() |
80dd89da56 | ||
![]() |
fa96c8c5ab | ||
![]() |
ff3784ab5f | ||
![]() |
8d26b1509f | ||
![]() |
54ed550f4a | ||
![]() |
11f251755b | ||
![]() |
b00f489d09 | ||
![]() |
4e5b344ce8 | ||
![]() |
b1f0ed201a | ||
![]() |
f8cfb74555 | ||
![]() |
e3c6ad807b | ||
![]() |
818f60b4d7 | ||
![]() |
6f878e5c4a | ||
![]() |
3a347d83b5 | ||
![]() |
b89a626ffc | ||
![]() |
fef16bca14 | ||
![]() |
bb536192c2 | ||
![]() |
00d7e1f97c | ||
![]() |
4f48f39f7b | ||
![]() |
7998d2d8b9 | ||
![]() |
5fc83782b1 | ||
![]() |
7269f6c36a | ||
![]() |
583bed6164 | ||
![]() |
056e923ae8 | ||
![]() |
856d701b3e | ||
![]() |
7845d37b2e | ||
![]() |
1b112dd887 | ||
![]() |
e84af6ab91 | ||
![]() |
bde54ca6fa | ||
![]() |
4b6ef50611 | ||
![]() |
8140ed29e7 | ||
![]() |
d269d668e5 | ||
![]() |
b076738b53 | ||
![]() |
09e6452a48 | ||
![]() |
03bcdc37ce | ||
![]() |
8734a07592 | ||
![]() |
548fbf54cf | ||
![]() |
050c007e44 | ||
![]() |
5346679880 | ||
![]() |
fad41b2ddf | ||
![]() |
fcac66caab | ||
![]() |
11493914a7 | ||
![]() |
e14ed8804c | ||
![]() |
8abd71ab73 | ||
![]() |
41c4225c0f | ||
![]() |
d8896f78b8 | ||
![]() |
3d167acecb | ||
![]() |
9b5d008d66 | ||
![]() |
5ef0a57438 | ||
![]() |
f01ccbdae2 | ||
![]() |
6001652101 | ||
![]() |
6a49cdb54b | ||
![]() |
b9e2ec15ed | ||
![]() |
9896eab6ac | ||
![]() |
5e8571de80 | ||
![]() |
452493a828 | ||
![]() |
3a05733256 | ||
![]() |
71d7a7b659 | ||
![]() |
9f19b481cf | ||
![]() |
f95596ba02 | ||
![]() |
bd2b5d3bc0 | ||
![]() |
fd2e9a49ee | ||
![]() |
8f95d7b5b3 | ||
![]() |
67c2b33bb0 | ||
![]() |
237f183a18 | ||
![]() |
0d78cd57b3 | ||
![]() |
5bdf0974fc | ||
![]() |
7864909466 | ||
![]() |
d3130bd433 | ||
![]() |
fd9d899574 | ||
![]() |
42c52d619b | ||
![]() |
ef57754e6d | ||
![]() |
0989e28c38 | ||
![]() |
45a3c11dfc | ||
![]() |
502597aadc | ||
![]() |
2fc84783a5 | ||
![]() |
fa8ffaefd0 | ||
![]() |
cadd1520ee | ||
![]() |
93fff41b8d | ||
![]() |
d3c0ef501c | ||
![]() |
33062214da | ||
![]() |
99c5506d7d | ||
![]() |
94dd8e7206 | ||
![]() |
88d59a2152 | ||
![]() |
da2c98aa32 | ||
![]() |
681efd9f52 | ||
![]() |
ef8735e5a2 | ||
![]() |
639bb3e558 | ||
![]() |
a58456deab | ||
![]() |
0567ea7a2d | ||
![]() |
a2882b0644 | ||
![]() |
4cea196eb2 | ||
![]() |
082018d44d | ||
![]() |
122d31b11b | ||
![]() |
7ab598d571 | ||
![]() |
bd06eded8c | ||
![]() |
13ae42f217 | ||
![]() |
c835b215af | ||
![]() |
c0bfc3a739 | ||
![]() |
261af2a2f5 | ||
![]() |
c9b2717344 | ||
![]() |
958b3b8c05 | ||
![]() |
cdbfcc7cc4 | ||
![]() |
112eb7b3ce | ||
![]() |
08fe940e70 | ||
![]() |
dcd32c48c3 | ||
![]() |
cd825ac0da | ||
![]() |
ca80812d0a | ||
![]() |
4a74a3ac99 | ||
![]() |
62904747fc | ||
![]() |
4c30e2d0be | ||
![]() |
d05096c015 | ||
![]() |
858ec87f2f | ||
![]() |
b837f63b7c | ||
![]() |
e838873235 | ||
![]() |
80948d98f0 | ||
![]() |
1e73fa626d | ||
![]() |
59cb19ae80 | ||
![]() |
8eaff78737 | ||
![]() |
daf2dc40d9 | ||
![]() |
e1b773c097 | ||
![]() |
2a39bd831b | ||
![]() |
b69b21af6c | ||
![]() |
1b83f18960 | ||
![]() |
58ff240c05 | ||
![]() |
888459cb56 | ||
![]() |
f0ff625b4b | ||
![]() |
b55a318676 | ||
![]() |
fa723c17a9 | ||
![]() |
4ffde928d4 | ||
![]() |
9406f7b085 | ||
![]() |
cee9d0422b | ||
![]() |
829d406808 | ||
![]() |
e06db4cbab | ||
![]() |
ff7c9ba955 | ||
![]() |
c71c6667e0 | ||
![]() |
010b2f9693 | ||
![]() |
edfaee6aab | ||
![]() |
cdec4880d2 | ||
![]() |
c38352ed22 | ||
![]() |
deb4a63c88 | ||
![]() |
ec016e80fb | ||
![]() |
ced163a25f | ||
![]() |
5a28e66b2f | ||
![]() |
9ac92e0196 | ||
![]() |
287d028331 | ||
![]() |
cae13942e2 | ||
![]() |
b16cec631d | ||
![]() |
dc8a699e94 | ||
![]() |
fdfa6e2c06 | ||
![]() |
ecb00a95a0 | ||
![]() |
fe90d39943 | ||
![]() |
bb7f0cd3a5 | ||
![]() |
ed0fcd982c | ||
![]() |
04515591e2 | ||
![]() |
37455a8b6c | ||
![]() |
82fd9539b7 | ||
![]() |
52eabec70d | ||
![]() |
cad086c945 | ||
![]() |
6a49df59af | ||
![]() |
d224a2c47d | ||
![]() |
b72ce73931 | ||
![]() |
8f708d0e28 | ||
![]() |
3cd5432034 | ||
![]() |
49b329edb1 | ||
![]() |
c4dfed0e02 | ||
![]() |
27eeefae5f | ||
![]() |
286de02158 |
@@ -15,6 +15,10 @@ trim_trailing_whitespace = false
|
||||
|
||||
[*.ts]
|
||||
quote_type = single
|
||||
ij_typescript_enforce_trailing_comma = whenmultiline
|
||||
|
||||
[*.js]
|
||||
ij_javascript_enforce_trailing_comma = whenmultiline
|
||||
|
||||
[*.json5]
|
||||
ij_json_keep_blank_lines_in_code = 3
|
||||
|
@@ -12,7 +12,6 @@
|
||||
"eslint-plugin-rxjs",
|
||||
"eslint-plugin-simple-import-sort",
|
||||
"eslint-plugin-import-newlines",
|
||||
"eslint-plugin-jsonc",
|
||||
"dspace-angular-ts",
|
||||
"dspace-angular-html"
|
||||
],
|
||||
@@ -161,6 +160,9 @@
|
||||
]
|
||||
}
|
||||
],
|
||||
"@angular-eslint/prefer-standalone": [
|
||||
"error"
|
||||
],
|
||||
"@angular-eslint/no-attribute-decorator": "error",
|
||||
"@angular-eslint/no-output-native": "warn",
|
||||
"@angular-eslint/no-output-on-prefix": "warn",
|
||||
@@ -261,9 +263,48 @@
|
||||
"rxjs/no-nested-subscribe": "off", // todo: go over _all_ cases
|
||||
|
||||
// Custom DSpace Angular rules
|
||||
"dspace-angular-ts/alias-imports": [
|
||||
"error",
|
||||
{
|
||||
"aliases": [
|
||||
{
|
||||
"package": "rxjs",
|
||||
"imported": "of",
|
||||
"local": "of"
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"dspace-angular-ts/themed-component-classes": "error",
|
||||
"dspace-angular-ts/themed-component-selectors": "error",
|
||||
"dspace-angular-ts/themed-component-usages": "error"
|
||||
"dspace-angular-ts/themed-component-usages": "error",
|
||||
"dspace-angular-ts/themed-decorators": [
|
||||
"off",
|
||||
{
|
||||
"decorators": {
|
||||
"listableObjectComponent": 3,
|
||||
"rendersSectionForMenu": 2
|
||||
}
|
||||
}
|
||||
],
|
||||
"dspace-angular-ts/themed-wrapper-no-input-defaults": "error",
|
||||
"dspace-angular-ts/unique-decorators": [
|
||||
"off",
|
||||
{
|
||||
"decorators": [
|
||||
"listableObjectComponent"
|
||||
]
|
||||
}
|
||||
],
|
||||
"dspace-angular-ts/sort-standalone-imports": [
|
||||
"error",
|
||||
{
|
||||
"locale": "en-US",
|
||||
"maxItems": 0,
|
||||
"indent": 2,
|
||||
"trailingComma": true
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -303,10 +344,13 @@
|
||||
"*.json5"
|
||||
],
|
||||
"extends": [
|
||||
"plugin:jsonc/recommended-with-jsonc"
|
||||
"plugin:jsonc/recommended-with-json5"
|
||||
],
|
||||
"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",
|
||||
"jsonc/comma-dangle": [
|
||||
"error",
|
||||
|
107
.github/workflows/build.yml
vendored
107
.github/workflows/build.yml
vendored
@@ -29,6 +29,8 @@ jobs:
|
||||
DSPACE_CACHE_SERVERSIDE_ANONYMOUSCACHE_MAX: 0
|
||||
# Tell Cypress to run e2e tests using the same UI URL
|
||||
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
|
||||
# Comment this out to use the latest release
|
||||
#CHROME_VERSION: "90.0.4430.212-1"
|
||||
@@ -190,12 +192,115 @@ jobs:
|
||||
# Get homepage and verify that the <meta name="title"> tag includes "DSpace".
|
||||
# If it does, then SSR is working, as this tag is created by our MetadataService.
|
||||
# This step also prints entire HTML of homepage for easier debugging if grep fails.
|
||||
- name: Verify SSR (server-side rendering)
|
||||
- name: Verify SSR (server-side rendering) on Homepage
|
||||
run: |
|
||||
result=$(wget -O- -q http://127.0.0.1:4000/home)
|
||||
echo "$result"
|
||||
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
|
||||
run: kill -9 $(lsof -t -i:4000)
|
||||
|
||||
|
@@ -23,10 +23,24 @@ ssr:
|
||||
# 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.
|
||||
inlineCriticalCss: false
|
||||
# Path prefixes to enable SSR for. By default these are limited to paths of primary DSpace objects.
|
||||
# NOTE: The "/handle/" path ensures Handle redirects work via SSR. The "/reload/" path ensures
|
||||
# hard refreshes (e.g. after login) trigger SSR while fully reloading the page.
|
||||
paths: [ '/home', '/items/', '/entities/', '/collections/', '/communities/', '/bitstream/', '/bitstreams/', '/handle/', '/reload/' ]
|
||||
# Patterns to be run as regexes against the path of the page to check if SSR is allowed.
|
||||
# If the path match any of the regexes it will be served directly in CSR.
|
||||
# By default, excludes community and collection browse, global browse, global search, community list, statistics and various administrative tools.
|
||||
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.
|
||||
# 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.
|
||||
@@ -260,6 +274,9 @@ languages:
|
||||
- code: gd
|
||||
label: Gàidhlig
|
||||
active: true
|
||||
- code: gu
|
||||
label: ગુજરાતી
|
||||
active: true
|
||||
- code: hi
|
||||
label: हिंदी
|
||||
active: true
|
||||
@@ -275,6 +292,9 @@ languages:
|
||||
- code: lv
|
||||
label: Latviešu
|
||||
active: true
|
||||
- code: mr
|
||||
label: मराठी
|
||||
active: true
|
||||
- code: nl
|
||||
label: Nederlands
|
||||
active: true
|
||||
@@ -287,6 +307,9 @@ languages:
|
||||
- code: pt-BR
|
||||
label: Português do Brasil
|
||||
active: true
|
||||
- code: ru
|
||||
label: Русский
|
||||
active: true
|
||||
- code: sr-lat
|
||||
label: Srpski (lat)
|
||||
active: true
|
||||
@@ -453,6 +476,8 @@ info:
|
||||
enableEndUserAgreement: true
|
||||
enablePrivacyStatement: true
|
||||
enableCOARNotifySupport: true
|
||||
# Whether to show the cookie consent popup and the cookie settings footer link or not.
|
||||
enableCookieConsentPopup: true
|
||||
|
||||
# Whether to enable Markdown (https://commonmark.org/) and MathJax (https://www.mathjax.org/)
|
||||
# display in supported metadata fields. By default, only dc.description.abstract is supported.
|
||||
@@ -593,3 +618,8 @@ geospatialMapViewer:
|
||||
defaultCentrePoint:
|
||||
lat: 41.015137
|
||||
lng: 28.979530
|
||||
|
||||
# Configuration for storing accessibility settings, used by the AccessibilitySettingsService
|
||||
accessibility:
|
||||
# The duration in days after which the accessibility settings cookie expires
|
||||
cookieExpirationDuration: 7
|
||||
|
@@ -34,6 +34,7 @@ export default defineConfig({
|
||||
DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME: 'People',
|
||||
// Account used to test basic submission process
|
||||
DSPACE_TEST_SUBMIT_USER: 'dspacedemo+submit@gmail.com',
|
||||
DSPACE_TEST_SUBMIT_USER_UUID: '914955b1-cf2e-4884-8af7-a166aa24cf73',
|
||||
DSPACE_TEST_SUBMIT_USER_PASSWORD: 'dspace',
|
||||
// Administrator users group
|
||||
DSPACE_ADMINISTRATOR_GROUP: 'e59f5659-bff9-451e-b28f-439e7bd467e4'
|
||||
|
@@ -15,24 +15,24 @@ describe('Header', () => {
|
||||
cy.visit('/');
|
||||
|
||||
// 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
|
||||
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"
|
||||
cy.get('html').invoke('attr', 'lang').should('eq', 'de');
|
||||
|
||||
// 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
|
||||
cy.get('a[data-test="lang-switch"]').click();
|
||||
cy.get('#language-menu-list li').contains('English').click();
|
||||
cy.get('button[data-test="lang-switch"]').click();
|
||||
cy.get('#language-menu-list div[role="option"]').contains('English').click();
|
||||
|
||||
// HTML "lang" attribute should switch to "en"
|
||||
cy.get('html').invoke('attr', 'lang').should('eq', 'en');
|
||||
|
||||
// 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');
|
||||
});
|
||||
});
|
||||
|
@@ -4,13 +4,14 @@ import { Options } from 'cypress-axe';
|
||||
|
||||
beforeEach(() => {
|
||||
// Must login as an Admin to see the page
|
||||
cy.intercept('GET', '/server/actuator/health').as('status');
|
||||
cy.intercept('GET', '/server/actuator/info').as('info');
|
||||
cy.visit('/health');
|
||||
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||
});
|
||||
|
||||
describe('Health Page > Status Tab', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.intercept('GET', '/server/actuator/health').as('status');
|
||||
cy.wait('@status');
|
||||
|
||||
cy.get('a[data-test="health-page.status-tab"]').click();
|
||||
@@ -36,7 +37,6 @@ describe('Health Page > Status Tab', () => {
|
||||
|
||||
describe('Health Page > Info Tab', () => {
|
||||
it('should pass accessibility tests', () => {
|
||||
cy.intercept('GET', '/server/actuator/info').as('info');
|
||||
cy.wait('@info');
|
||||
|
||||
cy.get('a[data-test="health-page.info-tab"]').click();
|
||||
|
@@ -26,6 +26,12 @@ describe('Homepage', () => {
|
||||
// Wait for homepage tag to appear
|
||||
cy.get('ds-home-page').should('be.visible');
|
||||
|
||||
// Wait for at least one loading component to show up
|
||||
cy.get('ds-loading').should('exist');
|
||||
|
||||
// Wait until all loading components have disappeared
|
||||
cy.get('ds-loading').should('not.exist');
|
||||
|
||||
// Analyze <ds-home-page> for accessibility issues
|
||||
testA11y('ds-home-page');
|
||||
});
|
||||
|
@@ -84,7 +84,7 @@ describe('My DSpace page', () => {
|
||||
cy.url().should('include', '/mydspace');
|
||||
|
||||
// Close any open notifications, to make sure they don't get in the way of next steps
|
||||
cy.get('[data-dismiss="alert"]').click({ multiple: true });
|
||||
cy.get('[data-bs-dismiss="alert"]').click({ multiple: true });
|
||||
|
||||
// This is the GET command that will actually run the search
|
||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||
|
@@ -95,7 +95,7 @@ describe('New Submission page', () => {
|
||||
// A success alert should be visible
|
||||
cy.get('ds-notification div.alert-success').should('be.visible');
|
||||
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
|
||||
cy.get('[data-dismiss="alert"]').click({ multiple: true });
|
||||
cy.get('[data-bs-dismiss="alert"]').click({ multiple: true });
|
||||
|
||||
// This is the GET command that will actually run the search
|
||||
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
|
||||
|
@@ -56,7 +56,7 @@ before(() => {
|
||||
beforeEach(() => {
|
||||
// Pre-agree to all Orejime cookies by setting the orejime-anonymous cookie
|
||||
// This just ensures it doesn't get in the way of matching other objects in the page.
|
||||
cy.setCookie('orejime-anonymous', '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true}');
|
||||
cy.setCookie('orejime-anonymous', '{"authentication":true,"preferences":true,"acknowledgement":true,"google-analytics":true,"correlation-id":true,"accessibility":true}');
|
||||
|
||||
// Remove any CSRF cookies saved from prior tests
|
||||
cy.clearCookie(DSPACE_XSRF_COOKIE);
|
||||
|
@@ -93,7 +93,10 @@ services:
|
||||
volumes:
|
||||
# Keep Solr data directory between reboots
|
||||
- solr_data:/var/solr/data
|
||||
# Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr
|
||||
# NOTE: We are not running Solr as "root", but we need root permissions to copy our cores to the mounted
|
||||
# /var/solr/data directory. Then we start Solr as the "solr" user.
|
||||
user: root
|
||||
# Initialize all DSpace Solr cores, then start Solr
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
@@ -111,7 +114,8 @@ services:
|
||||
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
|
||||
chown -R solr:solr /var/solr
|
||||
runuser -u solr -- solr-foreground
|
||||
volumes:
|
||||
assetstore:
|
||||
pgdata:
|
||||
|
@@ -97,11 +97,16 @@ services:
|
||||
volumes:
|
||||
# Keep Solr data directory between reboots
|
||||
- solr_data:/var/solr/data
|
||||
# NOTE: We are not running Solr as "root", but we need root permissions to copy our cores to the mounted
|
||||
# /var/solr/data directory. Then we start Solr as the "solr" user.
|
||||
user: root
|
||||
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
|
||||
# * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op
|
||||
# * Second, copy configsets to this core:
|
||||
# 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`
|
||||
# * Third, ensure all new folders are owned by "solr" user
|
||||
# * Finally, start Solr as the "solr" user via the provided solr-foreground script
|
||||
entrypoint:
|
||||
- /bin/bash
|
||||
- '-c'
|
||||
@@ -119,7 +124,8 @@ services:
|
||||
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
|
||||
chown -R solr:solr /var/solr
|
||||
runuser -u solr -- solr-foreground
|
||||
volumes:
|
||||
assetstore:
|
||||
pgdata:
|
||||
|
@@ -9,6 +9,8 @@ _______
|
||||
|
||||
[Source code](../../../../lint/src/rules/html/no-disabled-attribute-on-button.ts)
|
||||
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
@@ -19,24 +21,28 @@ _______
|
||||
```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>
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -47,6 +53,9 @@ _______
|
||||
|
||||
```html
|
||||
<button disabled>Submit</button>
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -63,6 +72,9 @@ Result of `yarn lint --fix`:
|
||||
|
||||
```html
|
||||
<button [disabled]="true">Submit</button>
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
|
@@ -11,6 +11,8 @@ _______
|
||||
|
||||
[Source code](../../../../lint/src/rules/html/themed-component-usages.ts)
|
||||
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
@@ -23,6 +25,7 @@ _______
|
||||
<ds-test-themeable></ds-test-themeable>
|
||||
<ds-test-themeable [test]="something"></ds-test-themeable>
|
||||
```
|
||||
|
||||
|
||||
##### use no-prefix selectors in TypeScript templates
|
||||
|
||||
@@ -33,6 +36,7 @@ _______
|
||||
class Test {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### use no-prefix selectors in TypeScript test templates
|
||||
|
||||
@@ -45,6 +49,7 @@ Filename: `lint/test/fixture/src/test.spec.ts`
|
||||
class Test {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### base selectors are also allowed in TypeScript test templates
|
||||
|
||||
@@ -57,6 +62,7 @@ Filename: `lint/test/fixture/src/test.spec.ts`
|
||||
class Test {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -69,6 +75,9 @@ class Test {
|
||||
<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):
|
||||
```
|
||||
@@ -91,6 +100,9 @@ Result of `yarn lint --fix`:
|
||||
<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):
|
||||
```
|
||||
|
@@ -1,6 +1,11 @@
|
||||
[DSpace ESLint plugins](../../../lint/README.md) > TypeScript rules
|
||||
_______
|
||||
|
||||
- [`dspace-angular-ts/alias-imports`](./rules/alias-imports.md): Unclear imports should be aliased for clarity
|
||||
- [`dspace-angular-ts/sort-standalone-imports`](./rules/sort-standalone-imports.md): Sorts the standalone `@Component` imports alphabetically
|
||||
- [`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
|
||||
- [`dspace-angular-ts/themed-decorators`](./rules/themed-decorators.md): Entry components with theme support should declare the correct theme
|
||||
- [`dspace-angular-ts/themed-wrapper-no-input-defaults`](./rules/themed-wrapper-no-input-defaults.md): ThemedComponent wrappers should not declare input defaults (see [DSpace Angular #2164](https://github.com/DSpace/dspace-angular/pull/2164))
|
||||
- [`dspace-angular-ts/unique-decorators`](./rules/unique-decorators.md): Some decorators must be called with unique arguments (e.g. when they construct a mapping based on the argument values)
|
||||
|
148
docs/lint/ts/rules/alias-imports.md
Normal file
148
docs/lint/ts/rules/alias-imports.md
Normal file
@@ -0,0 +1,148 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/alias-imports`
|
||||
_______
|
||||
|
||||
Unclear imports should be aliased for clarity
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/alias-imports.ts)
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
#### `aliases`
|
||||
|
||||
A list of all the imports that you want to alias for clarity. Every alias should be declared in the following format:
|
||||
```json
|
||||
{
|
||||
"package": "rxjs",
|
||||
"imported": "of",
|
||||
"local": "observableOf"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### correctly aliased imports
|
||||
|
||||
```typescript
|
||||
import { of as observableOf } from 'rxjs';
|
||||
```
|
||||
|
||||
With options:
|
||||
|
||||
```json
|
||||
{
|
||||
"aliases": [
|
||||
{
|
||||
"package": "rxjs",
|
||||
"imported": "of",
|
||||
"local": "observableOf"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### enforce unaliased import
|
||||
|
||||
```typescript
|
||||
import { combineLatest } from 'rxjs';
|
||||
```
|
||||
|
||||
With options:
|
||||
|
||||
```json
|
||||
{
|
||||
"aliases": [
|
||||
{
|
||||
"package": "rxjs",
|
||||
"imported": "combineLatest",
|
||||
"local": "combineLatest"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### imports without alias
|
||||
|
||||
```typescript
|
||||
import { of } from 'rxjs';
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
This import must be aliased
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { of as observableOf } from 'rxjs';
|
||||
```
|
||||
|
||||
|
||||
##### imports under the wrong alias
|
||||
|
||||
```typescript
|
||||
import { of as ofSomething } from 'rxjs';
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
This import uses the wrong alias (should be {{ local }})
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { of as observableOf } from 'rxjs';
|
||||
```
|
||||
|
||||
|
||||
##### disallow aliasing import
|
||||
|
||||
```typescript
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
|
||||
|
||||
With options:
|
||||
|
||||
```json
|
||||
{
|
||||
"aliases": [
|
||||
{
|
||||
"package": "rxjs",
|
||||
"imported": "combineLatest",
|
||||
"local": "combineLatest"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
This import should not use an alias
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
import { combineLatest } from 'rxjs';
|
||||
```
|
||||
|
||||
|
||||
|
245
docs/lint/ts/rules/sort-standalone-imports.md
Normal file
245
docs/lint/ts/rules/sort-standalone-imports.md
Normal file
@@ -0,0 +1,245 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/sort-standalone-imports`
|
||||
_______
|
||||
|
||||
Sorts the standalone `@Component` imports alphabetically
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/sort-standalone-imports.ts)
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
#### `locale`
|
||||
|
||||
The locale used to sort the imports.,
|
||||
#### `maxItems`
|
||||
|
||||
The maximum number of imports that should be displayed before each import is separated onto its own line.,
|
||||
#### `indent`
|
||||
|
||||
The indent used for the project.,
|
||||
#### `trailingComma`
|
||||
|
||||
Whether the last import should have a trailing comma (only applicable for multiline imports).
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### should sort multiple imports on separate lines
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
|
||||
##### should not inlines singular imports when maxItems is 0
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
|
||||
##### should inline singular imports when maxItems is 1
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [RootComponent],
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
With options:
|
||||
|
||||
```json
|
||||
{
|
||||
"maxItems": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### should sort multiple imports alphabetically
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RootComponent,
|
||||
AsyncPipe,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Standalone imports should be sorted alphabetically
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
|
||||
##### should not put singular imports on one line when maxItems is 0
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [RootComponent],
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Standalone imports should be sorted alphabetically
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
|
||||
##### should not put singular imports on a separate line when maxItems is 1
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
|
||||
With options:
|
||||
|
||||
```json
|
||||
{
|
||||
"maxItems": 1
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Standalone imports should be sorted alphabetically
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [RootComponent],
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
|
||||
##### should not display multiple imports on the same line
|
||||
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, RootComponent],
|
||||
})
|
||||
export class AppComponent {}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Standalone imports should be sorted alphabetically
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}
|
||||
```
|
||||
|
||||
|
||||
|
@@ -11,6 +11,8 @@ _______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/themed-component-classes.ts)
|
||||
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
@@ -26,6 +28,7 @@ _______
|
||||
class Something {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Base component
|
||||
|
||||
@@ -34,9 +37,10 @@ class Something {
|
||||
selector: 'ds-base-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class TestThemeableTomponent {
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Wrapper component
|
||||
|
||||
@@ -50,9 +54,10 @@ Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
TestThemeableComponent,
|
||||
],
|
||||
})
|
||||
class ThemedTestThemeableTomponent extends ThemedComponent<TestThemeableComponent> {
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Override component
|
||||
|
||||
@@ -66,6 +71,7 @@ Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.t
|
||||
class Override extends BaseComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -80,6 +86,9 @@ class Override extends BaseComponent {
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -107,6 +116,9 @@ Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -137,6 +149,9 @@ Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -171,6 +186,9 @@ import { SomethingElse } from './somewhere-else';
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -207,6 +225,9 @@ import { Something, SomethingElse } from './somewhere-else';
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -237,6 +258,9 @@ Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.t
|
||||
})
|
||||
class Override extends BaseComponent {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
|
@@ -17,6 +17,8 @@ _______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/themed-component-selectors.ts)
|
||||
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
@@ -31,6 +33,7 @@ _______
|
||||
class Something {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Themeable component selector should replace the original version, unthemed version should be changed to ds-base-
|
||||
|
||||
@@ -53,6 +56,7 @@ class ThemedSomething extends ThemedComponent<Something> {
|
||||
class OverrideSomething extends Something {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Other themed component wrappers should not interfere
|
||||
|
||||
@@ -69,6 +73,7 @@ class Something {
|
||||
class ThemedSomethingElse extends ThemedComponent<SomethingElse> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -85,6 +90,9 @@ Filename: `lint/test/fixture/src/app/test/test-themeable.component.ts`
|
||||
})
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -111,6 +119,9 @@ Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
|
||||
})
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -137,6 +148,9 @@ Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.t
|
||||
})
|
||||
class TestThememeableComponent extends BaseComponent {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
|
@@ -15,6 +15,8 @@ _______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/themed-component-usages.ts)
|
||||
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
@@ -30,6 +32,7 @@ const config = {
|
||||
b: ChipsComponent,
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### allow base class in class declaration
|
||||
|
||||
@@ -37,6 +40,7 @@ const config = {
|
||||
export class TestThemeableComponent {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### allow inheriting from base class
|
||||
|
||||
@@ -46,6 +50,7 @@ import { TestThemeableComponent } from './app/test/test-themeable.component';
|
||||
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### allow base class in ViewChild
|
||||
|
||||
@@ -56,6 +61,7 @@ export class Something {
|
||||
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### allow wrapper selectors in test queries
|
||||
|
||||
@@ -65,6 +71,7 @@ Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
```
|
||||
|
||||
|
||||
##### allow wrapper selectors in cypress queries
|
||||
|
||||
@@ -74,6 +81,7 @@ Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
|
||||
By.css('ds-themeable');
|
||||
By.css('#test > ds-themeable > #nest');
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -90,6 +98,9 @@ const config = {
|
||||
a: TestThemeableComponent,
|
||||
b: TestComponent,
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -120,6 +131,9 @@ const config = {
|
||||
b: TestComponent,
|
||||
c: Something,
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -150,6 +164,9 @@ const DECLARATIONS = [
|
||||
Something,
|
||||
ThemedTestThemeableComponent,
|
||||
];
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -173,6 +190,9 @@ 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):
|
||||
```
|
||||
@@ -194,6 +214,9 @@ 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):
|
||||
```
|
||||
@@ -215,6 +238,9 @@ 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):
|
||||
```
|
||||
@@ -236,6 +262,9 @@ 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):
|
||||
```
|
||||
@@ -266,6 +295,9 @@ import { TestThemeableComponent } from '../../../../app/test/test-themeable.comp
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
@@ -306,6 +338,9 @@ import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-t
|
||||
})
|
||||
export class UsageComponent {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
|
183
docs/lint/ts/rules/themed-decorators.md
Normal file
183
docs/lint/ts/rules/themed-decorators.md
Normal file
@@ -0,0 +1,183 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-decorators`
|
||||
_______
|
||||
|
||||
Entry components with theme support should declare the correct theme
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/themed-decorators.ts)
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
#### `decorators`
|
||||
|
||||
A mapping for all the existing themeable decorators, with the decorator name as the key and the index of the `theme` argument as the value.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### theme file declares the correct theme in @listableObjectComponent
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test/app/dynamic-component/dynamic-component.ts`
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### plain file declares no theme in @listableObjectComponent
|
||||
|
||||
Filename: `lint/test/fixture/src/app/dynamic-component/dynamic-component.ts`
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### plain file declares explicit undefined theme in @listableObjectComponent
|
||||
|
||||
Filename: `lint/test/fixture/src/app/dynamic-component/dynamic-component.ts`
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### test file declares theme outside of theme directory
|
||||
|
||||
Filename: `lint/test/fixture/src/app/dynamic-component/dynamic-component.spec.ts`
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### only track configured decorators
|
||||
|
||||
Filename: `lint/test/fixture/src/app/dynamic-component/dynamic-component.ts`
|
||||
|
||||
```typescript
|
||||
@something('test')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code & automatic fixes
|
||||
|
||||
##### theme file declares the wrong theme in @listableObjectComponent
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test/app/dynamic-component/dynamic-component.ts`
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test-2')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Wrong theme declaration in decorator
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### plain file declares a theme in @listableObjectComponent
|
||||
|
||||
Filename: `lint/test/fixture/src/app/dynamic-component/dynamic-component.ts`
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test-2')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
There is a theme declaration in decorator, but this file is not part of a theme
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### theme file declares no theme in @listableObjectComponent
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test-2/app/dynamic-component/dynamic-component.ts`
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
No theme declaration in decorator
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test-2')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### theme file declares explicit undefined theme in @listableObjectComponent
|
||||
|
||||
Filename: `lint/test/fixture/src/themes/test-2/app/dynamic-component/dynamic-component.ts`
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
No theme declaration in decorator
|
||||
```
|
||||
|
||||
Result of `yarn lint --fix`:
|
||||
```typescript
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test-2')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
92
docs/lint/ts/rules/themed-wrapper-no-input-defaults.md
Normal file
92
docs/lint/ts/rules/themed-wrapper-no-input-defaults.md
Normal file
@@ -0,0 +1,92 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-wrapper-no-input-defaults`
|
||||
_______
|
||||
|
||||
ThemedComponent wrappers should not declare input defaults (see [DSpace Angular #2164](https://github.com/DSpace/dspace-angular/pull/2164))
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/themed-wrapper-no-input-defaults.ts)
|
||||
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### ThemedComponent wrapper defines an input without a default value
|
||||
|
||||
```typescript
|
||||
export class TTest extends ThemedComponent<Test> {
|
||||
|
||||
@Input()
|
||||
test;
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### Regular class defines an input with a default value
|
||||
|
||||
```typescript
|
||||
export class Test {
|
||||
|
||||
@Input()
|
||||
test = 'test';
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code
|
||||
|
||||
##### ThemedComponent wrapper defines an input with a default value
|
||||
|
||||
```typescript
|
||||
export class TTest extends ThemedComponent<Test> {
|
||||
|
||||
@Input()
|
||||
test1 = 'test';
|
||||
|
||||
@Input()
|
||||
test2 = true;
|
||||
|
||||
@Input()
|
||||
test2: number = 123;
|
||||
|
||||
@Input()
|
||||
test3: number[] = [1,2,3];
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
ThemedComponent wrapper declares inputs with defaults
|
||||
ThemedComponent wrapper declares inputs with defaults
|
||||
ThemedComponent wrapper declares inputs with defaults
|
||||
ThemedComponent wrapper declares inputs with defaults
|
||||
```
|
||||
|
||||
|
||||
##### ThemedComponent wrapper defines an input with an undefined default value
|
||||
|
||||
```typescript
|
||||
export class TTest extends ThemedComponent<Test> {
|
||||
|
||||
@Input()
|
||||
test = undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
ThemedComponent wrapper declares inputs with defaults
|
||||
```
|
||||
|
||||
|
||||
|
86
docs/lint/ts/rules/unique-decorators.md
Normal file
86
docs/lint/ts/rules/unique-decorators.md
Normal file
@@ -0,0 +1,86 @@
|
||||
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/unique-decorators`
|
||||
_______
|
||||
|
||||
Some decorators must be called with unique arguments (e.g. when they construct a mapping based on the argument values)
|
||||
|
||||
_______
|
||||
|
||||
[Source code](../../../../lint/src/rules/ts/unique-decorators.ts)
|
||||
|
||||
|
||||
### Options
|
||||
|
||||
#### `decorators`
|
||||
|
||||
The list of all the decorators for which you want to enforce this behavior.
|
||||
|
||||
|
||||
### Examples
|
||||
|
||||
|
||||
#### Valid code
|
||||
|
||||
##### checked decorator, no repetitions
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(a)
|
||||
export class A {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a, 'b')
|
||||
export class B {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a, 'b', 3)
|
||||
export class C {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a, 'b', 3, Enum.TEST1)
|
||||
export class C {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a, 'b', 3, Enum.TEST2)
|
||||
export class C {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
##### unchecked decorator, some repetitions
|
||||
|
||||
```typescript
|
||||
@something(a)
|
||||
export class A {
|
||||
}
|
||||
|
||||
@something(a)
|
||||
export class B {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
#### Invalid code
|
||||
|
||||
##### checked decorator, some repetitions
|
||||
|
||||
```typescript
|
||||
@listableObjectComponent(a)
|
||||
export class A {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a)
|
||||
export class B {
|
||||
}
|
||||
|
||||
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
Duplicate decorator call
|
||||
```
|
||||
|
||||
|
||||
|
@@ -33,6 +33,7 @@ export const info = {
|
||||
[Message.USE_DSBTN_DISABLED]: 'Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.',
|
||||
},
|
||||
},
|
||||
optionDocs: [],
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
|
@@ -45,6 +45,7 @@ The only exception to this rule are unit tests, where we may want to use the bas
|
||||
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper\'s selector',
|
||||
},
|
||||
},
|
||||
optionDocs: [],
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
|
304
lint/src/rules/ts/alias-imports.ts
Normal file
304
lint/src/rules/ts/alias-imports.ts
Normal file
@@ -0,0 +1,304 @@
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
import { Scope } from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
import {
|
||||
DSpaceESLintRuleInfo,
|
||||
NamedTests,
|
||||
OptionDoc,
|
||||
} from '../../util/structure';
|
||||
|
||||
export enum Message {
|
||||
MISSING_ALIAS = 'missingAlias',
|
||||
WRONG_ALIAS = 'wrongAlias',
|
||||
MULTIPLE_ALIASES = 'multipleAliases',
|
||||
UNNECESSARY_ALIAS = 'unnecessaryAlias',
|
||||
}
|
||||
|
||||
interface AliasImportOptions {
|
||||
aliases: AliasImportOption[];
|
||||
}
|
||||
|
||||
interface AliasImportOption {
|
||||
package: string;
|
||||
imported: string;
|
||||
local: string;
|
||||
}
|
||||
|
||||
interface AliasImportDocOptions {
|
||||
aliases: OptionDoc;
|
||||
}
|
||||
|
||||
export const info: DSpaceESLintRuleInfo<[AliasImportOptions], [AliasImportDocOptions]> = {
|
||||
name: 'alias-imports',
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Unclear imports should be aliased for clarity',
|
||||
},
|
||||
messages: {
|
||||
[Message.MISSING_ALIAS]: 'This import must be aliased',
|
||||
[Message.WRONG_ALIAS]: 'This import uses the wrong alias (should be {{ local }})',
|
||||
[Message.MULTIPLE_ALIASES]: 'This import was used twice with a different alias (should be {{ local }})',
|
||||
[Message.UNNECESSARY_ALIAS]: 'This import should not use an alias',
|
||||
},
|
||||
fixable: 'code',
|
||||
type: 'problem',
|
||||
schema: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
package: { type: 'string' },
|
||||
imported: { type: 'string' },
|
||||
local: { type: 'string' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
optionDocs: [
|
||||
{
|
||||
aliases: {
|
||||
title: '`aliases`',
|
||||
description: `A list of all the imports that you want to alias for clarity. Every alias should be declared in the following format:
|
||||
\`\`\`json
|
||||
{
|
||||
"package": "rxjs",
|
||||
"imported": "of",
|
||||
"local": "observableOf"
|
||||
}
|
||||
\`\`\``,
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
aliases: [
|
||||
{
|
||||
package: 'rxjs',
|
||||
imported: 'of',
|
||||
local: 'observableOf',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>, options: any) {
|
||||
return (options[0] as AliasImportOptions).aliases.reduce((selectors: any, option: AliasImportOption) => {
|
||||
selectors[`ImportDeclaration[source.value = "${option.package}"] > ImportSpecifier[imported.name = "${option.imported}"][local.name != "${option.local}"]`] = (node: TSESTree.ImportSpecifier) => handleUnaliasedImport(context, option, node);
|
||||
return selectors;
|
||||
}, {});
|
||||
},
|
||||
});
|
||||
|
||||
export const tests: NamedTests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'correctly aliased imports',
|
||||
code: `
|
||||
import { of as observableOf } from 'rxjs';
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
aliases: [
|
||||
{
|
||||
package: 'rxjs',
|
||||
imported: 'of',
|
||||
local: 'observableOf',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'enforce unaliased import',
|
||||
code: `
|
||||
import { combineLatest } from 'rxjs';
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
aliases: [
|
||||
{
|
||||
package: 'rxjs',
|
||||
imported: 'combineLatest',
|
||||
local: 'combineLatest',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'imports without alias',
|
||||
code: `
|
||||
import { of } from 'rxjs';
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'missingAlias',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { of as observableOf } from 'rxjs';
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'imports under the wrong alias',
|
||||
code: `
|
||||
import { of as ofSomething } from 'rxjs';
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'wrongAlias',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { of as observableOf } from 'rxjs';
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'disallow aliasing import',
|
||||
code: `
|
||||
import { combineLatest as observableCombineLatest } from 'rxjs';
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'unnecessaryAlias',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
import { combineLatest } from 'rxjs';
|
||||
`,
|
||||
options: [
|
||||
{
|
||||
aliases: [
|
||||
{
|
||||
package: 'rxjs',
|
||||
imported: 'combineLatest',
|
||||
local: 'combineLatest',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
* Replaces the incorrectly aliased imports with the ones defined in the defaultOptions
|
||||
*
|
||||
* @param context The current {@link TSESLint.RuleContext}
|
||||
* @param option The current {@link AliasImportOption} that needs to be handled
|
||||
* @param node The incorrect import node that should be fixed
|
||||
*/
|
||||
function handleUnaliasedImport(context: TSESLint.RuleContext<Message, unknown[]>, option: AliasImportOption, node: TSESTree.ImportSpecifier): void {
|
||||
const hasCorrectAliasedImport: boolean = (node.parent as TSESTree.ImportDeclaration).specifiers.find((specifier: TSESTree.ImportClause) => specifier.local.name === option.local && specifier.type === AST_NODE_TYPES.ImportSpecifier && (specifier as TSESTree.ImportSpecifier).imported.name === option.imported) !== undefined;
|
||||
if (option.imported === option.local) {
|
||||
if (hasCorrectAliasedImport) {
|
||||
context.report({
|
||||
messageId: Message.MULTIPLE_ALIASES,
|
||||
data: { local: option.local },
|
||||
node: node,
|
||||
fix(fixer: TSESLint.RuleFixer) {
|
||||
const fixes: TSESLint.RuleFix[] = [];
|
||||
|
||||
const commaAfter = context.sourceCode.getTokenAfter(node, {
|
||||
filter: (token: TSESTree.Token) => token.value === ',',
|
||||
});
|
||||
if (commaAfter) {
|
||||
fixes.push(fixer.removeRange([node.range[0], commaAfter.range[1]]));
|
||||
} else {
|
||||
fixes.push(fixer.remove(node));
|
||||
}
|
||||
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
|
||||
|
||||
return fixes;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
messageId: Message.UNNECESSARY_ALIAS,
|
||||
data: { local: option.local },
|
||||
node: node,
|
||||
fix(fixer: TSESLint.RuleFixer) {
|
||||
const fixes: TSESLint.RuleFix[] = [];
|
||||
|
||||
fixes.push(fixer.replaceText(node, option.imported));
|
||||
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
|
||||
|
||||
return fixes;
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
if (hasCorrectAliasedImport) {
|
||||
context.report({
|
||||
messageId: Message.MULTIPLE_ALIASES,
|
||||
data: { local: option.local },
|
||||
node: node,
|
||||
fix(fixer: TSESLint.RuleFixer) {
|
||||
const fixes: TSESLint.RuleFix[] = [];
|
||||
|
||||
const commaAfter = context.sourceCode.getTokenAfter(node, {
|
||||
filter: (token: TSESTree.Token) => token.value === ',',
|
||||
});
|
||||
if (commaAfter) {
|
||||
fixes.push(fixer.removeRange([node.range[0], commaAfter.range[1]]));
|
||||
} else {
|
||||
fixes.push(fixer.remove(node));
|
||||
}
|
||||
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
|
||||
|
||||
return fixes;
|
||||
},
|
||||
});
|
||||
} else if (node.local.name === node.imported.name) {
|
||||
context.report({
|
||||
messageId: Message.MISSING_ALIAS,
|
||||
node: node,
|
||||
fix(fixer: TSESLint.RuleFixer) {
|
||||
const fixes: TSESLint.RuleFix[] = [];
|
||||
|
||||
fixes.push(fixer.replaceText(node.local, `${option.imported} as ${option.local}`));
|
||||
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
|
||||
|
||||
return fixes;
|
||||
},
|
||||
});
|
||||
} else {
|
||||
context.report({
|
||||
messageId: Message.WRONG_ALIAS,
|
||||
data: { local: option.local },
|
||||
node: node,
|
||||
fix(fixer: TSESLint.RuleFixer) {
|
||||
const fixes: TSESLint.RuleFix[] = [];
|
||||
|
||||
fixes.push(fixer.replaceText(node.local, option.local));
|
||||
fixes.push(...retrieveUsageReplacementFixes(context, fixer, node, option.local));
|
||||
|
||||
return fixes;
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the {@link TSESLint.RuleFix}s for all the usages of the incorrect import.
|
||||
*
|
||||
* @param context The current {@link TSESLint.RuleContext}
|
||||
* @param fixer The instance {@link TSESLint.RuleFixer}
|
||||
* @param node The node which needs to be replaced
|
||||
* @param newAlias The new import name
|
||||
*/
|
||||
function retrieveUsageReplacementFixes(context: TSESLint.RuleContext<Message, unknown[]>, fixer: TSESLint.RuleFixer, node: TSESTree.ImportSpecifier, newAlias: string): TSESLint.RuleFix[] {
|
||||
return context.sourceCode.getDeclaredVariables(node)[0].references.map((reference: Scope.Reference) => fixer.replaceText(reference.identifier, newAlias));
|
||||
}
|
@@ -10,14 +10,24 @@ import {
|
||||
RuleExports,
|
||||
} from '../../util/structure';
|
||||
/* eslint-disable import/no-namespace */
|
||||
import * as aliasImports from './alias-imports';
|
||||
import * as sortStandaloneImports from './sort-standalone-imports';
|
||||
import * as themedComponentClasses from './themed-component-classes';
|
||||
import * as themedComponentSelectors from './themed-component-selectors';
|
||||
import * as themedComponentUsages from './themed-component-usages';
|
||||
import * as themedDecorators from './themed-decorators';
|
||||
import * as themedWrapperNoInputDefaults from './themed-wrapper-no-input-defaults';
|
||||
import * as uniqueDecorators from './unique-decorators';
|
||||
|
||||
const index = [
|
||||
aliasImports,
|
||||
sortStandaloneImports,
|
||||
themedComponentClasses,
|
||||
themedComponentSelectors,
|
||||
themedComponentUsages,
|
||||
themedDecorators,
|
||||
themedWrapperNoInputDefaults,
|
||||
uniqueDecorators,
|
||||
] as unknown as RuleExports[];
|
||||
|
||||
export = {
|
||||
|
306
lint/src/rules/ts/sort-standalone-imports.ts
Normal file
306
lint/src/rules/ts/sort-standalone-imports.ts
Normal file
@@ -0,0 +1,306 @@
|
||||
import {
|
||||
ASTUtils as TSESLintASTUtils,
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import {
|
||||
DSpaceESLintRuleInfo,
|
||||
NamedTests,
|
||||
OptionDoc,
|
||||
} from '../../util/structure';
|
||||
|
||||
const DEFAULT_LOCALE = 'en-US';
|
||||
const DEFAULT_MAX_SIZE = 0;
|
||||
const DEFAULT_SPACE_INDENT_AMOUNT = 2;
|
||||
const DEFAULT_TRAILING_COMMA = true;
|
||||
|
||||
export enum Message {
|
||||
SORT_STANDALONE_IMPORTS_ARRAYS = 'sortStandaloneImportsArrays',
|
||||
}
|
||||
|
||||
export interface UniqueDecoratorsOptions {
|
||||
locale: string;
|
||||
maxItems: number;
|
||||
indent: number;
|
||||
trailingComma: boolean;
|
||||
}
|
||||
|
||||
export interface UniqueDecoratorsDocOptions {
|
||||
locale: OptionDoc;
|
||||
maxItems: OptionDoc;
|
||||
indent: OptionDoc;
|
||||
trailingComma: OptionDoc;
|
||||
}
|
||||
|
||||
export const info: DSpaceESLintRuleInfo<[UniqueDecoratorsOptions], [UniqueDecoratorsDocOptions]> = {
|
||||
name: 'sort-standalone-imports',
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Sorts the standalone `@Component` imports alphabetically',
|
||||
},
|
||||
messages: {
|
||||
[Message.SORT_STANDALONE_IMPORTS_ARRAYS]: 'Standalone imports should be sorted alphabetically',
|
||||
},
|
||||
fixable: 'code',
|
||||
type: 'problem',
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
locale: {
|
||||
type: 'string',
|
||||
},
|
||||
maxItems: {
|
||||
type: 'number',
|
||||
},
|
||||
indent: {
|
||||
type: 'number',
|
||||
},
|
||||
trailingComma: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
additionalProperties: false,
|
||||
},
|
||||
],
|
||||
},
|
||||
optionDocs: [
|
||||
{
|
||||
locale: {
|
||||
title: '`locale`',
|
||||
description: 'The locale used to sort the imports.',
|
||||
},
|
||||
maxItems: {
|
||||
title: '`maxItems`',
|
||||
description: 'The maximum number of imports that should be displayed before each import is separated onto its own line.',
|
||||
},
|
||||
indent: {
|
||||
title: '`indent`',
|
||||
description: 'The indent used for the project.',
|
||||
},
|
||||
trailingComma: {
|
||||
title: '`trailingComma`',
|
||||
description: 'Whether the last import should have a trailing comma (only applicable for multiline imports).',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
locale: DEFAULT_LOCALE,
|
||||
maxItems: DEFAULT_MAX_SIZE,
|
||||
indent: DEFAULT_SPACE_INDENT_AMOUNT,
|
||||
trailingComma: DEFAULT_TRAILING_COMMA,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>, [{ locale, maxItems, indent, trailingComma }]: any) {
|
||||
return {
|
||||
['ClassDeclaration > Decorator > CallExpression[callee.name="Component"] > ObjectExpression > Property[key.name="imports"] > ArrayExpression']: (node: TSESTree.ArrayExpression) => {
|
||||
const elements = node.elements.filter((element) => element !== null && (TSESLintASTUtils.isIdentifier(element) || element?.type === TSESTree.AST_NODE_TYPES.CallExpression));
|
||||
const sortedNames: string[] = elements
|
||||
.map((element) => context.sourceCode.getText(element!))
|
||||
.sort((a: string, b: string) => a.localeCompare(b, locale));
|
||||
|
||||
const isSorted: boolean = elements.every((identifier, index) => context.sourceCode.getText(identifier!) === sortedNames[index]);
|
||||
|
||||
const requiresMultiline: boolean = maxItems < node.elements.length;
|
||||
const isMultiline: boolean = /\n/.test(context.sourceCode.getText(node));
|
||||
|
||||
const incorrectFormat: boolean = requiresMultiline !== isMultiline;
|
||||
|
||||
if (isSorted && !incorrectFormat) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.report({
|
||||
node: node.parent,
|
||||
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
|
||||
fix: (fixer: TSESLint.RuleFixer) => {
|
||||
if (requiresMultiline) {
|
||||
const multilineImports: string = sortedNames
|
||||
.map((name: string) => `${' '.repeat(2 * indent)}${name}${trailingComma ? ',' : ''}`)
|
||||
.join(trailingComma ? '\n' : ',\n');
|
||||
|
||||
return fixer.replaceText(node, `[\n${multilineImports}\n${' '.repeat(indent)}]`);
|
||||
} else {
|
||||
return fixer.replaceText(node, `[${sortedNames.join(', ')}]`);
|
||||
}
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests: NamedTests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'should sort multiple imports on separate lines',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
},
|
||||
{
|
||||
name: 'should not inlines singular imports when maxItems is 0',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
},
|
||||
{
|
||||
name: 'should inline singular imports when maxItems is 1',
|
||||
options: [{ maxItems: 1 }],
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [RootComponent],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'should sort multiple imports alphabetically',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RootComponent,
|
||||
AsyncPipe,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
},
|
||||
{
|
||||
name: 'should not put singular imports on one line when maxItems is 0',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [RootComponent],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
},
|
||||
{
|
||||
name: 'should not put singular imports on a separate line when maxItems is 1',
|
||||
options: [{ maxItems: 1 }],
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [RootComponent],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
},
|
||||
{
|
||||
name: 'should not display multiple imports on the same line',
|
||||
code: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [AsyncPipe, RootComponent],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
errors: [
|
||||
{
|
||||
messageId: Message.SORT_STANDALONE_IMPORTS_ARRAYS,
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@Component({
|
||||
selector: 'ds-app',
|
||||
templateUrl: './app.component.html',
|
||||
styleUrls: ['./app.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
RootComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {}`,
|
||||
},
|
||||
],
|
||||
};
|
@@ -52,6 +52,7 @@ export const info = {
|
||||
[Message.WRAPPER_IMPORTS_BASE]: 'Themed component wrapper classes must only import the base class',
|
||||
},
|
||||
},
|
||||
optionDocs: [],
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
@@ -180,7 +181,7 @@ class Something {
|
||||
selector: 'ds-base-test-themable',
|
||||
standalone: true,
|
||||
})
|
||||
class TestThemeableTomponent {
|
||||
class TestThemeableComponent {
|
||||
}
|
||||
`,
|
||||
},
|
||||
@@ -195,7 +196,7 @@ class TestThemeableTomponent {
|
||||
TestThemeableComponent,
|
||||
],
|
||||
})
|
||||
class ThemedTestThemeableTomponent extends ThemedComponent<TestThemeableComponent> {
|
||||
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
|
||||
}
|
||||
`,
|
||||
},
|
||||
|
@@ -53,6 +53,7 @@ Unit tests are exempt from this rule, because they may redefine components using
|
||||
[Message.THEMED]: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'',
|
||||
},
|
||||
},
|
||||
optionDocs: [],
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
|
@@ -63,6 +63,7 @@ There are a few exceptions where the base class can still be used:
|
||||
[Message.BASE_IN_MODULE]: 'Base themeable components shouldn\'t be declared in modules',
|
||||
},
|
||||
},
|
||||
optionDocs: [],
|
||||
defaultOptions: [],
|
||||
} as DSpaceESLintRuleInfo;
|
||||
|
||||
|
280
lint/src/rules/ts/themed-decorators.ts
Normal file
280
lint/src/rules/ts/themed-decorators.ts
Normal file
@@ -0,0 +1,280 @@
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import { fixture } from '../../../test/fixture';
|
||||
import { isTestFile } from '../../util/filter';
|
||||
import {
|
||||
DSpaceESLintRuleInfo,
|
||||
NamedTests,
|
||||
OptionDoc,
|
||||
} from '../../util/structure';
|
||||
import { getFileTheme } from '../../util/theme-support';
|
||||
|
||||
export enum Message {
|
||||
NO_THEME_DECLARED_IN_THEME_FILE = 'noThemeDeclaredInThemeFile',
|
||||
THEME_DECLARED_IN_NON_THEME_FILE = 'themeDeclaredInNonThemeFile',
|
||||
WRONG_THEME_DECLARED_IN_THEME_FILE = 'wrongThemeDeclaredInThemeFile',
|
||||
}
|
||||
|
||||
interface ThemedDecoratorsOption {
|
||||
decorators: { [name: string]: number };
|
||||
}
|
||||
|
||||
interface ThemedDecoratorsDocsOption {
|
||||
decorators: OptionDoc;
|
||||
}
|
||||
|
||||
export const info: DSpaceESLintRuleInfo<[ThemedDecoratorsOption], [ThemedDecoratorsDocsOption]> = {
|
||||
name: 'themed-decorators',
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Entry components with theme support should declare the correct theme',
|
||||
},
|
||||
fixable: 'code',
|
||||
messages: {
|
||||
[Message.NO_THEME_DECLARED_IN_THEME_FILE]: 'No theme declaration in decorator',
|
||||
[Message.THEME_DECLARED_IN_NON_THEME_FILE]: 'There is a theme declaration in decorator, but this file is not part of a theme',
|
||||
[Message.WRONG_THEME_DECLARED_IN_THEME_FILE]: 'Wrong theme declaration in decorator',
|
||||
},
|
||||
type: 'problem',
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
decorators: {
|
||||
type: 'object',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
optionDocs: [
|
||||
{
|
||||
decorators: {
|
||||
title: '`decorators`',
|
||||
description: 'A mapping for all the existing themeable decorators, with the decorator name as the key and the index of the `theme` argument as the value.',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
decorators: {
|
||||
listableObjectComponent: 3,
|
||||
rendersSectionForMenu: 2,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>, options: any) {
|
||||
return {
|
||||
[`ClassDeclaration > Decorator > CallExpression[callee.name=/^(${Object.keys(options[0].decorators).join('|')})$/]`]: (node: TSESTree.CallExpression) => {
|
||||
if (isTestFile(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
||||
// We only support regular method identifiers
|
||||
return;
|
||||
}
|
||||
|
||||
const fileTheme = getFileTheme(context);
|
||||
const themeDeclaration = getDeclaredTheme(options, node as TSESTree.CallExpression);
|
||||
|
||||
if (themeDeclaration === undefined) {
|
||||
if (fileTheme !== undefined) {
|
||||
context.report({
|
||||
messageId: Message.NO_THEME_DECLARED_IN_THEME_FILE,
|
||||
node: node,
|
||||
fix(fixer) {
|
||||
return fixer.insertTextAfter(node.arguments[node.arguments.length - 1], `, '${fileTheme as string}'`);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (themeDeclaration?.type === AST_NODE_TYPES.Literal) {
|
||||
if (fileTheme === undefined) {
|
||||
context.report({
|
||||
messageId: Message.THEME_DECLARED_IN_NON_THEME_FILE,
|
||||
node: themeDeclaration,
|
||||
fix(fixer) {
|
||||
const idx = node.arguments.findIndex((v) => v.range === themeDeclaration.range);
|
||||
|
||||
if (idx === 0) {
|
||||
return fixer.remove(themeDeclaration);
|
||||
} else {
|
||||
const previousArgument = node.arguments[idx - 1];
|
||||
return fixer.removeRange([previousArgument.range[1], themeDeclaration.range[1]]); // todo: comma?
|
||||
}
|
||||
},
|
||||
});
|
||||
} else if (fileTheme !== themeDeclaration?.value) {
|
||||
context.report({
|
||||
messageId: Message.WRONG_THEME_DECLARED_IN_THEME_FILE,
|
||||
node: themeDeclaration,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(themeDeclaration, `'${fileTheme as string}'`);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else if (themeDeclaration?.type === AST_NODE_TYPES.Identifier && themeDeclaration.name === 'undefined') {
|
||||
if (fileTheme !== undefined) {
|
||||
context.report({
|
||||
messageId: Message.NO_THEME_DECLARED_IN_THEME_FILE,
|
||||
node: node,
|
||||
fix(fixer) {
|
||||
return fixer.replaceText(node.arguments[node.arguments.length - 1], `'${fileTheme as string}'`);
|
||||
},
|
||||
});
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected theme declaration');
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests: NamedTests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'theme file declares the correct theme in @listableObjectComponent',
|
||||
code: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/themes/test/app/dynamic-component/dynamic-component.ts'),
|
||||
},
|
||||
{
|
||||
name: 'plain file declares no theme in @listableObjectComponent',
|
||||
code: `
|
||||
@listableObjectComponent(something, somethingElse, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/app/dynamic-component/dynamic-component.ts'),
|
||||
},
|
||||
{
|
||||
name: 'plain file declares explicit undefined theme in @listableObjectComponent',
|
||||
code: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/app/dynamic-component/dynamic-component.ts'),
|
||||
},
|
||||
{
|
||||
name: 'test file declares theme outside of theme directory',
|
||||
code: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/app/dynamic-component/dynamic-component.spec.ts'),
|
||||
},
|
||||
{
|
||||
name: 'only track configured decorators',
|
||||
code: `
|
||||
@something('test')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/app/dynamic-component/dynamic-component.ts'),
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'theme file declares the wrong theme in @listableObjectComponent',
|
||||
code: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test-2')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/themes/test/app/dynamic-component/dynamic-component.ts'),
|
||||
errors: [
|
||||
{
|
||||
messageId: 'wrongThemeDeclaredInThemeFile',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'plain file declares a theme in @listableObjectComponent',
|
||||
code: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test-2')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/app/dynamic-component/dynamic-component.ts'),
|
||||
errors: [
|
||||
{
|
||||
messageId: 'themeDeclaredInNonThemeFile',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@listableObjectComponent(something, somethingElse, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'theme file declares no theme in @listableObjectComponent',
|
||||
code: `
|
||||
@listableObjectComponent(something, somethingElse, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/themes/test-2/app/dynamic-component/dynamic-component.ts'),
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noThemeDeclaredInThemeFile',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test-2')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'theme file declares explicit undefined theme in @listableObjectComponent',
|
||||
code: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, undefined)
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
filename: fixture('src/themes/test-2/app/dynamic-component/dynamic-component.ts'),
|
||||
errors: [
|
||||
{
|
||||
messageId: 'noThemeDeclaredInThemeFile',
|
||||
},
|
||||
],
|
||||
output: `
|
||||
@listableObjectComponent(something, somethingElse, undefined, 'test-2')
|
||||
export class Something extends SomethingElse {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function getDeclaredTheme(options: [ThemedDecoratorsOption], decoratorCall: TSESTree.CallExpression): TSESTree.Node | undefined {
|
||||
const index: number = options[0].decorators[(decoratorCall.callee as TSESTree.Identifier).name];
|
||||
|
||||
if (decoratorCall.arguments.length >= index + 1) {
|
||||
return decoratorCall.arguments[index];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
158
lint/src/rules/ts/themed-wrapper-no-input-defaults.ts
Normal file
158
lint/src/rules/ts/themed-wrapper-no-input-defaults.ts
Normal file
@@ -0,0 +1,158 @@
|
||||
import {
|
||||
ESLintUtils,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
import {
|
||||
DSpaceESLintRuleInfo,
|
||||
NamedTests,
|
||||
} from '../../util/structure';
|
||||
import { isThemedComponentWrapper } from '../../util/theme-support';
|
||||
|
||||
export enum Message {
|
||||
WRAPPER_HAS_INPUT_DEFAULTS = 'wrapperHasInputDefaults',
|
||||
}
|
||||
|
||||
export const info: DSpaceESLintRuleInfo = {
|
||||
name: 'themed-wrapper-no-input-defaults',
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'ThemedComponent wrappers should not declare input defaults (see [DSpace Angular #2164](https://github.com/DSpace/dspace-angular/pull/2164))',
|
||||
},
|
||||
messages: {
|
||||
[Message.WRAPPER_HAS_INPUT_DEFAULTS]: 'ThemedComponent wrapper declares inputs with defaults',
|
||||
},
|
||||
type: 'problem',
|
||||
schema: [],
|
||||
},
|
||||
optionDocs: [],
|
||||
defaultOptions: [],
|
||||
};
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: RuleContext<any, any>, options: any) {
|
||||
return {
|
||||
'ClassBody > PropertyDefinition > Decorator > CallExpression[callee.name=\'Input\']': (node: TSESTree.CallExpression) => {
|
||||
const classDeclaration = (node?.parent?.parent?.parent as TSESTree.Decorator); // todo: clean this up
|
||||
if (!isThemedComponentWrapper(classDeclaration)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const propertyDefinition: TSESTree.PropertyDefinition = (node.parent.parent as any); // todo: clean this up
|
||||
|
||||
if (propertyDefinition.value !== null) {
|
||||
context.report({
|
||||
messageId: Message.WRAPPER_HAS_INPUT_DEFAULTS,
|
||||
node: propertyDefinition.value,
|
||||
// fix(fixer) {
|
||||
// // todo: don't strip type annotations!
|
||||
// // todo: replace default with appropriate type annotation if not present!
|
||||
// return fixer.removeRange([propertyDefinition.key.range[1], (propertyDefinition.value as any).range[1]]);
|
||||
// }
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests: NamedTests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'ThemedComponent wrapper defines an input without a default value',
|
||||
code: `
|
||||
export class TTest extends ThemedComponent<Test> {
|
||||
|
||||
@Input()
|
||||
test;
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'Regular class defines an input with a default value',
|
||||
code: `
|
||||
export class Test {
|
||||
|
||||
@Input()
|
||||
test = 'test';
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'ThemedComponent wrapper defines an input with a default value',
|
||||
code: `
|
||||
export class TTest extends ThemedComponent<Test> {
|
||||
|
||||
@Input()
|
||||
test1 = 'test';
|
||||
|
||||
@Input()
|
||||
test2 = true;
|
||||
|
||||
@Input()
|
||||
test2: number = 123;
|
||||
|
||||
@Input()
|
||||
test3: number[] = [1,2,3];
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'wrapperHasInputDefaults',
|
||||
},
|
||||
{
|
||||
messageId: 'wrapperHasInputDefaults',
|
||||
},
|
||||
{
|
||||
messageId: 'wrapperHasInputDefaults',
|
||||
},
|
||||
{
|
||||
messageId: 'wrapperHasInputDefaults',
|
||||
},
|
||||
],
|
||||
// output: `
|
||||
// export class TTest extends ThemedComponent<Test> {
|
||||
//
|
||||
// @Input()
|
||||
// test1: string;
|
||||
//
|
||||
// @Input()
|
||||
// test2: boolean;
|
||||
//
|
||||
// @Input()
|
||||
// test2: number;
|
||||
//
|
||||
// @Input()
|
||||
// test3: number[];
|
||||
// }
|
||||
// `,
|
||||
},
|
||||
{
|
||||
name: 'ThemedComponent wrapper defines an input with an undefined default value',
|
||||
code: `
|
||||
export class TTest extends ThemedComponent<Test> {
|
||||
|
||||
@Input()
|
||||
test = undefined;
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'wrapperHasInputDefaults',
|
||||
},
|
||||
],
|
||||
// output: `
|
||||
// export class TTest extends ThemedComponent<Test> {
|
||||
//
|
||||
// @Input()
|
||||
// test;
|
||||
// }
|
||||
// `,
|
||||
},
|
||||
],
|
||||
};
|
226
lint/src/rules/ts/unique-decorators.ts
Normal file
226
lint/src/rules/ts/unique-decorators.ts
Normal file
@@ -0,0 +1,226 @@
|
||||
import {
|
||||
AST_NODE_TYPES,
|
||||
ESLintUtils,
|
||||
TSESLint,
|
||||
TSESTree,
|
||||
} from '@typescript-eslint/utils';
|
||||
|
||||
import { isTestFile } from '../../util/filter';
|
||||
import {
|
||||
DSpaceESLintRuleInfo,
|
||||
NamedTests,
|
||||
OptionDoc,
|
||||
} from '../../util/structure';
|
||||
|
||||
export enum Message {
|
||||
DUPLICATE_DECORATOR_CALL = 'duplicateDecoratorCall',
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves the decorators by decoratorName → file → Set<String>
|
||||
*/
|
||||
const decoratorCalls: Map<string, Map<string, Set<string>>> = new Map();
|
||||
|
||||
/**
|
||||
* Keep a list of the files wo contain a decorator. This is done in order to prevent the `Program` selector from being
|
||||
* run for every file.
|
||||
*/
|
||||
const fileWithDecorators: Set<string> = new Set();
|
||||
|
||||
export interface UniqueDecoratorsOptions {
|
||||
decorators: string[];
|
||||
}
|
||||
|
||||
export interface UniqueDecoratorsDocOptions {
|
||||
decorators: OptionDoc;
|
||||
}
|
||||
|
||||
export const info: DSpaceESLintRuleInfo<[UniqueDecoratorsOptions], [UniqueDecoratorsDocOptions]> = {
|
||||
name: 'unique-decorators',
|
||||
meta: {
|
||||
docs: {
|
||||
description: 'Some decorators must be called with unique arguments (e.g. when they construct a mapping based on the argument values)',
|
||||
},
|
||||
messages: {
|
||||
[Message.DUPLICATE_DECORATOR_CALL]: 'Duplicate decorator call',
|
||||
},
|
||||
type: 'problem',
|
||||
schema: [
|
||||
{
|
||||
type: 'object',
|
||||
properties: {
|
||||
decorators: {
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
optionDocs: [
|
||||
{
|
||||
decorators: {
|
||||
title: '`decorators`',
|
||||
description: 'The list of all the decorators for which you want to enforce this behavior.',
|
||||
},
|
||||
},
|
||||
],
|
||||
defaultOptions: [
|
||||
{
|
||||
decorators: [
|
||||
'listableObjectComponent', // todo: must take default arguments into account!
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
export const rule = ESLintUtils.RuleCreator.withoutDocs({
|
||||
...info,
|
||||
create(context: TSESLint.RuleContext<Message, unknown[]>, options: any) {
|
||||
|
||||
return {
|
||||
['Program']: () => {
|
||||
if (fileWithDecorators.has(context.physicalFilename)) {
|
||||
for (const decorator of options[0].decorators) {
|
||||
decoratorCalls.get(decorator)?.get(context.physicalFilename)?.clear();
|
||||
}
|
||||
}
|
||||
},
|
||||
[`ClassDeclaration > Decorator > CallExpression[callee.name=/^(${options[0].decorators.join('|')})$/]`]: (node: TSESTree.CallExpression) => {
|
||||
if (isTestFile(context)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.callee.type !== AST_NODE_TYPES.Identifier) {
|
||||
// We only support regular method identifiers
|
||||
return;
|
||||
}
|
||||
|
||||
fileWithDecorators.add(context.physicalFilename);
|
||||
|
||||
if (!isUnique(node, context.physicalFilename)) {
|
||||
context.report({
|
||||
messageId: Message.DUPLICATE_DECORATOR_CALL,
|
||||
node: node,
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const tests: NamedTests = {
|
||||
plugin: info.name,
|
||||
valid: [
|
||||
{
|
||||
name: 'checked decorator, no repetitions',
|
||||
code: `
|
||||
@listableObjectComponent(a)
|
||||
export class A {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a, 'b')
|
||||
export class B {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a, 'b', 3)
|
||||
export class C {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a, 'b', 3, Enum.TEST1)
|
||||
export class C {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a, 'b', 3, Enum.TEST2)
|
||||
export class C {
|
||||
}
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: 'unchecked decorator, some repetitions',
|
||||
code: `
|
||||
@something(a)
|
||||
export class A {
|
||||
}
|
||||
|
||||
@something(a)
|
||||
export class B {
|
||||
}
|
||||
`,
|
||||
},
|
||||
],
|
||||
invalid: [
|
||||
{
|
||||
name: 'checked decorator, some repetitions',
|
||||
code: `
|
||||
@listableObjectComponent(a)
|
||||
export class A {
|
||||
}
|
||||
|
||||
@listableObjectComponent(a)
|
||||
export class B {
|
||||
}
|
||||
`,
|
||||
errors: [
|
||||
{
|
||||
messageId: 'duplicateDecoratorCall',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function callKey(node: TSESTree.CallExpression): string {
|
||||
let key = '';
|
||||
|
||||
for (const arg of node.arguments) {
|
||||
switch ((arg as TSESTree.Node).type) {
|
||||
// todo: can we make this more generic somehow?
|
||||
case AST_NODE_TYPES.Identifier:
|
||||
key += (arg as TSESTree.Identifier).name;
|
||||
break;
|
||||
case AST_NODE_TYPES.Literal:
|
||||
// eslint-disable-next-line @typescript-eslint/no-base-to-string
|
||||
key += (arg as TSESTree.Literal).value;
|
||||
break;
|
||||
case AST_NODE_TYPES.MemberExpression:
|
||||
key += (arg as any).object.name + '.' + (arg as any).property.name;
|
||||
break;
|
||||
default:
|
||||
throw new Error(`Unrecognized decorator argument type: ${arg.type}`);
|
||||
}
|
||||
|
||||
key += ', ';
|
||||
}
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
function isUnique(node: TSESTree.CallExpression, filePath: string): boolean {
|
||||
const decorator = (node.callee as TSESTree.Identifier).name;
|
||||
|
||||
if (!decoratorCalls.has(decorator)) {
|
||||
decoratorCalls.set(decorator, new Map());
|
||||
}
|
||||
|
||||
if (!decoratorCalls.get(decorator)!.has(filePath)) {
|
||||
decoratorCalls.get(decorator)!.set(filePath, new Set());
|
||||
}
|
||||
|
||||
const key = callKey(node);
|
||||
|
||||
let unique = true;
|
||||
|
||||
for (const decoratorCallsByFile of decoratorCalls.get(decorator)!.values()) {
|
||||
if (decoratorCallsByFile.has(key)) {
|
||||
unique = !unique;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
decoratorCalls.get(decorator)?.get(filePath)?.add(key);
|
||||
|
||||
return unique;
|
||||
}
|
10
lint/src/util/filter.ts
Normal file
10
lint/src/util/filter.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
/**
|
||||
* Determine whether the current file is a test file
|
||||
* @param context the current ESLint rule context
|
||||
*/
|
||||
export function isTestFile(context: RuleContext<any, any>): boolean {
|
||||
// note: shouldn't use plain .filename (doesn't work in DSpace Angular 7.4)
|
||||
return context.getFilename()?.endsWith('.spec.ts') ;
|
||||
}
|
@@ -17,10 +17,16 @@ export type Meta = RuleMetaData<string, unknown[]>;
|
||||
export type Valid = ValidTestCase<unknown[]>;
|
||||
export type Invalid = InvalidTestCase<string, unknown[]>;
|
||||
|
||||
export interface DSpaceESLintRuleInfo {
|
||||
export interface DSpaceESLintRuleInfo<T = unknown[], D = unknown[]> {
|
||||
name: string;
|
||||
meta: Meta,
|
||||
defaultOptions: unknown[],
|
||||
optionDocs: D,
|
||||
defaultOptions: T,
|
||||
}
|
||||
|
||||
export interface OptionDoc {
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface NamedTests {
|
||||
|
@@ -7,6 +7,11 @@ _______
|
||||
|
||||
[Source code](../../../../lint/src/rules/<%- plugin.name.replace('dspace-angular-', '') %>/<%- rule.name %>.ts)
|
||||
|
||||
<% if (rule.optionDocs?.length > 0) { %>
|
||||
### Options
|
||||
<%- rule.optionDocs.map(optionDoc => Object.keys(optionDoc).map(option => '\n#### ' + optionDoc[option].title + '\n\n' + optionDoc[option].description)) %>
|
||||
<% } %>
|
||||
|
||||
### Examples
|
||||
|
||||
<% if (tests.valid) {%>
|
||||
@@ -19,6 +24,13 @@ Filename: `<%- test.filename %>`
|
||||
```<%- plugin.language.toLowerCase() %>
|
||||
<%- test.code.trim() %>
|
||||
```
|
||||
<% if (test?.options?.length > 0) { %>
|
||||
With options:
|
||||
|
||||
```json
|
||||
<%- JSON.stringify(test.options[0], null, 2) %>
|
||||
```
|
||||
<% }%>
|
||||
<% }) %>
|
||||
<% } %>
|
||||
|
||||
@@ -31,6 +43,15 @@ Filename: `<%- test.filename %>`
|
||||
<% } %>
|
||||
```<%- plugin.language.toLowerCase() %>
|
||||
<%- test.code.trim() %>
|
||||
|
||||
<% if (test?.options?.length > 0) { %>
|
||||
With options:
|
||||
|
||||
```json
|
||||
<%- JSON.stringify(test.options[0], null, 2) %>
|
||||
```
|
||||
<% }%>
|
||||
|
||||
```
|
||||
Will produce the following error(s):
|
||||
```
|
||||
|
@@ -7,6 +7,7 @@
|
||||
*/
|
||||
|
||||
import { TSESTree } from '@typescript-eslint/utils';
|
||||
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
|
||||
import { readFileSync } from 'fs';
|
||||
import { basename } from 'path';
|
||||
import ts, { Identifier } from 'typescript';
|
||||
@@ -263,3 +264,18 @@ export const DISALLOWED_THEME_SELECTORS = 'ds-(base|themed)-';
|
||||
export function fixSelectors(text: string): string {
|
||||
return text.replaceAll(/ds-(base|themed)-/g, 'ds-');
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the theme of the current file based on its path in the project.
|
||||
* @param context the current ESLint rule context
|
||||
*/
|
||||
export function getFileTheme(context: RuleContext<any, any>): string | undefined {
|
||||
// note: shouldn't use plain .filename (doesn't work in DSpace Angular 7.4)
|
||||
const m = context.getFilename()?.match(/\/src\/themes\/([^/]+)\//);
|
||||
|
||||
if (m?.length === 2) {
|
||||
return m[1];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
@@ -6,6 +6,8 @@
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { RuleMetaData } from '@typescript-eslint/utils/ts-eslint';
|
||||
|
||||
import { default as html } from '../src/rules/html';
|
||||
import { default as ts } from '../src/rules/ts';
|
||||
|
||||
@@ -69,6 +71,16 @@ describe('plugin structure', () => {
|
||||
expect(ruleExports.tests.valid.length).toBeGreaterThan(0);
|
||||
expect(ruleExports.tests.invalid.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('should contain a valid ESLint rule', () => {
|
||||
// we don't have a better way to enforce this, but it's something at least
|
||||
expect((ruleExports.rule as any).name).toBeUndefined(
|
||||
'Rules should be passed to RuleCreator, omitting info.name since it is not part of the RuleWithMeta interface',
|
||||
);
|
||||
|
||||
expect(ruleExports.rule.create).toBeTruthy();
|
||||
expect(ruleExports.rule.meta).toEqual(ruleExports.info.meta as RuleMetaData<string, []>);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
595
package-lock.json
generated
595
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
28
package.json
28
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dspace-angular",
|
||||
"version": "9.0.0-next",
|
||||
"version": "9.1.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"config:watch": "nodemon",
|
||||
@@ -102,8 +102,8 @@
|
||||
"@angular/platform-browser-dynamic": "^18.2.12",
|
||||
"@angular/platform-server": "^18.2.12",
|
||||
"@angular/router": "^18.2.12",
|
||||
"@angular/ssr": "^18.2.18",
|
||||
"@babel/runtime": "7.26.0",
|
||||
"@angular/ssr": "^18.2.20",
|
||||
"@babel/runtime": "7.27.6",
|
||||
"@kolkov/ngx-gallery": "^2.0.1",
|
||||
"@ng-bootstrap/ng-bootstrap": "^12.0.0",
|
||||
"@ng-dynamic-forms/core": "^16.0.0",
|
||||
@@ -117,14 +117,14 @@
|
||||
"@terraformer/wkt": "^2.2.1",
|
||||
"altcha": "^0.9.0",
|
||||
"angulartics2": "^12.2.0",
|
||||
"axios": "^1.8.4",
|
||||
"axios": "^1.10.0",
|
||||
"bootstrap": "^5.3",
|
||||
"cerialize": "0.1.18",
|
||||
"cli-progress": "^3.12.0",
|
||||
"colors": "^1.4.0",
|
||||
"compression": "^1.7.5",
|
||||
"cookie-parser": "1.4.7",
|
||||
"core-js": "^3.41.0",
|
||||
"core-js": "^3.42.0",
|
||||
"date-fns": "^2.29.3",
|
||||
"date-fns-tz": "^1.3.7",
|
||||
"deepmerge": "^4.3.1",
|
||||
@@ -133,9 +133,9 @@
|
||||
"express-rate-limit": "^5.1.3",
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"filesize": "^10.1.6",
|
||||
"http-proxy-middleware": "^2.0.7",
|
||||
"http-proxy-middleware": "^2.0.9",
|
||||
"http-terminator": "^3.2.0",
|
||||
"isbot": "^5.1.25",
|
||||
"isbot": "^5.1.28",
|
||||
"js-cookie": "2.2.1",
|
||||
"js-yaml": "^4.1.0",
|
||||
"json5": "^2.2.3",
|
||||
@@ -168,7 +168,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@angular-builders/custom-webpack": "~18.0.0",
|
||||
"@angular-devkit/build-angular": "^18.2.18",
|
||||
"@angular-devkit/build-angular": "^18.2.20",
|
||||
"@angular-eslint/builder": "^18.4.1",
|
||||
"@angular-eslint/bundled-angular-compiler": "^18.4.1",
|
||||
"@angular-eslint/eslint-plugin": "^18.4.1",
|
||||
@@ -176,13 +176,13 @@
|
||||
"@angular-eslint/schematics": "^18.4.1",
|
||||
"@angular-eslint/template-parser": "^18.4.1",
|
||||
"@angular-eslint/utils": "^18.4.1",
|
||||
"@angular/cli": "^18.2.18",
|
||||
"@angular/cli": "^18.2.20",
|
||||
"@angular/compiler-cli": "^18.2.12",
|
||||
"@angular/language-service": "^18.2.12",
|
||||
"@cypress/schematic": "^1.5.0",
|
||||
"@fortawesome/fontawesome-free": "^6.7.2",
|
||||
"@ngrx/store-devtools": "^18.1.1",
|
||||
"@ngtools/webpack": "^18.2.18",
|
||||
"@ngtools/webpack": "^18.2.19",
|
||||
"@types/deep-freeze": "0.1.5",
|
||||
"@types/ejs": "^3.1.2",
|
||||
"@types/express": "^4.17.17",
|
||||
@@ -209,7 +209,7 @@
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"eslint-plugin-import-newlines": "^1.3.1",
|
||||
"eslint-plugin-jsdoc": "^45.0.0",
|
||||
"eslint-plugin-jsonc": "^2.19.1",
|
||||
"eslint-plugin-jsonc": "^2.20.1",
|
||||
"eslint-plugin-lodash": "^7.4.0",
|
||||
"eslint-plugin-rxjs": "^5.0.3",
|
||||
"eslint-plugin-simple-import-sort": "^10.0.0",
|
||||
@@ -224,7 +224,7 @@
|
||||
"karma-jasmine": "~4.0.0",
|
||||
"karma-jasmine-html-reporter": "^1.5.0",
|
||||
"karma-mocha-reporter": "2.2.5",
|
||||
"ng-mocks": "^14.13.4",
|
||||
"ng-mocks": "^14.13.5",
|
||||
"ngx-mask": "14.2.4",
|
||||
"nodemon": "^2.0.22",
|
||||
"postcss": "^8.5",
|
||||
@@ -232,12 +232,12 @@
|
||||
"postcss-loader": "^4.0.3",
|
||||
"postcss-preset-env": "^7.4.2",
|
||||
"rimraf": "^3.0.2",
|
||||
"sass": "~1.86.3",
|
||||
"sass": "~1.89.2",
|
||||
"sass-loader": "^12.6.0",
|
||||
"sass-resources-loader": "^2.2.5",
|
||||
"ts-node": "^8.10.2",
|
||||
"typescript": "~5.4.5",
|
||||
"webpack": "5.99.5",
|
||||
"webpack": "5.99.9",
|
||||
"webpack-cli": "^5.1.4",
|
||||
"webpack-dev-server": "^4.15.1"
|
||||
}
|
||||
|
18
server.ts
18
server.ts
@@ -58,6 +58,7 @@ import {
|
||||
REQUEST,
|
||||
RESPONSE,
|
||||
} from './src/express.tokens';
|
||||
import { SsrExcludePatterns } from "./src/config/ssr-config.interface";
|
||||
|
||||
/*
|
||||
* Set path for the browser application's dist folder
|
||||
@@ -221,7 +222,7 @@ export function app() {
|
||||
* The callback function to serve server side angular
|
||||
*/
|
||||
function ngApp(req, res, next) {
|
||||
if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || environment.ssr.paths.some(pathPrefix => req.path.startsWith(pathPrefix)))) {
|
||||
if (environment.ssr.enabled && req.method === 'GET' && (req.path === '/' || !isExcludedFromSsr(req.path, environment.ssr.excludePathPatterns))) {
|
||||
// Render the page to user via SSR (server side rendering)
|
||||
serverSideRender(req, res, next);
|
||||
} else {
|
||||
@@ -627,6 +628,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
|
||||
*/
|
||||
|
@@ -51,16 +51,16 @@ import { BrowserOnlyPipe } from '../../../shared/utils/browser-only.pipe';
|
||||
},
|
||||
],
|
||||
imports: [
|
||||
PaginationComponent,
|
||||
AsyncPipe,
|
||||
NgbAccordionModule,
|
||||
TranslateModule,
|
||||
NgbNavModule,
|
||||
ThemedSearchComponent,
|
||||
BrowserOnlyPipe,
|
||||
NgxPaginationModule,
|
||||
SelectableListItemControlComponent,
|
||||
ListableObjectComponentLoaderComponent,
|
||||
NgbAccordionModule,
|
||||
NgbNavModule,
|
||||
NgxPaginationModule,
|
||||
PaginationComponent,
|
||||
SelectableListItemControlComponent,
|
||||
ThemedSearchComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -26,10 +26,10 @@ import { BulkAccessSettingsComponent } from './settings/bulk-access-settings.com
|
||||
templateUrl: './bulk-access.component.html',
|
||||
styleUrls: ['./bulk-access.component.scss'],
|
||||
imports: [
|
||||
TranslateModule,
|
||||
BulkAccessSettingsComponent,
|
||||
BulkAccessBrowseComponent,
|
||||
BtnDisabledDirective,
|
||||
BulkAccessBrowseComponent,
|
||||
BulkAccessSettingsComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
|
@@ -14,9 +14,9 @@ import { AccessControlFormContainerComponent } from '../../../shared/access-cont
|
||||
styleUrls: ['./bulk-access-settings.component.scss'],
|
||||
exportAs: 'dsBulkSettings',
|
||||
imports: [
|
||||
AccessControlFormContainerComponent,
|
||||
NgbAccordionModule,
|
||||
TranslateModule,
|
||||
AccessControlFormContainerComponent,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -27,7 +27,7 @@ import {
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||
@@ -85,7 +85,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
}), this.allEpeople));
|
||||
},
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
return of(this.activeEPerson);
|
||||
},
|
||||
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
if (scope === 'email') {
|
||||
@@ -129,7 +129,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||
return (ePerson2.uuid !== ePerson.uuid);
|
||||
});
|
||||
return observableOf(true);
|
||||
return of(true);
|
||||
},
|
||||
editEPerson(ePerson: EPerson) {
|
||||
this.activeEPerson = ePerson;
|
||||
@@ -145,7 +145,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
},
|
||||
};
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
isAuthorized: of(true),
|
||||
});
|
||||
builderService = getMockFormBuilderService();
|
||||
|
||||
@@ -180,7 +180,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
fixture = TestBed.createComponent(EPeopleRegistryComponent);
|
||||
component = fixture.componentInstance;
|
||||
modalService = TestBed.inject(NgbModal);
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: of(true) }) }));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
@@ -261,7 +261,7 @@ describe('EPeopleRegistryComponent', () => {
|
||||
|
||||
|
||||
it('should hide delete EPerson button when the isAuthorized returns false', () => {
|
||||
spyOn(authorizationService, 'isAuthorized').and.returnValue(observableOf(false));
|
||||
spyOn(authorizationService, 'isAuthorized').and.returnValue(of(false));
|
||||
component.initialisePage();
|
||||
fixture.detectChanges();
|
||||
|
||||
|
@@ -67,14 +67,14 @@ import { EPersonFormComponent } from './eperson-form/eperson-form.component';
|
||||
selector: 'ds-epeople-registry',
|
||||
templateUrl: './epeople-registry.component.html',
|
||||
imports: [
|
||||
TranslateModule,
|
||||
RouterModule,
|
||||
AsyncPipe,
|
||||
EPersonFormComponent,
|
||||
ReactiveFormsModule,
|
||||
ThemedLoadingComponent,
|
||||
PaginationComponent,
|
||||
NgClass,
|
||||
PaginationComponent,
|
||||
ReactiveFormsModule,
|
||||
RouterModule,
|
||||
ThemedLoadingComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -30,7 +30,7 @@
|
||||
</div>
|
||||
}
|
||||
@if (canImpersonate$ | async) {
|
||||
<div between class="btn-group ms-1">
|
||||
<div between class="btn-group">
|
||||
@if (!isImpersonated) {
|
||||
<button class="btn btn-primary" type="button" (click)="impersonate()">
|
||||
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}
|
||||
|
@@ -25,7 +25,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthService } from '../../../core/auth/auth.service';
|
||||
@@ -91,7 +91,7 @@ describe('EPersonFormComponent', () => {
|
||||
activeEPerson: null,
|
||||
allEpeople: mockEPeople,
|
||||
getActiveEPerson(): Observable<EPerson> {
|
||||
return observableOf(this.activeEPerson);
|
||||
return of(this.activeEPerson);
|
||||
},
|
||||
searchByScope(scope: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<EPerson>>> {
|
||||
if (scope === 'email') {
|
||||
@@ -115,7 +115,7 @@ describe('EPersonFormComponent', () => {
|
||||
this.allEpeople = this.allEpeople.filter((ePerson2: EPerson) => {
|
||||
return (ePerson2.uuid !== ePerson.uuid);
|
||||
});
|
||||
return observableOf(true);
|
||||
return of(true);
|
||||
},
|
||||
create(ePerson: EPerson): Observable<RemoteData<EPerson>> {
|
||||
this.allEpeople = [...this.allEpeople, ePerson];
|
||||
@@ -210,7 +210,7 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
authService = new AuthServiceStub();
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
isAuthorized: of(true),
|
||||
|
||||
});
|
||||
groupsDataService = jasmine.createSpyObj('groupsDataService', {
|
||||
@@ -389,7 +389,7 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
describe('without active EPerson', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(undefined));
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(of(undefined));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -429,7 +429,7 @@ describe('EPersonFormComponent', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(observableOf(expectedWithId));
|
||||
spyOn(ePersonDataServiceStub, 'getActiveEPerson').and.returnValue(of(expectedWithId));
|
||||
component.ngOnInit();
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
@@ -485,10 +485,10 @@ describe('EPersonFormComponent', () => {
|
||||
spyOn(authService, 'impersonate').and.callThrough();
|
||||
eperson = EPersonMock;
|
||||
component.epersonInitial = eperson;
|
||||
component.canDelete$ = observableOf(true);
|
||||
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(observableOf(eperson));
|
||||
component.canDelete$ = of(true);
|
||||
spyOn(component.epersonService, 'getActiveEPerson').and.returnValue(of(eperson));
|
||||
modalService = (component as any).modalService;
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: of(true) }) }));
|
||||
component.ngOnInit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -499,7 +499,7 @@ describe('EPersonFormComponent', () => {
|
||||
});
|
||||
|
||||
it('the delete button should be hidden if the ePerson cannot be deleted', () => {
|
||||
component.canDelete$ = observableOf(false);
|
||||
component.canDelete$ = of(false);
|
||||
fixture.detectChanges();
|
||||
const deleteButton = fixture.debugElement.query(By.css('.delete-button'));
|
||||
expect(deleteButton).toBeNull();
|
||||
|
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
} from '@angular/common';
|
||||
import { AsyncPipe } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
@@ -30,7 +27,7 @@ import {
|
||||
import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
@@ -81,15 +78,14 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
|
||||
selector: 'ds-eperson-form',
|
||||
templateUrl: './eperson-form.component.html',
|
||||
imports: [
|
||||
FormComponent,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
NgClass,
|
||||
ThemedLoadingComponent,
|
||||
BtnDisabledDirective,
|
||||
FormComponent,
|
||||
HasNoValuePipe,
|
||||
PaginationComponent,
|
||||
RouterLink,
|
||||
HasNoValuePipe,
|
||||
BtnDisabledDirective,
|
||||
ThemedLoadingComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -361,7 +357,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
|
||||
this.groups$ = this.activeEPerson$.pipe(
|
||||
switchMap((eperson) => {
|
||||
return observableCombineLatest([observableOf(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||
return observableCombineLatest([of(eperson), this.paginationService.getFindListOptions(this.config.id, {
|
||||
currentPage: 1,
|
||||
elementsPerPage: this.config.pageSize,
|
||||
})]);
|
||||
@@ -370,7 +366,7 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
if (eperson != null) {
|
||||
return this.groupsDataService.findListByHref(eperson._links.groups.href, findListOptions, true, true, followLink('object'));
|
||||
}
|
||||
return observableOf(undefined);
|
||||
return of(undefined);
|
||||
}),
|
||||
);
|
||||
|
||||
@@ -383,14 +379,14 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
if (hasValue(eperson)) {
|
||||
return this.authorizationService.isAuthorized(FeatureID.LoginOnBehalfOf, eperson.self);
|
||||
} else {
|
||||
return observableOf(false);
|
||||
return of(false);
|
||||
}
|
||||
}),
|
||||
);
|
||||
this.canDelete$ = this.activeEPerson$.pipe(
|
||||
switchMap((eperson) => this.authorizationService.isAuthorized(FeatureID.CanDelete, hasValue(eperson) ? eperson.self : undefined)),
|
||||
);
|
||||
this.canReset$ = observableOf(true);
|
||||
this.canReset$ = of(true);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -544,16 +540,16 @@ export class EPersonFormComponent implements OnInit, OnDestroy {
|
||||
take(1),
|
||||
switchMap((confirm: boolean) => {
|
||||
if (confirm && hasValue(eperson.id)) {
|
||||
this.canDelete$ = observableOf(false);
|
||||
this.canDelete$ = of(false);
|
||||
return this.epersonService.deleteEPerson(eperson).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((restResponse: RemoteData<NoContent>) => ({ restResponse, eperson })),
|
||||
);
|
||||
} else {
|
||||
return observableOf(null);
|
||||
return of(null);
|
||||
}
|
||||
}),
|
||||
finalize(() => this.canDelete$ = observableOf(true)),
|
||||
finalize(() => this.canDelete$ = of(true)),
|
||||
);
|
||||
}),
|
||||
).subscribe(({ restResponse, eperson }: { restResponse: RemoteData<NoContent> | null, eperson: EPerson }) => {
|
||||
|
@@ -27,7 +27,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||
@@ -116,7 +116,7 @@ describe('GroupFormComponent', () => {
|
||||
activeGroup: null,
|
||||
createdGroup: null,
|
||||
getActiveGroup(): Observable<Group> {
|
||||
return observableOf(this.activeGroup);
|
||||
return of(this.activeGroup);
|
||||
},
|
||||
getGroupRegistryRouterLink(): string {
|
||||
return '/access-control/groups';
|
||||
@@ -137,7 +137,7 @@ describe('GroupFormComponent', () => {
|
||||
this.activeGroup = null;
|
||||
},
|
||||
findById(id: string) {
|
||||
return observableOf({ payload: null, hasSucceeded: true });
|
||||
return of({ payload: null, hasSucceeded: true });
|
||||
},
|
||||
findByHref(href: string) {
|
||||
return createSuccessfulRemoteDataObject$(this.createdGroup);
|
||||
@@ -164,7 +164,7 @@ describe('GroupFormComponent', () => {
|
||||
},
|
||||
};
|
||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
||||
isAuthorized: observableOf(true),
|
||||
isAuthorized: of(true),
|
||||
});
|
||||
dsoDataServiceStub = {
|
||||
findByHref(href: string): Observable<RemoteData<DSpaceObject>> {
|
||||
@@ -330,7 +330,7 @@ describe('GroupFormComponent', () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(observableOf(expected));
|
||||
spyOn(groupsDataServiceStub, 'getActiveGroup').and.returnValue(of(expected));
|
||||
spyOn(groupsDataServiceStub, 'patch').and.returnValue(createSuccessfulRemoteDataObject$(expected2));
|
||||
component.ngOnInit();
|
||||
});
|
||||
@@ -417,7 +417,7 @@ describe('GroupFormComponent', () => {
|
||||
},
|
||||
});
|
||||
spyOn(component.submitForm, 'emit');
|
||||
spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(observableOf(expected));
|
||||
spyOn(dsoDataServiceStub, 'findByHref').and.returnValue(of(expected));
|
||||
|
||||
fixture.detectChanges();
|
||||
component.initialisePage();
|
||||
@@ -471,11 +471,11 @@ describe('GroupFormComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
spyOn(groupsDataServiceStub, 'delete').and.callThrough();
|
||||
component.activeGroup$ = observableOf({
|
||||
component.activeGroup$ = of({
|
||||
id: 'active-group',
|
||||
permanent: false,
|
||||
} as Group);
|
||||
component.canEdit$ = observableOf(true);
|
||||
component.canEdit$ = of(true);
|
||||
|
||||
component.initialisePage();
|
||||
|
||||
|
@@ -87,13 +87,13 @@ import { ValidateGroupExists } from './validators/group-exists.validator';
|
||||
selector: 'ds-group-form',
|
||||
templateUrl: './group-form.component.html',
|
||||
imports: [
|
||||
FormComponent,
|
||||
AlertComponent,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
ContextHelpDirective,
|
||||
FormComponent,
|
||||
MembersListComponent,
|
||||
SubgroupsListComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -83,7 +83,7 @@
|
||||
|
||||
<form [formGroup]="searchForm" (ngSubmit)="search(searchForm.value)" class="d-flex justify-content-between">
|
||||
<div class="flex-grow-1 me-3">
|
||||
<div class="form-group input-group me-3">
|
||||
<div class="mb-3 input-group me-3">
|
||||
<input type="text" name="query" id="query" formControlName="query"
|
||||
class="form-control" aria-label="Search input">
|
||||
<span class="input-group-append">
|
||||
|
@@ -32,7 +32,7 @@ import {
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||
@@ -113,7 +113,7 @@ describe('MembersListComponent', () => {
|
||||
epersonMembers: epersonMembers,
|
||||
epersonNonMembers: epersonNonMembers,
|
||||
getActiveGroup(): Observable<Group> {
|
||||
return observableOf(activeGroup);
|
||||
return of(activeGroup);
|
||||
},
|
||||
getEPersonMembers() {
|
||||
return this.epersonMembers;
|
||||
@@ -127,7 +127,7 @@ describe('MembersListComponent', () => {
|
||||
this.epersonNonMembers.splice(index, 1);
|
||||
}
|
||||
});
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
return of(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
clearGroupsRequests() {
|
||||
// empty
|
||||
@@ -147,7 +147,7 @@ describe('MembersListComponent', () => {
|
||||
});
|
||||
// Add eperson to list of non-members
|
||||
this.epersonNonMembers = [...this.epersonNonMembers, epersonToDelete];
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
return of(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
};
|
||||
builderService = getMockFormBuilderService();
|
||||
|
@@ -25,7 +25,7 @@ import {
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
ObservedValueOf,
|
||||
of as observableOf,
|
||||
of,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
@@ -103,14 +103,14 @@ export interface EPersonListActionConfig {
|
||||
selector: 'ds-members-list',
|
||||
templateUrl: './members-list.component.html',
|
||||
imports: [
|
||||
TranslateModule,
|
||||
ContextHelpDirective,
|
||||
ReactiveFormsModule,
|
||||
PaginationComponent,
|
||||
AsyncPipe,
|
||||
RouterLink,
|
||||
NgClass,
|
||||
BtnDisabledDirective,
|
||||
ContextHelpDirective,
|
||||
NgClass,
|
||||
PaginationComponent,
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -260,7 +260,7 @@ export class MembersListComponent implements OnInit, OnDestroy {
|
||||
* @param possibleMember EPerson that is a possible member (being tested) of the group currently being edited
|
||||
*/
|
||||
isMemberOfGroup(possibleMember: EPerson): Observable<boolean> {
|
||||
return observableOf(true);
|
||||
return of(true);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -31,7 +31,7 @@ import {
|
||||
} from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import { EPersonMock2 } from 'src/app/shared/testing/eperson.mock';
|
||||
|
||||
@@ -108,7 +108,7 @@ describe('SubgroupsListComponent', () => {
|
||||
subgroups: subgroups,
|
||||
groupNonMembers: groupNonMembers,
|
||||
getActiveGroup(): Observable<Group> {
|
||||
return observableOf(this.activeGroup);
|
||||
return of(this.activeGroup);
|
||||
},
|
||||
getSubgroups(): Group {
|
||||
return this.subgroups;
|
||||
@@ -138,7 +138,7 @@ describe('SubgroupsListComponent', () => {
|
||||
this.groupNonMembers.splice(index, 1);
|
||||
}
|
||||
});
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
return of(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
clearGroupsRequests() {
|
||||
// empty
|
||||
@@ -155,7 +155,7 @@ describe('SubgroupsListComponent', () => {
|
||||
});
|
||||
// Add group to list of non-members
|
||||
this.groupNonMembers = [...this.groupNonMembers, subgroupToDelete];
|
||||
return observableOf(new RestResponse(true, 200, 'Success'));
|
||||
return of(new RestResponse(true, 200, 'Success'));
|
||||
},
|
||||
};
|
||||
routerStub = new RouterMock();
|
||||
|
@@ -59,12 +59,12 @@ enum SubKey {
|
||||
selector: 'ds-subgroups-list',
|
||||
templateUrl: './subgroups-list.component.html',
|
||||
imports: [
|
||||
RouterLink,
|
||||
AsyncPipe,
|
||||
ContextHelpDirective,
|
||||
TranslateModule,
|
||||
ReactiveFormsModule,
|
||||
PaginationComponent,
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
} from '@angular/router';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
@@ -37,7 +37,7 @@ describe('GroupPageGuard', () => {
|
||||
|
||||
function init() {
|
||||
halEndpointService = jasmine.createSpyObj(['getEndpoint']);
|
||||
( halEndpointService as any ).getEndpoint.and.returnValue(observableOf(groupsEndpointUrl));
|
||||
( halEndpointService as any ).getEndpoint.and.returnValue(of(groupsEndpointUrl));
|
||||
|
||||
authorizationService = jasmine.createSpyObj(['isAuthorized']);
|
||||
// NOTE: value is set in beforeEach
|
||||
@@ -46,7 +46,7 @@ describe('GroupPageGuard', () => {
|
||||
( router as any ).parseUrl.and.returnValue = {};
|
||||
|
||||
authService = jasmine.createSpyObj(['isAuthenticated']);
|
||||
( authService as any ).isAuthenticated.and.returnValue(observableOf(true));
|
||||
( authService as any ).isAuthenticated.and.returnValue(of(true));
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
@@ -69,7 +69,7 @@ describe('GroupPageGuard', () => {
|
||||
describe('canActivate', () => {
|
||||
describe('when the current user can manage the group', () => {
|
||||
beforeEach(() => {
|
||||
( authorizationService as any ).isAuthorized.and.returnValue(observableOf(true));
|
||||
( authorizationService as any ).isAuthorized.and.returnValue(of(true));
|
||||
});
|
||||
|
||||
it('should return true', (done) => {
|
||||
@@ -89,7 +89,7 @@ describe('GroupPageGuard', () => {
|
||||
|
||||
describe('when the current user can not manage the group', () => {
|
||||
beforeEach(() => {
|
||||
(authorizationService as any).isAuthorized.and.returnValue(observableOf(false));
|
||||
(authorizationService as any).isAuthorized.and.returnValue(of(false));
|
||||
});
|
||||
|
||||
it('should not return true', (done) => {
|
||||
|
@@ -6,7 +6,7 @@ import {
|
||||
} from '@angular/router';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@@ -33,6 +33,6 @@ export const groupPageGuard = (
|
||||
getObjectUrl = defaultGroupPageGetObjectUrl,
|
||||
getEPersonUuid?: StringGuardParamFn,
|
||||
): CanActivateFn => someFeatureAuthorizationGuard(
|
||||
() => observableOf([FeatureID.CanManageGroup]),
|
||||
() => of([FeatureID.CanManageGroup]),
|
||||
getObjectUrl,
|
||||
getEPersonUuid);
|
||||
|
@@ -85,7 +85,7 @@
|
||||
}
|
||||
@if (!groupDto.group?.permanent && groupDto.ableToDelete) {
|
||||
<button
|
||||
(click)="deleteGroup(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
|
||||
(click)="confirmDelete(groupDto)" class="btn btn-outline-danger btn-sm btn-delete"
|
||||
title="{{messagePrefix + 'table.edit.buttons.remove' | translate: {name: dsoNameService.getName(groupDto.group) } }}">
|
||||
<i class="fas fa-trash-alt fa-fw"></i>
|
||||
</button>
|
||||
|
@@ -25,7 +25,6 @@ import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
|
||||
@@ -95,11 +94,11 @@ describe('GroupsRegistryComponent', () => {
|
||||
(authorizationService as any).isAuthorized.and.callFake((featureId?: FeatureID) => {
|
||||
switch (featureId) {
|
||||
case FeatureID.AdministratorOf:
|
||||
return observableOf(isAdmin);
|
||||
return of(isAdmin);
|
||||
case FeatureID.CanManageGroup:
|
||||
return observableOf(canManageGroup);
|
||||
return of(canManageGroup);
|
||||
case FeatureID.CanDelete:
|
||||
return observableOf(true);
|
||||
return of(true);
|
||||
default:
|
||||
throw new Error(`setIsAuthorized: this fake implementation does not support ${featureId}.`);
|
||||
}
|
||||
@@ -382,6 +381,8 @@ describe('GroupsRegistryComponent', () => {
|
||||
deleteButton.click();
|
||||
fixture.detectChanges();
|
||||
|
||||
(document as any).querySelector('.modal-footer .confirm').click();
|
||||
|
||||
expect(groupsDataServiceStub.delete).toHaveBeenCalledWith(mockGroups[0].id);
|
||||
});
|
||||
});
|
||||
|
@@ -9,7 +9,10 @@ import {
|
||||
UntypedFormBuilder,
|
||||
} from '@angular/forms';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
NgbModal,
|
||||
NgbTooltipModule,
|
||||
} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
@@ -19,7 +22,7 @@ import {
|
||||
combineLatest as observableCombineLatest,
|
||||
EMPTY,
|
||||
Observable,
|
||||
of as observableOf,
|
||||
of,
|
||||
Subscription,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
@@ -27,6 +30,7 @@ import {
|
||||
defaultIfEmpty,
|
||||
map,
|
||||
switchMap,
|
||||
takeUntil,
|
||||
tap,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
@@ -57,6 +61,7 @@ import {
|
||||
} from '../../core/shared/operators';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
|
||||
import { ConfirmationModalComponent } from '../../shared/confirmation-modal/confirmation-modal.component';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
@@ -68,14 +73,14 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
selector: 'ds-groups-registry',
|
||||
templateUrl: './groups-registry.component.html',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
BtnDisabledDirective,
|
||||
NgbTooltipModule,
|
||||
PaginationComponent,
|
||||
ReactiveFormsModule,
|
||||
RouterLink,
|
||||
ThemedLoadingComponent,
|
||||
TranslateModule,
|
||||
RouterLink,
|
||||
ReactiveFormsModule,
|
||||
AsyncPipe,
|
||||
PaginationComponent,
|
||||
NgbTooltipModule,
|
||||
BtnDisabledDirective,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -142,6 +147,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
private paginationService: PaginationService,
|
||||
public requestService: RequestService,
|
||||
public dsoNameService: DSONameService,
|
||||
private modalService: NgbModal,
|
||||
) {
|
||||
this.currentSearchQuery = '';
|
||||
this.searchForm = this.formBuilder.group(({
|
||||
@@ -179,7 +185,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
getRemoteDataPayload(),
|
||||
switchMap((groups: PaginatedList<Group>) => {
|
||||
if (groups.page.length === 0) {
|
||||
return observableOf(buildPaginatedList(groups.pageInfo, []));
|
||||
return of(buildPaginatedList(groups.pageInfo, []));
|
||||
}
|
||||
return this.authorizationService.isAuthorized(FeatureID.AdministratorOf).pipe(
|
||||
switchMap((isSiteAdmin: boolean) => {
|
||||
@@ -224,7 +230,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
|
||||
canManageGroup$(isSiteAdmin: boolean, group: Group): Observable<boolean> {
|
||||
if (isSiteAdmin) {
|
||||
return observableOf(true);
|
||||
return of(true);
|
||||
} else {
|
||||
return this.authorizationService.isAuthorized(FeatureID.CanManageGroup, group.self);
|
||||
}
|
||||
@@ -283,7 +289,7 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
return this.dSpaceObjectDataService.findByHref(group._links.object.href).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
map((rd: RemoteData<DSpaceObject>) => hasValue(rd) && hasValue(rd.payload)),
|
||||
catchError(() => observableOf(false)),
|
||||
catchError(() => of(false)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -314,4 +320,30 @@ export class GroupsRegistryComponent implements OnInit, OnDestroy {
|
||||
this.paginationService.clearPagination(this.config.id);
|
||||
}
|
||||
|
||||
confirmDelete(group: GroupDtoModel): void {
|
||||
const modalRef = this.modalService.open(ConfirmationModalComponent);
|
||||
modalRef.componentInstance.name = this.dsoNameService.getName(group.group);
|
||||
modalRef.componentInstance.headerLabel = 'admin.access-control.epeople.table.edit.buttons.remove.modal.header';
|
||||
modalRef.componentInstance.infoLabel = 'admin.access-control.epeople.table.edit.buttons.remove.modal.info';
|
||||
modalRef.componentInstance.cancelLabel = 'admin.access-control.epeople.table.edit.buttons.remove.modal.cancel';
|
||||
modalRef.componentInstance.confirmLabel = 'admin.access-control.epeople.table.edit.buttons.remove.modal.confirm';
|
||||
modalRef.componentInstance.brandColor = 'danger';
|
||||
modalRef.componentInstance.confirmIcon = 'fas fa-trash';
|
||||
|
||||
const modalSub: Subscription = modalRef.componentInstance.response.pipe(
|
||||
takeUntil(modalRef.closed),
|
||||
).subscribe((result: boolean) => {
|
||||
if (result === true) {
|
||||
this.deleteGroup(group);
|
||||
}
|
||||
});
|
||||
|
||||
void modalRef.result.then().finally(() => {
|
||||
modalRef.close();
|
||||
if (modalSub && !modalSub.closed) {
|
||||
modalSub.unsubscribe();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
11
src/app/accessibility/accessibility-settings.config.ts
Normal file
11
src/app/accessibility/accessibility-settings.config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Config } from '../../config/config.interface';
|
||||
|
||||
/**
|
||||
* Configuration interface used by the AccessibilitySettingsService
|
||||
*/
|
||||
export class AccessibilitySettingsConfig implements Config {
|
||||
/**
|
||||
* The duration in days after which the accessibility settings cookie expires
|
||||
*/
|
||||
cookieExpirationDuration: number;
|
||||
}
|
419
src/app/accessibility/accessibility-settings.service.spec.ts
Normal file
419
src/app/accessibility/accessibility-settings.service.spec.ts
Normal file
@@ -0,0 +1,419 @@
|
||||
import {
|
||||
fakeAsync,
|
||||
flush,
|
||||
} from '@angular/core/testing';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { AppConfig } from '../../config/app-config.interface';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||
import { CookieService } from '../core/services/cookie.service';
|
||||
import { OrejimeServiceStub } from '../shared/cookies/orejime.service.stub';
|
||||
import { CookieServiceMock } from '../shared/mocks/cookie.service.mock';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject$,
|
||||
} from '../shared/remote-data.utils';
|
||||
import { AuthServiceStub } from '../shared/testing/auth-service.stub';
|
||||
import {
|
||||
ACCESSIBILITY_COOKIE,
|
||||
ACCESSIBILITY_SETTINGS_METADATA_KEY,
|
||||
AccessibilitySettings,
|
||||
AccessibilitySettingsFormValues,
|
||||
AccessibilitySettingsService,
|
||||
FullAccessibilitySettings,
|
||||
} from './accessibility-settings.service';
|
||||
|
||||
|
||||
describe('accessibilitySettingsService', () => {
|
||||
let service: AccessibilitySettingsService;
|
||||
let cookieService: CookieServiceMock;
|
||||
let authService: AuthServiceStub;
|
||||
let ePersonService: EPersonDataService;
|
||||
let orejimeService: OrejimeServiceStub;
|
||||
let appConfig: AppConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
cookieService = new CookieServiceMock();
|
||||
authService = new AuthServiceStub();
|
||||
orejimeService = new OrejimeServiceStub();
|
||||
appConfig = { accessibility: { cookieExpirationDuration: 10 } } as AppConfig;
|
||||
|
||||
orejimeService.getSavedPreferences.and.returnValue(of({ accessibility: true }));
|
||||
|
||||
ePersonService = jasmine.createSpyObj('ePersonService', {
|
||||
createPatchFromCache: of([{
|
||||
op: 'add',
|
||||
value: null,
|
||||
}]),
|
||||
patch: of({}),
|
||||
});
|
||||
|
||||
service = new AccessibilitySettingsService(
|
||||
cookieService as unknown as CookieService,
|
||||
authService as unknown as AuthService,
|
||||
ePersonService,
|
||||
orejimeService,
|
||||
appConfig,
|
||||
);
|
||||
});
|
||||
|
||||
describe('get', () => {
|
||||
it('should return the setting if it is set', () => {
|
||||
const settings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.getAll = jasmine.createSpy('getAll').and.returnValue(of(settings));
|
||||
|
||||
service.get('notificationTimeOut', 'default').subscribe(value =>
|
||||
expect(value).toEqual('1000'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the default value if the setting is not set', () => {
|
||||
const settings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.getAll = jasmine.createSpy('getAll').and.returnValue(of(settings));
|
||||
|
||||
service.get('liveRegionTimeOut', 'default').subscribe(value =>
|
||||
expect(value).toEqual('default'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAsNumber', () => {
|
||||
it('should return the setting as number if the value for the setting can be parsed to a number', () => {
|
||||
service.get = jasmine.createSpy('get').and.returnValue(of('1000'));
|
||||
|
||||
service.getAsNumber('notificationTimeOut').subscribe(value =>
|
||||
expect(value).toEqual(1000),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the default value if no value is set for the setting', () => {
|
||||
service.get = jasmine.createSpy('get').and.returnValue(of(null));
|
||||
|
||||
service.getAsNumber('notificationTimeOut', 123).subscribe(value =>
|
||||
expect(value).toEqual(123),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return the default value if the value for the setting can not be parsed to a number', () => {
|
||||
service.get = jasmine.createSpy('get').and.returnValue(of('text'));
|
||||
|
||||
service.getAsNumber('notificationTimeOut', 123).subscribe(value =>
|
||||
expect(value).toEqual(123),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAll', () => {
|
||||
it('should attempt to get the settings from metadata first', () => {
|
||||
service.getAllSettingsFromAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ }));
|
||||
|
||||
service.getAll().subscribe();
|
||||
expect(service.getAllSettingsFromAuthenticatedUserMetadata).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should attempt to get the settings from the cookie if the settings from metadata are empty', () => {
|
||||
service.getAllSettingsFromAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ }));
|
||||
|
||||
service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ });
|
||||
|
||||
service.getAll().subscribe();
|
||||
expect(service.getAllSettingsFromCookie).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not attempt to get the settings from the cookie if the settings from metadata are not empty', () => {
|
||||
const settings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.getAllSettingsFromAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of(settings));
|
||||
|
||||
service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ });
|
||||
|
||||
service.getAll().subscribe();
|
||||
expect(service.getAllSettingsFromCookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return an empty object if both are empty', () => {
|
||||
service.getAllSettingsFromAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata').and.returnValue(of({ }));
|
||||
|
||||
service.getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({ });
|
||||
|
||||
service.getAll().subscribe(value => expect(value).toEqual({}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllSettingsFromCookie', () => {
|
||||
it('should retrieve the settings from the cookie', () => {
|
||||
cookieService.get = jasmine.createSpy();
|
||||
|
||||
service.getAllSettingsFromCookie();
|
||||
expect(cookieService.get).toHaveBeenCalledWith(ACCESSIBILITY_COOKIE);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllSettingsFromAuthenticatedUserMetadata', () => {
|
||||
it('should retrieve all settings from the user\'s metadata', () => {
|
||||
const settings = { 'liveRegionTimeOut': '1000' };
|
||||
|
||||
const user = new EPerson();
|
||||
user.setMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY, null, JSON.stringify(settings));
|
||||
|
||||
authService.getAuthenticatedUserFromStoreIfAuthenticated =
|
||||
jasmine.createSpy('getAuthenticatedUserFromStoreIfAuthenticated').and.returnValue(of(user));
|
||||
|
||||
service.getAllSettingsFromAuthenticatedUserMetadata().subscribe(value =>
|
||||
expect(value).toEqual(settings),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('set', () => {
|
||||
it('should correctly update the chosen setting', () => {
|
||||
service.updateSettings = jasmine.createSpy('updateSettings');
|
||||
|
||||
service.set('liveRegionTimeOut', '500');
|
||||
expect(service.updateSettings).toHaveBeenCalledWith({ liveRegionTimeOut: '500' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSettings', () => {
|
||||
beforeEach(() => {
|
||||
service.setSettingsInCookie = jasmine.createSpy('setSettingsInCookie').and.returnValue(of('cookie'));
|
||||
});
|
||||
|
||||
it('should attempt to set settings in metadata', () => {
|
||||
service.setSettingsInAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of('failed'));
|
||||
|
||||
const settings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.setSettings(settings).subscribe();
|
||||
expect(service.setSettingsInAuthenticatedUserMetadata).toHaveBeenCalledWith(settings);
|
||||
});
|
||||
|
||||
it('should set settings in cookie if metadata failed', () => {
|
||||
service.setSettingsInAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(false));
|
||||
|
||||
const settings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.setSettings(settings).subscribe();
|
||||
expect(service.setSettingsInCookie).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not set settings in cookie if metadata succeeded', () => {
|
||||
service.setSettingsInAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of('metadata'));
|
||||
|
||||
const settings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.setSettings(settings).subscribe();
|
||||
expect(service.setSettingsInCookie).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should return \'metadata\' if settings are stored in metadata', () => {
|
||||
service.setSettingsInAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of('metadata'));
|
||||
|
||||
const settings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.setSettings(settings).subscribe(value =>
|
||||
expect(value).toEqual('metadata'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should return \'cookie\' if settings are stored in cookie', () => {
|
||||
service.setSettingsInAuthenticatedUserMetadata =
|
||||
jasmine.createSpy('setSettingsInAuthenticatedUserMetadata').and.returnValue(of(false));
|
||||
|
||||
const settings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.setSettings(settings).subscribe(value =>
|
||||
expect(value).toEqual('cookie'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateSettings', () => {
|
||||
it('should call setSettings with the updated settings', () => {
|
||||
const beforeSettings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
};
|
||||
|
||||
service.getAll = jasmine.createSpy('getAll').and.returnValue(of(beforeSettings));
|
||||
service.setSettings = jasmine.createSpy('setSettings').and.returnValue(of('cookie'));
|
||||
|
||||
const newSettings: AccessibilitySettings = {
|
||||
liveRegionTimeOut: '2000',
|
||||
};
|
||||
|
||||
const combinedSettings: AccessibilitySettings = {
|
||||
notificationTimeOut: '1000',
|
||||
liveRegionTimeOut: '2000',
|
||||
};
|
||||
|
||||
service.updateSettings(newSettings).subscribe();
|
||||
expect(service.setSettings).toHaveBeenCalledWith(combinedSettings);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setSettingsInAuthenticatedUserMetadata', () => {
|
||||
beforeEach(() => {
|
||||
service.setSettingsInMetadata = jasmine.createSpy('setSettingsInMetadata').and.returnValue(of(null));
|
||||
});
|
||||
|
||||
it('should store settings in metadata when the user is authenticated', fakeAsync(() => {
|
||||
const user = new EPerson();
|
||||
authService.getAuthenticatedUserFromStoreIfAuthenticated = jasmine.createSpy().and.returnValue(of(user));
|
||||
|
||||
service.setSettingsInAuthenticatedUserMetadata({}).subscribe();
|
||||
flush();
|
||||
|
||||
expect(service.setSettingsInMetadata).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should emit "failed" when the user is not authenticated', fakeAsync(() => {
|
||||
authService.getAuthenticatedUserFromStoreIfAuthenticated = jasmine.createSpy().and.returnValue(of(null));
|
||||
|
||||
service.setSettingsInAuthenticatedUserMetadata({})
|
||||
.subscribe(value => expect(value).toEqual('failed'));
|
||||
flush();
|
||||
|
||||
expect(service.setSettingsInMetadata).not.toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('setSettingsInMetadata', () => {
|
||||
const ePerson = new EPerson();
|
||||
|
||||
beforeEach(() => {
|
||||
ePerson.setMetadata = jasmine.createSpy('setMetadata');
|
||||
ePerson.removeMetadata = jasmine.createSpy('removeMetadata');
|
||||
});
|
||||
|
||||
it('should set the settings in metadata', () => {
|
||||
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe();
|
||||
expect(ePerson.setMetadata).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should remove the metadata when the settings are emtpy', () => {
|
||||
service.setSettingsInMetadata(ePerson, {}).subscribe();
|
||||
expect(ePerson.setMetadata).not.toHaveBeenCalled();
|
||||
expect(ePerson.removeMetadata).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create a patch with the metadata changes', () => {
|
||||
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe();
|
||||
expect(ePersonService.createPatchFromCache).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should send the patch request', () => {
|
||||
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' }).subscribe();
|
||||
expect(ePersonService.patch).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should emit "metadata" when the update succeeded', fakeAsync(() => {
|
||||
ePersonService.patch = jasmine.createSpy().and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||
|
||||
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' })
|
||||
.subscribe(value => {
|
||||
expect(value).toEqual('metadata');
|
||||
});
|
||||
|
||||
flush();
|
||||
}));
|
||||
|
||||
it('should emit "failed" when the update failed', fakeAsync(() => {
|
||||
ePersonService.patch = jasmine.createSpy().and.returnValue(createFailedRemoteDataObject$());
|
||||
|
||||
service.setSettingsInMetadata(ePerson, { ['liveRegionTimeOut']: '500' })
|
||||
.subscribe(value => {
|
||||
expect(value).toEqual('failed');
|
||||
});
|
||||
|
||||
flush();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('setSettingsInCookie', () => {
|
||||
beforeEach(() => {
|
||||
cookieService.set = jasmine.createSpy('set');
|
||||
cookieService.remove = jasmine.createSpy('remove');
|
||||
});
|
||||
|
||||
it('should fail to store settings in the cookie when the user has not accepted the cookie', fakeAsync(() => {
|
||||
orejimeService.getSavedPreferences.and.returnValue(of({ accessibility: false }));
|
||||
|
||||
service.setSettingsInCookie({ ['liveRegionTimeOut']: '500' }).subscribe(value => {
|
||||
expect(value).toEqual('failed');
|
||||
});
|
||||
flush();
|
||||
expect(cookieService.set).not.toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should store the settings in a cookie', fakeAsync(() => {
|
||||
service.setSettingsInCookie({ ['liveRegionTimeOut']: '500' }).subscribe(value => {
|
||||
expect(value).toEqual('cookie');
|
||||
});
|
||||
flush();
|
||||
expect(cookieService.set).toHaveBeenCalled();
|
||||
}));
|
||||
|
||||
it('should remove the cookie when the settings are empty', fakeAsync(() => {
|
||||
service.setSettingsInCookie({}).subscribe(value => {
|
||||
expect(value).toEqual('cookie');
|
||||
});
|
||||
|
||||
flush();
|
||||
|
||||
expect(cookieService.set).not.toHaveBeenCalled();
|
||||
expect(cookieService.remove).toHaveBeenCalled();
|
||||
}));
|
||||
});
|
||||
|
||||
describe('convertFormValuesToStoredValues', () => {
|
||||
it('should reset the notificationTimeOut when timeOut is enabled but set to "0"', () => {
|
||||
const formValues: AccessibilitySettingsFormValues = {
|
||||
notificationTimeOutEnabled: true,
|
||||
notificationTimeOut: '0',
|
||||
liveRegionTimeOut: null,
|
||||
};
|
||||
|
||||
const storedValues: FullAccessibilitySettings = service.convertFormValuesToStoredValues(formValues);
|
||||
expect('notificationTimeOut' in storedValues).toBeFalse();
|
||||
});
|
||||
});
|
||||
|
||||
it('should keep the notificationTimeOut when timeOut is enabled and differs from "0"', () => {
|
||||
const formValues: AccessibilitySettingsFormValues = {
|
||||
notificationTimeOutEnabled: true,
|
||||
notificationTimeOut: '3',
|
||||
liveRegionTimeOut: null,
|
||||
};
|
||||
|
||||
const storedValues: FullAccessibilitySettings = service.convertFormValuesToStoredValues(formValues);
|
||||
expect('notificationTimeOut' in storedValues).toBeTrue();
|
||||
});
|
||||
});
|
45
src/app/accessibility/accessibility-settings.service.stub.ts
Normal file
45
src/app/accessibility/accessibility-settings.service.stub.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { AccessibilitySettingsService } from './accessibility-settings.service';
|
||||
|
||||
export function getAccessibilitySettingsServiceStub(): AccessibilitySettingsService {
|
||||
return new AccessibilitySettingsServiceStub() as unknown as AccessibilitySettingsService;
|
||||
}
|
||||
|
||||
export class AccessibilitySettingsServiceStub {
|
||||
getAllAccessibilitySettingKeys = jasmine.createSpy('getAllAccessibilitySettingKeys').and.returnValue([]);
|
||||
|
||||
get = jasmine.createSpy('get').and.returnValue(of(null));
|
||||
|
||||
getAsNumber = jasmine.createSpy('getAsNumber').and.returnValue(of(0));
|
||||
|
||||
getAll = jasmine.createSpy('getAll').and.returnValue(of({}));
|
||||
|
||||
getAllSettingsFromCookie = jasmine.createSpy('getAllSettingsFromCookie').and.returnValue({});
|
||||
|
||||
getAllSettingsFromAuthenticatedUserMetadata = jasmine.createSpy('getAllSettingsFromAuthenticatedUserMetadata')
|
||||
.and.returnValue(of({}));
|
||||
|
||||
set = jasmine.createSpy('setSettings').and.returnValue(of('cookie'));
|
||||
|
||||
updateSettings = jasmine.createSpy('updateSettings').and.returnValue(of('cookie'));
|
||||
|
||||
setSettingsInAuthenticatedUserMetadata = jasmine.createSpy('setSettingsInAuthenticatedUserMetadata')
|
||||
.and.returnValue(of(false));
|
||||
|
||||
setSettingsInMetadata = jasmine.createSpy('setSettingsInMetadata').and.returnValue(of(false));
|
||||
|
||||
setSettingsInCookie = jasmine.createSpy('setSettingsInCookie');
|
||||
|
||||
getInputType = jasmine.createSpy('getInputType').and.returnValue('text');
|
||||
|
||||
convertFormValuesToStoredValues = jasmine.createSpy('convertFormValuesToStoredValues').and.returnValue({});
|
||||
|
||||
convertStoredValuesToFormValues = jasmine.createSpy('convertStoredValuesToFormValues').and.returnValue({});
|
||||
|
||||
getDefaultValue = jasmine.createSpy('getPlaceholder').and.returnValue('placeholder');
|
||||
|
||||
isValid = jasmine.createSpy('isValid').and.returnValue(true);
|
||||
|
||||
formValuesValid = jasmine.createSpy('allValid').and.returnValue(true);
|
||||
}
|
381
src/app/accessibility/accessibility-settings.service.ts
Normal file
381
src/app/accessibility/accessibility-settings.service.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
import {
|
||||
Inject,
|
||||
Injectable,
|
||||
Optional,
|
||||
} from '@angular/core';
|
||||
import cloneDeep from 'lodash/cloneDeep';
|
||||
import {
|
||||
combineLatest,
|
||||
Observable,
|
||||
of,
|
||||
switchMap,
|
||||
} from 'rxjs';
|
||||
import {
|
||||
map,
|
||||
take,
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import {
|
||||
APP_CONFIG,
|
||||
AppConfig,
|
||||
} from '../../config/app-config.interface';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
import { EPersonDataService } from '../core/eperson/eperson-data.service';
|
||||
import { EPerson } from '../core/eperson/models/eperson.model';
|
||||
import { CookieService } from '../core/services/cookie.service';
|
||||
import { getFirstCompletedRemoteData } from '../core/shared/operators';
|
||||
import { OrejimeService } from '../shared/cookies/orejime.service';
|
||||
import {
|
||||
hasNoValue,
|
||||
hasValue,
|
||||
isNotEmpty,
|
||||
} from '../shared/empty.util';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
|
||||
/**
|
||||
* Name of the cookie used to store the settings locally
|
||||
*/
|
||||
export const ACCESSIBILITY_COOKIE = 'dsAccessibilityCookie';
|
||||
|
||||
/**
|
||||
* Name of the metadata field to store settings on the ePerson
|
||||
*/
|
||||
export const ACCESSIBILITY_SETTINGS_METADATA_KEY = 'dspace.accessibility.settings';
|
||||
|
||||
/**
|
||||
* Array containing all possible accessibility settings.
|
||||
* When adding new settings, make sure to add the new setting to the accessibility-settings component form.
|
||||
* The converter methods to convert from stored format to form format (and vice-versa) need to be updated as well.
|
||||
*/
|
||||
export const accessibilitySettingKeys = ['notificationTimeOut', 'liveRegionTimeOut'] as const;
|
||||
|
||||
/**
|
||||
* Type representing the possible accessibility settings
|
||||
*/
|
||||
export type AccessibilitySetting = typeof accessibilitySettingKeys[number];
|
||||
|
||||
/**
|
||||
* Type representing an object that contains accessibility settings values for all accessibility settings.
|
||||
*/
|
||||
export type FullAccessibilitySettings = { [key in AccessibilitySetting]: string };
|
||||
|
||||
/**
|
||||
* Type representing an object that contains accessibility settings values for some accessibility settings.
|
||||
*/
|
||||
export type AccessibilitySettings = Partial<FullAccessibilitySettings>;
|
||||
|
||||
/**
|
||||
* The accessibility settings object format used by the accessibility-settings component form.
|
||||
*/
|
||||
export interface AccessibilitySettingsFormValues {
|
||||
notificationTimeOutEnabled: boolean,
|
||||
notificationTimeOut: string,
|
||||
liveRegionTimeOut: string,
|
||||
}
|
||||
|
||||
/**
|
||||
* Service handling the retrieval and configuration of accessibility settings.
|
||||
*
|
||||
* This service stores the configured settings in either a cookie or on the user's metadata depending on whether
|
||||
* the user is authenticated.
|
||||
*/
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AccessibilitySettingsService {
|
||||
|
||||
constructor(
|
||||
protected cookieService: CookieService,
|
||||
protected authService: AuthService,
|
||||
protected ePersonService: EPersonDataService,
|
||||
@Optional() protected orejimeService: OrejimeService,
|
||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored value for the provided {@link AccessibilitySetting}. If the value does not exist or if it is empty,
|
||||
* the provided defaultValue is emitted instead.
|
||||
*/
|
||||
get(setting: AccessibilitySetting, defaultValue: string = null): Observable<string> {
|
||||
return this.getAll().pipe(
|
||||
map(settings => settings[setting]),
|
||||
map(value => isNotEmpty(value) ? value : defaultValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the stored value for the provided {@link AccessibilitySetting} as a number. If the stored value
|
||||
* could not be converted to a number, the value of the defaultValue parameter is emitted instead.
|
||||
*/
|
||||
getAsNumber(setting: AccessibilitySetting, defaultValue: number = null): Observable<number> {
|
||||
return this.get(setting).pipe(
|
||||
map(value => hasValue(value) ? parseInt(value, 10) : NaN),
|
||||
map(number => !isNaN(number) ? number : defaultValue),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all currently stored accessibility settings
|
||||
*/
|
||||
getAll(): Observable<AccessibilitySettings> {
|
||||
return this.getAllSettingsFromAuthenticatedUserMetadata().pipe(
|
||||
map(value => isNotEmpty(value) ? value : this.getAllSettingsFromCookie()),
|
||||
map(value => isNotEmpty(value) ? value : {}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all settings from the accessibility settings cookie
|
||||
*/
|
||||
getAllSettingsFromCookie(): AccessibilitySettings {
|
||||
return this.cookieService.get(ACCESSIBILITY_COOKIE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to retrieve all settings from the authenticated user's metadata.
|
||||
* Returns an empty object when no user is authenticated.
|
||||
*/
|
||||
getAllSettingsFromAuthenticatedUserMetadata(): Observable<AccessibilitySettings> {
|
||||
return this.authService.getAuthenticatedUserFromStoreIfAuthenticated().pipe(
|
||||
take(1),
|
||||
map(user => hasValue(user) && hasValue(user.firstMetadataValue(ACCESSIBILITY_SETTINGS_METADATA_KEY)) ?
|
||||
JSON.parse(user.firstMetadataValue(ACCESSIBILITY_SETTINGS_METADATA_KEY)) :
|
||||
{},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a single accessibility setting value, leaving all other settings unchanged.
|
||||
* When setting all values, {@link AccessibilitySettingsService#setSettings} should be used.
|
||||
* When updating multiple values, {@link AccessibilitySettingsService#updateSettings} should be used.
|
||||
*
|
||||
* Returns 'cookie' when the changes were stored in the cookie.
|
||||
* Returns 'metadata' when the changes were stored in metadata.
|
||||
* Returns 'failed' when both options failed.
|
||||
*/
|
||||
set(setting: AccessibilitySetting, value: string): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||
return this.updateSettings({ [setting]: value });
|
||||
}
|
||||
|
||||
/**
|
||||
* Set all accessibility settings simultaneously.
|
||||
* This method removes existing settings if they are missing from the provided {@link AccessibilitySettings} object.
|
||||
* Removes all settings if the provided object is empty.
|
||||
*
|
||||
* Returns 'cookie' when the changes were stored in the cookie.
|
||||
* Returns 'metadata' when the changes were stored in metadata.
|
||||
* Returns 'failed' when both options failed.
|
||||
*/
|
||||
setSettings(settings: AccessibilitySettings): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||
return this.setSettingsInAuthenticatedUserMetadata(settings).pipe(
|
||||
take(1),
|
||||
map(saveLocation => saveLocation === 'metadata'),
|
||||
switchMap((savedInMetadata) =>
|
||||
savedInMetadata ? ofMetadata() : this.setSettingsInCookie(settings),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update multiple accessibility settings simultaneously.
|
||||
* This method does not change the settings that are missing from the provided {@link AccessibilitySettings} object.
|
||||
*
|
||||
* Returns 'cookie' when the changes were stored in the cookie.
|
||||
* Returns 'metadata' when the changes were stored in metadata.
|
||||
* Returns 'failed' when both options failed.
|
||||
*/
|
||||
updateSettings(settings: AccessibilitySettings): Observable<'metadata' | 'cookie' | 'failed'> {
|
||||
return this.getAll().pipe(
|
||||
take(1),
|
||||
map(currentSettings => Object.assign({}, currentSettings, settings)),
|
||||
switchMap(newSettings => this.setSettings(newSettings)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set the provided settings on the currently authorized user's metadata.
|
||||
* Emits false when no user is authenticated or when the metadata update failed.
|
||||
* Emits 'metadata' when the metadata update succeeded, and 'failed' otherwise.
|
||||
*/
|
||||
setSettingsInAuthenticatedUserMetadata(settings: AccessibilitySettings): Observable<'metadata' | 'failed'> {
|
||||
return this.authService.getAuthenticatedUserFromStoreIfAuthenticated().pipe(
|
||||
take(1),
|
||||
switchMap(user => {
|
||||
if (hasValue(user)) {
|
||||
// EPerson has to be cloned, otherwise the EPerson's metadata can't be modified
|
||||
const clonedUser = cloneDeep(user);
|
||||
return this.setSettingsInMetadata(clonedUser, settings);
|
||||
} else {
|
||||
return ofFailed();
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set the provided settings on the user's metadata.
|
||||
* Emits false when the update failed, true when the update succeeded.
|
||||
*/
|
||||
setSettingsInMetadata(
|
||||
user: EPerson,
|
||||
settings: AccessibilitySettings,
|
||||
): Observable<'metadata' | 'failed'> {
|
||||
if (isNotEmpty(settings)) {
|
||||
user.setMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY, null, JSON.stringify(settings));
|
||||
} else {
|
||||
user.removeMetadata(ACCESSIBILITY_SETTINGS_METADATA_KEY);
|
||||
}
|
||||
|
||||
return this.ePersonService.createPatchFromCache(user).pipe(
|
||||
take(1),
|
||||
switchMap(operations =>
|
||||
isNotEmpty(operations) ? this.ePersonService.patch(user, operations) : createSuccessfulRemoteDataObject$({})),
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap(rd => rd.hasSucceeded ? ofMetadata() : ofFailed()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to set the provided settings in a cookie.
|
||||
* Emits 'failed' when setting in a cookie failed due to the cookie not being accepted, 'cookie' when it succeeded.
|
||||
*/
|
||||
setSettingsInCookie(settings: AccessibilitySettings): Observable<'cookie' | 'failed'> {
|
||||
if (hasNoValue(this.orejimeService)) {
|
||||
return of('failed');
|
||||
}
|
||||
|
||||
return this.orejimeService.getSavedPreferences().pipe(
|
||||
map(preferences => preferences.accessibility),
|
||||
map((accessibilityCookieAccepted: boolean) => {
|
||||
if (accessibilityCookieAccepted) {
|
||||
if (isNotEmpty(settings)) {
|
||||
this.cookieService.set(ACCESSIBILITY_COOKIE, settings, { expires: this.appConfig.accessibility.cookieExpirationDuration });
|
||||
} else {
|
||||
this.cookieService.remove(ACCESSIBILITY_COOKIE);
|
||||
}
|
||||
|
||||
return 'cookie';
|
||||
} else {
|
||||
return 'failed';
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all settings in the cookie and attempts to clear settings in metadata.
|
||||
* Emits an array mentioning which settings succeeded or failed.
|
||||
*/
|
||||
clearSettings(): Observable<['cookie' | 'failed', 'metadata' | 'failed']> {
|
||||
return combineLatest([
|
||||
this.setSettingsInCookie({}),
|
||||
this.setSettingsInAuthenticatedUserMetadata({}),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the default value to be used for the provided AccessibilitySetting.
|
||||
* Returns an empty string when no default value is specified for the provided setting.
|
||||
*/
|
||||
getDefaultValue(setting: AccessibilitySetting): string {
|
||||
switch (setting) {
|
||||
case 'notificationTimeOut':
|
||||
return millisecondsToSeconds(environment.notifications.timeOut.toString());
|
||||
case 'liveRegionTimeOut':
|
||||
return millisecondsToSeconds(environment.liveRegion.messageTimeOutDurationMs.toString());
|
||||
default:
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert values in the provided accessibility settings object to values ready to be stored.
|
||||
*/
|
||||
convertFormValuesToStoredValues(settings: AccessibilitySettingsFormValues): FullAccessibilitySettings {
|
||||
const storedValues = {
|
||||
notificationTimeOut: settings.notificationTimeOutEnabled ?
|
||||
secondsToMilliseconds(settings.notificationTimeOut) : '0',
|
||||
liveRegionTimeOut: secondsToMilliseconds(settings.liveRegionTimeOut),
|
||||
};
|
||||
|
||||
// When the user enables the timeout but does not change the timeout duration from 0,
|
||||
// it is removed from the values to be stored so the default value is used.
|
||||
// Keeping it at 0 would mean the notifications are not automatically removed.
|
||||
if (settings.notificationTimeOutEnabled && settings.notificationTimeOut === '0') {
|
||||
delete storedValues.notificationTimeOut;
|
||||
}
|
||||
|
||||
return storedValues;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert values in the provided accessibility settings object to values ready to show in the form.
|
||||
*/
|
||||
convertStoredValuesToFormValues(settings: AccessibilitySettings): AccessibilitySettingsFormValues {
|
||||
return {
|
||||
notificationTimeOutEnabled: parseFloat(settings.notificationTimeOut) !== 0,
|
||||
notificationTimeOut: millisecondsToSeconds(settings.notificationTimeOut),
|
||||
liveRegionTimeOut: millisecondsToSeconds(settings.liveRegionTimeOut),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the provided AccessibilitySetting is valid in regard to the provided formValues.
|
||||
*/
|
||||
isValid(setting: AccessibilitySetting, formValues: AccessibilitySettingsFormValues): boolean {
|
||||
switch (setting) {
|
||||
case 'notificationTimeOut':
|
||||
return formValues.notificationTimeOutEnabled ?
|
||||
hasNoValue(formValues.notificationTimeOut) || parseFloat(formValues.notificationTimeOut) > 0 :
|
||||
true;
|
||||
case 'liveRegionTimeOut':
|
||||
return hasNoValue(formValues.liveRegionTimeOut) || parseFloat(formValues.liveRegionTimeOut) > 0;
|
||||
default:
|
||||
throw new Error(`Unhandled accessibility setting during validity check: ${setting}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if all settings in the provided AccessibilitySettingsFormValues object are valid
|
||||
*/
|
||||
formValuesValid(formValues: AccessibilitySettingsFormValues) {
|
||||
return accessibilitySettingKeys.every(setting => this.isValid(setting, formValues));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string representing seconds to a string representing milliseconds
|
||||
* Returns null if the input could not be parsed to a float
|
||||
*/
|
||||
function secondsToMilliseconds(secondsStr: string): string {
|
||||
const seconds = parseFloat(secondsStr);
|
||||
if (isNaN(seconds)) {
|
||||
return null;
|
||||
} else {
|
||||
return (seconds * 1000).toString();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a string representing milliseconds to a string representing seconds
|
||||
* Returns null if the input could not be parsed to a float
|
||||
*/
|
||||
function millisecondsToSeconds(millisecondsStr: string): string {
|
||||
const milliseconds = parseFloat(millisecondsStr);
|
||||
if (isNaN(milliseconds)) {
|
||||
return null;
|
||||
} else {
|
||||
return (milliseconds / 1000).toString();
|
||||
}
|
||||
}
|
||||
|
||||
function ofMetadata(): Observable<'metadata'> {
|
||||
return of('metadata');
|
||||
}
|
||||
|
||||
function ofFailed(): Observable<'failed'> {
|
||||
return of('failed');
|
||||
}
|
@@ -33,10 +33,10 @@ import { FileDropzoneNoUploaderComponent } from '../../shared/upload/file-dropzo
|
||||
selector: 'ds-batch-import-page',
|
||||
templateUrl: './batch-import-page.component.html',
|
||||
imports: [
|
||||
TranslateModule,
|
||||
FormsModule,
|
||||
UiSwitchModule,
|
||||
FileDropzoneNoUploaderComponent,
|
||||
FormsModule,
|
||||
TranslateModule,
|
||||
UiSwitchModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -10,7 +10,9 @@ import { MetadataImportPageComponent } from './metadata-import-page.component';
|
||||
selector: 'ds-metadata-import-page',
|
||||
templateUrl: '../../shared/theme-support/themed.component.html',
|
||||
standalone: true,
|
||||
imports: [MetadataImportPageComponent],
|
||||
imports: [
|
||||
MetadataImportPageComponent,
|
||||
],
|
||||
})
|
||||
export class ThemedMetadataImportPageComponent extends ThemedComponent<MetadataImportPageComponent> {
|
||||
protected getComponentName(): string {
|
||||
|
@@ -6,7 +6,7 @@
|
||||
<!-- In the toggle section -->
|
||||
@if (!isNewService) {
|
||||
<div class="toggle-switch-container">
|
||||
<label class="status-label font-weight-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label>
|
||||
<label class="status-label fw-bold" for="enabled">{{ 'ldn-service-status' | translate }}</label>
|
||||
<div>
|
||||
<input formControlName="enabled" hidden id="enabled" name="enabled" type="checkbox">
|
||||
<div (click)="toggleEnabled()" [class.checked]="formModel.get('enabled').value" class="toggle-switch">
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
<!-- In the Name section -->
|
||||
<div class="mb-5">
|
||||
<label for="name" class="font-weight-bold">{{ 'ldn-new-service.form.label.name' | translate }}</label>
|
||||
<label for="name" class="fw-bold">{{ 'ldn-new-service.form.label.name' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('name').invalid && formModel.get('name').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.name' | translate" class="form-control"
|
||||
formControlName="name"
|
||||
@@ -33,7 +33,7 @@
|
||||
|
||||
<!-- In the description section -->
|
||||
<div class="mb-5 mt-5 d-flex flex-column">
|
||||
<label for="description" class="font-weight-bold">{{ 'ldn-new-service.form.label.description' | translate }}</label>
|
||||
<label for="description" class="fw-bold">{{ 'ldn-new-service.form.label.description' | translate }}</label>
|
||||
<textarea [placeholder]="'ldn-new-service.form.placeholder.description' | translate"
|
||||
class="form-control" formControlName="description" id="description" name="description"></textarea>
|
||||
</div>
|
||||
@@ -42,7 +42,7 @@
|
||||
<!-- In the url section -->
|
||||
<div class="d-flex align-items-center">
|
||||
<div class="d-flex flex-column w-50 me-2">
|
||||
<label for="url" class="font-weight-bold">{{ 'ldn-new-service.form.label.url' | translate }}</label>
|
||||
<label for="url" class="fw-bold">{{ 'ldn-new-service.form.label.url' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('url').invalid && formModel.get('url').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.url' | translate" class="form-control"
|
||||
formControlName="url"
|
||||
@@ -57,7 +57,7 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-column w-50">
|
||||
<label for="score" class="font-weight-bold">{{ 'ldn-new-service.form.label.score' | translate }}</label>
|
||||
<label for="score" class="fw-bold">{{ 'ldn-new-service.form.label.score' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('score').invalid && formModel.get('score').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.score' | translate" formControlName="score"
|
||||
id="score"
|
||||
@@ -78,7 +78,7 @@
|
||||
|
||||
<!-- In the IP range section -->
|
||||
<div class="mb-5 mt-5">
|
||||
<label for="lowerIp" class="font-weight-bold">{{ 'ldn-new-service.form.label.ip-range' | translate }}</label>
|
||||
<label for="lowerIp" class="fw-bold">{{ 'ldn-new-service.form.label.ip-range' | translate }}</label>
|
||||
<div class="d-flex">
|
||||
<input [class.invalid-field]="formModel.get('lowerIp').invalid && formModel.get('lowerIp').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.lowerIp' | translate" class="form-control me-2"
|
||||
@@ -105,7 +105,7 @@
|
||||
|
||||
<!-- In the ldnUrl section -->
|
||||
<div class="mb-5 mt-5">
|
||||
<label for="ldnUrl" class="font-weight-bold">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
|
||||
<label for="ldnUrl" class="fw-bold">{{ 'ldn-new-service.form.label.ldnUrl' | translate }}</label>
|
||||
<input [class.invalid-field]="formModel.get('ldnUrl').invalid && formModel.get('ldnUrl').touched"
|
||||
[placeholder]="'ldn-new-service.form.placeholder.ldnUrl' | translate" class="form-control"
|
||||
formControlName="ldnUrl"
|
||||
@@ -130,7 +130,7 @@
|
||||
|
||||
<!-- In the usesActorEmailId section -->
|
||||
<div class="mb-5 mt-5">
|
||||
<label class="status-label font-weight-bold" for="usesActorEmailId">{{ 'ldn-service-usesActorEmailId' | translate }}</label>
|
||||
<label class="status-label fw-bold" for="usesActorEmailId">{{ 'ldn-service-usesActorEmailId' | translate }}</label>
|
||||
<div>
|
||||
<input formControlName="usesActorEmailId" hidden id="usesActorEmailId"
|
||||
name="usesActorEmailId" type="checkbox">
|
||||
@@ -149,14 +149,14 @@
|
||||
@if (areControlsInitialized) {
|
||||
<div class="row mb-1 mt-5">
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
|
||||
<label class="fw-bold">{{ 'ldn-new-service.form.label.inboundPattern' | translate }} </label>
|
||||
</div>
|
||||
@if (formModel.get('notifyServiceInboundPatterns')['controls'][0]?.value?.pattern) {
|
||||
<div class="col">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
<label class="fw-bold">{{ 'ldn-new-service.form.label.ItemFilter' | translate }}</label>
|
||||
</div>
|
||||
<div class="col-sm-1">
|
||||
<label class="font-weight-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
|
||||
<label class="fw-bold">{{ 'ldn-new-service.form.label.automatic' | translate }}</label>
|
||||
</div>
|
||||
}
|
||||
<div class="col-sm-2">
|
||||
@@ -295,8 +295,8 @@
|
||||
<span (click)="addInboundPattern()"
|
||||
class="add-pattern-link mb-2">{{ 'ldn-new-service.form.label.addPattern' | translate }}</span>
|
||||
<hr>
|
||||
<div class="form-group row">
|
||||
<div class="col text-right space-children-mr">
|
||||
<div class="mb-3 row">
|
||||
<div class="col text-end space-children-mr">
|
||||
<ng-content select="[before]"></ng-content>
|
||||
<button (click)="resetFormAndLeave()" class="btn btn-outline-secondary" type="button">
|
||||
<span> {{ 'submission.general.back.submit' | translate }}</span>
|
||||
@@ -317,9 +317,7 @@
|
||||
@if (isNewService) {
|
||||
<h4>{{'service.overview.create.modal' | translate }}</h4>
|
||||
}
|
||||
<button (click)="closeModal()" aria-label="Close"
|
||||
class="close" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
<button (click)="closeModal()" aria-label="Close" class="btn-close" type="button">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
@@ -30,10 +30,7 @@ import {
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { PaginationService } from 'ngx-pagination';
|
||||
import {
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { RouteService } from '../../../core/services/route.service';
|
||||
import { MockActivatedRoute } from '../../../shared/mocks/active-router.mock';
|
||||
@@ -94,8 +91,8 @@ describe('LdnServiceFormEditComponent', () => {
|
||||
|
||||
beforeEach(async () => {
|
||||
ldnServicesService = jasmine.createSpyObj('ldnServicesService', {
|
||||
create: observableOf(null),
|
||||
update: observableOf(null),
|
||||
create: of(null),
|
||||
update: of(null),
|
||||
findById: createSuccessfulRemoteDataObject$({}),
|
||||
});
|
||||
|
||||
|
@@ -71,10 +71,10 @@ import { notifyPatterns } from '../ldn-services-patterns/ldn-service-coar-patter
|
||||
]),
|
||||
],
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
NgbDropdownModule,
|
||||
ReactiveFormsModule,
|
||||
TranslateModule,
|
||||
NgbDropdownModule,
|
||||
AsyncPipe,
|
||||
],
|
||||
})
|
||||
export class LdnServiceFormComponent implements OnInit, OnDestroy {
|
||||
|
@@ -2,7 +2,7 @@ import {
|
||||
cold,
|
||||
getTestScheduler,
|
||||
} from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
|
||||
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||
@@ -71,12 +71,12 @@ describe('LdnServicesService test', () => {
|
||||
generateRequestId: requestUUID,
|
||||
send: true,
|
||||
removeByHrefSubstring: {},
|
||||
getByHref: observableOf(responseCacheEntry),
|
||||
getByUUID: observableOf(responseCacheEntry),
|
||||
getByHref: of(responseCacheEntry),
|
||||
getByUUID: of(responseCacheEntry),
|
||||
});
|
||||
|
||||
halService = jasmine.createSpyObj('halService', {
|
||||
getEndpoint: observableOf(endpointURL),
|
||||
getEndpoint: of(endpointURL),
|
||||
});
|
||||
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
@@ -107,7 +107,7 @@ describe('LdnServicesService test', () => {
|
||||
it('should find service by inbound pattern', (done) => {
|
||||
const params = [new RequestParam('pattern', 'testPattern')];
|
||||
const findListOptions = Object.assign(new FindListOptions(), {}, { searchParams: params });
|
||||
spyOn(service, 'searchBy').and.returnValue(observableOf(null));
|
||||
spyOn(service, 'searchBy').and.returnValue(of(null));
|
||||
spyOn((service as any).searchData, 'searchBy').and.returnValue(createSuccessfulRemoteDataObject$(createPaginatedList([mockLdnService])));
|
||||
|
||||
service.findByInboundPattern('testPattern').subscribe(() => {
|
||||
@@ -120,7 +120,7 @@ describe('LdnServicesService test', () => {
|
||||
const constraints = [{ void: true }];
|
||||
const files = [new File([],'fileName')];
|
||||
spyOn(service as any, 'getInvocationFormData');
|
||||
spyOn(service, 'getBrowseEndpoint').and.returnValue(observableOf('testEndpoint'));
|
||||
spyOn(service, 'getBrowseEndpoint').and.returnValue(of('testEndpoint'));
|
||||
service.invoke('serviceName', 'serviceId', constraints, files).subscribe(result => {
|
||||
expect((service as any).getInvocationFormData).toHaveBeenCalledWith(constraints, files);
|
||||
expect(service.getBrowseEndpoint).toHaveBeenCalled();
|
||||
|
@@ -77,8 +77,7 @@
|
||||
</div>
|
||||
<button (click)="closeModal()" aria-label="Close"
|
||||
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
|
||||
class="close" type="button">
|
||||
<span aria-hidden="true">×</span>
|
||||
class="btn-close" type="button">
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -86,7 +85,7 @@
|
||||
<div>
|
||||
{{ 'service.overview.delete.body' | translate }}
|
||||
</div>
|
||||
<div class="mt-4 text-right">
|
||||
<div class="mt-4 text-end">
|
||||
<button (click)="closeModal()"
|
||||
[attr.aria-label]="'ldn-service-overview-close-modal' | translate"
|
||||
class="btn btn-outline-secondary me-2">{{ 'service.detail.delete.cancel' | translate }}</button>
|
||||
|
@@ -52,13 +52,13 @@ import { LdnService } from '../ldn-services-model/ldn-services.model';
|
||||
styleUrls: ['./ldn-services-directory.component.scss'],
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
imports: [
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
NgClass,
|
||||
PaginationComponent,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
TruncatableComponent,
|
||||
TruncatablePartComponent,
|
||||
NgClass,
|
||||
RouterLink,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -6,7 +6,9 @@ import { SuggestionSourcesComponent } from '../../../notifications/suggestions/s
|
||||
selector: 'ds-admin-notifications-publication-claim-page',
|
||||
templateUrl: './admin-notifications-publication-claim-page.component.html',
|
||||
styleUrls: ['./admin-notifications-publication-claim-page.component.scss'],
|
||||
imports: [ SuggestionSourcesComponent ],
|
||||
imports: [
|
||||
SuggestionSourcesComponent,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
export class AdminNotificationsPublicationClaimPageComponent {
|
||||
|
@@ -42,9 +42,9 @@ import {
|
||||
standalone: true,
|
||||
imports: [
|
||||
AdminNotifyMetricsComponent,
|
||||
AsyncPipe,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
],
|
||||
})
|
||||
|
||||
|
@@ -1,15 +1,14 @@
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">{{'notify-message-modal.title' | translate}}</h4>
|
||||
<button type="button" class="close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||
<span aria-hidden="true">×</span>
|
||||
<button type="button" class="btn-close" aria-label="Close" (click)="activeModal.dismiss('Cross click')">
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body p-4">
|
||||
@for (key of notifyMessageKeys; track key) {
|
||||
<div>
|
||||
<div class="row mb-4">
|
||||
<div class="font-weight-bold col">{{ key + '.notify-detail-modal' | translate}}</div>
|
||||
<div class="col text-right">{{'notify-detail-modal.' + notifyMessage[key] | translate: {default: notifyMessage[key] ?? "n/a" } }}</div>
|
||||
<div class="fw-bold col">{{ key + '.notify-detail-modal' | translate}}</div>
|
||||
<div class="col text-end">{{'notify-detail-modal.' + notifyMessage[key] | translate: {default: notifyMessage[key] ?? "n/a" } }}</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
@@ -20,8 +20,8 @@ import { AdminNotifyLogsResultComponent } from '../admin-notify-logs-result/admi
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RouterLink,
|
||||
AdminNotifyLogsResultComponent,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<div class="container my-4">
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-3 text-left h4">{{((isInbound$ | async) ? 'admin.notify.dashboard.inbound' : 'admin.notify.dashboard.outbound') | translate}}</div>
|
||||
<div class="col-12 col-md-3 text-start h4">{{((isInbound$ | async) ? 'admin.notify.dashboard.inbound' : 'admin.notify.dashboard.outbound') | translate}}</div>
|
||||
<div class="col-md-9">
|
||||
<div class="h4">
|
||||
@if ((selectedSearchConfig$ | async) !== defaultConfiguration) {
|
||||
|
@@ -32,9 +32,9 @@ import { ThemedSearchComponent } from '../../../../shared/search/themed-search.c
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
SearchLabelsComponent,
|
||||
ThemedSearchComponent,
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
|
@@ -20,8 +20,8 @@ import { AdminNotifyLogsResultComponent } from '../admin-notify-logs-result/admi
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
RouterLink,
|
||||
AdminNotifyLogsResultComponent,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
],
|
||||
})
|
||||
|
@@ -7,10 +7,7 @@ import {
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import {
|
||||
of as observableOf,
|
||||
of,
|
||||
} from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service';
|
||||
import { SEARCH_CONFIG_SERVICE } from '../../../my-dspace-page/my-dspace-configuration.service';
|
||||
@@ -120,7 +117,7 @@ describe('AdminNotifySearchResultComponent', () => {
|
||||
fixture = TestBed.createComponent(AdminNotifySearchResultComponent);
|
||||
component = fixture.componentInstance;
|
||||
modalService = TestBed.inject(NgbModal);
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: observableOf(true) }) }));
|
||||
spyOn(modalService, 'open').and.returnValue(Object.assign({ componentInstance: Object.assign({ response: of(true) }) }));
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@@ -39,12 +39,12 @@ import { AdminNotifyMessagesService } from '../services/admin-notify-messages.se
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
TranslateModule,
|
||||
DatePipe,
|
||||
AsyncPipe,
|
||||
DatePipe,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
TruncatableComponent,
|
||||
TruncatablePartComponent,
|
||||
RouterLink,
|
||||
],
|
||||
})
|
||||
/**
|
||||
|
@@ -9,7 +9,7 @@ import { Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
||||
@@ -51,7 +51,7 @@ describe('AddBitstreamFormatComponent', () => {
|
||||
notificationService = new NotificationsServiceStub();
|
||||
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||
createBitstreamFormat: createSuccessfulRemoteDataObject$({}),
|
||||
clearBitStreamFormatRequests: observableOf(null),
|
||||
clearBitStreamFormatRequests: of(null),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
@@ -98,7 +98,7 @@ describe('AddBitstreamFormatComponent', () => {
|
||||
notificationService = new NotificationsServiceStub();
|
||||
bitstreamFormatDataService = jasmine.createSpyObj('bitstreamFormatDataService', {
|
||||
createBitstreamFormat: createFailedRemoteDataObject$('Error', 500),
|
||||
clearBitStreamFormatRequests: observableOf(null),
|
||||
clearBitStreamFormatRequests: of(null),
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
|
@@ -9,7 +9,7 @@ import { RouterModule } from '@angular/router';
|
||||
import { provideMockStore } from '@ngrx/store/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { hot } from 'jasmine-marbles';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service';
|
||||
import { PaginationService } from '../../../core/pagination/pagination.service';
|
||||
@@ -88,14 +88,14 @@ describe('BitstreamFormatsComponent', () => {
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||
findAll: observableOf(mockFormatsRD),
|
||||
findAll: of(mockFormatsRD),
|
||||
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
||||
getSelectedBitstreamFormats: hot('a', { a: mockFormatsList }),
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: createSuccessfulRemoteDataObject$({}),
|
||||
clearBitStreamFormatRequests: observableOf('cleared'),
|
||||
clearBitStreamFormatRequests: of('cleared'),
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
@@ -225,14 +225,14 @@ describe('BitstreamFormatsComponent', () => {
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||
findAll: observableOf(mockFormatsRD),
|
||||
findAll: of(mockFormatsRD),
|
||||
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
||||
getSelectedBitstreamFormats: observableOf(mockFormatsList),
|
||||
getSelectedBitstreamFormats: of(mockFormatsList),
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: createNoContentRemoteDataObject$(),
|
||||
clearBitStreamFormatRequests: observableOf('cleared'),
|
||||
clearBitStreamFormatRequests: of('cleared'),
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
@@ -282,14 +282,14 @@ describe('BitstreamFormatsComponent', () => {
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
bitstreamFormatService = jasmine.createSpyObj('bitstreamFormatService', {
|
||||
findAll: observableOf(mockFormatsRD),
|
||||
findAll: of(mockFormatsRD),
|
||||
find: createSuccessfulRemoteDataObject$(mockFormatsList[0]),
|
||||
getSelectedBitstreamFormats: observableOf(mockFormatsList),
|
||||
getSelectedBitstreamFormats: of(mockFormatsList),
|
||||
selectBitstreamFormat: {},
|
||||
deselectBitstreamFormat: {},
|
||||
deselectAllBitstreamFormats: {},
|
||||
delete: createFailedRemoteDataObject$(),
|
||||
clearBitStreamFormatRequests: observableOf('cleared'),
|
||||
clearBitStreamFormatRequests: of('cleared'),
|
||||
});
|
||||
|
||||
paginationService = new PaginationServiceStub();
|
||||
|
@@ -38,9 +38,9 @@ import { PaginationComponentOptions } from '../../../shared/pagination/paginatio
|
||||
templateUrl: './bitstream-formats.component.html',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
PaginationComponent,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
PaginationComponent,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -12,7 +12,7 @@ import {
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { BitstreamFormatDataService } from '../../../../core/data/bitstream-format-data.service';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
@@ -44,7 +44,7 @@ describe('EditBitstreamFormatComponent', () => {
|
||||
bitstreamFormat.extensions = null;
|
||||
|
||||
const routeStub = {
|
||||
data: observableOf({
|
||||
data: of({
|
||||
bitstreamFormat: createSuccessfulRemoteDataObject(bitstreamFormat),
|
||||
}),
|
||||
};
|
||||
|
@@ -30,9 +30,9 @@ import { FormatFormComponent } from '../format-form/format-form.component';
|
||||
selector: 'ds-edit-bitstream-format',
|
||||
templateUrl: './edit-bitstream-format.component.html',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
FormatFormComponent,
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -14,7 +14,7 @@ import { RouterLink } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
import { FormBuilderService } from 'src/app/shared/form/builder/form-builder.service';
|
||||
|
||||
import { ConfigurationDataService } from '../../../core/data/configuration-data.service';
|
||||
@@ -178,7 +178,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
}));
|
||||
|
||||
it('should cancel editing the selected schema when clicked again', waitForAsync(() => {
|
||||
comp.activeMetadataSchema$ = observableOf(mockSchemasList[0] as MetadataSchema);
|
||||
comp.activeMetadataSchema$ = of(mockSchemasList[0] as MetadataSchema);
|
||||
spyOn(registryService, 'cancelEditMetadataSchema');
|
||||
row.click();
|
||||
fixture.detectChanges();
|
||||
@@ -193,7 +193,7 @@ describe('MetadataRegistryComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
|
||||
comp.selectedMetadataSchemaIDs$ = observableOf(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id));
|
||||
comp.selectedMetadataSchemaIDs$ = of(selectedSchemas.map((selectedSchema: MetadataSchema) => selectedSchema.id));
|
||||
comp.deleteSchemas();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -43,12 +43,12 @@ import { MetadataSchemaFormComponent } from './metadata-schema-form/metadata-sch
|
||||
templateUrl: './metadata-registry.component.html',
|
||||
styleUrls: ['./metadata-registry.component.scss'],
|
||||
imports: [
|
||||
MetadataSchemaFormComponent,
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
PaginationComponent,
|
||||
MetadataSchemaFormComponent,
|
||||
NgClass,
|
||||
PaginationComponent,
|
||||
RouterLink,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||
import { RegistryService } from '../../../../core/registry/registry.service';
|
||||
@@ -72,7 +72,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
|
||||
describe('without an active schema', () => {
|
||||
beforeEach(() => {
|
||||
component.activeMetadataSchema$ = observableOf(undefined);
|
||||
component.activeMetadataSchema$ = of(undefined);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -91,7 +91,7 @@ describe('MetadataSchemaFormComponent', () => {
|
||||
} as MetadataSchema);
|
||||
|
||||
beforeEach(() => {
|
||||
component.activeMetadataSchema$ = observableOf(expectedWithId);
|
||||
component.activeMetadataSchema$ = of(expectedWithId);
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -37,8 +37,8 @@ import { FormComponent } from '../../../../shared/form/form.component';
|
||||
templateUrl: './metadata-schema-form.component.html',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
TranslateModule,
|
||||
FormComponent,
|
||||
TranslateModule,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
|
@@ -1,4 +1,4 @@
|
||||
@if (registryService.getActiveMetadataField() | async) {
|
||||
@if (activeMetadataField$ | async) {
|
||||
<h2>{{messagePrefix + '.edit' | translate}}</h2>
|
||||
} @else {
|
||||
<h2>{{messagePrefix + '.create' | translate}}</h2>
|
||||
|
@@ -9,7 +9,7 @@ import {
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
||||
import { MetadataSchema } from '../../../../core/metadata/metadata-schema.model';
|
||||
@@ -86,7 +86,7 @@ describe('MetadataFieldFormComponent', () => {
|
||||
|
||||
describe('without an active field', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(undefined));
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(of(undefined));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
@@ -107,7 +107,7 @@ describe('MetadataFieldFormComponent', () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(expectedWithId));
|
||||
spyOn(registryService, 'getActiveMetadataField').and.returnValue(of(expectedWithId));
|
||||
component.onSubmit();
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -19,7 +19,7 @@ import {
|
||||
TranslateModule,
|
||||
TranslateService,
|
||||
} from '@ngx-translate/core';
|
||||
import { combineLatest } from 'rxjs';
|
||||
import { Observable } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
|
||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
||||
@@ -32,9 +32,9 @@ import { FormComponent } from '../../../../shared/form/form.component';
|
||||
selector: 'ds-metadata-field-form',
|
||||
templateUrl: './metadata-field-form.component.html',
|
||||
imports: [
|
||||
AsyncPipe,
|
||||
FormComponent,
|
||||
TranslateModule,
|
||||
AsyncPipe,
|
||||
],
|
||||
standalone: true,
|
||||
})
|
||||
@@ -109,6 +109,8 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
@Output() submitForm: EventEmitter<any> = new EventEmitter();
|
||||
|
||||
activeMetadataField$: Observable<MetadataField>;
|
||||
|
||||
constructor(public registryService: RegistryService,
|
||||
private formBuilderService: FormBuilderService,
|
||||
private translateService: TranslateService) {
|
||||
@@ -117,71 +119,65 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
|
||||
/**
|
||||
* Initialize the component, setting up the necessary Models for the dynamic form
|
||||
*/
|
||||
ngOnInit() {
|
||||
combineLatest([
|
||||
this.translateService.get(`${this.messagePrefix}.element`),
|
||||
this.translateService.get(`${this.messagePrefix}.qualifier`),
|
||||
this.translateService.get(`${this.messagePrefix}.scopenote`),
|
||||
]).subscribe(([element, qualifier, scopenote]) => {
|
||||
this.element = new DynamicInputModel({
|
||||
id: 'element',
|
||||
label: element,
|
||||
name: 'element',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 64,
|
||||
},
|
||||
required: true,
|
||||
errorMessages: {
|
||||
pattern: 'error.validation.metadata.element.invalid-pattern',
|
||||
maxLength: 'error.validation.metadata.element.max-length',
|
||||
},
|
||||
});
|
||||
this.qualifier = new DynamicInputModel({
|
||||
id: 'qualifier',
|
||||
label: qualifier,
|
||||
name: 'qualifier',
|
||||
validators: {
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 64,
|
||||
},
|
||||
required: false,
|
||||
errorMessages: {
|
||||
pattern: 'error.validation.metadata.qualifier.invalid-pattern',
|
||||
maxLength: 'error.validation.metadata.qualifier.max-length',
|
||||
},
|
||||
});
|
||||
this.scopeNote = new DynamicTextAreaModel({
|
||||
id: 'scopeNote',
|
||||
label: scopenote,
|
||||
name: 'scopeNote',
|
||||
required: false,
|
||||
rows: 5,
|
||||
});
|
||||
this.formModel = [
|
||||
new DynamicFormGroupModel(
|
||||
{
|
||||
id: 'metadatadatafieldgroup',
|
||||
group:[this.element, this.qualifier, this.scopeNote],
|
||||
}),
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => {
|
||||
if (field == null) {
|
||||
this.clearFields();
|
||||
} else {
|
||||
this.formGroup.patchValue({
|
||||
metadatadatafieldgroup: {
|
||||
element: field.element,
|
||||
qualifier: field.qualifier,
|
||||
scopeNote: field.scopeNote,
|
||||
},
|
||||
});
|
||||
this.element.disabled = true;
|
||||
this.qualifier.disabled = true;
|
||||
}
|
||||
});
|
||||
ngOnInit(): void {
|
||||
this.activeMetadataField$ = this.registryService.getActiveMetadataField();
|
||||
this.element = new DynamicInputModel({
|
||||
id: 'element',
|
||||
label: this.translateService.instant(`${this.messagePrefix}.element`),
|
||||
name: 'element',
|
||||
validators: {
|
||||
required: null,
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 64,
|
||||
},
|
||||
required: true,
|
||||
errorMessages: {
|
||||
pattern: 'error.validation.metadata.element.invalid-pattern',
|
||||
maxLength: 'error.validation.metadata.element.max-length',
|
||||
},
|
||||
});
|
||||
this.qualifier = new DynamicInputModel({
|
||||
id: 'qualifier',
|
||||
label: this.translateService.instant(`${this.messagePrefix}.qualifier`),
|
||||
name: 'qualifier',
|
||||
validators: {
|
||||
pattern: '^[^. ,]*$',
|
||||
maxLength: 64,
|
||||
},
|
||||
required: false,
|
||||
errorMessages: {
|
||||
pattern: 'error.validation.metadata.qualifier.invalid-pattern',
|
||||
maxLength: 'error.validation.metadata.qualifier.max-length',
|
||||
},
|
||||
});
|
||||
this.scopeNote = new DynamicTextAreaModel({
|
||||
id: 'scopeNote',
|
||||
label: this.translateService.instant(`${this.messagePrefix}.scopenote`),
|
||||
name: 'scopeNote',
|
||||
required: false,
|
||||
rows: 5,
|
||||
});
|
||||
this.formModel = [
|
||||
new DynamicFormGroupModel({
|
||||
id: 'metadatadatafieldgroup',
|
||||
group:[this.element, this.qualifier, this.scopeNote],
|
||||
}),
|
||||
];
|
||||
this.formGroup = this.formBuilderService.createFormGroup(this.formModel);
|
||||
this.registryService.getActiveMetadataField().subscribe((field: MetadataField): void => {
|
||||
if (field == null) {
|
||||
this.clearFields();
|
||||
} else {
|
||||
this.formGroup.patchValue({
|
||||
metadatadatafieldgroup: {
|
||||
element: field.element,
|
||||
qualifier: field.qualifier,
|
||||
scopeNote: field.scopeNote,
|
||||
},
|
||||
});
|
||||
this.element.disabled = true;
|
||||
this.qualifier.disabled = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user