Compare commits

..

55 Commits

Author SHA1 Message Date
dependabot[bot]
acf45a14e9 Bump isbot from 5.1.30 to 5.1.31
Bumps [isbot](https://github.com/omrilotan/isbot) from 5.1.30 to 5.1.31.
- [Changelog](https://github.com/omrilotan/isbot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/omrilotan/isbot/compare/v5.1.30...v5.1.31)

---
updated-dependencies:
- dependency-name: isbot
  dependency-version: 5.1.31
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-01 05:30:22 +00:00
Tim Donohue
d6c4dad800 Merge pull request #4730 from alanorth/collection-mapper-trash-icon-7x
[Port dspace-7_x] src/app: use correct icon in item mapper
2025-09-22 16:59:47 -05:00
Alan Orth
15525b5df1 src/app: use correct icon in item mapper
When adding mappings in the item mapper we should use the save icon
rather than the trash icon!
2025-09-19 16:41:37 +03:00
Alan Orth
92ffac6eaa Merge pull request #4369 from atmire/w2p-130679_fix-back-button-on-group-roles-page-contribute_7.6 2025-09-15 08:56:29 +03:00
Tim Donohue
ab53a7e7db Merge pull request #4702 from DSpace/dependabot/npm_and_yarn/dspace-7_x/core-js-3.45.1
Bump core-js from 3.44.0 to 3.45.1
2025-09-12 14:07:07 -05:00
Tim Donohue
b97f02ed7a Merge pull request #4704 from DSpace/dependabot/npm_and_yarn/dspace-7_x/babel/runtime-7.28.4
Bump @babel/runtime from 7.28.2 to 7.28.4
2025-09-12 12:32:39 -05:00
Tim Donohue
50123adc36 Merge pull request #4703 from DSpace/dependabot/npm_and_yarn/dspace-7_x/sass-2e0f0fb0cc
Bump sass from 1.89.2 to 1.92.1 in the sass group
2025-09-10 16:58:51 -05:00
Tim Donohue
a36131557f Merge pull request #4705 from DSpace/dependabot/npm_and_yarn/dspace-7_x/isbot-5.1.30
Bump isbot from 5.1.29 to 5.1.30
2025-09-10 16:36:19 -05:00
Tim Donohue
2c4790c9ae Merge pull request #4701 from DSpace/dependabot/npm_and_yarn/dspace-7_x/testing-c9201e7a88
Bump cypress-axe from 1.6.0 to 1.7.0 in the testing group
2025-09-10 15:55:49 -05:00
Tim Donohue
7154bc0689 Merge pull request #4717 from DSpace/backport-4634-to-dspace-7_x
[Port dspace-7_x] server.ts: remove port and host fallbacks in run
2025-09-10 15:51:18 -05:00
Tim Donohue
42419d12ee Merge pull request #4714 from DSpace/backport-4636-to-dspace-7_x
[Port dspace-7_x] src/themes/dspace: rework banner image
2025-09-10 14:57:10 -05:00
Alan Orth
65cd1df6ef server.ts: remove port fallback
If a user has messed up their config so much that the fallbacks in
our default configuration interface don't work then we should just
fail here.

(cherry picked from commit 6c09677510)
2025-09-10 19:48:58 +00:00
Alan Orth
66cdc972d2 server.ts: remove invalid host fallback
If `environment.ui.host` is Falsy, then we attempt to fall back to
a host value of '/', which is invalid. I think that, if a user has
messed up their config so much that defaults in our configuration
interface don't work, then we should actually fail here.

(cherry picked from commit f622d58917)
2025-09-10 19:48:58 +00:00
Alan Orth
be06c4fcd1 src/themes/dspace: rework banner image
For some reason our version of this image is twice the size of the
original (~860KiB vs 1.6MiB). To make matters worse, the dimensions
of the image are 4777x3166 pixels, but we display it at 2000 pixels
wide.

This regenerates the images based on the original and adjusts the
dimensions to be more appropriate. Generated using ImageMagick 7
on Linux:

$ magick pexels-inspiredimages-132477.jpg -resize 2000x src/themes/dspace/assets/images/banner.jpg
$ magick pexels-inspiredimages-132477.jpg -resize 2000x src/themes/dspace/assets/images/banner.webp
$ magick pexels-inspiredimages-132477.jpg -resize 1200x src/themes/dspace/assets/images/banner-half.jpg
$ magick pexels-inspiredimages-132477.jpg -resize 1200x src/themes/dspace/assets/images/banner-half.webp

See: https://www.pexels.com/photo/clear-water-drops-132477/
(cherry picked from commit d9e8fea5bf)
2025-09-10 19:07:59 +00:00
Tim Donohue
ccd8d433ee Merge pull request #4180 from alanorth/port-4136-dspace-7
[Port dspace-7_x] Clean up spacing in input forms
2025-09-10 13:36:46 -05:00
Tim Donohue
c186423ad7 Merge pull request #4707 from DSpace/backport-4644-to-dspace-7_x
[Port dspace-7_x] fix: call parent ngOnDestroy in dynamic form control container
2025-09-10 08:52:50 -05:00
Jukka Lipka
826157c1d4 fix: call parent ngOnDestroy in dynamic form control container
(cherry picked from commit 4d803a58d4)
2025-09-09 21:36:50 +00:00
dependabot[bot]
96422c2909 Bump isbot from 5.1.29 to 5.1.30
Bumps [isbot](https://github.com/omrilotan/isbot) from 5.1.29 to 5.1.30.
- [Changelog](https://github.com/omrilotan/isbot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/omrilotan/isbot/compare/v5.1.29...v5.1.30)

---
updated-dependencies:
- dependency-name: isbot
  dependency-version: 5.1.30
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 22:40:59 +00:00
dependabot[bot]
57c75d06f9 Bump @babel/runtime from 7.28.2 to 7.28.4
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.28.2 to 7.28.4.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.4/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-version: 7.28.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 22:40:21 +00:00
dependabot[bot]
0a63a34533 Bump sass from 1.89.2 to 1.92.1 in the sass group
Bumps the sass group with 1 update: [sass](https://github.com/sass/dart-sass).


Updates `sass` from 1.89.2 to 1.92.1
- [Release notes](https://github.com/sass/dart-sass/releases)
- [Changelog](https://github.com/sass/dart-sass/blob/main/CHANGELOG.md)
- [Commits](https://github.com/sass/dart-sass/compare/1.89.2...1.92.1)

---
updated-dependencies:
- dependency-name: sass
  dependency-version: 1.92.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: sass
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 22:39:53 +00:00
dependabot[bot]
fffbe2d62c Bump core-js from 3.44.0 to 3.45.1
Bumps [core-js](https://github.com/zloirock/core-js/tree/HEAD/packages/core-js) from 3.44.0 to 3.45.1.
- [Release notes](https://github.com/zloirock/core-js/releases)
- [Changelog](https://github.com/zloirock/core-js/blob/master/CHANGELOG.md)
- [Commits](https://github.com/zloirock/core-js/commits/v3.45.1/packages/core-js)

---
updated-dependencies:
- dependency-name: core-js
  dependency-version: 3.45.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 22:39:48 +00:00
dependabot[bot]
b1bd60ca79 Bump cypress-axe from 1.6.0 to 1.7.0 in the testing group
Bumps the testing group with 1 update: [cypress-axe](https://github.com/component-driven/cypress-axe).


Updates `cypress-axe` from 1.6.0 to 1.7.0
- [Release notes](https://github.com/component-driven/cypress-axe/releases)
- [Commits](https://github.com/component-driven/cypress-axe/compare/v1.6.0...v1.7.0)

---
updated-dependencies:
- dependency-name: cypress-axe
  dependency-version: 1.7.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: testing
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 22:39:16 +00:00
Atmire-Kristof
90a1ca6f5d 7.x - Clear url parameters for entity search tabs (#2735)
* 110878: Entity search - clear params on tab switch

* 110878: Properly clear browse-by values when navigating

* 132230: Related entity tab switching: only keep query & scope, reset page to 1

---------

Co-authored-by: Nona Luypaert <nona.luypaert@atmire.com>
2025-09-05 16:47:44 -05:00
Art Lowel
6071065c12 Merge pull request #4669 from DSpace/backport-4664-to-dspace-7_x
[Port dspace-7_x] Inspect event key characters, not keyCodes on tag keyUp for dynamic tag input
2025-09-04 17:38:01 +02:00
Kim Shepherd
86a18f31d9 Create dynamic tag test events with key chars, not keyCodes
(cherry picked from commit 98fc76d867)
2025-09-04 14:46:06 +00:00
Kim Shepherd
1ade04398a Inspect event key characters, not keyCodes on tag keyUp
(cherry picked from commit f313b4a8fd)
2025-09-04 14:46:06 +00:00
Tim Donohue
f5314156f6 Merge pull request #4616 from nimabehforouz/translate-7x-fr
Adding the missing French translations (dspace-7_x)
2025-08-28 16:01:12 -05:00
Tim Donohue
a833ba3d3c Merge pull request #4649 from DSpace/backport-4640-to-dspace-7_x
[Port dspace-7_x] .github/workflows/codescan.yml: use codeql-action v3
2025-08-20 17:16:34 -05:00
Alan Orth
4392a4b67e .github/workflows/codescan.yml: use codeql-action v3
Version 2 was deprecated in January, 2024 after the release of v3.

See: https://github.blog/changelog/2025-01-10-code-scanning-codeql-action-v2-is-now-deprecated/
(cherry picked from commit 3333ee4fbb)
2025-08-20 21:10:47 +00:00
Tim Donohue
d98963c9d1 Merge pull request #4588 from DSpace/dependabot/npm_and_yarn/dspace-7_x/babel/runtime-7.28.2
Bump @babel/runtime from 7.27.6 to 7.28.2
2025-08-01 10:47:59 -05:00
Tim Donohue
d6a39f2164 Merge pull request #4618 from DSpace/dependabot/npm_and_yarn/dspace-7_x/webpack-dev-server-5.2.2
Bump webpack-dev-server from 5.2.1 to 5.2.2
2025-08-01 10:00:59 -05:00
dependabot[bot]
81648512e9 Bump webpack-dev-server from 5.2.1 to 5.2.2
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 5.2.1 to 5.2.2.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v5.2.1...v5.2.2)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-31 21:28:11 +00:00
Nima Behforouz
414589ee89 Updates French translations for 7.x 2025-07-31 11:28:55 -04:00
dependabot[bot]
21d6c54183 Bump @babel/runtime from 7.27.6 to 7.28.2
Bumps [@babel/runtime](https://github.com/babel/babel/tree/HEAD/packages/babel-runtime) from 7.27.6 to 7.28.2.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.28.2/packages/babel-runtime)

---
updated-dependencies:
- dependency-name: "@babel/runtime"
  dependency-version: 7.28.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 20:57:54 +00:00
Tim Donohue
e92a96bd92 Merge pull request #4608 from DSpace-Labs/dependabot/npm_and_yarn/babel/helpers-7.27.6
[Dependabot] Bump @babel/helpers from 7.21.0 to 7.27.6
2025-07-30 15:36:29 -05:00
Tim Donohue
37d6c0d61d Merge pull request #4609 from DSpace-Labs/dependabot/npm_and_yarn/webpack-dev-server-5.2.1
[Dependabot] Bump webpack-dev-server from 4.15.2 to 5.2.1
2025-07-30 15:19:03 -05:00
Tim Donohue
88e18a3559 Merge pull request #4542 from DSpace/dependabot/npm_and_yarn/dspace-7_x/core-js-3.44.0
Bump core-js from 3.42.0 to 3.44.0
2025-07-30 11:44:20 -05:00
Tim Donohue
18e2446a7e Merge pull request #4601 from DSpace/backport-4344-to-dspace-7_x
[Port dspace-7_x] fix(versioning): Encode summary
2025-07-30 11:02:00 -05:00
dependabot[bot]
376a731beb Bump webpack-dev-server from 4.15.2 to 5.2.1
Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 4.15.2 to 5.2.1.
- [Release notes](https://github.com/webpack/webpack-dev-server/releases)
- [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md)
- [Commits](https://github.com/webpack/webpack-dev-server/compare/v4.15.2...v5.2.1)

---
updated-dependencies:
- dependency-name: webpack-dev-server
  dependency-version: 5.2.1
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-30 15:43:23 +00:00
Tim Donohue
e41f576d3a Merge pull request #4535 from DSpace/dependabot/npm_and_yarn/dspace-7_x/types/lodash-4.17.20
Bump @types/lodash from 4.17.17 to 4.17.20
2025-07-30 10:09:10 -05:00
Tim Donohue
d919d255c1 Merge pull request #4587 from DSpace/dependabot/npm_and_yarn/dspace-7_x/axios-1.11.0
Bump axios from 1.10.0 to 1.11.0
2025-07-30 10:00:53 -05:00
Jukka Lipka
0988c41c19 fix(versioning): encode summary
The reason for creating a new version is now encoded in the URL.

(cherry picked from commit 2dd870ea7e)
2025-07-30 14:21:07 +00:00
Tim Donohue
a7a543cfb0 Merge pull request #4596 from DSpace/dependabot/npm_and_yarn/dspace-7_x/isbot-5.1.29
Bump isbot from 5.1.28 to 5.1.29
2025-07-29 13:29:58 -05:00
dependabot[bot]
07d3c7aa15 Bump isbot from 5.1.28 to 5.1.29
Bumps [isbot](https://github.com/omrilotan/isbot) from 5.1.28 to 5.1.29.
- [Changelog](https://github.com/omrilotan/isbot/blob/main/CHANGELOG.md)
- [Commits](https://github.com/omrilotan/isbot/compare/v5.1.28...v5.1.29)

---
updated-dependencies:
- dependency-name: isbot
  dependency-version: 5.1.29
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-29 14:33:25 +00:00
dependabot[bot]
57b1de3b94 Bump axios from 1.10.0 to 1.11.0
Bumps [axios](https://github.com/axios/axios) from 1.10.0 to 1.11.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.10.0...v1.11.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-version: 1.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-28 07:31:25 +00:00
Tim Donohue
5a08173f5e Merge pull request #4579 from DSpace/backport-4536-to-dspace-7_x
[Port dspace-7_x] Fix empty search query after previous non-empty search
2025-07-24 13:42:09 -05:00
Jesiel Viana
be8def0064 fix empty search query after previous non-empty search
(cherry picked from commit cf34198d54)
2025-07-24 17:58:57 +00:00
dependabot[bot]
d9d0032378 Bump @babel/helpers from 7.21.0 to 7.27.6
Bumps [@babel/helpers](https://github.com/babel/babel/tree/HEAD/packages/babel-helpers) from 7.21.0 to 7.27.6.
- [Release notes](https://github.com/babel/babel/releases)
- [Changelog](https://github.com/babel/babel/blob/main/CHANGELOG.md)
- [Commits](https://github.com/babel/babel/commits/v7.27.6/packages/babel-helpers)

---
updated-dependencies:
- dependency-name: "@babel/helpers"
  dependency-version: 7.27.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-23 19:13:16 +00:00
Tim Donohue
da99511ff3 Merge pull request #4571 from DSpace/backport-4568-to-dspace-7_x
[Port dspace-7_x] Fix current parent order when creating subcommunity
2025-07-23 12:31:24 -05:00
Jesiel Viana
08eb6def3e Fix current parent order when creating subcommunity
(cherry picked from commit 44fcc7221b)
2025-07-23 15:42:12 +00:00
Tim Donohue
1419ed9a22 Update version tag for development of next release 2025-07-22 15:33:00 -05:00
dependabot[bot]
7376ae2b26 Bump core-js from 3.42.0 to 3.44.0
---
updated-dependencies:
- dependency-name: core-js
  dependency-version: 3.44.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-22 15:55:48 +00:00
dependabot[bot]
ac5ef72bbe Bump @types/lodash from 4.17.17 to 4.17.20
Bumps [@types/lodash](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/lodash) from 4.17.17 to 4.17.20.
- [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases)
- [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/lodash)

---
updated-dependencies:
- dependency-name: "@types/lodash"
  dependency-version: 4.17.20
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-07 07:34:47 +00:00
abhinav
084cad6e46 Fix back button behavior on roles page 2025-05-23 13:56:46 +02:00
Alan Orth
377c7034ff [Port dspace-7_x] Clean up spacing in input forms
Manual port of #4136 by @gingyx to DSpace 7.x.
2025-04-12 16:28:11 +03:00
3754 changed files with 90604 additions and 300583 deletions

View File

@@ -25,6 +25,4 @@ npm-debug.log.*
# Webpack files
webpack.records.json
# Yarn no longer used
yarn.lock
package-lock.json

View File

@@ -15,10 +15,6 @@ 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

View File

@@ -8,15 +8,7 @@
"eslint-plugin-deprecation",
"unused-imports",
"eslint-plugin-lodash",
"eslint-plugin-jsonc",
"eslint-plugin-rxjs",
"eslint-plugin-simple-import-sort",
"eslint-plugin-import-newlines",
"dspace-angular-ts",
"dspace-angular-html"
],
"ignorePatterns": [
"lint/test/fixture"
"eslint-plugin-jsonc"
],
"overrides": [
{
@@ -26,8 +18,7 @@
"parserOptions": {
"project": [
"./tsconfig.json",
"./cypress/tsconfig.json",
"./lint/tsconfig.json"
"./cypress/tsconfig.json"
],
"createDefaultProgram": true
},
@@ -36,32 +27,17 @@
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/recommended-requiring-type-checking",
"plugin:@angular-eslint/recommended",
"plugin:@angular-eslint/template/process-inline-templates",
"plugin:rxjs/recommended"
"plugin:@angular-eslint/template/process-inline-templates"
],
"rules": {
"indent": [
"error",
2,
{
"SwitchCase": 1,
"ignoredNodes": [
"ClassBody.body > PropertyDefinition[decorators.length > 0] > .key"
]
}
],
"max-classes-per-file": [
"error",
1
],
"comma-dangle": [
"error",
"off",
"always-multiline"
],
"object-curly-spacing": [
"error",
"always"
],
"eol-last": [
"error",
"always"
@@ -128,13 +104,15 @@
"allowTernary": true
}
],
"prefer-const": "error",
"no-case-declarations": "error",
"no-extra-boolean-cast": "error",
"prefer-const": "off", // todo: re-enable & fix errors (more strict than it used to be in TSLint)
"prefer-spread": "off",
"no-underscore-dangle": "off",
// todo: disabled rules from eslint:recommended, consider re-enabling & fixing
"no-prototype-builtins": "off",
"no-useless-escape": "off",
"no-case-declarations": "off",
"no-extra-boolean-cast": "off",
"@angular-eslint/directive-selector": [
"error",
@@ -160,14 +138,11 @@
]
}
],
"@angular-eslint/prefer-standalone": [
"error"
],
"@angular-eslint/no-attribute-decorator": "error",
"@angular-eslint/no-forward-ref": "error",
"@angular-eslint/no-output-native": "warn",
"@angular-eslint/no-output-on-prefix": "warn",
"@angular-eslint/no-conflicting-lifecycle": "warn",
"@angular-eslint/use-lifecycle-interface": "error",
"@typescript-eslint/no-inferrable-types":[
"error",
@@ -208,7 +183,7 @@
],
"@typescript-eslint/type-annotation-spacing": "error",
"@typescript-eslint/unified-signatures": "error",
"@typescript-eslint/ban-types": "error",
"@typescript-eslint/ban-types": "warn", // todo: deal with {} type issues & re-enable
"@typescript-eslint/no-floating-promises": "warn",
"@typescript-eslint/no-misused-promises": "warn",
"@typescript-eslint/restrict-plus-operands": "warn",
@@ -225,110 +200,19 @@
"@typescript-eslint/no-unsafe-return": "off",
"@typescript-eslint/restrict-template-expressions": "off",
"@typescript-eslint/require-await": "off",
"@typescript-eslint/no-base-to-string": [
"error",
{
"ignoredTypeNames": [
"ResourceType",
"Error"
]
}
],
"deprecation/deprecation": "warn",
"simple-import-sort/imports": "error",
"simple-import-sort/exports": "error",
"import/order": "off",
"import/first": "error",
"import/newline-after-import": "error",
"import/no-duplicates": "error",
"import/no-deprecated": "warn",
"import/no-namespace": "error",
"import-newlines/enforce": [
"error",
{
"items": 1,
"semi": true,
"forceSingleLine": true
}
],
"import/enforce-node-protocol-usage": [
"error",
"always"
],
"unused-imports/no-unused-imports": "error",
"lodash/import-scope": [
"error",
"method"
],
"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-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
}
]
}
},
{
"files": [
"*.spec.ts"
],
"parserOptions": {
"project": [
"./tsconfig.json",
"./cypress/tsconfig.json"
],
"createDefaultProgram": true
},
"rules": {
"prefer-const": "off",
// Custom DSpace Angular rules
"dspace-angular-ts/themed-component-usages": "error"
}
},
{
"files": [
"*.html"
@@ -337,10 +221,9 @@
"plugin:@angular-eslint/template/recommended"
],
"rules": {
// Custom DSpace Angular rules
"dspace-angular-html/themed-component-usages": "error",
"dspace-angular-html/no-disabled-attribute-on-button": "error",
"@angular-eslint/template/prefer-control-flow": "error"
// todo: re-enable & fix errors
"@angular-eslint/template/no-negated-async": "off",
"@angular-eslint/template/eqeqeq": "off"
}
},
{

5
.gitattributes vendored
View File

@@ -13,7 +13,4 @@
*.css eol=lf
*.scss eol=lf
*.html eol=lf
*.svg eol=lf
# Generated documentation should have LF line endings to reduce git noise
docs/lint/**/*.md eol=lf
*.svg eol=lf

View File

@@ -7,16 +7,16 @@ assignees: ''
---
## Describe the bug
**Describe the bug**
A clear and concise description of what the bug is. Include the version(s) of DSpace where you've seen this problem & what *web browser* you were using. Link to examples if they are public.
## To Reproduce
**To Reproduce**
Steps to reproduce the behavior:
1. Do this
2. Then this...
## Expected behavior
**Expected behavior**
A clear and concise description of what you expected to happen.
## Related work
**Related work**
Link to any related tickets or PRs here.

View File

@@ -7,14 +7,14 @@ assignees: ''
---
## Is your feature request related to a problem? Please describe.
A clear and concise description of what the problem or use case is. For example, I'm always frustrated when [...]
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
## Describe the solution you'd like
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
## Describe alternatives or workarounds you've considered
**Describe alternatives or workarounds you've considered**
A clear and concise description of any alternative solutions or features you've considered.
## Additional information
Add any other information, related tickets or screenshots about the feature request here.
**Additional context**
Add any other context or screenshots about the feature request here.

348
.github/dependabot.yml vendored
View File

@@ -1,348 +0,0 @@
#-------------------
# DSpace's dependabot rules. Enables npm updates for all dependencies on a weekly basis
# for main and any maintenance branches. Security updates only apply to main.
#-------------------
version: 2
updates:
###############
## Main branch
###############
# NOTE: At this time, "security-updates" rules only apply if "target-branch" is unspecified
# So, only this first section can include "applies-to: security-updates"
- package-ecosystem: "npm"
directory: "/"
# Monthly dependency updates (NOTE: "schedule" doesn't apply to security updates)
schedule:
interval: "monthly"
time: "05:00"
# Allow up to 10 open PRs for dependencies
open-pull-requests-limit: 10
# Group together Angular package upgrades
groups:
# Group together all minor/patch version updates for Angular in a single PR
angular:
applies-to: version-updates
patterns:
- "@angular*"
- "@ngtools/webpack"
update-types:
- "minor"
- "patch"
# Group together all minor/patch version updates for NgRx in a single PR
ngrx:
applies-to: version-updates
patterns:
- "@ngrx*"
update-types:
- "minor"
- "patch"
# Group together all patch version updates for eslint in a single PR
eslint:
applies-to: version-updates
patterns:
- "@typescript-eslint*"
- "eslint*"
update-types:
- "minor"
- "patch"
# Group together any testing related version updates
testing:
applies-to: version-updates
patterns:
- "@cypress*"
- "axe-*"
- "cypress*"
- "jasmine*"
- "karma*"
- "ng-mocks"
update-types:
- "minor"
- "patch"
# Group together any postcss related version updates
postcss:
applies-to: version-updates
patterns:
- "postcss*"
update-types:
- "minor"
- "patch"
# Group together any sass related version updates
sass:
applies-to: version-updates
patterns:
- "sass*"
update-types:
- "minor"
- "patch"
# Group together any webpack related version updates
webpack:
applies-to: version-updates
patterns:
- "webpack*"
update-types:
- "minor"
- "patch"
ignore:
# Restrict zone.js updates to patch level to avoid dependency conflicts with @angular/core
- dependency-name: "zone.js"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
# Restrict typescript updates to patch level because that's what our package.json says
- dependency-name: "typescript"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
- dependency-name: "*"
update-types: ["version-update:semver-major"]
#####################
## dspace-9_x branch
#####################
- package-ecosystem: "npm"
directory: "/"
target-branch: dspace-9_x
schedule:
interval: "monthly"
time: "05:00"
# Allow up to 10 open PRs for dependencies
open-pull-requests-limit: 10
# Group together Angular package upgrades
groups:
# Group together all patch version updates for Angular in a single PR
angular:
applies-to: version-updates
patterns:
- "@angular*"
- "@ngtools/webpack"
update-types:
- "minor"
- "patch"
# Group together all minor/patch version updates for NgRx in a single PR
ngrx:
applies-to: version-updates
patterns:
- "@ngrx*"
- "@ngtools/webpack"
update-types:
- "minor"
- "patch"
# Group together all patch version updates for eslint in a single PR
eslint:
applies-to: version-updates
patterns:
- "@typescript-eslint*"
- "eslint*"
update-types:
- "minor"
- "patch"
# Group together any testing related version updates
testing:
applies-to: version-updates
patterns:
- "@cypress*"
- "axe-*"
- "cypress*"
- "jasmine*"
- "karma*"
- "ng-mocks"
update-types:
- "minor"
- "patch"
# Group together any postcss related version updates
postcss:
applies-to: version-updates
patterns:
- "postcss*"
update-types:
- "minor"
- "patch"
# Group together any sass related version updates
sass:
applies-to: version-updates
patterns:
- "sass*"
update-types:
- "minor"
- "patch"
# Group together any webpack related version updates
webpack:
applies-to: version-updates
patterns:
- "webpack*"
update-types:
- "minor"
- "patch"
ignore:
# Restrict zone.js updates to patch level to avoid dependency conflicts with @angular/core
- dependency-name: "zone.js"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
# Restrict typescript updates to patch level because that's what our package.json says
- dependency-name: "typescript"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
- dependency-name: "*"
update-types: ["version-update:semver-major"]
#####################
## dspace-8_x branch
#####################
- package-ecosystem: "npm"
directory: "/"
target-branch: dspace-8_x
schedule:
interval: "monthly"
time: "05:00"
# Allow up to 10 open PRs for dependencies
open-pull-requests-limit: 10
# Group together Angular package upgrades
groups:
# Group together all patch version updates for Angular in a single PR
angular:
applies-to: version-updates
patterns:
- "@angular*"
- "@ngtools/webpack"
update-types:
- "minor"
- "patch"
# Group together all minor/patch version updates for NgRx in a single PR
ngrx:
applies-to: version-updates
patterns:
- "@ngrx*"
update-types:
- "minor"
- "patch"
# Group together all patch version updates for eslint in a single PR
eslint:
applies-to: version-updates
patterns:
- "@typescript-eslint*"
- "eslint*"
update-types:
- "minor"
- "patch"
# Group together any testing related version updates
testing:
applies-to: version-updates
patterns:
- "@cypress*"
- "axe-*"
- "cypress*"
- "jasmine*"
- "karma*"
- "ng-mocks"
update-types:
- "minor"
- "patch"
# Group together any postcss related version updates
postcss:
applies-to: version-updates
patterns:
- "postcss*"
update-types:
- "minor"
- "patch"
# Group together any sass related version updates
sass:
applies-to: version-updates
patterns:
- "sass*"
update-types:
- "minor"
- "patch"
# Group together any webpack related version updates
webpack:
applies-to: version-updates
patterns:
- "webpack*"
update-types:
- "minor"
- "patch"
ignore:
# Restrict zone.js updates to patch level to avoid dependency conflicts with @angular/core
- dependency-name: "zone.js"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
# Restrict typescript updates to patch level because that's what our package.json says
- dependency-name: "typescript"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
- dependency-name: "*"
update-types: ["version-update:semver-major"]
#####################
## dspace-7_x branch
#####################
- package-ecosystem: "npm"
directory: "/"
target-branch: dspace-7_x
schedule:
interval: "monthly"
time: "05:00"
# Allow up to 10 open PRs for dependencies
open-pull-requests-limit: 10
# Group together Angular package upgrades
groups:
# Group together all minor/patch version updates for Angular in a single PR
angular:
applies-to: version-updates
patterns:
- "@angular*"
- "@ngtools/webpack"
update-types:
- "minor"
- "patch"
# Group together all minor/patch version updates for NgRx in a single PR
ngrx:
applies-to: version-updates
patterns:
- "@ngrx*"
update-types:
- "minor"
- "patch"
# Group together all patch version updates for eslint in a single PR
eslint:
applies-to: version-updates
patterns:
- "@typescript-eslint*"
- "eslint*"
update-types:
- "minor"
- "patch"
# Group together any testing related version updates
testing:
applies-to: version-updates
patterns:
- "@cypress*"
- "axe-*"
- "cypress*"
- "jasmine*"
- "karma*"
- "ng-mocks"
update-types:
- "minor"
- "patch"
# Group together any postcss related version updates
postcss:
applies-to: version-updates
patterns:
- "postcss*"
update-types:
- "minor"
- "patch"
# Group together any sass related version updates
sass:
applies-to: version-updates
patterns:
- "sass*"
update-types:
- "minor"
- "patch"
ignore:
# 7.x Cannot update Webpack past v5.76.1 as later versions not supported by Angular 15
# See also https://github.com/DSpace/dspace-angular/pull/3283#issuecomment-2372488489
- dependency-name: "webpack"
# Restrict zone.js updates to patch level to avoid dependency conflicts with @angular/core
- dependency-name: "zone.js"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
# Restrict typescript updates to patch level because that's what our package.json says
- dependency-name: "typescript"
update-types: ["version-update:semver-major", "version-update:semver-minor"]
# Ignore all major version updates for all dependencies. We'll only automate minor/patch updates.
- dependency-name: "*"
update-types: ["version-update:semver-major"]

View File

@@ -1,7 +1,7 @@
## References
_Add references/links to any related issues or PRs. These may include:_
* Fixes #issue-number (if this fixes an issue ticket)
* Requires DSpace/DSpace#pr-number (if a REST API PR is required to test this)
* Fixes #`issue-number` (if this fixes an issue ticket)
* Requires DSpace/DSpace#`pr-number` (if a REST API PR is required to test this)
## Description
Short summary of changes (1-2 sentences).
@@ -16,18 +16,13 @@ List of changes in this PR:
**Include guidance for how to test or review your PR.** This may include: steps to reproduce a bug, screenshots or description of a new feature, or reasons behind specific changes.
## Checklist
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You do not need to complete this checklist prior creating your PR (draft PRs are always welcome).
However, reviewers may request that you complete any actions in this list if you have not done so. If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
_This checklist provides a reminder of what we are going to look for when reviewing your PR. You need not complete this checklist prior to creating your PR (draft PRs are always welcome). If you are unsure about an item in the checklist, don't hesitate to ask. We're here to help!_
- [ ] My PR is **created against the `main` branch** of code (unless it is a backport or is fixing an issue specific to an older branch).
- [ ] My PR is **small in size** (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
- [ ] My PR **passes [ESLint](https://eslint.org/)** validation using `npm run lint`
- [ ] My PR **doesn't introduce circular dependencies** (verified via `npm run check-circ-deps`)
- [ ] My PR **includes [TypeDoc](https://typedoc.org/) comments** for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
- [ ] My PR **passes all specs/tests and includes new/updated specs or tests** based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
- [ ] My PR **aligns with [Accessibility guidelines](https://wiki.lyrasis.org/display/DSDOC9x/Accessibility)** if it makes changes to the user interface.
- [ ] My PR **uses i18n (internationalization) keys** instead of hardcoded English text, to allow for translations.
- [ ] My PR **includes details on how to test it**. I've provided clear instructions to reviewers on how to successfully test this fix or feature.
- [ ] My PR is small in size (e.g. less than 1,000 lines of code, not including comments & specs/tests), or I have provided reasons as to why that's not possible.
- [ ] My PR passes [ESLint](https://eslint.org/) validation using `yarn lint`
- [ ] My PR doesn't introduce circular dependencies (verified via `yarn check-circ-deps`)
- [ ] My PR includes [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. It also includes TypeDoc for large or complex private methods.
- [ ] My PR passes all specs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
- [ ] If my PR includes new libraries/dependencies (in `package.json`), I've made sure their licenses align with the [DSpace BSD License](https://github.com/DSpace/DSpace/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
- [ ] If my PR includes new features or configurations, I've provided basic technical documentation in the PR itself.
- [ ] If my PR fixes an issue ticket, I've [linked them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).

View File

@@ -75,39 +75,33 @@ jobs:
fi
google-chrome --version
# https://github.com/actions/cache/blob/main/examples.md#node---npm
- name: Get NPM cache directory
id: npm-cache-dir
run: echo "dir=$(npm config get cache)" >> $GITHUB_OUTPUT
- name: Cache NPM dependencies
# https://github.com/actions/cache/blob/main/examples.md#node---yarn
- name: Get Yarn cache directory
id: yarn-cache-dir-path
run: echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT
- name: Cache Yarn dependencies
uses: actions/cache@v4
with:
# Cache entire NPM cache directory (see previous step)
path: ${{ steps.npm-cache-dir.outputs.dir }}
# Cache key is hash of package-lock.json. Therefore changes to package-lock.json will invalidate cache
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: ${{ runner.os }}-npm-
# Cache entire Yarn cache directory (see previous step)
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
# Cache key is hash of yarn.lock. Therefore changes to yarn.lock will invalidate cache
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: ${{ runner.os }}-yarn-
- name: Install NPM dependencies
run: npm clean-install
- name: Build lint plugins
run: npm run build:lint
- name: Run lint plugin tests
run: npm run test:lint:nobuild
- name: Install Yarn dependencies
run: yarn install --frozen-lockfile
- name: Run lint
run: npm run lint:nobuild -- --quiet
run: yarn run lint --quiet
- name: Check for circular dependencies
run: npm run check-circ-deps
run: yarn run check-circ-deps
- name: Run build
run: npm run build:prod
run: yarn run build:prod
- name: Run specs (unit tests)
run: npm run test:headless
run: yarn run test:headless
# Upload code coverage report to artifact (for one version of Node only),
# so that it can be shared with the 'codecov' job (see below)
@@ -145,7 +139,7 @@ jobs:
# Run tests in Chrome, headless mode (default)
browser: chrome
# Start app before running tests (will be stopped automatically after tests finish)
start: npm run serve:ssr
start: yarn run serve:ssr
# Wait for backend & frontend to be available
# NOTE: We use the 'sites' REST endpoint to also ensure the database is ready
wait-on: http://127.0.0.1:8080/server/api/core/sites, http://127.0.0.1:4000
@@ -181,7 +175,7 @@ jobs:
# Start up the app with SSR enabled (run in background)
- name: Start app in SSR (server-side rendering) mode
run: |
nohup npm run serve:ssr &
nohup yarn run serve:ssr &
printf 'Waiting for app to start'
until curl --output /dev/null --silent --head --fail http://127.0.0.1:4000/home; do
printf '.'

View File

@@ -4,7 +4,7 @@ name: Docker images
# Run this Build for all pushes to 'main' or maintenance branches, or tagged releases.
# Also run for PRs to ensure PR doesn't break Docker build process
# NOTE: uses "reusable-docker-build.yml" in DSpace/DSpace to actually build each of the Docker images
# https://github.com/DSpace/DSpace/blob/main/.github/workflows/reusable-docker-build.yml
# https://github.com/DSpace/DSpace/blob/dspace-7_x/.github/workflows/reusable-docker-build.yml
#
on:
push:
@@ -27,7 +27,7 @@ jobs:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular'
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@dspace-7_x
with:
build_id: dspace-angular-dev
image_name: dspace/dspace-angular
@@ -43,7 +43,7 @@ jobs:
# Ensure this job never runs on forked repos. It's only executed for 'dspace/dspace-angular'
if: github.repository == 'dspace/dspace-angular'
# Use the reusable-docker-build.yml script from DSpace/DSpace repo to build our Docker image
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@main
uses: DSpace/DSpace/.github/workflows/reusable-docker-build.yml@dspace-7_x
with:
build_id: dspace-angular-dist
image_name: dspace/dspace-angular

5
.gitignore vendored
View File

@@ -1,5 +1,4 @@
/.angular/cache
/.nx
/__build__
/__server_build__
/node_modules
@@ -28,12 +27,12 @@ webpack.records.json
morgan.log
# Yarn no longer used
yarn.lock
yarn-error.log
*.css
package-lock.json
.java-version
.env

View File

@@ -10,7 +10,7 @@ DSpace is a community built and supported project. We do not have a centralized
## Contribute new code via a Pull Request
We accept [GitHub Pull Requests (PRs)](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request-from-a-fork) at any time from anyone.
Contributors to each release are recognized in our [Release Notes](https://wiki.lyrasis.org/display/DSDOC9x/Release+Notes).
Contributors to each release are recognized in our [Release Notes](https://wiki.lyrasis.org/display/DSDOC7x/Release+Notes).
Code Contribution Checklist
- [ ] PRs _should_ be smaller in size (ideally less than 1,000 lines of code, not including comments & tests)
@@ -18,9 +18,6 @@ Code Contribution Checklist
- [ ] PRs **must** not introduce circular dependencies (verified via `yarn check-circ-deps`)
- [ ] PRs **must** include [TypeDoc](https://typedoc.org/) comments for _all new (or modified) public methods and classes_. Large or complex private methods should also have TypeDoc.
- [ ] PRs **must** pass all automated pecs/tests and includes new/updated specs or tests based on the [Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code+Testing+Guide).
- [ ] User interface changes **must** align with [Accessibility guidelines](https://wiki.lyrasis.org/display/DSDOC9x/Accessibility)
- [ ] PRs **must** use i18n (internationalization) keys instead of hardcoded English text, to allow for translations.
- [ ] Details on how to test the PR **must** be provided. Reviewers must be aware of any steps they need to take to successfully test your fix or feature.
- [ ] If a PR includes new libraries/dependencies (in `package.json`), then their software licenses **must** align with the [DSpace BSD License](https://github.com/DSpace/dspace-angular/blob/main/LICENSE) based on the [Licensing of Contributions](https://wiki.lyrasis.org/display/DSPACE/Code+Contribution+Guidelines#CodeContributionGuidelines-LicensingofContributions) documentation.
- [ ] Basic technical documentation _should_ be provided for any new features or configuration, either in the PR itself or in the DSpace Wiki documentation.
- [ ] If a PR fixes an issue ticket, please [link them together](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue).
@@ -29,7 +26,7 @@ Additional details on the code contribution process can be found in our [Code Co
## Contribute documentation
DSpace Documentation is a collaborative effort in a shared Wiki. The latest documentation is at https://wiki.lyrasis.org/display/DSDOC
DSpace Documentation is a collaborative effort in a shared Wiki. The latest documentation is at https://wiki.lyrasis.org/display/DSDOC7x
If you find areas of the DSpace Documentation which you wish to improve, please request a Wiki account by emailing wikihelp@lyrasis.org.
Once you have an account setup, contact @tdonohue (via [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) or email) for access to edit our Documentation.
@@ -37,7 +34,7 @@ Once you have an account setup, contact @tdonohue (via [Slack](https://wiki.lyra
## Help others on mailing lists or Slack
DSpace has our own [Slack](https://wiki.lyrasis.org/display/DSPACE/Slack) community and [Mailing Lists](https://wiki.lyrasis.org/display/DSPACE/Mailing+Lists) where discussions take place and questions are answered.
Anyone is welcome to join and help others. We just ask you to follow our [Code of Conduct](https://www.lyrasis.org/about/Pages/Code-of-Conduct.aspx) (adopted via Lyrasis).
Anyone is welcome to join and help others. We just ask you to follow our [Code of Conduct](https://www.lyrasis.org/about/Pages/Code-of-Conduct.aspx) (adopted via LYRASIS).
## Join a working or interest group
@@ -45,5 +42,5 @@ Most of the work in building/improving DSpace comes via [Working Groups](https:/
All working/interest groups are open to anyone to join and participate. A few key groups to be aware of include:
* [DSpace Developer Team](https://wiki.lyrasis.org/display/DSPACE/Developer+Meetings) - This is the primary, volunteer development team. We meet weekly to review our current development [project board](https://github.com/orgs/DSpace/projects), assigning tickets and/or PRs. This is also were discussions of the next release or major issues occur. Anyone is welcome to attend.
* [DSpace Community Advisory Team (DCAT)](https://wiki.lyrasis.org/display/cmtygp/DSpace+Community+Advisory+Team) - This is an interest group for repository managers/administrators. We meet monthly to discuss DSpace, share tips & provide feedback back to developers. Anyone is welcome to attend.
* [DSpace 7 Working Group](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+Working+Group) - This is the main (mostly volunteer) development team. We meet weekly to review our current development [project board](https://github.com/orgs/DSpace/projects), assigning tickets and/or PRs.
* [DSpace Community Advisory Team (DCAT)](https://wiki.lyrasis.org/display/cmtygp/DSpace+Community+Advisory+Team) - This is an interest group for repository managers/administrators. We meet monthly to discuss DSpace, share tips & provide feedback back to developers.

View File

@@ -11,7 +11,9 @@ WORKDIR /app
ADD . /app/
EXPOSE 4000
RUN npm install
# We run yarn install with an increased network timeout (5min) to avoid "ESOCKETTIMEDOUT" errors from hub.docker.com
# See, for example https://github.com/yarnpkg/yarn/issues/5540
RUN yarn install --network-timeout 300000
# When running in dev mode, 4GB of memory is required to build & launch the app.
# This default setting can be overridden as needed in your shell, via an env file or in docker-compose.
@@ -23,4 +25,4 @@ ENV NODE_OPTIONS="--max_old_space_size=4096"
# NOTE: At this time it is only possible to run Docker container in Production mode
# if you have a public URL. See https://github.com/DSpace/dspace-angular/issues/1485
ENV NODE_ENV=development
CMD npm run serve -- --host 0.0.0.0
CMD yarn serve --host 0.0.0.0

View File

@@ -2,7 +2,7 @@
# See https://github.com/DSpace/dspace-angular/tree/main/docker for usage details
# Test build:
# docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
# docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
FROM docker.io/node:18-alpine AS build
@@ -11,11 +11,11 @@ FROM docker.io/node:18-alpine AS build
RUN apk add --update python3 make g++ && rm -rf /var/cache/apk/*
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm install
COPY package.json yarn.lock ./
RUN yarn install --network-timeout 300000
ADD . /app/
RUN npm run build:prod
RUN yarn build:prod
FROM node:18-alpine
RUN npm install --global pm2

View File

@@ -10,7 +10,7 @@ Overview
DSpace open source software is a turnkey repository application used by more than
2,000 organizations and institutions worldwide to provide durable access to digital resources.
For more information, visit https://www.dspace.org/
For more information, visit http://www.dspace.org/
DSpace consists of both a Java-based backend and an Angular-based frontend.
@@ -30,12 +30,12 @@ Downloads
Documentation for each release may be viewed online or downloaded via our [Documentation Wiki](https://wiki.lyrasis.org/display/DSDOC/).
The latest DSpace Installation instructions are available at:
https://wiki.lyrasis.org/display/DSDOC9x/Installing+DSpace
https://wiki.lyrasis.org/display/DSDOC7x/Installing+DSpace
Quick start
-----------
**Ensure you're running [Node](https://nodejs.org) `v18.x` or `v20.x`, [npm](https://www.npmjs.com/) >= `v10.x`**
**Ensure you're running [Node](https://nodejs.org) `v16.x` or `v18.x`, [npm](https://www.npmjs.com/) >= `v5.x` and [yarn](https://yarnpkg.com) == `v1.x`**
```bash
# clone the repo
@@ -45,10 +45,10 @@ git clone https://github.com/DSpace/dspace-angular.git
cd dspace-angular
# install the local dependencies
npm install
yarn install
# start the server
npm start
yarn start
```
Then go to [http://localhost:4000](http://localhost:4000) in your browser
@@ -77,7 +77,7 @@ Table of Contents
- [Recommended Editors/IDEs](#recommended-editorsides)
- [Collaborating](#collaborating)
- [File Structure](#file-structure)
- [Managing Dependencies (via npm)](#managing-dependencies-via-npm)
- [Managing Dependencies (via yarn)](#managing-dependencies-via-yarn)
- [Frequently asked questions](#frequently-asked-questions)
- [License](#license)
@@ -89,15 +89,15 @@ You can find more information on the technologies used in this project (Angular.
Requirements
------------
- [Node.js](https://nodejs.org)
- Ensure you're running node `v18.x` or `v20.x`
- [Node.js](https://nodejs.org) and [yarn](https://yarnpkg.com)
- Ensure you're running node `v16.x` or `v18.x` and yarn == `v1.x`
If you have [`nvm`](https://github.com/creationix/nvm#install-script) or [`nvm-windows`](https://github.com/coreybutler/nvm-windows) installed, which is highly recommended, you can run `nvm install --lts && nvm use` to install and start using the latest Node LTS.
Installing
----------
- `npm install` to install the local dependencies
- `yarn install` to install the local dependencies
### Configuring
@@ -202,7 +202,7 @@ import { environment } from '../environment.ts';
Running the app
---------------
After you have installed all dependencies you can now run the app. Run `npm run start:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:4000`.
After you have installed all dependencies you can now run the app. Run `yarn run start:dev` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:4000`.
### Running in production mode
@@ -211,20 +211,20 @@ When building for production we're using Ahead of Time (AoT) compilation. With A
To build the app for production and start the server (in one command) run:
```bash
npm start
yarn start
```
This will run the application in an instance of the Express server, which is included.
If you only want to build for production, without starting, run:
```bash
npm run build:prod
yarn run build:prod
```
This will build the application and put the result in the `dist` folder. You can copy this folder to wherever you need it for your application server. If you will be using the built-in Express server, you'll also need a copy of the `node_modules` folder tucked inside your copy of `dist`.
After building the app for production, it can be started by running:
```bash
npm run serve:ssr
yarn run serve:ssr
```
### Running the application with Docker
@@ -238,14 +238,14 @@ Cleaning
--------
```bash
# clean everything, including node_modules. You'll need to run npm install again afterwards.
npm run clean
# clean everything, including node_modules. You'll need to run yarn install again afterwards.
yarn run clean
# clean files generated by the production build (.ngfactory files, css files, etc)
npm run clean:prod
yarn run clean:prod
# cleans the distribution directory
npm run clean:dist
yarn run clean:dist
```
@@ -259,9 +259,9 @@ If you would like to contribute by testing a Pull Request (PR), here's how to do
1. Pull down the branch that the Pull Request was built from. Easy instructions for doing so can be found on the Pull Request itself.
* Next to the "Merge" button, you'll see a link that says "command line instructions".
* Click it, and follow "Step 1" of those instructions to checkout the pull down the PR branch.
2. `npm run clean` (This resets your local dependencies to ensure you are up-to-date with this PR)
3. `npm install` (Updates your local dependencies to those in the PR)
4. `npm start` (Rebuilds the project, and deploys to localhost:4000, by default)
2. `yarn run clean` (This resets your local dependencies to ensure you are up-to-date with this PR)
3. `yarn install` (Updates your local dependencies to those in the PR)
4. `yarn start` (Rebuilds the project, and deploys to localhost:4000, by default)
5. At this point, the code from the PR will be deployed to http://localhost:4000. Test it out, and ensure that it does what is described in the PR (or fixes the bug described in the ticket linked to the PR).
Once you have tested the Pull Request, please add a comment and/or approval to the PR to let us know whether you found it to be successful (or not). Thanks!
@@ -271,13 +271,13 @@ Once you have tested the Pull Request, please add a comment and/or approval to t
Unit tests use the [Jasmine test framework](https://jasmine.github.io/), and are run via [Karma](https://karma-runner.github.io/).
You can find the Karma configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `npm run coverage`.
You can find the Karma configuration file at the same level of this README file:`./karma.conf.js` If you are going to use a remote test environment you need to edit the `./karma.conf.js`. Follow the instructions you will find inside it. To executing tests whenever any file changes you can modify the 'autoWatch' option to 'true' and 'singleRun' option to 'false'. A coverage report is also available at: http://localhost:9876/ after you run: `yarn run coverage`.
The default browser is Google Chrome.
Place your tests in the same location of the application source code files that they test, e.g. ending with `*.component.spec.ts`
and run: `npm test`
and run: `yarn test`
If you run into odd test errors, see the Angular guide to debugging tests: https://angular.io/guide/test-debugging
@@ -330,9 +330,9 @@ All E2E tests must be created under the `./cypress/integration/` folder, and mus
* In the [Cypress Test Runner](https://docs.cypress.io/guides/core-concepts/test-runner), you'll Cypress automatically visit the page. This first test will succeed, as all you are doing is making sure the _page exists_.
* From here, you can use the [Selector Playground](https://docs.cypress.io/guides/core-concepts/test-runner#Selector-Playground) in the Cypress Test Runner window to determine how to tell Cypress to interact with a specific HTML element on that page.
* Most commands start by telling Cypress to [get()](https://docs.cypress.io/api/commands/get) a specific element, using a CSS or jQuery style selector
* It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test.
* It's generally best not to rely on attributes like `class` and `id` in tests, as those are likely to change later on. Instead, you can add a `data-test` attribute to makes it clear that it's required for a test.
* Cypress can then do actions like [click()](https://docs.cypress.io/api/commands/click) an element, or [type()](https://docs.cypress.io/api/commands/type) text in an input field, etc.
* When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail.
* When running with server-side rendering enabled, the client first receives HTML without the JS; only once the page is rendered client-side do some elements (e.g. a button that toggles a Bootstrap dropdown) become fully interactive. This can trip up Cypress in some cases as it may try to `click` or `type` in an element that's not fully loaded yet, causing tests to fail.
* To work around this issue, define the attributes you use for Cypress selectors as `[attr.data-test]="'button' | ngBrowserOnly"`. This will only show the attribute in CSR HTML, forcing Cypress to wait until CSR is complete before interacting with the element.
* Cypress can also validate that something occurs, using [should()](https://docs.cypress.io/api/commands/should) assertions.
* Any time you save your test file, the Cypress Test Runner will reload & rerun it. This allows you can see your results quickly as you write the tests & correct any broken tests rapidly.
@@ -349,7 +349,7 @@ See our [DSpace Code Testing Guide](https://wiki.lyrasis.org/display/DSPACE/Code
Documentation
--------------
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC9x/
Official DSpace documentation is available in the DSpace wiki at https://wiki.lyrasis.org/display/DSDOC7x/
Some UI specific configuration documentation is also found in the [`./docs`](docs) folder of this codebase.
@@ -357,14 +357,14 @@ Some UI specific configuration documentation is also found in the [`./docs`](doc
To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts information from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments.
Run:`npm run docs` to produce the documentation that will be available in the 'doc' folder.
Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder.
Other commands
--------------
There are many more commands in the `scripts` section of `package.json`. Most of these are executed by one of the commands mentioned above.
A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `npm run start` the `prestart` script will run first, then the `start` script will trigger.
A command with a name that starts with `pre` or `post` will be executed automatically before or after the script with the matching name. e.g. if you type `yarn run start` the `prestart` script will run first, then the `start` script will trigger.
Recommended Editors/IDEs
------------------------
@@ -456,7 +456,6 @@ dspace-angular
├── LICENSES_THIRD_PARTY *
├── nodemon.json * Nodemon (https://nodemon.io/) configuration
├── package.json * This file describes the npm package for this project, its dependencies, scripts, etc.
├── package-lock.json * npm lockfile (https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json)
├── postcss.config.js * PostCSS (http://postcss.org/) configuration
├── README.md * This document
├── SECURITY.md *
@@ -467,29 +466,30 @@ dspace-angular
├── tsconfig.spec.json * TypeScript config for tests
├── tsconfig.ts-node.json * TypeScript config for using ts-node directly
├── tslint.json * TSLint (https://palantir.github.io/tslint/) configuration
── typedoc.json * TYPEDOC configuration
── typedoc.json * TYPEDOC configuration
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock)
```
Managing Dependencies (via npm)
Managing Dependencies (via yarn)
-------------
This project makes use of [`npm`](https://docs.npmjs.com/about-npm) to ensure that the exact same dependency versions are used every time you install it.
This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it.
* `npm` creates a [`package-lock.json`](https://docs.npmjs.com/cli/v10/configuring-npm/package-lock-json) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via npm.
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`npm install`](https://docs.npmjs.com/cli/v10/commands/npm-install). For example: `npm install some-lib`.
* If you are adding a new build tool dependency (to `devDependencies`), use `npm install some-lib --save--dev`
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`npm update`](https://docs.npmjs.com/cli/v10/commands/npm-update). For example: `npm update some-lib` or `npm update some-lib@version`
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`npm uninstall`](https://docs.npmjs.com/cli/v10/commands/npm-uninstall) to remove it.
* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn.
* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`.
* If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev`
* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version`
* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it.
As you can see above, using `npm` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `npm` to keep dependencies updated / in sync.*
As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.*
### Adding Typings for libraries
If the library does not include typings, you can install them using npm:
If the library does not include typings, you can install them using yarn:
```bash
npm install d3
npm install @types/d3 --save-dev
yarn add d3
yarn add @types/d3 --dev
```
If the library doesn't have typings available at `@types/`, you can still use it by manually adding typings for it:
@@ -527,13 +527,13 @@ Frequently asked questions
- What are the naming conventions for Angular?
- See [the official angular style guide](https://angular.io/styleguide)
- Why is the size of my app larger in development?
- The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the interest of build speed.
- node-pre-gyp ERR in npm install (Windows)
- The production build uses a whole host of techniques (ahead-of-time compilation, rollup to remove unreachable code, minification, etc.) to reduce the size, that aren't used during development in the intrest of build speed.
- node-pre-gyp ERR in yarn install (Windows)
- install Python x86 version between 2.5 and 3.0 on windows. See [this issue](https://github.com/AngularClass/angular2-webpack-starter/issues/626)
- How do I handle merge conflicts in package-lock.json?
- first check out the package-lock.json file from the branch you're merging in to yours: e.g. `git checkout --theirs package-lock.json`
- now run `npm install` again. NPM will create a new lockfile that contains both sets of changes.
- then run `git add package-lock.json` to stage the lockfile for commit
- How do I handle merge conflicts in yarn.lock?
- first check out the yarn.lock file from the branch you're merging in to yours: e.g. `git checkout --theirs yarn.lock`
- now run `yarn install` again. Yarn will create a new lockfile that contains both sets of changes.
- then run `git add yarn.lock` to stage the lockfile for commit
- and `git commit` to conclude the merge
Getting Help
@@ -552,7 +552,7 @@ Additional support options are at https://wiki.lyrasis.org/display/DSPACE/Suppor
DSpace also has an active service provider network. If you'd rather hire a service provider to
install, upgrade, customize or host DSpace, then we recommend getting in touch with one of our
[Registered Service Providers](https://dspace.org/registered-service-providers/).
[Registered Service Providers](http://www.dspace.org/service-providers).
Issue Tracker
@@ -565,7 +565,7 @@ DSpace uses GitHub to track issues:
License
-------
DSpace source code is freely available under a standard [BSD 3-Clause license](https://opensource.org/licenses/BSD-3-Clause).
The full license is available in the [LICENSE](LICENSE) file or online at https://www.dspace.org/license/
The full license is available in the [LICENSE](LICENSE) file or online at http://www.dspace.org/license/
DSpace uses third-party libraries which may be distributed under different licenses. Those licenses are listed
in the [LICENSES_THIRD_PARTY](LICENSES_THIRD_PARTY) file.

View File

@@ -58,10 +58,7 @@
"input": "src/themes/dspace/styles/theme.scss",
"inject": false,
"bundleName": "dspace-theme"
},
"node_modules/leaflet/dist/leaflet.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.Default.css",
"node_modules/leaflet.markercluster/dist/MarkerCluster.css"
}
],
"scripts": [],
"baseHref": "/"
@@ -111,22 +108,22 @@
"serve": {
"builder": "@angular-builders/custom-webpack:dev-server",
"options": {
"buildTarget": "dspace-angular:build",
"browserTarget": "dspace-angular:build",
"port": 4000
},
"configurations": {
"development": {
"buildTarget": "dspace-angular:build:development"
"browserTarget": "dspace-angular:build:development"
},
"production": {
"buildTarget": "dspace-angular:build:production"
"browserTarget": "dspace-angular:build:production"
}
}
},
"extract-i18n": {
"builder": "@angular-devkit/build-angular:extract-i18n",
"options": {
"buildTarget": "dspace-angular:build"
"browserTarget": "dspace-angular:build"
}
},
"test": {
@@ -219,23 +216,23 @@
}
},
"serve-ssr": {
"builder": "@angular-devkit/build-angular:ssr-dev-server",
"builder": "@nguniversal/builders:ssr-dev-server",
"options": {
"buildTarget": "dspace-angular:build",
"browserTarget": "dspace-angular:build",
"serverTarget": "dspace-angular:server",
"port": 4000
},
"configurations": {
"production": {
"buildTarget": "dspace-angular:build:production",
"browserTarget": "dspace-angular:build:production",
"serverTarget": "dspace-angular:server:production"
}
}
},
"prerender": {
"builder": "@angular-devkit/build-angular:prerender",
"builder": "@nguniversal/builders:prerender",
"options": {
"buildTarget": "dspace-angular:build:production",
"browserTarget": "dspace-angular:build:production",
"serverTarget": "dspace-angular:server:production",
"routes": [
"/"
@@ -268,8 +265,6 @@
"options": {
"lintFilePatterns": [
"src/**/*.ts",
"cypress/**/*.ts",
"lint/**/*.ts",
"src/**/*.html",
"src/**/*.json5"
]

View File

@@ -17,8 +17,8 @@ ui:
# Trust X-FORWARDED-* headers from proxies (default = true)
useProxies: true
# Angular Server Side Rendering (SSR) settings
ssr:
# Angular Universal / Server Side Rendering (SSR) settings
universal:
# Whether to tell Angular to inline "critical" styles into the server-side rendered HTML.
# Determining which styles are critical is a relatively expensive operation; this option is
# disabled (false) by default to boost server performance at the expense of loading smoothness.
@@ -59,16 +59,13 @@ ssr:
# REST resources with the internal URL configured. By default, these internal URLs are replaced with public URLs.
# Disable this setting to avoid URL replacement during SSR. In this the state is not transferred to avoid security issues.
replaceRestUrl: true
# Enable request performance profiling data collection and printing the results in the server console.
# Defaults to false. Enabling in production is NOT recommended
#enablePerformanceProfiler: false
# The REST API server settings
# NOTE: these settings define which (publicly available) REST API to use. They are usually
# 'synced' with the 'dspace.server.url' setting in your backend's local.cfg.
rest:
ssl: true
host: sandbox.dspace.org
host: demo.dspace.org
port: 443
# NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
nameSpace: /server
@@ -101,7 +98,7 @@ cache:
# Set to true to see all cache hits/misses/refreshes in your console logs. Useful for debugging SSR caching issues.
debug: false
# When enabled (i.e. max > 0), known bots will be sent pages from a server side cache specific for bots.
# (Keep in mind, bot detection cannot be guaranteed. It is possible some bots will bypass this cache.)
# (Keep in mind, bot detection cannot be guarranteed. It is possible some bots will bypass this cache.)
botCache:
# Maximum number of pages to cache for known bots. Set to zero (0) to disable server side caching for bots.
# Default is 1000, which means the 1000 most recently accessed public pages will be cached.
@@ -180,16 +177,12 @@ submission:
# NOTE: after how many time (milliseconds) submission is saved automatically
# eg. timer: 5 * (1000 * 60); // 5 minutes
timer: 0
# Always show the duplicate detection section if enabled, even if there are no potential duplicates detected
# (a message will be displayed to indicate no matches were found)
duplicateDetection:
alwaysShowSection: false
icons:
metadata:
# NOTE: example of configuration
# # NOTE: metadata name
# - name: dc.author
# # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
# style: fas fa-user
- name: dc.author
style: fas fa-user
@@ -200,40 +193,18 @@ submission:
confidence:
# NOTE: example of configuration
# # NOTE: confidence value
# - value: 600
# # NOTE: fontawesome (v6.x) icon classes and bootstrap utility classes can be used
# style: text-success
# icon: fa-circle-check
# # NOTE: the class configured in property style is used by default, the icon property could be used in component
# configured to use a 'icon mode' display (mainly in edit-item page)
# - name: dc.author
# # NOTE: fontawesome (v5.x) icon classes and bootstrap utility classes can be used
# style: fa-user
- value: 600
style: text-success
icon: fa-circle-check
- value: 500
style: text-info
icon: fa-gear
- value: 400
style: text-warning
icon: fa-circle-question
- value: 300
style: text-muted
icon: fa-thumbs-down
- value: 200
style: text-muted
icon: fa-circle-exclamation
- value: 100
style: text-muted
icon: fa-circle-stop
- value: 0
style: text-muted
icon: fa-ban
- value: -1
style: text-muted
icon: fa-circle-xmark
# default configuration
- value: default
style: text-muted
icon: fa-circle-xmark
# Default Language in which the UI will be rendered if the user's browser language is not an active language
defaultLanguage: en
@@ -265,9 +236,6 @@ languages:
- code: es
label: Español
active: true
- code: fa
label: فارسی
active: true
- code: fi
label: Suomi
active: true
@@ -277,9 +245,6 @@ languages:
- code: gd
label: Gàidhlig
active: true
- code: gu
label: ગુજરાતી
active: true
- code: hi
label: हिंदी
active: true
@@ -295,9 +260,6 @@ languages:
- code: lv
label: Latviešu
active: true
- code: mr
label: मराठी
active: true
- code: nl
label: Nederlands
active: true
@@ -310,9 +272,6 @@ languages:
- code: pt-BR
label: Português do Brasil
active: true
- code: ru
label: Русский
active: true
- code: sr-lat
label: Srpski (lat)
active: true
@@ -362,8 +321,6 @@ homePage:
# No. of communities to list per page on the home page
# This will always round to the nearest number from the list of page sizes. e.g. if you set it to 7 it'll use 10
pageSize: 5
# Enable or disable the Discover filters on the homepage
showDiscoverFilters: false
# Item Config
item:
@@ -376,28 +333,9 @@ item:
# Rounded to the nearest size in the list of selectable sizes on the
# settings menu. See pageSizeOptions in 'pagination-component-options.model.ts'.
pageSize: 5
# Show the bitstream access status label on the item page
showAccessStatuses: false
# Community Page Config
community:
# Default tab to be shown when browsing a Community. Valid values are: comcols, search, or browse_<field>
# <field> must be any of the configured "browse by" fields, e.g., dateissued, author, title, or subject
# When the default tab is not the 'search' tab, the search tab is moved to the last position
defaultBrowseTab: search
# Search tab config
searchSection:
showSidebar: true
# Collection Page Config
collection:
# Default tab to be shown when browsing a Collection. Valid values are: search, or browse_<field>
# <field> must be any of the configured "browse by" fields, e.g., dateissued, author, title, or subject
# When the default tab is not the 'search' tab, the search tab is moved to the last position
defaultBrowseTab: search
# Search tab config
searchSection:
showSidebar: true
edit:
undoTimeout: 10000 # 10 seconds
@@ -474,13 +412,10 @@ mediaViewer:
# Whether the end user agreement is required before users use the repository.
# If enabled, the user will be required to accept the agreement before they can use the repository.
# And whether the privacy statement/COAR notify support page should exist or not.
# And whether the privacy statement should exist or not.
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.
@@ -501,79 +436,6 @@ comcolSelectionSort:
sortField: 'dc.title'
sortDirection: 'ASC'
# Example of fallback collection for suggestions import
# suggestion:
# - collectionId: 8f7df5ca-f9c2-47a4-81ec-8a6393d6e5af
# source: "openaire"
# Search settings
search:
# Settings to enable/disable or configure advanced search filters.
advancedFilters:
enabled: false
# List of filters to enable in "Advanced Search" dropdown
filter: [ 'title', 'author', 'subject', 'entityType' ]
#
# Number used to render n UI elements called loading skeletons that act as placeholders.
# These elements indicate that some content will be loaded in their stead.
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
defaultFiltersCount: 5
# Notify metrics
# Configuration for Notify Admin Dashboard for metrics visualization
notifyMetrics:
# Configuration for received messages
- title: 'admin-notify-dashboard.received-ldn'
boxes:
- color: '#B8DAFF'
title: 'admin-notify-dashboard.NOTIFY.incoming.accepted'
config: 'NOTIFY.incoming.accepted'
description: 'admin-notify-dashboard.NOTIFY.incoming.accepted.description'
- color: '#D4EDDA'
title: 'admin-notify-dashboard.NOTIFY.incoming.processed'
config: 'NOTIFY.incoming.processed'
description: 'admin-notify-dashboard.NOTIFY.incoming.processed.description'
- color: '#FDBBC7'
title: 'admin-notify-dashboard.NOTIFY.incoming.failure'
config: 'NOTIFY.incoming.failure'
description: 'admin-notify-dashboard.NOTIFY.incoming.failure.description'
- color: '#FDBBC7'
title: 'admin-notify-dashboard.NOTIFY.incoming.untrusted'
config: 'NOTIFY.incoming.untrusted'
description: 'admin-notify-dashboard.NOTIFY.incoming.untrusted.description'
- color: '#43515F'
title: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems'
textColor: '#fff'
config: 'NOTIFY.incoming.involvedItems'
description: 'admin-notify-dashboard.NOTIFY.incoming.involvedItems.description'
# Configuration for outgoing messages
- title: 'admin-notify-dashboard.generated-ldn'
boxes:
- color: '#B8DAFF'
title: 'admin-notify-dashboard.NOTIFY.outgoing.queued'
config: 'NOTIFY.outgoing.queued'
description: 'admin-notify-dashboard.NOTIFY.outgoing.queued.description'
- color: '#FDEEBB'
title: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry'
config: 'NOTIFY.outgoing.queued_for_retry'
description: 'admin-notify-dashboard.NOTIFY.outgoing.queued_for_retry.description'
- color: '#FDBBC7'
title: 'admin-notify-dashboard.NOTIFY.outgoing.failure'
config: 'NOTIFY.outgoing.failure'
description: 'admin-notify-dashboard.NOTIFY.outgoing.failure.description'
- color: '#43515F'
title: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems'
textColor: '#fff'
config: 'NOTIFY.outgoing.involvedItems'
description: 'admin-notify-dashboard.NOTIFY.outgoing.involvedItems.description'
- color: '#D4EDDA'
title: 'admin-notify-dashboard.NOTIFY.outgoing.delivered'
config: 'NOTIFY.outgoing.delivered'
description: 'admin-notify-dashboard.NOTIFY.outgoing.delivered.description'
# Live Region configuration
# Live Region as defined by w3c, https://www.w3.org/TR/wai-aria-1.1/#terms:
# Live regions are perceivable regions of a web page that are typically updated as a
@@ -588,39 +450,14 @@ liveRegion:
# The visibility of the live region. Setting this to true is only useful for debugging purposes.
isVisible: false
# Geospatial Map display options
geospatialMapViewer:
# Which fields to use for parsing as geospatial points in search maps
# (note, the item page field component allows any field(s) to be used
# and is set as an input when declaring the component)
spatialMetadataFields:
- 'dcterms.spatial'
# Which discovery configuration to use for 'geospatial search', used
# in the browse map
spatialFacetDiscoveryConfiguration: 'geospatial'
# Which filter / facet name to use for faceted geospatial search
# used in the browse map
spatialPointFilterName: 'point'
# Whether item page geospatial metadata should be displayed
# (assumes they are wrapped in a test for this config in the template as
# per the default templates supplied with DSpace for untyped-item and publication)
enableItemPageFields: false
# Whether the browse map should be enabled and included in the browse menu
enableBrowseMap: false
# Whether a 'map view' mode should be included alongside list and grid views
# in search result pages
enableSearchViewMode: false
# The tile provider(s) to use for the map tiles drawn in the leaflet maps.
# (see https://leaflet-extras.github.io/leaflet-providers/preview/) for a full list
tileProviders:
- 'OpenStreetMap.Mapnik'
# Starting centre point for the map, as lat and lng coordinates. This is useful
# to set the centre of the map when the map is first loaded and if there are no
# points, shapes or markers to display.
# Defaults to the centre of Istanbul
defaultCentrePoint:
lat: 41.015137
lng: 28.979530
# Search settings
search:
# Number used to render n UI elements called loading skeletons that act as placeholders.
# These elements indicate that some content will be loaded in their stead.
# Since we don't know how many filters will be loaded before we receive a response from the server we use this parameter for the skeletons count.
# e.g. If we set 5 then 5 loading skeletons will be visualized before the actual filters are retrieved.
defaultFiltersCount: 5
# Configuration for storing accessibility settings, used by the AccessibilitySettingsService
accessibility:

View File

@@ -1,5 +1,5 @@
rest:
ssl: true
host: sandbox.dspace.org
host: demo.dspace.org
port: 443
nameSpace: /server

View File

@@ -34,7 +34,6 @@ 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'

View File

@@ -9,12 +9,12 @@ describe('Admin Add New Modals', () => {
it('Add new Community modal should pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click();
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
cy.get('#sidebar-collapse-toggle').click();
// Click on entry of menu
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-new-title"]').click();
cy.get('#admin-menu-section-new-title').should('be.visible');
cy.get('#admin-menu-section-new-title').click();
cy.get('a[data-test="menu.section.new_community"]').click();
@@ -24,12 +24,12 @@ describe('Admin Add New Modals', () => {
it('Add new Collection modal should pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click();
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
cy.get('#sidebar-collapse-toggle').click();
// Click on entry of menu
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-new-title"]').click();
cy.get('#admin-menu-section-new-title').should('be.visible');
cy.get('#admin-menu-section-new-title').click();
cy.get('a[data-test="menu.section.new_collection"]').click();
@@ -39,12 +39,12 @@ describe('Admin Add New Modals', () => {
it('Add new Item modal should pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click();
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
cy.get('#sidebar-collapse-toggle').click();
// Click on entry of menu
cy.get('[data-test="admin-menu-section-new-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-new-title"]').click();
cy.get('#admin-menu-section-new-title').should('be.visible');
cy.get('#admin-menu-section-new-title').click();
cy.get('a[data-test="menu.section.new_item"]').click();

View File

@@ -9,12 +9,12 @@ describe('Admin Edit Modals', () => {
it('Edit Community modal should pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click();
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
cy.get('#sidebar-collapse-toggle').click();
// Click on entry of menu
cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-edit-title"]').click();
cy.get('#admin-menu-section-edit-title').should('be.visible');
cy.get('#admin-menu-section-edit-title').click();
cy.get('a[data-test="menu.section.edit_community"]').click();
@@ -24,12 +24,12 @@ describe('Admin Edit Modals', () => {
it('Edit Collection modal should pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click();
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
cy.get('#sidebar-collapse-toggle').click();
// Click on entry of menu
cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-edit-title"]').click();
cy.get('#admin-menu-section-edit-title').should('be.visible');
cy.get('#admin-menu-section-edit-title').click();
cy.get('a[data-test="menu.section.edit_collection"]').click();
@@ -39,12 +39,12 @@ describe('Admin Edit Modals', () => {
it('Edit Item modal should pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click();
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
cy.get('#sidebar-collapse-toggle').click();
// Click on entry of menu
cy.get('[data-test="admin-menu-section-edit-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-edit-title"]').click();
cy.get('#admin-menu-section-edit-title').should('be.visible');
cy.get('#admin-menu-section-edit-title').click();
cy.get('a[data-test="menu.section.edit_item"]').click();

View File

@@ -9,12 +9,12 @@ describe('Admin Export Modals', () => {
it('Export metadata modal should pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click();
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
cy.get('#sidebar-collapse-toggle').click();
// Click on entry of menu
cy.get('[data-test="admin-menu-section-export-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-export-title"]').click();
cy.get('#admin-menu-section-export-title').should('be.visible');
cy.get('#admin-menu-section-export-title').click();
cy.get('a[data-test="menu.section.export_metadata"]').click();
@@ -24,16 +24,16 @@ describe('Admin Export Modals', () => {
it('Export batch modal should pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').trigger('mouseover');
cy.get('[data-test="sidebar-collapse-toggle"]').click();
cy.get('#sidebar-collapse-toggle').trigger('mouseover');
cy.get('#sidebar-collapse-toggle').click();
// Click on entry of menu
cy.get('[data-test="admin-menu-section-export-title"]').should('be.visible');
cy.get('[data-test="admin-menu-section-export-title"]').click();
cy.get('#admin-menu-section-export-title').should('be.visible');
cy.get('#admin-menu-section-export-title').click();
cy.get('a[data-test="menu.section.export_batch"]').click();
// Analyze <ds-export-batch-selector> for accessibility
testA11y('ds-export-batch-selector');
testA11y('ds-export-metadata-selector');
});
});

View File

@@ -1,17 +0,0 @@
import { testA11y } from 'cypress/support/utils';
describe('Admin Notifications Publication Claim Page', () => {
beforeEach(() => {
// Must login as an Admin to see the page
cy.visit('/admin/notifications/publication-claim');
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
it('should pass accessibility tests', () => {
//Page must first be visible
cy.get('ds-admin-notifications-publication-claim-page').should('be.visible');
// Analyze <ds-admin-notifications-publication-claim-page> for accessibility issues
testA11y('ds-admin-notifications-publication-claim-page');
});
});

View File

@@ -1,20 +1,28 @@
import { Options } from 'cypress-axe';
import { testA11y } from 'cypress/support/utils';
describe('Admin Sidebar', () => {
beforeEach(() => {
// Must login as an Admin for sidebar to appear
cy.visit('/login');
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
beforeEach(() => {
// Must login as an Admin for sidebar to appear
cy.visit('/login');
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
it('should be pinnable and pass accessibility tests', () => {
// Pin the sidebar open
cy.get('[data-test="sidebar-collapse-toggle"]').click();
it('should be pinnable and pass accessibility tests', () => {
// Pin the sidebar open
cy.get('#sidebar-collapse-toggle').click();
// Click on every expandable section to open all menus
cy.get('ds-expandable-admin-sidebar-section').click({ multiple: true });
// Click on every expandable section to open all menus
cy.get('ds-expandable-admin-sidebar-section').click({multiple: true});
// Analyze <ds-admin-sidebar> for accessibility
testA11y('ds-admin-sidebar');
});
// Analyze <ds-admin-sidebar> for accessibility
testA11y('ds-admin-sidebar',
{
rules: {
// Currently all expandable sections have nested interactive elements
// See https://github.com/DSpace/dspace-angular/issues/2178
'nested-interactive': { enabled: false },
}
} as Options);
});
});

View File

@@ -1,14 +1,14 @@
import { testA11y } from 'cypress/support/utils';
describe('Breadcrumbs', () => {
it('should pass accessibility tests', () => {
// Visit an Item, as those have more breadcrumbs
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
it('should pass accessibility tests', () => {
// Visit an Item, as those have more breadcrumbs
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
// Wait for breadcrumbs to be visible
cy.get('ds-breadcrumbs').should('be.visible');
// Wait for breadcrumbs to be visible
cy.get('ds-breadcrumbs').should('be.visible');
// Analyze <ds-breadcrumbs> for accessibility
testA11y('ds-breadcrumbs');
});
// Analyze <ds-breadcrumbs> for accessibility
testA11y('ds-breadcrumbs');
});
});

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Browse By Author', () => {
it('should pass accessibility tests', () => {
cy.visit('/browse/author');
it('should pass accessibility tests', () => {
cy.visit('/browse/author');
// Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata').should('be.visible');
// Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata-page').should('be.visible');
// Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata');
});
// Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata-page');
});
});

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Browse By Date Issued', () => {
it('should pass accessibility tests', () => {
cy.visit('/browse/dateissued');
it('should pass accessibility tests', () => {
cy.visit('/browse/dateissued');
// Wait for <ds-browse-by-date-page> to be visible
cy.get('ds-browse-by-date').should('be.visible');
// Wait for <ds-browse-by-date-page> to be visible
cy.get('ds-browse-by-date-page').should('be.visible');
// Analyze <ds-browse-by-date-page> for accessibility
testA11y('ds-browse-by-date');
});
// Analyze <ds-browse-by-date-page> for accessibility
testA11y('ds-browse-by-date-page');
});
});

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Browse By Subject', () => {
it('should pass accessibility tests', () => {
cy.visit('/browse/subject');
it('should pass accessibility tests', () => {
cy.visit('/browse/subject');
// Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata').should('be.visible');
// Wait for <ds-browse-by-metadata-page> to be visible
cy.get('ds-browse-by-metadata-page').should('be.visible');
// Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata');
});
// Analyze <ds-browse-by-metadata-page> for accessibility
testA11y('ds-browse-by-metadata-page');
});
});

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Browse By Title', () => {
it('should pass accessibility tests', () => {
cy.visit('/browse/title');
it('should pass accessibility tests', () => {
cy.visit('/browse/title');
// Wait for <ds-browse-by-title-page> to be visible
cy.get('ds-browse-by-title').should('be.visible');
// Wait for <ds-browse-by-title-page> to be visible
cy.get('ds-browse-by-title-page').should('be.visible');
// Analyze <ds-browse-by-title-page> for accessibility
testA11y('ds-browse-by-title');
});
// Analyze <ds-browse-by-title-page> for accessibility
testA11y('ds-browse-by-title-page');
});
});

View File

@@ -1,13 +0,0 @@
beforeEach(() => {
cy.visit('/collections/create?parent='.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
it('should show loading component while saving', () => {
const title = 'Test Collection Title';
cy.get('#title').type(title);
cy.get('button[type="submit"]').click();
cy.get('ds-loading').should('be.visible');
});

View File

@@ -3,126 +3,126 @@ import { testA11y } from 'cypress/support/utils';
const COLLECTION_EDIT_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/edit');
beforeEach(() => {
// All tests start with visiting the Edit Collection Page
cy.visit(COLLECTION_EDIT_PAGE);
// All tests start with visiting the Edit Collection Page
cy.visit(COLLECTION_EDIT_PAGE);
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
describe('Edit Collection > Edit Metadata tab', () => {
it('should pass accessibility tests', () => {
// <ds-edit-collection> tag must be loaded
cy.get('ds-edit-collection').should('be.visible');
it('should pass accessibility tests', () => {
// <ds-edit-collection> tag must be loaded
cy.get('ds-edit-collection').should('be.visible');
// Analyze <ds-edit-collection> for accessibility issues
testA11y('ds-edit-collection');
});
// Analyze <ds-edit-collection> for accessibility issues
testA11y('ds-edit-collection');
});
});
describe('Edit Collection > Assign Roles tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="roles"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="roles"]').click();
// <ds-collection-roles> tag must be loaded
cy.get('ds-collection-roles').should('be.visible');
// <ds-collection-roles> tag must be loaded
cy.get('ds-collection-roles').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-collection-roles');
});
// Analyze for accessibility issues
testA11y('ds-collection-roles');
});
});
describe('Edit Collection > Content Source tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="source"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="source"]').click();
// <ds-collection-source> tag must be loaded
cy.get('ds-collection-source').should('be.visible');
// <ds-collection-source> tag must be loaded
cy.get('ds-collection-source').should('be.visible');
// Check the external source checkbox (to display all fields on the page)
cy.get('#externalSourceCheck').check();
// Check the external source checkbox (to display all fields on the page)
cy.get('#externalSourceCheck').check();
// Wait for the source controls to appear
// cy.get('ds-collection-source-controls').should('be.visible');
// Wait for the source controls to appear
// cy.get('ds-collection-source-controls').should('be.visible');
// Analyze entire page for accessibility issues
testA11y('ds-collection-source');
});
// Analyze entire page for accessibility issues
testA11y('ds-collection-source');
});
});
describe('Edit Collection > Curate tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').click();
// <ds-collection-curate> tag must be loaded
cy.get('ds-collection-curate').should('be.visible');
// <ds-collection-curate> tag must be loaded
cy.get('ds-collection-curate').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-collection-curate');
});
// Analyze for accessibility issues
testA11y('ds-collection-curate');
});
});
describe('Edit Collection > Access Control tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').click();
// <ds-collection-access-control> tag must be loaded
cy.get('ds-collection-access-control').should('be.visible');
// <ds-collection-access-control> tag must be loaded
cy.get('ds-collection-access-control').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-collection-access-control');
});
// Analyze for accessibility issues
testA11y('ds-collection-access-control');
});
});
describe('Edit Collection > Authorizations tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="authorizations"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="authorizations"]').click();
// <ds-collection-authorizations> tag must be loaded
cy.get('ds-collection-authorizations').should('be.visible');
// <ds-collection-authorizations> tag must be loaded
cy.get('ds-collection-authorizations').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-collection-authorizations');
});
// Analyze for accessibility issues
testA11y('ds-collection-authorizations');
});
});
describe('Edit Collection > Item Mapper tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="mapper"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="mapper"]').click();
// <ds-collection-item-mapper> tag must be loaded
cy.get('ds-collection-item-mapper').should('be.visible');
// <ds-collection-item-mapper> tag must be loaded
cy.get('ds-collection-item-mapper').should('be.visible');
// Analyze entire page for accessibility issues
testA11y('ds-collection-item-mapper');
// Analyze entire page for accessibility issues
testA11y('ds-collection-item-mapper');
// Click on the "Map new Items" tab
cy.get('li[data-test="mapTab"] a').click();
// Click on the "Map new Items" tab
cy.get('li[data-test="mapTab"] a').click();
// Make sure search form is now visible
cy.get('ds-search-form').should('be.visible');
// Make sure search form is now visible
cy.get('ds-search-form').should('be.visible');
// Analyze entire page (again) for accessibility issues
testA11y('ds-collection-item-mapper');
});
// Analyze entire page (again) for accessibility issues
testA11y('ds-collection-item-mapper');
});
});
describe('Edit Collection > Delete page', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="delete-button"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="delete-button"]').click();
// <ds-delete-collection> tag must be loaded
cy.get('ds-delete-collection').should('be.visible');
// <ds-delete-collection> tag must be loaded
cy.get('ds-delete-collection').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-delete-collection');
});
// Analyze for accessibility issues
testA11y('ds-delete-collection');
});
});

View File

@@ -3,8 +3,15 @@ import { testA11y } from 'cypress/support/utils';
describe('Collection Page', () => {
it('should pass accessibility tests', () => {
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
// Visit Collections page
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
// Wait for the "viewevent" to trigger on the Collection page.
// This ensures our <ds-collection-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
cy.wait('@viewevent');
// <ds-collection-page> tag must be loaded
cy.get('ds-collection-page').should('be.visible');

View File

@@ -2,36 +2,36 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
describe('Collection Statistics Page', () => {
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'));
const COLLECTIONSTATISTICSPAGE = '/statistics/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION'));
it('should load if you click on "Statistics" from a Collection page', () => {
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
});
it('should load if you click on "Statistics" from a Collection page', () => {
cy.visit('/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', COLLECTIONSTATISTICSPAGE);
});
it('should contain a "Total visits" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
// Check just for existence because this table is empty in CI environment as it's historical data
cy.get('.'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('_TotalVisitsPerMonth')).should('exist');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
// Check just for existence because this table is empty in CI environment as it's historical data
cy.get('.'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('_TotalVisitsPerMonth')).should('exist');
});
it('should pass accessibility tests', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
it('should pass accessibility tests', () => {
cy.visit(COLLECTIONSTATISTICSPAGE);
// <ds-collection-statistics-page> tag must be loaded
cy.get('ds-collection-statistics-page').should('be.visible');
// <ds-collection-statistics-page> tag must be loaded
cy.get('ds-collection-statistics-page').should('be.visible');
// Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Analyze <ds-collection-statistics-page> for accessibility issues
testA11y('ds-collection-statistics-page');
});
// Analyze <ds-collection-statistics-page> for accessibility issues
testA11y('ds-collection-statistics-page');
});
});

View File

@@ -1,13 +0,0 @@
beforeEach(() => {
cy.visit('/communities/create');
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
it('should show loading component while saving', () => {
const title = 'Test Community Title';
cy.get('#title').type(title);
cy.get('button[type="submit"]').click();
cy.get('ds-loading').should('be.visible');
});

View File

@@ -3,84 +3,84 @@ import { testA11y } from 'cypress/support/utils';
const COMMUNITY_EDIT_PAGE = '/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('/edit');
beforeEach(() => {
// All tests start with visiting the Edit Community Page
cy.visit(COMMUNITY_EDIT_PAGE);
// All tests start with visiting the Edit Community Page
cy.visit(COMMUNITY_EDIT_PAGE);
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
describe('Edit Community > Edit Metadata tab', () => {
it('should pass accessibility tests', () => {
// <ds-edit-community> tag must be loaded
cy.get('ds-edit-community').should('be.visible');
it('should pass accessibility tests', () => {
// <ds-edit-community> tag must be loaded
cy.get('ds-edit-community').should('be.visible');
// Analyze <ds-edit-community> for accessibility issues
testA11y('ds-edit-community');
});
// Analyze <ds-edit-community> for accessibility issues
testA11y('ds-edit-community');
});
});
describe('Edit Community > Assign Roles tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="roles"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="roles"]').click();
// <ds-community-roles> tag must be loaded
cy.get('ds-community-roles').should('be.visible');
// <ds-community-roles> tag must be loaded
cy.get('ds-community-roles').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-community-roles');
});
// Analyze for accessibility issues
testA11y('ds-community-roles');
});
});
describe('Edit Community > Curate tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="curate"]').click();
// <ds-community-curate> tag must be loaded
cy.get('ds-community-curate').should('be.visible');
// <ds-community-curate> tag must be loaded
cy.get('ds-community-curate').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-community-curate');
});
// Analyze for accessibility issues
testA11y('ds-community-curate');
});
});
describe('Edit Community > Access Control tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="access-control"]').click();
// <ds-community-access-control> tag must be loaded
cy.get('ds-community-access-control').should('be.visible');
// <ds-community-access-control> tag must be loaded
cy.get('ds-community-access-control').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-community-access-control');
});
// Analyze for accessibility issues
testA11y('ds-community-access-control');
});
});
describe('Edit Community > Authorizations tab', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="authorizations"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="authorizations"]').click();
// <ds-community-authorizations> tag must be loaded
cy.get('ds-community-authorizations').should('be.visible');
// <ds-community-authorizations> tag must be loaded
cy.get('ds-community-authorizations').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-community-authorizations');
});
// Analyze for accessibility issues
testA11y('ds-community-authorizations');
});
});
describe('Edit Community > Delete page', () => {
it('should pass accessibility tests', () => {
cy.get('a[data-test="delete-button"]').click();
it('should pass accessibility tests', () => {
cy.get('a[data-test="delete-button"]').click();
// <ds-delete-community> tag must be loaded
cy.get('ds-delete-community').should('be.visible');
// <ds-delete-community> tag must be loaded
cy.get('ds-delete-community').should('be.visible');
// Analyze for accessibility issues
testA11y('ds-delete-community');
});
// Analyze for accessibility issues
testA11y('ds-delete-community');
});
});

View File

@@ -1,98 +1,17 @@
import { testA11y } from 'cypress/support/utils';
describe('Community List Page', () => {
function validateHierarchyLevel(currentLevel = 1): void {
// Find all <cdk-tree-node> elements with the current aria-level
cy.get(`ds-community-list cdk-tree-node.expandable-node[aria-level="${currentLevel}"]`).should('exist').then(($nodes) => {
let sublevelExists = false;
cy.wrap($nodes).each(($node) => {
// Check if the current node has an expand button and click it
if ($node.find('[data-test="expand-button"]').length) {
sublevelExists = true;
cy.wrap($node).find('[data-test="expand-button"]').click();
}
}).then(() => {
// After expanding all buttons, validate if a sublevel exists
if (sublevelExists) {
const nextLevelSelector = `ds-community-list cdk-tree-node.expandable-node[aria-level="${currentLevel + 1}"]`;
cy.get(nextLevelSelector).then(($nextLevel) => {
if ($nextLevel.length) {
// Recursively validate the next level
validateHierarchyLevel(currentLevel + 1);
}
});
}
});
it('should pass accessibility tests', () => {
cy.visit('/community-list');
// <ds-community-list-page> tag must be loaded
cy.get('ds-community-list-page').should('be.visible');
// Open every expand button on page, so that we can scan sub-elements as well
cy.get('[data-test="expand-button"]').click({ multiple: true });
// Analyze <ds-community-list-page> for accessibility issues
testA11y('ds-community-list-page');
});
}
beforeEach(() => {
cy.visit('/community-list');
// <ds-community-list-page> tag must be loaded
cy.get('ds-community-list-page').should('be.visible');
// <ds-community-list-list> tag must be loaded
cy.get('ds-community-list').should('be.visible');
});
it('should expand community/collection hierarchy', () => {
// Execute Hierarchy levels validation recursively
validateHierarchyLevel(1);
});
it('should display community/collections name with item count', () => {
// Open every <cdk-tree-node>
cy.get('[data-test="expand-button"]').click({ multiple: true });
cy.wait(300);
// A first <cdk-tree-node> must be found and validate that <a> tag (community name) and <span> tag (item count) exists in it
cy.get('ds-community-list').find('cdk-tree-node.expandable-node').then(($nodes) => {
cy.wrap($nodes).each(($node) => {
cy.wrap($node).find('a').should('exist');
cy.wrap($node).find('span').should('exist');
});
});
});
it('should enable "show more" button when 20 top-communities or more are presents', () => {
cy.get('ds-community-list').find('cdk-tree-node.expandable-node[aria-level="1"]').then(($nodes) => {
//Validate that there are 20 or more top-community elements
if ($nodes.length >= 20) {
//Validate that "show more" button is visible and then click on it
cy.get('[data-test="show-more-button"]').should('be.visible');
} else {
cy.get('[data-test="show-more-button"]').should('not.exist');
}
});
});
it('should show 21 or more top-communities if click "show more" button', () => {
cy.get('ds-community-list').find('cdk-tree-node.expandable-node[aria-level="1"]').then(($nodes) => {
//Validate that there are 20 or more top-community elements
if ($nodes.length >= 20) {
//Validate that "show more" button is visible and then click on it
cy.get('[data-test="show-more-button"]').click();
cy.wait(300);
cy.get('ds-community-list').find('cdk-tree-node.expandable-node[aria-level="1"]').should('have.length.at.least', 21);
} else {
cy.get('[data-test="show-more-button"]').should('not.exist');
}
});
});
it('should pass accessibility tests', () => {
// Open every expand button on page, so that we can scan sub-elements as well
cy.get('[data-test="expand-button"]').click({ multiple: true });
// Analyze <ds-community-list-page> for accessibility issues
testA11y('ds-community-list-page', {
rules: {
// When expanding a cdk node on the community-list page, the 'aria-posinset' property becomes 0.
// 0 is not a valid value for 'aria-posinset' so the test fails.
// see https://github.com/DSpace/dspace-angular/issues/4068
'aria-valid-attr-value': { enabled: false },
},
});
});
});

View File

@@ -2,13 +2,13 @@ import { testA11y } from 'cypress/support/utils';
describe('Community Page', () => {
it('should pass accessibility tests', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
it('should pass accessibility tests', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
// <ds-community-page> tag must be loaded
cy.get('ds-community-page').should('be.visible');
// <ds-community-page> tag must be loaded
cy.get('ds-community-page').should('be.visible');
// Analyze <ds-community-page> for accessibility issues
testA11y('ds-community-page');
});
// Analyze <ds-community-page> for accessibility issues
testA11y('ds-community-page');
});
});

View File

@@ -2,36 +2,36 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
describe('Community Statistics Page', () => {
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'));
const COMMUNITYSTATISTICSPAGE = '/statistics/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY'));
it('should load if you click on "Statistics" from a Community page', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
});
it('should load if you click on "Statistics" from a Community page', () => {
cy.visit('/communities/'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')));
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', COMMUNITYSTATISTICSPAGE);
});
it('should contain a "Total visits" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
// Check just for existence because this table is empty in CI environment as it's historical data
cy.get('.'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('_TotalVisitsPerMonth')).should('exist');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
// Check just for existence because this table is empty in CI environment as it's historical data
cy.get('.'.concat(Cypress.env('DSPACE_TEST_COMMUNITY')).concat('_TotalVisitsPerMonth')).should('exist');
});
it('should pass accessibility tests', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
it('should pass accessibility tests', () => {
cy.visit(COMMUNITYSTATISTICSPAGE);
// <ds-community-statistics-page> tag must be loaded
cy.get('ds-community-statistics-page').should('be.visible');
// <ds-community-statistics-page> tag must be loaded
cy.get('ds-community-statistics-page').should('be.visible');
// Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Analyze <ds-community-statistics-page> for accessibility issues
testA11y('ds-community-statistics-page');
});
// Analyze <ds-community-statistics-page> for accessibility issues
testA11y('ds-community-statistics-page');
});
});

View File

@@ -1,13 +1,13 @@
import { testA11y } from 'cypress/support/utils';
describe('Footer', () => {
it('should pass accessibility tests', () => {
cy.visit('/');
it('should pass accessibility tests', () => {
cy.visit('/');
// Footer must first be visible
cy.get('ds-footer').should('be.visible');
// Footer must first be visible
cy.get('ds-footer').should('be.visible');
// Analyze <ds-footer> for accessibility
testA11y('ds-footer');
});
// Analyze <ds-footer> for accessibility
testA11y('ds-footer');
});
});

View File

@@ -1,38 +1,38 @@
import { testA11y } from 'cypress/support/utils';
describe('Header', () => {
it('should pass accessibility tests', () => {
cy.visit('/');
it('should pass accessibility tests', () => {
cy.visit('/');
// Header must first be visible
cy.get('ds-header').should('be.visible');
// Header must first be visible
cy.get('ds-header').should('be.visible');
// Analyze <ds-header> for accessibility
testA11y('ds-header');
});
// Analyze <ds-header> for accessibility
testA11y('ds-header');
});
it('should allow for changing language to German (for example)', () => {
cy.visit('/');
it('should allow for changing language to German (for example)', () => {
cy.visit('/');
// Click the language switcher (globe) in header
cy.get('button[data-test="lang-switch"]').click();
// Click on the "Deusch" language in dropdown
cy.get('#language-menu-list div[role="option"]').contains('Deutsch').click();
// Click the language switcher (globe) in header
cy.get('button[data-test="lang-switch"]').click();
// Click on the "Deusch" language in dropdown
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');
// 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('[data-test="login-menu"]').contains('Anmelden');
// Login menu should now be in German
cy.get('[data-test="login-menu"]').contains('Anmelden');
// Change back to English from language switcher
cy.get('button[data-test="lang-switch"]').click();
cy.get('#language-menu-list div[role="option"]').contains('English').click();
// Change back to English from language switcher
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');
// 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('[data-test="login-menu"]').contains('Log In');
});
// Login menu should now be in English
cy.get('[data-test="login-menu"]').contains('Log In');
});
});

View File

@@ -4,14 +4,13 @@ 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();
@@ -37,6 +36,7 @@ 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();

View File

@@ -1,32 +1,31 @@
import '../support/commands';
import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
import '../support/commands';
describe('Site Statistics Page', () => {
it('should load if you click on "Statistics" from homepage', () => {
cy.visit('/');
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', '/statistics');
});
it('should load if you click on "Statistics" from homepage', () => {
cy.visit('/');
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', '/statistics');
});
it('should pass accessibility tests', () => {
// generate 2 view events on an Item's page
cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item');
cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item');
it('should pass accessibility tests', () => {
// generate 2 view events on an Item's page
cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item');
cy.generateViewEvent(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'), 'item');
cy.visit('/statistics');
cy.visit('/statistics');
// <ds-site-statistics-page> tag must be visible
cy.get('ds-site-statistics-page').should('be.visible');
// <ds-site-statistics-page> tag must be visable
cy.get('ds-site-statistics-page').should('be.visible');
// Verify / wait until "Total Visits" table's *last* label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').last().contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Wait an extra 500ms, just so all entries in Total Visits have loaded.
cy.wait(500);
// Verify / wait until "Total Visits" table's *last* label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').last().contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Wait an extra 500ms, just so all entries in Total Visits have loaded.
cy.wait(500);
// Analyze <ds-site-statistics-page> for accessibility issues
testA11y('ds-site-statistics-page');
});
// Analyze <ds-site-statistics-page> for accessibility issues
testA11y('ds-site-statistics-page');
});
});

View File

@@ -26,12 +26,6 @@ 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');
});

View File

@@ -1,32 +1,45 @@
import { testA11y } from 'cypress/support/utils';
describe('Item Page', () => {
const ITEMPAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
const ITEMPAGE = '/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
it('should redirect to the entity page when navigating to an item page', () => {
cy.visit(ITEMPAGE);
cy.location('pathname').should('eq', ENTITYPAGE);
});
// Test that entities will redirect to /entities/[type]/[uuid] when accessed via /items/[uuid]
it('should redirect to the entity page when navigating to an item page', () => {
cy.visit(ITEMPAGE);
cy.wait(1000);
cy.location('pathname').should('eq', ENTITYPAGE);
});
it('should pass accessibility tests', () => {
cy.visit(ENTITYPAGE);
it('should pass accessibility tests', () => {
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
// <ds-item-page> tag must be loaded
cy.get('ds-item-page').should('be.visible');
cy.visit(ENTITYPAGE);
// Analyze <ds-item-page> for accessibility issues
testA11y('ds-item-page');
});
// Wait for the "viewevent" to trigger on the Item page.
// This ensures our <ds-item-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
cy.wait('@viewevent');
it('should pass accessibility tests on full item page', () => {
cy.visit(ENTITYPAGE + '/full');
// <ds-item-page> tag must be loaded
cy.get('ds-item-page').should('be.visible');
// <ds-full-item-page> tag must be loaded
cy.get('ds-full-item-page').should('be.visible');
// Analyze <ds-item-page> for accessibility issues
testA11y('ds-item-page');
});
// Analyze <ds-full-item-page> for accessibility issues
testA11y('ds-full-item-page');
});
it('should pass accessibility tests on full item page', () => {
cy.intercept('POST', '/server/api/statistics/viewevents').as('viewevent');
cy.visit(ENTITYPAGE + '/full');
// Wait for the "viewevent" to trigger on the Item page.
// This ensures our <ds-item-page> tag is fully loaded, as the <ds-view-event> tag is contained within it.
cy.wait('@viewevent');
// <ds-full-item-page> tag must be loaded
cy.get('ds-full-item-page').should('be.visible');
// Analyze <ds-full-item-page> for accessibility issues
testA11y('ds-full-item-page');
});
});

View File

@@ -2,42 +2,41 @@ import { REGEX_MATCH_NON_EMPTY_TEXT } from 'cypress/support/e2e';
import { testA11y } from 'cypress/support/utils';
describe('Item Statistics Page', () => {
const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
const ITEMSTATISTICSPAGE = '/statistics/items/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
it('should load if you click on "Statistics" from an Item/Entity page', () => {
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
cy.get('ds-navbar ds-link-menu-item a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', '/statistics/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
});
it('should load if you click on "Statistics" from an Item/Entity page', () => {
cy.visit('/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')));
cy.get('a[data-test="link-menu-item.menu.section.statistics"]').click();
cy.location('pathname').should('eq', ITEMSTATISTICSPAGE);
});
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('ds-item-statistics-page').should('be.visible');
cy.get('ds-item-page').should('not.exist');
});
it('should contain element ds-item-statistics-page when navigating to an item statistics page', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('ds-item-statistics-page').should('be.visible');
cy.get('ds-item-page').should('not.exist');
});
it('should contain a "Total visits" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('table[data-test="TotalVisits"]').should('be.visible');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
// Check just for existence because this table is empty in CI environment as it's historical data
cy.get('.'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION')).concat('_TotalVisitsPerMonth')).should('exist');
});
it('should contain a "Total visits per month" section', () => {
cy.visit(ITEMSTATISTICSPAGE);
cy.get('table[data-test="TotalVisitsPerMonth"]').should('be.visible');
});
it('should pass accessibility tests', () => {
cy.visit(ITEMSTATISTICSPAGE);
it('should pass accessibility tests', () => {
cy.visit(ITEMSTATISTICSPAGE);
// <ds-item-statistics-page> tag must be loaded
cy.get('ds-item-statistics-page').should('be.visible');
// <ds-item-statistics-page> tag must be loaded
cy.get('ds-item-statistics-page').should('be.visible');
// Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Verify / wait until "Total Visits" table's label is non-empty
// (This table loads these labels asynchronously, so we want to wait for them before analyzing page)
cy.get('table[data-test="TotalVisits"] th[data-test="statistics-label"]').contains(REGEX_MATCH_NON_EMPTY_TEXT);
// Analyze <ds-item-statistics-page> for accessibility issues
testA11y('ds-item-statistics-page');
});
// Analyze <ds-item-statistics-page> for accessibility issues
testA11y('ds-item-statistics-page');
});
});

View File

@@ -1,15 +0,0 @@
const ADD_TEMPLATE_ITEM_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/itemtemplate');
describe('Item Template', () => {
beforeEach(() => {
cy.visit(ADD_TEMPLATE_ITEM_PAGE);
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
it('should load properly', () => {
cy.contains('.ds-header-row .lbl-cell', 'Field', { timeout: 10000 }).should('exist').should('be.visible');
cy.contains('.ds-header-row b', 'Value', { timeout: 10000 }).should('exist').should('be.visible');
cy.contains('.ds-header-row b', 'Lang', { timeout: 10000 }).should('exist').should('be.visible');
cy.contains('.ds-header-row b', 'Edit', { timeout: 10000 }).should('exist').should('be.visible');
});
});

View File

@@ -35,116 +35,116 @@ const page = {
};
describe('Login Modal', () => {
it('should login when clicking button & stay on same page', () => {
const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
cy.visit(ENTITYPAGE);
it('should login when clicking button & stay on same page', () => {
const ENTITYPAGE = '/entities/publication/'.concat(Cypress.env('DSPACE_TEST_ENTITY_PUBLICATION'));
cy.visit(ENTITYPAGE);
// Login menu should exist
cy.get('ds-log-in').should('exist');
// Login menu should exist
cy.get('ds-log-in').should('exist');
// Login, and the <ds-log-in> tag should no longer exist
page.openLoginMenu();
cy.get('.form-login').should('be.visible');
// Login, and the <ds-log-in> tag should no longer exist
page.openLoginMenu();
cy.get('.form-login').should('be.visible');
page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('ds-log-in').should('not.exist');
page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('ds-log-in').should('not.exist');
// Verify we are still on the same page
cy.url().should('include', ENTITYPAGE);
// Verify we are still on the same page
cy.url().should('include', ENTITYPAGE);
// Open user menu, verify user menu & logout button now available
page.openUserMenu();
cy.get('ds-user-menu').should('be.visible');
cy.get('ds-log-out').should('be.visible');
});
// Open user menu, verify user menu & logout button now available
page.openUserMenu();
cy.get('ds-user-menu').should('be.visible');
cy.get('ds-log-out').should('be.visible');
});
it('should login when clicking enter key & stay on same page', () => {
cy.visit('/home');
it('should login when clicking enter key & stay on same page', () => {
cy.visit('/home');
// Open login menu in header & verify <ds-log-in> tag is visible
page.openLoginMenu();
cy.get('.form-login').should('be.visible');
// Open login menu in header & verify <ds-log-in> tag is visible
page.openLoginMenu();
cy.get('.form-login').should('be.visible');
// Login, and the <ds-log-in> tag should no longer exist
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('ds-log-in').should('not.exist');
// Login, and the <ds-log-in> tag should no longer exist
page.submitLoginAndPasswordByPressingEnter(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('ds-log-in').should('not.exist');
// Verify we are still on homepage
cy.url().should('include', '/home');
// Verify we are still on homepage
cy.url().should('include', '/home');
// Open user menu, verify user menu & logout button now available
page.openUserMenu();
cy.get('ds-user-menu').should('be.visible');
cy.get('ds-log-out').should('be.visible');
});
// Open user menu, verify user menu & logout button now available
page.openUserMenu();
cy.get('ds-user-menu').should('be.visible');
cy.get('ds-log-out').should('be.visible');
});
it('should support logout', () => {
// First authenticate & access homepage
cy.login(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.visit('/');
it('should support logout', () => {
// First authenticate & access homepage
cy.login(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.visit('/');
// Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist
cy.get('ds-log-in').should('not.exist');
cy.get('ds-log-out').should('exist');
// Verify ds-log-in tag doesn't exist, but ds-log-out tag does exist
cy.get('ds-log-in').should('not.exist');
cy.get('ds-log-out').should('exist');
// Click logout button
page.openUserMenu();
page.submitLogoutByPressingButton();
// Click logout button
page.openUserMenu();
page.submitLogoutByPressingButton();
// Verify ds-log-in tag now exists
cy.get('ds-log-in').should('exist');
cy.get('ds-log-out').should('not.exist');
});
// Verify ds-log-in tag now exists
cy.get('ds-log-in').should('exist');
cy.get('ds-log-out').should('not.exist');
});
it('should allow new user registration', () => {
cy.visit('/');
it('should allow new user registration', () => {
cy.visit('/');
page.openLoginMenu();
page.openLoginMenu();
// Registration link should be visible
cy.get('ds-header [data-test="register"]').should('be.visible');
// Registration link should be visible
cy.get('ds-themed-header [data-test="register"]').should('be.visible');
// Click registration link & you should go to registration page
cy.get('ds-header [data-test="register"]').click();
cy.location('pathname').should('eq', '/register');
cy.get('ds-register-email').should('exist');
// Click registration link & you should go to registration page
cy.get('ds-themed-header [data-test="register"]').click();
cy.location('pathname').should('eq', '/register');
cy.get('ds-register-email').should('exist');
// Test accessibility of this page
testA11y('ds-register-email');
});
// Test accessibility of this page
testA11y('ds-register-email');
});
it('should allow forgot password', () => {
cy.visit('/');
it('should allow forgot password', () => {
cy.visit('/');
page.openLoginMenu();
page.openLoginMenu();
// Forgot password link should be visible
cy.get('ds-header [data-test="forgot"]').should('be.visible');
// Forgot password link should be visible
cy.get('ds-themed-header [data-test="forgot"]').should('be.visible');
// Click link & you should go to Forgot Password page
cy.get('ds-header [data-test="forgot"]').click();
cy.location('pathname').should('eq', '/forgot');
cy.get('ds-forgot-email').should('exist');
// Click link & you should go to Forgot Password page
cy.get('ds-themed-header [data-test="forgot"]').click();
cy.location('pathname').should('eq', '/forgot');
cy.get('ds-forgot-email').should('exist');
// Test accessibility of this page
testA11y('ds-forgot-email');
});
// Test accessibility of this page
testA11y('ds-forgot-email');
});
it('should pass accessibility tests in menus', () => {
cy.visit('/');
it('should pass accessibility tests in menus', () => {
cy.visit('/');
// Open login menu & verify accessibility
page.openLoginMenu();
cy.get('ds-log-in').should('exist');
testA11y('ds-log-in');
// Open login menu & verify accessibility
page.openLoginMenu();
cy.get('ds-log-in').should('exist');
testA11y('ds-log-in');
// Now login
page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('ds-log-in').should('not.exist');
// Now login
page.submitLoginAndPasswordByPressingButton(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
cy.get('ds-log-in').should('not.exist');
// Open user menu, verify user menu accessibility
page.openUserMenu();
cy.get('ds-user-menu').should('be.visible');
testA11y('ds-user-menu');
});
// Open user menu, verify user menu accesibility
page.openUserMenu();
cy.get('ds-user-menu').should('be.visible');
testA11y('ds-user-menu');
});
});

View File

@@ -1,134 +1,134 @@
import { testA11y } from 'cypress/support/utils';
describe('My DSpace page', () => {
it('should display recent submissions and pass accessibility tests', () => {
cy.visit('/mydspace');
it('should display recent submissions and pass accessibility tests', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
cy.get('ds-my-dspace-page').should('be.visible');
cy.get('ds-my-dspace-page').should('be.visible');
// At least one recent submission should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// At least one recent submission should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true });
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('.filter-toggle').click({ multiple: true });
// Analyze <ds-my-dspace-page> for accessibility issues
testA11y('ds-my-dspace-page');
});
it('should have a working detailed view that passes accessibility tests', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
cy.get('ds-my-dspace-page').should('be.visible');
// Click button in sidebar to display detailed view
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
cy.get('ds-object-detail').should('be.visible');
// Analyze <ds-my-dspace-page> for accessibility issues
testA11y('ds-my-dspace-page');
});
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
it('should let you start a new submission & edit in-progress submissions', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Item" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// Type in a known Collection name in the search box
cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// Click on the button matching that known Collection name
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')).concat('"]')).click();
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & its value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// Now that we've created a submission, we'll test that we can go back and Edit it.
// Get our Submission URL, to parse out the ID of this new submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Click the "Save for Later" button to save this submission
cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// Close any open notifications, to make sure they don't get in the way of next steps
cy.get('[data-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');
// On MyDSpace, find the submission we just created via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Click the Edit button for this in-progress submission
cy.get('#edit_' + id).click();
// Should send us back to the submission form
cy.url().should('include', '/workspaceitems/' + id + '/edit');
// Discard our new submission by clicking Discard in Submission form & confirming
cy.get('ds-submission-form-footer [data-test="discard"]').click();
cy.get('button#discard_submit').click();
// Discarding should send us back to MyDSpace
cy.url().should('include', '/mydspace');
// Analyze <ds-my-dspace-page> for accessibility issues
testA11y('ds-my-dspace-page');
});
});
it('should let you import from external sources', () => {
cy.visit('/mydspace');
it('should have a working detailed view that passes accessibility tests', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Open the New Import dropdown
cy.get('button[data-test="import-dropdown"]').click();
// Click on the "Item" type in that dropdown
cy.get('#importControlsDropdownMenu button[title="none"]').click();
cy.get('ds-my-dspace-page').should('be.visible');
// New URL should include /import-external, as we've moved to the import page
cy.url().should('include', '/import-external');
// Click button in sidebar to display detailed view
cy.get('ds-search-sidebar [data-test="detail-view"]').click();
// The external import searchbox should be visible
cy.get('ds-submission-import-external-searchbar').should('be.visible');
cy.get('ds-object-detail').should('be.visible');
// Test for accessibility issues
testA11y('ds-submission-import-external');
});
// Analyze <ds-my-dspace-page> for accessibility issues
testA11y('ds-my-dspace-page');
});
// NOTE: Deleting existing submissions is exercised by submission.spec.ts
it('should let you start a new submission & edit in-progress submissions', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Item" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="none"]').click();
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// Type in a known Collection name in the search box
cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// Click on the button matching that known Collection name
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME')).concat('"]')).click();
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & its value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// Now that we've created a submission, we'll test that we can go back and Edit it.
// Get our Submission URL, to parse out the ID of this new submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Click the "Save for Later" button to save this submission
cy.get('ds-submission-form-footer [data-test="save-for-later"]').click();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// Close any open notifications, to make sure they don't get in the way of next steps
cy.get('[data-dismiss="alert"]').click({multiple: true});
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// On MyDSpace, find the submission we just created via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Click the Edit button for this in-progress submission
cy.get('#edit_' + id).click();
// Should send us back to the submission form
cy.url().should('include', '/workspaceitems/' + id + '/edit');
// Discard our new submission by clicking Discard in Submission form & confirming
cy.get('ds-submission-form-footer [data-test="discard"]').click();
cy.get('button#discard_submit').click();
// Discarding should send us back to MyDSpace
cy.url().should('include', '/mydspace');
});
});
it('should let you import from external sources', () => {
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Open the New Import dropdown
cy.get('button[data-test="import-dropdown"]').click();
// Click on the "Item" type in that dropdown
cy.get('#importControlsDropdownMenu button[title="none"]').click();
// New URL should include /import-external, as we've moved to the import page
cy.url().should('include', '/import-external');
// The external import searchbox should be visible
cy.get('ds-submission-import-external-searchbar').should('be.visible');
// Test for accessibility issues
testA11y('ds-submission-import-external');
});
});

View File

@@ -1,18 +1,18 @@
import { testA11y } from 'cypress/support/utils';
describe('PageNotFound', () => {
it('should contain element ds-pagenotfound when navigating to page that does not exist', () => {
// request an invalid page (UUIDs at root path aren't valid)
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
cy.get('ds-pagenotfound').should('be.visible');
it('should contain element ds-pagenotfound when navigating to page that doesnt exist', () => {
// request an invalid page (UUIDs at root path aren't valid)
cy.visit('/e9019a69-d4f1-4773-b6a3-bd362caa46f2', { failOnStatusCode: false });
cy.get('ds-pagenotfound').should('be.visible');
// Analyze <ds-pagenotfound> for accessibility issues
testA11y('ds-pagenotfound');
});
// Analyze <ds-pagenotfound> for accessibility issues
testA11y('ds-pagenotfound');
});
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
cy.visit('/home');
cy.get('ds-pagenotfound').should('not.exist');
});
it('should not contain element ds-pagenotfound when navigating to existing page', () => {
cy.visit('/home');
cy.get('ds-pagenotfound').should('not.exist');
});
});

View File

@@ -1,16 +0,0 @@
import { testA11y } from 'cypress/support/utils';
describe('Quality Assurance Source Page', () => {
beforeEach(() => {
// Must login as an Admin to see the page
cy.visit('/notifications/quality-assurance');
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
});
it('should pass accessibility tests', () => {
// Source page must first be visible
cy.get('ds-quality-assurance-source-page-component').should('be.visible');
// Analyze <ds-quality-assurance-source-page-component> for accessibility issues
testA11y('ds-quality-assurance-source-page-component');
});
});

View File

@@ -1,64 +1,64 @@
const page = {
fillOutQueryInNavBar(query) {
// Click the magnifying glass
cy.get('ds-header [data-test="header-search-icon"]').click();
// Fill out a query in input that appears
cy.get('ds-header [data-test="header-search-box"]').type(query);
},
submitQueryByPressingEnter() {
cy.get('ds-header [data-test="header-search-box"]').type('{enter}');
},
submitQueryByPressingIcon() {
cy.get('ds-header [data-test="header-search-icon"]').click();
},
fillOutQueryInNavBar(query) {
// Click the magnifying glass
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
// Fill out a query in input that appears
cy.get('ds-themed-header [data-test="header-search-box"]').type(query);
},
submitQueryByPressingEnter() {
cy.get('ds-themed-header [data-test="header-search-box"]').type('{enter}');
},
submitQueryByPressingIcon() {
cy.get('ds-themed-header [data-test="header-search-icon"]').click();
}
};
describe('Search from Navigation Bar', () => {
// NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
// NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
it('should go to search page with correct query if submitted (from home)', () => {
cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should go to search page with correct query if submitted (from home)', () => {
cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should go to search page with correct query if submitted (from search)', () => {
cy.visit('/search');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should go to search page with correct query if submitted (from search)', () => {
cy.visit('/search');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingEnter();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should allow user to also submit query by clicking icon', () => {
cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingIcon();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
it('should allow user to also submit query by clicking icon', () => {
cy.visit('/');
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// Run the search
page.fillOutQueryInNavBar(query);
page.submitQueryByPressingIcon();
// New URL should include query param
cy.url().should('include', 'query='.concat(query));
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
});
});

View File

@@ -1,57 +1,57 @@
import { testA11y } from 'cypress/support/utils';
import { Options } from 'cypress-axe';
import { testA11y } from 'cypress/support/utils';
describe('Search Page', () => {
// NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
// NOTE: these tests currently assume this query will return results!
const query = Cypress.env('DSPACE_TEST_SEARCH_TERM');
it('should redirect to the correct url when query was set and submit button was triggered', () => {
const queryString = 'Another interesting query string';
cy.visit('/search');
// Type query in searchbox & click search button
cy.get('[data-test="search-box"]').type(queryString);
cy.get('[data-test="search-button"]').click();
cy.url().should('include', 'query=' + encodeURI(queryString));
});
it('should redirect to the correct url when query was set and submit button was triggered', () => {
const queryString = 'Another interesting query string';
cy.visit('/search');
// Type query in searchbox & click search button
cy.get('[data-test="search-box"]').type(queryString);
cy.get('[data-test="search-button"]').click();
cy.url().should('include', 'query=' + encodeURI(queryString));
});
it('should load results and pass accessibility tests', () => {
cy.visit('/search?query='.concat(query));
cy.get('[data-test="search-box"]').should('have.value', query);
it('should load results and pass accessibility tests', () => {
cy.visit('/search?query='.concat(query));
cy.get('[data-test="search-box"]').should('have.value', query);
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible');
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// At least one search result should be displayed
cy.get('[data-test="list-object"]').should('be.visible');
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Click each filter toggle to open *every* filter
// (As we want to scan filter section for accessibility issues as well)
cy.get('[data-test="filter-toggle"]').click({ multiple: true });
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page');
});
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page');
});
it('should have a working grid view that passes accessibility tests', () => {
cy.visit('/search?query='.concat(query));
it('should have a working grid view that passes accessibility tests', () => {
cy.visit('/search?query='.concat(query));
// Click button in sidebar to display grid view
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
// Click button in sidebar to display grid view
cy.get('ds-search-sidebar [data-test="grid-view"]').click();
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible');
// <ds-search-page> tag must be loaded
cy.get('ds-search-page').should('be.visible');
// At least one grid object (card) should be displayed
cy.get('[data-test="grid-object"]').should('be.visible');
// At least one grid object (card) should be displayed
cy.get('[data-test="grid-object"]').should('be.visible');
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page',
// Analyze <ds-search-page> for accessibility issues
testA11y('ds-search-page',
{
rules: {
// Card titles fail this test currently
'heading-order': { enabled: false },
},
} as Options,
);
});
rules: {
// Card titles fail this test currently
'heading-order': { enabled: false }
}
} as Options
);
});
});

View File

@@ -4,224 +4,224 @@ import { Options } from 'cypress-axe';
describe('New Submission page', () => {
// NOTE: We already test that new Item submissions can be started from MyDSpace in my-dspace.spec.ts
it('should create a new submission when using /submit path & pass accessibility', () => {
// Test that calling /submit with collection & entityType will create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// NOTE: We already test that new Item submissions can be started from MyDSpace in my-dspace.spec.ts
it('should create a new submission when using /submit path & pass accessibility', () => {
// Test that calling /submit with collection & entityType will create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Should redirect to /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// Should redirect to /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & it's value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// A Collection menu button should exist & it's value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_NAME'));
// 4 sections should be visible by default
cy.get('div#section_traditionalpageone').should('be.visible');
cy.get('div#section_traditionalpagetwo').should('be.visible');
cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible');
// 4 sections should be visible by default
cy.get('div#section_traditionalpageone').should('be.visible');
cy.get('div#section_traditionalpagetwo').should('be.visible');
cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible');
// Test entire page for accessibility
testA11y('ds-submission-edit',
// Test entire page for accessibility
testA11y('ds-submission-edit',
{
rules: {
// Author & Subject fields have invalid "aria-multiline" attrs.
// See https://github.com/DSpace/dspace-angular/issues/1272
'aria-allowed-attr': { enabled: false },
// All panels are accordions & fail "aria-required-children" and "nested-interactive".
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false },
// All select boxes fail to have a name / aria-label.
// This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216
'select-name': { enabled: false },
},
rules: {
// Author & Subject fields have invalid "aria-multiline" attrs.
// See https://github.com/DSpace/dspace-angular/issues/1272
'aria-allowed-attr': { enabled: false },
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false },
// All select boxes fail to have a name / aria-label.
// This is a bug in ng-dynamic-forms and may require https://github.com/DSpace/dspace-angular/issues/2216
'select-name': { enabled: false },
}
} as Options,
);
} as Options
);
// Discard button should work
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
});
it('should block submission & show errors if required fields are missing', () => {
// Create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Attempt an immediate deposit without filling out any fields
cy.get('button#deposit').click();
// A warning alert should display.
cy.get('ds-notification div.alert-success').should('not.exist');
cy.get('ds-notification div.alert-warning').should('be.visible');
// First section should have an exclamation error in the header
// (as it has required fields)
cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible');
// Title field should have class "is-invalid" applied, as it's required
cy.get('input#dc_title').should('have.class', 'is-invalid');
// Date Year field should also have "is-valid" class
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
// FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button.
// Get our Submission URL, to parse out the ID of this submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Even though form is incomplete, the "Save for Later" button should still work
cy.get('button#saveForLater').click();
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// A success alert should be visible
cy.get('ds-notification div.alert-success').should('be.visible');
// Now, dismiss any open alert boxes (may be multiple, as tests run quickly)
cy.get('[data-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');
// On MyDSpace, find the submission we just saved via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Delete our created submission & confirm deletion
cy.get('button#delete_' + id).click();
cy.get('button#delete_confirm').click();
});
});
it('should allow for deposit if all required fields completed & file uploaded', () => {
// Create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Fill out all required fields (Title, Date)
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
cy.get('input#dc_date_issued_year').type('2022');
// Confirm the required license by checking checkbox
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
cy.get('input#granted').check( { force: true } );
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
// This ensures our UI displays the dropzone that covers the entire submission page.
// (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger)
cy.get('ds-uploader').trigger('dragover');
// This is the POST command that will upload the file
cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload');
// Upload our DSpace logo via drag & drop onto submission form
// cy.get('div#section_upload')
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', {
action: 'drag-drop',
// Discard button should work
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
});
// Wait for upload to complete before proceeding
cy.wait('@upload');
it('should block submission & show errors if required fields are missing', () => {
// Create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// Wait for deposit button to not be disabled & click it.
cy.get('button#deposit').should('not.be.disabled').click();
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// No warnings should exist. Instead, just successful deposit alert is displayed
cy.get('ds-notification div.alert-warning').should('not.exist');
cy.get('ds-notification div.alert-success').should('be.visible');
});
// Attempt an immediate deposit without filling out any fields
cy.get('button#deposit').click();
it('is possible to submit a new "Person" and that form passes accessibility', () => {
// To submit a different entity type, we'll start from MyDSpace
cy.visit('/mydspace');
// A warning alert should display.
cy.get('ds-notification div.alert-success').should('not.exist');
cy.get('ds-notification div.alert-warning').should('be.visible');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
// NOTE: At this time, we MUST login as admin to submit Person objects
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
// First section should have an exclamation error in the header
// (as it has required fields)
cy.get('div#traditionalpageone-header i.fa-exclamation-circle').should('be.visible');
// Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Person" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="Person"]').click();
// Title field should have class "is-invalid" applied, as it's required
cy.get('input#dc_title').should('have.class', 'is-invalid');
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// Date Year field should also have "is-valid" class
cy.get('input#dc_date_issued_year').should('have.class', 'is-invalid');
// Type in a known Collection name in the search box
cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME'));
// FINALLY, cleanup after ourselves. This also exercises the MyDSpace delete button.
// Get our Submission URL, to parse out the ID of this submission
cy.location().then(fullUrl => {
// This will be the full path (/workspaceitems/[id]/edit)
const path = fullUrl.pathname;
// Split on the slashes
const subpaths = path.split('/');
// Part 2 will be the [id] of the submission
const id = subpaths[2];
// Click on the button matching that known Collection name
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')).concat('"]')).click();
// Even though form is incomplete, the "Save for Later" button should still work
cy.get('button#saveForLater').click();
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// "Save for Later" should send us to MyDSpace
cy.url().should('include', '/mydspace');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// 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});
// A Collection menu button should exist & its value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME'));
// This is the GET command that will actually run the search
cy.intercept('GET', '/server/api/discover/search/objects*').as('search-results');
// On MyDSpace, find the submission we just saved via its ID
cy.get('[data-test="search-box"]').type(id);
cy.get('[data-test="search-button"]').click();
// 3 sections should be visible by default
cy.get('div#section_personStep').should('be.visible');
cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible');
// Wait for search results to come back from the above GET command
cy.wait('@search-results');
// Test entire page for accessibility
testA11y('ds-submission-edit',
// Delete our created submission & confirm deletion
cy.get('button#delete_' + id).click();
cy.get('button#delete_confirm').click();
});
});
it('should allow for deposit if all required fields completed & file uploaded', () => {
// Create a new submission
cy.visit('/submit?collection='.concat(Cypress.env('DSPACE_TEST_SUBMIT_COLLECTION_UUID')).concat('&entityType=none'));
// This page is restricted, so we will be shown the login form. Fill it out & submit.
cy.loginViaForm(Cypress.env('DSPACE_TEST_SUBMIT_USER'), Cypress.env('DSPACE_TEST_SUBMIT_USER_PASSWORD'));
// Fill out all required fields (Title, Date)
cy.get('input#dc_title').type('DSpace logo uploaded via e2e tests');
cy.get('input#dc_date_issued_year').type('2022');
// Confirm the required license by checking checkbox
// (NOTE: requires "force:true" cause Cypress claims this checkbox is covered by its own <span>)
cy.get('input#granted').check( {force: true} );
// Before using Cypress drag & drop, we have to manually trigger the "dragover" event.
// This ensures our UI displays the dropzone that covers the entire submission page.
// (For some reason Cypress drag & drop doesn't trigger this even itself & upload won't work without this trigger)
cy.get('ds-uploader').trigger('dragover');
// This is the POST command that will upload the file
cy.intercept('POST', '/server/api/submission/workspaceitems/*').as('upload');
// Upload our DSpace logo via drag & drop onto submission form
// cy.get('div#section_upload')
cy.get('div.ds-document-drop-zone').selectFile('src/assets/images/dspace-logo.svg', {
action: 'drag-drop'
});
// Wait for upload to complete before proceeding
cy.wait('@upload');
// Wait for deposit button to not be disabled & click it.
cy.get('button#deposit').should('not.be.disabled').click();
// No warnings should exist. Instead, just successful deposit alert is displayed
cy.get('ds-notification div.alert-warning').should('not.exist');
cy.get('ds-notification div.alert-success').should('be.visible');
});
it('is possible to submit a new "Person" and that form passes accessibility', () => {
// To submit a different entity type, we'll start from MyDSpace
cy.visit('/mydspace');
// This page is restricted, so we will be shown the login form. Fill it out & submit.
// NOTE: At this time, we MUST login as admin to submit Person objects
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
// Open the New Submission dropdown
cy.get('button[data-test="submission-dropdown"]').click();
// Click on the "Person" type in that dropdown
cy.get('#entityControlsDropdownMenu button[title="Person"]').click();
// This should display the <ds-create-item-parent-selector> (popup window)
cy.get('ds-create-item-parent-selector').should('be.visible');
// Type in a known Collection name in the search box
cy.get('ds-authorized-collection-selector input[type="search"]').type(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME'));
// Click on the button matching that known Collection name
cy.get('ds-authorized-collection-selector button[title="'.concat(Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME')).concat('"]')).click();
// New URL should include /workspaceitems, as we've started a new submission
cy.url().should('include', '/workspaceitems');
// The Submission edit form tag should be visible
cy.get('ds-submission-edit').should('be.visible');
// A Collection menu button should exist & its value should be the selected collection
cy.get('#collectionControlsMenuButton span').should('have.text', Cypress.env('DSPACE_TEST_SUBMIT_PERSON_COLLECTION_NAME'));
// 3 sections should be visible by default
cy.get('div#section_personStep').should('be.visible');
cy.get('div#section_upload').should('be.visible');
cy.get('div#section_license').should('be.visible');
// Test entire page for accessibility
testA11y('ds-submission-edit',
{
rules: {
// All panels are accordions & fail "aria-required-children" and "nested-interactive".
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false },
},
rules: {
// All panels are accordians & fail "aria-required-children" and "nested-interactive".
// Seem to require updating ng-bootstrap and https://github.com/DSpace/dspace-angular/issues/2216
'aria-required-children': { enabled: false },
'nested-interactive': { enabled: false },
}
} as Options,
);
} as Options
);
// Click the lookup button next to "Publication" field
cy.get('button[data-test="lookup-button"]').click();
// Click the lookup button next to "Publication" field
cy.get('button[data-test="lookup-button"]').click();
// A popup modal window should be visible
cy.get('ds-dynamic-lookup-relation-modal').should('be.visible');
// A popup modal window should be visible
cy.get('ds-dynamic-lookup-relation-modal').should('be.visible');
// Popup modal should also pass accessibility tests
//testA11y('ds-dynamic-lookup-relation-modal');
testA11y({
include: ['ds-dynamic-lookup-relation-modal'],
exclude: [
['ul.nav-tabs'], // Tabs at top of model have several issues which seem to be caused by ng-bootstrap
],
// Popup modal should also pass accessibility tests
//testA11y('ds-dynamic-lookup-relation-modal');
testA11y({
include: ['ds-dynamic-lookup-relation-modal'],
exclude: [
['ul.nav-tabs'] // Tabs at top of model have several issues which seem to be caused by ng-bootstrap
],
});
// Close popup window
cy.get('ds-dynamic-lookup-relation-modal button.close').click();
// Back on the form, click the discard button to remove new submission
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
});
// Close popup window
cy.get('ds-dynamic-lookup-relation-modal button.btn-close').click();
// Back on the form, click the discard button to remove new submission
// Clicking it will display a confirmation, which we will confirm with another click
cy.get('button#discard').click();
cy.get('button#discard_submit').click();
});
});

View File

@@ -1,4 +1,4 @@
const fs = require('node:fs');
const fs = require('fs');
// These two global variables are used to store information about the REST API used
// by these e2e tests. They are filled out prior to running any tests in the before()
@@ -9,51 +9,51 @@ let REST_DOMAIN: string;
// Plugins enable you to tap into, modify, or extend the internal behavior of Cypress
// For more info, visit https://on.cypress.io/plugins-api
module.exports = (on, config) => {
on('task', {
// Define "log" and "table" tasks, used for logging accessibility errors during CI
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
log(message: string) {
console.log(message);
return null;
},
table(message: string) {
console.table(message);
return null;
},
// Cypress doesn't have access to the running application in Node.js.
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
// Instead, we'll read our running application's config.json, which contains the configs &
// is regenerated at runtime each time the Angular UI application starts up.
readUIConfig() {
// Check if we have a config.json in the src/assets. If so, use that.
// This is where it's written when running "ng e2e" or "yarn serve"
if (fs.existsSync('./src/assets/config.json')) {
return fs.readFileSync('./src/assets/config.json', 'utf8');
// Otherwise, check the dist/browser/assets
// This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
}
on('task', {
// Define "log" and "table" tasks, used for logging accessibility errors during CI
// Borrowed from https://github.com/component-driven/cypress-axe#in-cypress-plugins-file
log(message: string) {
console.log(message);
return null;
},
table(message: string) {
console.table(message);
return null;
},
// Cypress doesn't have access to the running application in Node.js.
// So, it's not possible to inject or load the AppConfig or environment of the Angular UI.
// Instead, we'll read our running application's config.json, which contains the configs &
// is regenerated at runtime each time the Angular UI application starts up.
readUIConfig() {
// Check if we have a config.json in the src/assets. If so, use that.
// This is where it's written when running "ng e2e" or "yarn serve"
if (fs.existsSync('./src/assets/config.json')) {
return fs.readFileSync('./src/assets/config.json', 'utf8');
// Otherwise, check the dist/browser/assets
// This is where it's written when running "serve:ssr", which is what CI uses to start the frontend
} else if (fs.existsSync('./dist/browser/assets/config.json')) {
return fs.readFileSync('./dist/browser/assets/config.json', 'utf8');
}
return null;
},
// Save value of REST Base URL, looked up before all tests.
// This allows other tests to use it easily via getRestBaseURL() below.
saveRestBaseURL(url: string) {
return (REST_BASE_URL = url);
},
// Retrieve currently saved value of REST Base URL
getRestBaseURL() {
return REST_BASE_URL ;
},
// Save value of REST Domain, looked up before all tests.
// This allows other tests to use it easily via getRestBaseDomain() below.
saveRestBaseDomain(domain: string) {
return (REST_DOMAIN = domain);
},
// Retrieve currently saved value of REST Domain
getRestBaseDomain() {
return REST_DOMAIN ;
},
});
return null;
},
// Save value of REST Base URL, looked up before all tests.
// This allows other tests to use it easily via getRestBaseURL() below.
saveRestBaseURL(url: string) {
return (REST_BASE_URL = url);
},
// Retrieve currently saved value of REST Base URL
getRestBaseURL() {
return REST_BASE_URL ;
},
// Save value of REST Domain, looked up before all tests.
// This allows other tests to use it easily via getRestBaseDomain() below.
saveRestBaseDomain(domain: string) {
return (REST_DOMAIN = domain);
},
// Retrieve currently saved value of REST Domain
getRestBaseDomain() {
return REST_DOMAIN ;
}
});
};

View File

@@ -3,14 +3,8 @@
// See docs at https://docs.cypress.io/api/cypress-api/custom-commands
// ***********************************************
import {
AuthTokenInfo,
TOKENITEM,
} from 'src/app/core/auth/models/auth-token-info.model';
import {
DSPACE_XSRF_COOKIE,
XSRF_REQUEST_HEADER,
} from 'src/app/core/xsrf/xsrf.constants';
import { AuthTokenInfo, TOKENITEM } from 'src/app/core/auth/models/auth-token-info.model';
import { DSPACE_XSRF_COOKIE, XSRF_REQUEST_HEADER } from 'src/app/core/xsrf/xsrf.constants';
import { v4 as uuidv4 } from 'uuid';
// Declare Cypress namespace to help with Intellisense & code completion in IDEs
@@ -63,33 +57,33 @@ declare global {
* @param password password to login as
*/
function login(email: string, password: string): void {
// Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send login POST request including that CSRF token
cy.request({
method: 'POST',
url: baseRestUrl + '/api/authn/login',
headers: { [XSRF_REQUEST_HEADER]: csrfToken },
form: true, // indicates the body should be form urlencoded
body: { user: email, password: password },
}).then((resp) => {
// We expect a successful login
expect(resp.status).to.eq(200);
// We expect to have a valid authorization header returned (with our auth token)
expect(resp.headers).to.have.property('authorization');
// Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send login POST request including that CSRF token
cy.request({
method: 'POST',
url: baseRestUrl + '/api/authn/login',
headers: { [XSRF_REQUEST_HEADER]: csrfToken},
form: true, // indicates the body should be form urlencoded
body: { user: email, password: password }
}).then((resp) => {
// We expect a successful login
expect(resp.status).to.eq(200);
// We expect to have a valid authorization header returned (with our auth token)
expect(resp.headers).to.have.property('authorization');
// Initialize our AuthTokenInfo object from the authorization header.
const authheader = resp.headers.authorization as string;
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
// Initialize our AuthTokenInfo object from the authorization header.
const authheader = resp.headers.authorization as string;
const authinfo: AuthTokenInfo = new AuthTokenInfo(authheader);
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
// This ensures the UI will recognize we are logged in on next "visit()"
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
});
// Save our AuthTokenInfo object to our dsAuthInfo UI cookie
// This ensures the UI will recognize we are logged in on next "visit()"
cy.setCookie(TOKENITEM, JSON.stringify(authinfo));
});
});
});
});
}
// Add as a Cypress command (i.e. assign to 'cy.login')
Cypress.Commands.add('login', login);
@@ -123,29 +117,29 @@ Cypress.Commands.add('loginViaForm', loginViaForm);
* @param dsoType type of DSpace Object (e.g. "item", "collection", "community")
*/
function generateViewEvent(uuid: string, dsoType: string): void {
// Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header
cy.request({
method: 'POST',
url: baseRestUrl + '/api/statistics/viewevents',
headers: {
[XSRF_REQUEST_HEADER] : csrfToken,
// use a known public IP address to avoid being seen as a "bot"
'X-Forwarded-For': '1.1.1.1',
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
},
//form: true, // indicates the body should be form urlencoded
body: { targetId: uuid, targetType: dsoType },
}).then((resp) => {
// We expect a 201 (which means statistics event was created)
expect(resp.status).to.eq(201);
});
// Create a fake CSRF cookie/token to use in POST
cy.createCSRFCookie().then((csrfToken: string) => {
// get our REST API's base URL, also needed for POST
cy.task('getRestBaseURL').then((baseRestUrl: string) => {
// Now, send 'statistics/viewevents' POST request including that fake CSRF token in required header
cy.request({
method: 'POST',
url: baseRestUrl + '/api/statistics/viewevents',
headers: {
[XSRF_REQUEST_HEADER] : csrfToken,
// use a known public IP address to avoid being seen as a "bot"
'X-Forwarded-For': '1.1.1.1',
// Use a user-agent of a Firefox browser on Windows. This again avoids being seen as a "bot"
'user-agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:109.0) Gecko/20100101 Firefox/119.0',
},
//form: true, // indicates the body should be form urlencoded
body: { targetId: uuid, targetType: dsoType },
}).then((resp) => {
// We expect a 201 (which means statistics event was created)
expect(resp.status).to.eq(201);
});
});
});
});
}
// Add as a Cypress command (i.e. assign to 'cy.generateViewEvent')
Cypress.Commands.add('generateViewEvent', generateViewEvent);
@@ -159,17 +153,17 @@ Cypress.Commands.add('generateViewEvent', generateViewEvent);
* @returns a Cypress Chainable which can be used to get the generated CSRF Token
*/
function createCSRFCookie(): Cypress.Chainable {
// Generate a new token which is a random UUID
const csrfToken: string = uuidv4();
// Generate a new token which is a random UUID
const csrfToken: string = uuidv4();
// Save it to our required cookie
cy.task('getRestBaseDomain').then((baseDomain: string) => {
// Create a fake CSRF Token. Set it in the required server-side cookie
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
});
// Save it to our required cookie
cy.task('getRestBaseDomain').then((baseDomain: string) => {
// Create a fake CSRF Token. Set it in the required server-side cookie
cy.setCookie(DSPACE_XSRF_COOKIE, csrfToken, { 'domain': baseDomain });
});
// return the generated token wrapped in a chainable
return cy.wrap(csrfToken);
// return the generated token wrapped in a chainable
return cy.wrap(csrfToken);
}
// Add as a Cypress command (i.e. assign to 'cy.createCSRFCookie')
Cypress.Commands.add('createCSRFCookie', createCSRFCookie);

View File

@@ -15,12 +15,13 @@
// Import all custom Commands (from commands.ts) for all tests
import './commands';
// Import Cypress Axe tools for all tests
// https://github.com/component-driven/cypress-axe
import 'cypress-axe';
import { DSPACE_XSRF_COOKIE } from 'src/app/core/xsrf/xsrf.constants';
// Runs once before all tests
before(() => {
// Cypress doesn't have access to the running application in Node.js.
@@ -34,18 +35,18 @@ before(() => {
// Find URL of our REST API & save to global variable via task
let baseRestUrl = FALLBACK_TEST_REST_BASE_URL;
if (!config.rest.baseUrl) {
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
console.warn("Could not load 'rest.baseUrl' from config.json. Falling back to " + FALLBACK_TEST_REST_BASE_URL);
} else {
baseRestUrl = config.rest.baseUrl;
baseRestUrl = config.rest.baseUrl;
}
cy.task('saveRestBaseURL', baseRestUrl);
// Find domain of our REST API & save to global variable via task.
let baseDomain = FALLBACK_TEST_REST_DOMAIN;
if (!config.rest.host) {
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
console.warn("Could not load 'rest.host' from config.json. Falling back to " + FALLBACK_TEST_REST_DOMAIN);
} else {
baseDomain = config.rest.host;
baseDomain = config.rest.host;
}
cy.task('saveRestBaseDomain', baseDomain);
@@ -54,12 +55,12 @@ before(() => {
// Runs once before the first test in each "block"
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,"correlation-id":true,"accessibility":true}');
// Pre-agree to all Klaro cookies by setting the klaro-anonymous cookie
// This just ensures it doesn't get in the way of matching other objects in the page.
cy.setCookie('klaro-anonymous', '{%22authentication%22:true%2C%22preferences%22:true%2C%22acknowledgement%22:true%2C%22google-analytics%22:true%2C%22google-recaptcha%22:true}');
// Remove any CSRF cookies saved from prior tests
cy.clearCookie(DSPACE_XSRF_COOKIE);
// Remove any CSRF cookies saved from prior tests
cy.clearCookie(DSPACE_XSRF_COOKIE);
});
// NOTE: FALLBACK_TEST_REST_BASE_URL is only used if Cypress cannot read the REST API BaseURL

View File

@@ -5,26 +5,26 @@ import { Options } from 'cypress-axe';
// Uses 'log' and 'table' tasks defined in ../plugins/index.ts
// Borrowed from https://github.com/component-driven/cypress-axe#in-your-spec-file
function terminalLog(violations: Result[]) {
cy.task(
'log',
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`,
);
// pluck specific keys to keep the table readable
const violationData = violations.map(
({ id, impact, description, helpUrl, nodes }) => ({
id,
impact,
description,
helpUrl,
nodes: nodes.length,
html: nodes.map(node => node.html),
}),
);
cy.task(
'log',
`${violations.length} accessibility violation${violations.length === 1 ? '' : 's'} ${violations.length === 1 ? 'was' : 'were'} detected`
);
// pluck specific keys to keep the table readable
const violationData = violations.map(
({ id, impact, description, helpUrl, nodes }) => ({
id,
impact,
description,
helpUrl,
nodes: nodes.length,
html: nodes.map(node => node.html)
})
);
// Print violations as an array, since 'node.html' above often breaks table alignment
cy.task('log', violationData);
// Optionally, uncomment to print as a table
// cy.task('table', violationData);
// Print violations as an array, since 'node.html' above often breaks table alignment
cy.task('log', violationData);
// Optionally, uncomment to print as a table
// cy.task('table', violationData);
}
@@ -32,13 +32,13 @@ function terminalLog(violations: Result[]) {
// while also ensuring any violations are logged to the terminal (see terminalLog above)
// This method MUST be called after cy.visit(), as cy.injectAxe() must be called after page load
export const testA11y = (context?: any, options?: Options) => {
cy.injectAxe();
cy.configureAxe({
rules: [
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
{ id: 'color-contrast', enabled: false },
],
});
cy.checkA11y(context, options, terminalLog);
cy.injectAxe();
cy.configureAxe({
rules: [
// Disable color contrast checks as they are inaccurate / result in a lot of false positives
// See also open issues in axe-core: https://github.com/dequelabs/axe-core/labels/color%20contrast
{ id: 'color-contrast', enabled: false },
]
});
cy.checkA11y(context, options, terminalLog);
};

View File

@@ -1,20 +1,13 @@
{
"extends": "../tsconfig.json",
"include": [
"**/*.ts",
"../cypress.config.ts"
"**/*.ts"
],
"compilerOptions": {
"sourceMap": false,
"typeRoots": [
"../node_modules",
"../node_modules/@types",
"../src/typings.d.ts"
],
"types": [
"cypress",
"cypress-axe",
"node"
]
}
}
}

View File

@@ -20,17 +20,17 @@ the Docker compose scripts in this 'docker' folder.
### Dockerfile
This Dockerfile is used to build a *development* DSpace Angular UI image, published as 'dspace/dspace-angular'
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
```
docker build -t dspace/dspace-angular:latest .
docker build -t dspace/dspace-angular:dspace-7_x .
```
This image is built *automatically* after each commit is made to the `main` branch.
Admins to our DockerHub repo can manually publish with the following command.
```
docker push dspace/dspace-angular:latest
docker push dspace/dspace-angular:dspace-7_x
```
### Dockerfile.dist
@@ -39,18 +39,18 @@ The `Dockerfile.dist` is used to generate a *production* build and runtime envir
```bash
# build the latest image
docker build -f Dockerfile.dist -t dspace/dspace-angular:latest-dist .
docker build -f Dockerfile.dist -t dspace/dspace-angular:dspace-7_x-dist .
```
A default/demo version of this image is built *automatically*.
## 'docker' directory
- docker-compose.yml
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace REST instance will also be started in Docker.
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
- docker-compose-rest.yml
- Runs a published instance of the DSpace REST API - persists data in Docker volumes
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
- docker-compose-ci.yml
- Runs a published instance of the DSpace REST API for CI testing. The database is re-populated from a SQL dump on each startup.
- Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
- cli.yml
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
- cli.assetstore.yml
@@ -59,19 +59,19 @@ A default/demo version of this image is built *automatically*.
## To refresh / pull DSpace images from Dockerhub
```
docker compose -f docker/docker-compose.yml pull
docker-compose -f docker/docker-compose.yml pull
```
## To build DSpace images using code in your branch
```
docker compose -f docker/docker-compose.yml build
docker-compose -f docker/docker-compose.yml build
```
## To start DSpace (REST and Angular) from your branch
This command provides a quick way to start both the frontend & backend from this single codebase
```
docker compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
```
Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section.
@@ -86,14 +86,14 @@ _The system will be started in 2 steps. Each step shares the same docker network
From 'DSpace/DSpace' clone (build first as needed):
```
docker compose -p d8 up -d
docker-compose -p d7 up -d
```
NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md).
From 'DSpace/dspace-angular' clone (build first as needed)
```
docker compose -p d8 -f docker/docker-compose.yml up -d
docker-compose -p d7 -f docker/docker-compose.yml up -d
```
At this point, you should be able to access the UI from http://localhost:4000,
@@ -105,21 +105,21 @@ This allows you to run the Angular UI in *production* mode, pointing it at the d
(https://demo.dspace.org/server/ or https://sandbox.dspace.org/server/).
```
docker compose -f docker/docker-compose-dist.yml pull
docker compose -f docker/docker-compose-dist.yml build
docker compose -p d8 -f docker/docker-compose-dist.yml up -d
docker-compose -f docker/docker-compose-dist.yml pull
docker-compose -f docker/docker-compose-dist.yml build
docker-compose -p d7 -f docker/docker-compose-dist.yml up -d
```
## Ingest test data from AIPDIR
Create an administrator
```
docker compose -p d8 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
```
Load content from AIP files
```
docker compose -p d8 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
```
## Alternative Ingest - Use Entities dataset
@@ -127,12 +127,12 @@ _Delete your docker volumes or use a unique project (-p) name_
Start DSpace with Database Content from a database dump
```
docker compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
```
Load assetstore content and trigger a re-index of the repository
```
docker compose -p d8 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
```
## End to end testing of the REST API (runs in GitHub Actions CI).
@@ -140,5 +140,5 @@ _In this instance, only the REST api runs in Docker using the Entities dataset.
This command is only really useful for testing our Continuous Integration process.
```
docker compose -p d8ci -f docker/docker-compose-ci.yml up -d
docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d
```

View File

@@ -32,7 +32,5 @@ services:
/dspace/bin/dspace packager -r -a -t AIP -e $${ADMIN_EMAIL} -f -u SITE*.zip
/dspace/bin/dspace database update-sequences
touch /dspace/solr/search/conf/reindex.flag
/dspace/bin/dspace oai import
/dspace/bin/dspace oai clean-cache
/dspace/bin/dspace index-discovery

View File

@@ -21,7 +21,7 @@ networks:
external: true
services:
dspace-cli:
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-latest}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
container_name: dspace-cli
environment:
# Below syntax may look odd, but it is how to override dspace.cfg settings via env variables.

View File

@@ -14,7 +14,7 @@
# # Therefore, it should be kept in sync with that file
services:
dspacedb:
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-loadsql:${DSPACE_VER:-latest}"
image: ${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql
environment:
# This LOADSQL should be kept in sync with the URL in DSpace/DSpace
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
@@ -27,11 +27,23 @@ services:
# 3. (Custom for Entities) enable Entity-specific collection submission mappings in item-submission.xml
# This 'sed' command inserts the sample configurations specific to the Entities data set, see:
# https://github.com/DSpace/DSpace/blob/main/dspace/config/item-submission.xml#L36-L49
# 4. Finally, start DSpace
# 4. Finally, start Tomcat
entrypoint:
- /bin/bash
- '-c'
- |
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
/dspace/bin/dspace database migrate ignored
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
sed -i '/name-map collection-handle="default".*/a \\n <name-map collection-handle="123456789/3" submission-name="Publication"/> \
<name-map collection-handle="123456789/4" submission-name="Publication"/> \
<name-map collection-handle="123456789/281" submission-name="Publication"/> \
<name-map collection-handle="123456789/5" submission-name="Publication"/> \
<name-map collection-handle="123456789/8" submission-name="OrgUnit"/> \
<name-map collection-handle="123456789/6" submission-name="Person"/> \
<name-map collection-handle="123456789/279" submission-name="Person"/> \
<name-map collection-handle="123456789/7" submission-name="Project"/> \
<name-map collection-handle="123456789/280" submission-name="Project"/> \
<name-map collection-handle="123456789/28" submission-name="Journal"/> \
<name-map collection-handle="123456789/29" submission-name="JournalVolume"/> \
<name-map collection-handle="123456789/30" submission-name="JournalIssue"/>' /dspace/config/item-submission.xml
catalina.sh run

View File

@@ -33,7 +33,7 @@ services:
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
solr__D__statistics__P__autoCommit: 'false'
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
depends_on:
- dspacedb
networks:
@@ -48,27 +48,25 @@ services:
# Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
# 2. Then, run database migration to init database tables (including any out-of-order ignored migrations, if any)
# 3. Finally, start DSpace
# 3. Finally, start Tomcat
entrypoint:
- /bin/bash
- '-c'
- |
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
/dspace/bin/dspace database migrate ignored
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
catalina.sh run
# DSpace database container
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
dspacedb:
container_name: dspacedb
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-loadsql:${DSPACE_VER:-latest}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}-loadsql"
environment:
# This LOADSQL should be kept in sync with the LOADSQL in
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
PGDATA: /pgdata
POSTGRES_DB: dspace
POSTGRES_USER: dspace
POSTGRES_PASSWORD: dspace
networks:
- dspacenet
@@ -83,7 +81,7 @@ services:
# DSpace Solr container
dspacesolr:
container_name: dspacesolr
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
networks:
- dspacenet
ports:
@@ -95,10 +93,7 @@ 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, then start Solr
# Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr
entrypoint:
- /bin/bash
- '-c'
@@ -112,12 +107,7 @@ services:
cp -r /opt/solr/server/solr/configsets/search/* search
precreate-core statistics /opt/solr/server/solr/configsets/statistics
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
chown -R solr:solr /var/solr
runuser -u solr -- solr-foreground
exec solr -f
volumes:
assetstore:
pgdata:

View File

@@ -23,10 +23,10 @@ services:
# This is because Server Side Rendering (SSR) currently requires a public URL,
# see this bug: https://github.com/DSpace/dspace-angular/issues/1485
DSPACE_REST_SSL: 'true'
DSPACE_REST_HOST: sandbox.dspace.org
DSPACE_REST_HOST: demo.dspace.org
DSPACE_REST_PORT: 443
DSPACE_REST_NAMESPACE: /server
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}-dist"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}-dist"
build:
context: ..
dockerfile: Dockerfile.dist

View File

@@ -40,7 +40,7 @@ services:
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
proxies__P__trusted__P__ipranges: '172.23.0'
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-dspace-7_x-test}"
depends_on:
- dspacedb
networks:
@@ -51,28 +51,25 @@ services:
stdin_open: true
tty: true
volumes:
# Keep DSpace assetstore directory between reboots
- assetstore:/dspace/assetstore
# Ensure that the database is ready BEFORE starting tomcat
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
# 2. Then, run database migration to init database tables
# 3. Finally, start DSpace
# 3. Finally, start Tomcat
entrypoint:
- /bin/bash
- '-c'
- |
while (!</dev/tcp/dspacedb/5432) > /dev/null 2>&1; do sleep 1; done;
/dspace/bin/dspace database migrate
java -jar /dspace/webapps/server-boot.jar --dspace.dir=/dspace
catalina.sh run
# DSpace database container
dspacedb:
container_name: dspacedb
# Uses the base PostgreSQL image
image: "docker.io/postgres:${POSTGRES_VERSION:-15}"
# Uses a custom Postgres image with pgcrypto installed
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-dspace-7_x}"
environment:
PGDATA: /pgdata
POSTGRES_DB: dspace
POSTGRES_USER: dspace
POSTGRES_PASSWORD: dspace
networks:
- dspacenet
@@ -87,7 +84,7 @@ services:
# DSpace Solr container
dspacesolr:
container_name: dspacesolr
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-latest}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-solr:${DSPACE_VER:-dspace-7_x}"
networks:
- dspacenet
ports:
@@ -99,16 +96,11 @@ 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
# `docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr`
entrypoint:
- /bin/bash
- '-c'
@@ -122,12 +114,7 @@ services:
cp -r /opt/solr/server/solr/configsets/search/* search
precreate-core statistics /opt/solr/server/solr/configsets/statistics
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
chown -R solr:solr /var/solr
runuser -u solr -- solr-foreground
exec solr -f
volumes:
assetstore:
pgdata:

View File

@@ -23,7 +23,7 @@ services:
DSPACE_REST_HOST: localhost
DSPACE_REST_PORT: 8080
DSPACE_REST_NAMESPACE: /server
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-latest}"
image: "${DOCKER_REGISTRY:-docker.io}/${DOCKER_OWNER:-dspace}/dspace-angular:${DSPACE_VER:-dspace-7_x}"
build:
context: ..
dockerfile: Dockerfile

View File

@@ -15,7 +15,7 @@ DSPACE_APP_CONFIG_PATH=/usr/local/dspace/config/config.yml
Configuration options can be overridden by setting environment variables.
## Nodejs server
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itself to, and if ssl should be enabled not. By default it listens on `localhost:4000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
When you start dspace-angular on node, it spins up an http server on which it listens for incoming connections. You can define the ip address and port the server should bind itsself to, and if ssl should be enabled not. By default it listens on `localhost:4000`. If you want it to listen on all your network connections, configure it to bind itself to `0.0.0.0`.
To change this configuration, change the options `ui.host`, `ui.port` and `ui.ssl` in the appropriate configuration file (see above):

View File

@@ -1,5 +0,0 @@
[DSpace ESLint plugins](../../../lint/README.md) > HTML rules
_______
- [`dspace-angular-html/themed-component-usages`](./rules/themed-component-usages.md): Themeable components should be used via the selector of their `ThemedComponent` wrapper class
- [`dspace-angular-html/no-disabled-attribute-on-button`](./rules/no-disabled-attribute-on-button.md): Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.

View File

@@ -1,90 +0,0 @@
[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/no-disabled-attribute-on-button`
_______
Buttons should use the `dsBtnDisabled` directive instead of the HTML `disabled` attribute.
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.
_______
[Source code](../../../../lint/src/rules/html/no-disabled-attribute-on-button.ts)
### Examples
#### Valid code
##### should use [dsBtnDisabled] in HTML templates
```html
<button [dsBtnDisabled]="true">Submit</button>
```
##### disabled attribute is still valid on non-button elements
```html
<input disabled>
```
##### [disabled] attribute is still valid on non-button elements
```html
<input [disabled]="true">
```
##### angular dynamic attributes that use disabled are still valid
```html
<button [class.disabled]="isDisabled">Submit</button>
```
#### Invalid code &amp; automatic fixes
##### should not use disabled attribute in HTML templates
```html
<button disabled>Submit</button>
```
Will produce the following error(s):
```
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
```
Result of `yarn lint --fix`:
```html
<button [dsBtnDisabled]="true">Submit</button>
```
##### should not use [disabled] attribute in HTML templates
```html
<button [disabled]="true">Submit</button>
```
Will produce the following error(s):
```
Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.
```
Result of `yarn lint --fix`:
```html
<button [dsBtnDisabled]="true">Submit</button>
```

View File

@@ -1,122 +0,0 @@
[DSpace ESLint plugins](../../../../lint/README.md) > [HTML rules](../index.md) > `dspace-angular-html/themed-component-usages`
_______
Themeable components should be used via the selector of their `ThemedComponent` wrapper class
This ensures that custom themes can correctly override _all_ instances of this component.
The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple.
_______
[Source code](../../../../lint/src/rules/html/themed-component-usages.ts)
### Examples
#### Valid code
##### use no-prefix selectors in HTML templates
```html
<ds-test-themeable/>
<ds-test-themeable></ds-test-themeable>
<ds-test-themeable [test]="something"></ds-test-themeable>
```
##### use no-prefix selectors in TypeScript templates
```html
@Component({
template: '<ds-test-themeable></ds-test-themeable>'
})
class Test {
}
```
##### use no-prefix selectors in TypeScript test templates
Filename: `lint/test/fixture/src/test.spec.ts`
```html
@Component({
template: '<ds-test-themeable></ds-test-themeable>'
})
class Test {
}
```
##### base selectors are also allowed in TypeScript test templates
Filename: `lint/test/fixture/src/test.spec.ts`
```html
@Component({
template: '<ds-base-test-themeable></ds-base-test-themeable>'
})
class Test {
}
```
#### Invalid code &amp; automatic fixes
##### themed override selectors are not allowed in HTML templates
```html
<ds-themed-test-themeable/>
<ds-themed-test-themeable></ds-themed-test-themeable>
<ds-themed-test-themeable [test]="something"></ds-themed-test-themeable>
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper's selector
Themeable components should be used via their ThemedComponent wrapper's selector
Themeable components should be used via their ThemedComponent wrapper's selector
```
Result of `yarn lint --fix`:
```html
<ds-test-themeable/>
<ds-test-themeable></ds-test-themeable>
<ds-test-themeable [test]="something"></ds-test-themeable>
```
##### base selectors are not allowed in HTML templates
```html
<ds-base-test-themeable/>
<ds-base-test-themeable></ds-base-test-themeable>
<ds-base-test-themeable [test]="something"></ds-base-test-themeable>
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper's selector
Themeable components should be used via their ThemedComponent wrapper's selector
Themeable components should be used via their ThemedComponent wrapper's selector
```
Result of `yarn lint --fix`:
```html
<ds-test-themeable/>
<ds-test-themeable></ds-test-themeable>
<ds-test-themeable [test]="something"></ds-test-themeable>
```

View File

@@ -1,11 +0,0 @@
[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)

View File

@@ -1,148 +0,0 @@
[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 &amp; 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';
```

View File

@@ -1,245 +0,0 @@
[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 &amp; 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 {}
```

View File

@@ -1,281 +0,0 @@
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-classes`
_______
Formatting rules for themeable component classes
- All themeable components must be standalone.
- The base component must always be imported in the `ThemedComponent` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component.
_______
[Source code](../../../../lint/src/rules/ts/themed-component-classes.ts)
### Examples
#### Valid code
##### Regular non-themeable component
```typescript
@Component({
selector: 'ds-something',
standalone: true,
})
class Something {
}
```
##### Base component
```typescript
@Component({
selector: 'ds-base-test-themable',
standalone: true,
})
class TestThemeableComponent {
}
```
##### Wrapper component
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
```typescript
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [
TestThemeableComponent,
],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
##### Override component
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
```typescript
@Component({
selector: 'ds-themed-test-themable',
standalone: true,
})
class Override extends BaseComponent {
}
```
#### Invalid code &amp; automatic fixes
##### Base component must be standalone
```typescript
@Component({
selector: 'ds-base-test-themable',
})
class TestThemeableComponent {
}
```
Will produce the following error(s):
```
Themeable components must be standalone
```
Result of `yarn lint --fix`:
```typescript
@Component({
selector: 'ds-base-test-themable',
standalone: true,
})
class TestThemeableComponent {
}
```
##### Wrapper component must be standalone and import base component
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
```typescript
@Component({
selector: 'ds-test-themable',
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
Will produce the following error(s):
```
Themeable component wrapper classes must be standalone and import the base class
```
Result of `yarn lint --fix`:
```typescript
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [TestThemeableComponent],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
##### Wrapper component must import base component (array present but empty)
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
```typescript
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
Will produce the following error(s):
```
Themed component wrapper classes must only import the base class
```
Result of `yarn lint --fix`:
```typescript
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [TestThemeableComponent],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
##### Wrapper component must import base component (array is wrong)
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
```typescript
import { SomethingElse } from './somewhere-else';
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [
SomethingElse,
],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
Will produce the following error(s):
```
Themed component wrapper classes must only import the base class
```
Result of `yarn lint --fix`:
```typescript
import { SomethingElse } from './somewhere-else';
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [TestThemeableComponent],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
##### Wrapper component must import base component (array is wrong)
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
```typescript
import { Something, SomethingElse } from './somewhere-else';
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [
SomethingElse,
],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
Will produce the following error(s):
```
Themed component wrapper classes must only import the base class
```
Result of `yarn lint --fix`:
```typescript
import { Something, SomethingElse } from './somewhere-else';
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [TestThemeableComponent],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
##### Override component must be standalone
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
```typescript
@Component({
selector: 'ds-themed-test-themable',
})
class Override extends BaseComponent {
}
```
Will produce the following error(s):
```
Themeable components must be standalone
```
Result of `yarn lint --fix`:
```typescript
@Component({
selector: 'ds-themed-test-themable',
standalone: true,
})
class Override extends BaseComponent {
}
```

View File

@@ -1,170 +0,0 @@
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-selectors`
_______
Themeable component selectors should follow the DSpace convention
Each themeable component is comprised of a base component, a wrapper component and any number of themed components
- Base components should have a selector starting with `ds-base-`
- Themed components should have a selector starting with `ds-themed-`
- Wrapper components should have a selector starting with `ds-`, but not `ds-base-` or `ds-themed-`
- This is the regular DSpace selector prefix
- **When making a regular component themeable, its selector prefix should be changed to `ds-base-`, and the new wrapper's component should reuse the previous selector**
Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source.
_______
[Source code](../../../../lint/src/rules/ts/themed-component-selectors.ts)
### Examples
#### Valid code
##### Regular non-themeable component selector
```typescript
@Component({
selector: 'ds-something',
})
class Something {
}
```
##### Themeable component selector should replace the original version, unthemed version should be changed to ds-base-
```typescript
@Component({
selector: 'ds-base-something',
})
class Something {
}
@Component({
selector: 'ds-something',
})
class ThemedSomething extends ThemedComponent<Something> {
}
@Component({
selector: 'ds-themed-something',
})
class OverrideSomething extends Something {
}
```
##### Other themed component wrappers should not interfere
```typescript
@Component({
selector: 'ds-something',
})
class Something {
}
@Component({
selector: 'ds-something-else',
})
class ThemedSomethingElse extends ThemedComponent<SomethingElse> {
}
```
#### Invalid code &amp; automatic fixes
##### Wrong selector for base component
Filename: `lint/test/fixture/src/app/test/test-themeable.component.ts`
```typescript
@Component({
selector: 'ds-something',
})
class TestThemeableComponent {
}
```
Will produce the following error(s):
```
Unthemed version of themeable component should have a selector starting with 'ds-base-'
```
Result of `yarn lint --fix`:
```typescript
@Component({
selector: 'ds-base-something',
})
class TestThemeableComponent {
}
```
##### Wrong selector for wrapper component
Filename: `lint/test/fixture/src/app/test/themed-test-themeable.component.ts`
```typescript
@Component({
selector: 'ds-themed-something',
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
Will produce the following error(s):
```
Themed component wrapper of themeable component shouldn't have a selector starting with 'ds-themed-'
```
Result of `yarn lint --fix`:
```typescript
@Component({
selector: 'ds-something',
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
```
##### Wrong selector for theme override
Filename: `lint/test/fixture/src/themes/test/app/test/test-themeable.component.ts`
```typescript
@Component({
selector: 'ds-something',
})
class TestThememeableComponent extends BaseComponent {
}
```
Will produce the following error(s):
```
Theme override of themeable component should have a selector starting with 'ds-themed-'
```
Result of `yarn lint --fix`:
```typescript
@Component({
selector: 'ds-themed-something',
})
class TestThememeableComponent extends BaseComponent {
}
```

View File

@@ -1,367 +0,0 @@
[DSpace ESLint plugins](../../../../lint/README.md) > [TypeScript rules](../index.md) > `dspace-angular-ts/themed-component-usages`
_______
Themeable components should be used via their `ThemedComponent` wrapper class
This ensures that custom themes can correctly override _all_ instances of this component.
There are a few exceptions where the base class can still be used:
- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place)
- Angular modules (except for routing modules)
- Angular `@ViewChild` decorators
- Type annotations
_______
[Source code](../../../../lint/src/rules/ts/themed-component-usages.ts)
### Examples
#### Valid code
##### allow wrapper class usages
```typescript
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
const config = {
a: ThemedTestThemeableComponent,
b: ChipsComponent,
}
```
##### allow base class in class declaration
```typescript
export class TestThemeableComponent {
}
```
##### allow inheriting from base class
```typescript
import { TestThemeableComponent } from './app/test/test-themeable.component';
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
}
```
##### allow base class in ViewChild
```typescript
import { TestThemeableComponent } from './app/test/test-themeable.component';
export class Something {
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
}
```
##### allow wrapper selectors in test queries
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
```typescript
By.css('ds-themeable');
By.css('#test > ds-themeable > #nest');
```
##### allow wrapper selectors in cypress queries
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
```typescript
By.css('ds-themeable');
By.css('#test > ds-themeable > #nest');
```
#### Invalid code &amp; automatic fixes
##### disallow direct usages of base class
```typescript
import { TestThemeableComponent } from './app/test/test-themeable.component';
import { TestComponent } from './app/test/test.component';
const config = {
a: TestThemeableComponent,
b: TestComponent,
}
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
import { TestComponent } from './app/test/test.component';
const config = {
a: ThemedTestThemeableComponent,
b: TestComponent,
}
```
##### disallow direct usages of base class, keep other imports
```typescript
import { Something, TestThemeableComponent } from './app/test/test-themeable.component';
import { TestComponent } from './app/test/test.component';
const config = {
a: TestThemeableComponent,
b: TestComponent,
c: Something,
}
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
import { Something } from './app/test/test-themeable.component';
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
import { TestComponent } from './app/test/test.component';
const config = {
a: ThemedTestThemeableComponent,
b: TestComponent,
c: Something,
}
```
##### handle array replacements correctly
```typescript
const DECLARATIONS = [
Something,
TestThemeableComponent,
Something,
ThemedTestThemeableComponent,
];
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
const DECLARATIONS = [
Something,
Something,
ThemedTestThemeableComponent,
];
```
##### disallow override selector in test queries
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
```typescript
By.css('ds-themed-themeable');
By.css('#test > ds-themed-themeable > #nest');
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
By.css('ds-themeable');
By.css('#test > ds-themeable > #nest');
```
##### disallow base selector in test queries
Filename: `lint/test/fixture/src/app/test/test.component.spec.ts`
```typescript
By.css('ds-base-themeable');
By.css('#test > ds-base-themeable > #nest');
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
By.css('ds-themeable');
By.css('#test > ds-themeable > #nest');
```
##### disallow override selector in cypress queries
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
```typescript
cy.get('ds-themed-themeable');
cy.get('#test > ds-themed-themeable > #nest');
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
cy.get('ds-themeable');
cy.get('#test > ds-themeable > #nest');
```
##### disallow base selector in cypress queries
Filename: `lint/test/fixture/src/app/test/test.component.cy.ts`
```typescript
cy.get('ds-base-themeable');
cy.get('#test > ds-base-themeable > #nest');
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
cy.get('ds-themeable');
cy.get('#test > ds-themeable > #nest');
```
##### edge case: unable to find usage node through usage token, but import is still flagged and fixed
Filename: `lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts`
```typescript
import { Component } from '@angular/core';
import { Context } from './app/core/shared/context.model';
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
@Component({
standalone: true,
imports: [TestThemeableComponent],
})
export class UsageComponent {
}
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
import { Component } from '@angular/core';
import { Context } from './app/core/shared/context.model';
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
@Component({
standalone: true,
imports: [ThemedTestThemeableComponent],
})
export class UsageComponent {
}
```
##### edge case edge case: both are imported, only wrapper is retained
Filename: `lint/test/fixture/src/themes/test/app/test/other-themeable.component.ts`
```typescript
import { Component } from '@angular/core';
import { Context } from './app/core/shared/context.model';
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
@Component({
standalone: true,
imports: [TestThemeableComponent, ThemedTestThemeableComponent],
})
export class UsageComponent {
}
```
Will produce the following error(s):
```
Themeable components should be used via their ThemedComponent wrapper
Themeable components should be used via their ThemedComponent wrapper
```
Result of `yarn lint --fix`:
```typescript
import { Component } from '@angular/core';
import { Context } from './app/core/shared/context.model';
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
@Component({
standalone: true,
imports: [ThemedTestThemeableComponent],
})
export class UsageComponent {
}
```

View File

@@ -1,183 +0,0 @@
[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 &amp; 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 {
}
```

View File

@@ -1,92 +0,0 @@
[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
```

View File

@@ -1,86 +0,0 @@
[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
```

View File

@@ -15,10 +15,7 @@ module.exports = function (config) {
],
client: {
clearContext: false, // leave Jasmine Spec Runner output visible in browser
captureConsole: false,
jasmine: {
failSpecWithNoExpectations: true
}
captureConsole: false
},
coverageIstanbulReporter: {
dir: require('path').join(__dirname, './coverage/dspace-angular'),

3
lint/.gitignore vendored
View File

@@ -1,3 +0,0 @@
/dist/
/coverage/
/node-modules/

View File

@@ -1,50 +0,0 @@
# DSpace ESLint plugins
Custom ESLint rules for DSpace Angular peculiarities.
## Usage
These plugins are included with the rest of our ESLint configuration in [.eslintc.json](../.eslintrc.json). Individual rules can be configured or disabled there, like usual.
- In order for the new rules to be picked up by your IDE, you should first run `yarn build:lint` to build the plugins.
- This will also happen automatically each time `yarn lint` is run.
## Documentation
The rules are split up into plugins by language:
- [TypeScript rules](../docs/lint/ts/index.md)
- [HTML rules](../docs/lint/html/index.md)
> Run `yarn docs:lint` to generate this documentation!
## Developing
### Overview
- All rules are written in TypeScript and compiled into [`dist`](./dist)
- The plugins are linked into the main project dependencies from here
- These directories already contain the necessary `package.json` files to mark them as ESLint plugins
- Rule source files are structured, so they can be imported all in one go
- Each rule must export the following:
- `Messages`: an Enum of error message IDs
- `info`: metadata about this rule (name, description, messages, options, ...)
- `rule`: the implementation of the rule
- `tests`: the tests for this rule, as a set of valid/invalid code snippets. These snippets are used as example in the documentation.
- New rules should be added to their plugin's `index.ts`
- Some useful links
- [Developing ESLint plugins](https://eslint.org/docs/latest/extend/plugins)
- [Custom rules in typescript-eslint](https://typescript-eslint.io/developers/custom-rules)
- [Angular ESLint](https://github.com/angular-eslint/angular-eslint)
### Parsing project metadata in advance ~ TypeScript AST
While it is possible to retain persistent state between files during the linting process, it becomes quite complicated if the content of one file determines how we want to lint another file.
Because the two files may be linted out of order, we may not know whether the first file is wrong before we pass by the second. This means that we cannot report or fix the issue, because the first file is already detached from the linting context.
For example, we cannot consistently determine which components are themeable (i.e. have a `ThemedComponent` wrapper) while linting.
To work around this issue, we construct a registry of themeable components _before_ linting anything.
- We don't have a good way to hook into the ESLint parser at this time
- Instead, we leverage the actual TypeScript AST parser
- Retrieve all `ThemedComponent` wrapper files by the pattern of their path (`themed-*.component.ts`)
- Determine the themed component they're linked to (by the actual type annotation/import path, since filenames are prone to errors)
- Store metadata describing these component pairs in a global registry that can be shared between rules
- This only needs to happen once, and only takes a fraction of a second (for ~100 themeable components)

View File

@@ -1,6 +0,0 @@
{
"name": "eslint-plugin-dspace-angular-html",
"version": "0.0.0",
"main": "./index.js",
"private": true
}

View File

@@ -1,6 +0,0 @@
{
"name": "eslint-plugin-dspace-angular-ts",
"version": "0.0.0",
"main": "./index.js",
"private": true
}

View File

@@ -1,85 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import {
existsSync,
mkdirSync,
readFileSync,
rmSync,
writeFileSync,
} from 'node:fs';
import { join } from 'node:path';
import { default as htmlPlugin } from './src/rules/html';
import { default as tsPlugin } from './src/rules/ts';
const templates = new Map();
function lazyEJS(path: string, data: object): string {
if (!templates.has(path)) {
templates.set(path, require('ejs').compile(readFileSync(path).toString()));
}
return templates.get(path)(data).replace(/\r\n/g, '\n');
}
const docsDir = join('docs', 'lint');
const tsDir = join(docsDir, 'ts');
const htmlDir = join(docsDir, 'html');
if (existsSync(docsDir)) {
rmSync(docsDir, { recursive: true });
}
mkdirSync(join(tsDir, 'rules'), { recursive: true });
mkdirSync(join(htmlDir, 'rules'), { recursive: true });
function template(name: string): string {
return join('lint', 'src', 'util', 'templates', name);
}
// TypeScript docs
writeFileSync(
join(tsDir, 'index.md'),
lazyEJS(template('index.ejs'), {
plugin: tsPlugin,
rules: tsPlugin.index.map(rule => rule.info),
}),
);
for (const rule of tsPlugin.index) {
writeFileSync(
join(tsDir, 'rules', rule.info.name + '.md'),
lazyEJS(template('rule.ejs'), {
plugin: tsPlugin,
rule: rule.info,
tests: rule.tests,
}),
);
}
// HTML docs
writeFileSync(
join(htmlDir, 'index.md'),
lazyEJS(template('index.ejs'), {
plugin: htmlPlugin,
rules: htmlPlugin.index.map(rule => rule.info),
}),
);
for (const rule of htmlPlugin.index) {
writeFileSync(
join(htmlDir, 'rules', rule.info.name + '.md'),
lazyEJS(template('rule.ejs'), {
plugin: htmlPlugin,
rule: rule.info,
tests: rule.tests,
}),
);
}

View File

@@ -1,7 +0,0 @@
{
"spec_files": ["**/*.spec.js"],
"spec_dir": "lint/dist/test",
"helpers": [
"./test/helpers.js"
]
}

View File

@@ -1,25 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
/* eslint-disable import/no-namespace */
import {
bundle,
RuleExports,
} from '../../util/structure';
import * as noDisabledAttributeOnButton from './no-disabled-attribute-on-button';
import * as themedComponentUsages from './themed-component-usages';
const index = [
themedComponentUsages,
noDisabledAttributeOnButton,
] as unknown as RuleExports[];
export = {
parser: require('@angular-eslint/template-parser'),
...bundle('dspace-angular-html', 'HTML', index),
};

View File

@@ -1,148 +0,0 @@
import {
TmplAstBoundAttribute,
TmplAstTextAttribute,
} from '@angular-eslint/bundled-angular-compiler';
import { TemplateParserServices } from '@angular-eslint/utils';
import {
ESLintUtils,
TSESLint,
} from '@typescript-eslint/utils';
import {
DSpaceESLintRuleInfo,
NamedTests,
} from '../../util/structure';
import { getSourceCode } from '../../util/typescript';
export enum Message {
USE_DSBTN_DISABLED = 'mustUseDsBtnDisabled',
}
export const info = {
name: 'no-disabled-attribute-on-button',
meta: {
docs: {
description: `Buttons should use the \`dsBtnDisabled\` directive instead of the HTML \`disabled\` attribute.
This should be done to ensure that users with a screen reader are able to understand that the a button button is present, and that it is disabled.
The native html disabled attribute does not allow users to navigate to the button by keyboard, and thus they have no way of knowing that the button is present.`,
},
type: 'problem',
fixable: 'code',
schema: [],
messages: {
[Message.USE_DSBTN_DISABLED]: 'Buttons should use the `dsBtnDisabled` directive instead of the `disabled` attribute.',
},
},
optionDocs: [],
defaultOptions: [],
} as DSpaceESLintRuleInfo;
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: TSESLint.RuleContext<Message, unknown[]>) {
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
/**
* Some dynamic angular inputs will have disabled as name because of how Angular handles this internally (e.g [class.disabled]="isDisabled")
* But these aren't actually the disabled attribute we're looking for, we can determine this by checking the details of the keySpan
*/
function isOtherAttributeDisabled(node: TmplAstBoundAttribute | TmplAstTextAttribute): boolean {
// if the details are not null, and the details are not 'disabled', then it's not the disabled attribute we're looking for
return node.keySpan?.details !== null && node.keySpan?.details !== 'disabled';
}
/**
* Replace the disabled text with [dsBtnDisabled] in the template
*/
function replaceDisabledText(text: string ): string {
const hasBrackets = text.includes('[') && text.includes(']');
const newDisabledText = hasBrackets ? 'dsBtnDisabled' : '[dsBtnDisabled]="true"';
return text.replace('disabled', newDisabledText);
}
function inputIsChildOfButton(node: any): boolean {
return (node.parent?.tagName === 'button' || node.parent?.name === 'button');
}
function reportAndFix(node: TmplAstBoundAttribute | TmplAstTextAttribute) {
if (!inputIsChildOfButton(node) || isOtherAttributeDisabled(node)) {
return;
}
const sourceSpan = node.sourceSpan;
context.report({
messageId: Message.USE_DSBTN_DISABLED,
loc: parserServices.convertNodeSourceSpanToLoc(sourceSpan),
fix(fixer) {
const templateText = sourceSpan.start.file.content;
const disabledText = templateText.slice(sourceSpan.start.offset, sourceSpan.end.offset);
const newText = replaceDisabledText(disabledText);
return fixer.replaceTextRange([sourceSpan.start.offset, sourceSpan.end.offset], newText);
},
});
}
return {
'BoundAttribute[name="disabled"]'(node: TmplAstBoundAttribute) {
reportAndFix(node);
},
'TextAttribute[name="disabled"]'(node: TmplAstTextAttribute) {
reportAndFix(node);
},
};
},
});
export const tests = {
plugin: info.name,
valid: [
{
name: 'should use [dsBtnDisabled] in HTML templates',
code: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
{
name: 'disabled attribute is still valid on non-button elements',
code: `
<input disabled>
`,
},
{
name: '[disabled] attribute is still valid on non-button elements',
code: `
<input [disabled]="true">
`,
},
{
name: 'angular dynamic attributes that use disabled are still valid',
code: `
<button [class.disabled]="isDisabled">Submit</button>
`,
},
],
invalid: [
{
name: 'should not use disabled attribute in HTML templates',
code: `
<button disabled>Submit</button>
`,
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
output: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
{
name: 'should not use [disabled] attribute in HTML templates',
code: `
<button [disabled]="true">Submit</button>
`,
errors: [{ messageId: Message.USE_DSBTN_DISABLED }],
output: `
<button [dsBtnDisabled]="true">Submit</button>
`,
},
],
} as NamedTests;
export default rule;

View File

@@ -1,190 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import { TmplAstElement } from '@angular-eslint/bundled-angular-compiler';
import { TemplateParserServices } from '@angular-eslint/utils';
import { ESLintUtils } from '@typescript-eslint/utils';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { fixture } from '../../../test/fixture';
import {
DSpaceESLintRuleInfo,
NamedTests,
} from '../../util/structure';
import {
DISALLOWED_THEME_SELECTORS,
fixSelectors,
} from '../../util/theme-support';
import {
getFilename,
getSourceCode,
} from '../../util/typescript';
export enum Message {
WRONG_SELECTOR = 'mustUseThemedWrapperSelector',
}
export const info = {
name: 'themed-component-usages',
meta: {
docs: {
description: `Themeable components should be used via the selector of their \`ThemedComponent\` wrapper class
This ensures that custom themes can correctly override _all_ instances of this component.
The only exception to this rule are unit tests, where we may want to use the base component in order to keep the test setup simple.
`,
},
type: 'problem',
fixable: 'code',
schema: [],
messages: {
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper\'s selector',
},
},
optionDocs: [],
defaultOptions: [],
} as DSpaceESLintRuleInfo;
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: RuleContext<Message, unknown[]>) {
if (getFilename(context).includes('.spec.ts')) {
// skip inline templates in unit tests
return {};
}
const parserServices = getSourceCode(context).parserServices as TemplateParserServices;
return {
[`Element$1[name = /^${DISALLOWED_THEME_SELECTORS}/]`](node: TmplAstElement) {
const { startSourceSpan, endSourceSpan } = node;
const openStart = startSourceSpan.start.offset as number;
context.report({
messageId: Message.WRONG_SELECTOR,
loc: parserServices.convertNodeSourceSpanToLoc(startSourceSpan),
fix(fixer) {
const oldSelector = node.name;
const newSelector = fixSelectors(oldSelector);
const ops = [
fixer.replaceTextRange([openStart + 1, openStart + 1 + oldSelector.length], newSelector),
];
// make sure we don't mangle self-closing tags
if (endSourceSpan !== null && startSourceSpan.end.offset !== endSourceSpan.end.offset) {
const closeStart = endSourceSpan.start.offset as number;
const closeEnd = endSourceSpan.end.offset as number;
ops.push(fixer.replaceTextRange([closeStart + 2, closeEnd - 1], newSelector));
}
return ops;
},
});
},
};
},
});
export const tests = {
plugin: info.name,
valid: [
{
name: 'use no-prefix selectors in HTML templates',
code: `
<ds-test-themeable/>
<ds-test-themeable></ds-test-themeable>
<ds-test-themeable [test]="something"></ds-test-themeable>
`,
},
{
name: 'use no-prefix selectors in TypeScript templates',
code: `
@Component({
template: '<ds-test-themeable></ds-test-themeable>'
})
class Test {
}
`,
},
{
name: 'use no-prefix selectors in TypeScript test templates',
filename: fixture('src/test.spec.ts'),
code: `
@Component({
template: '<ds-test-themeable></ds-test-themeable>'
})
class Test {
}
`,
},
{
name: 'base selectors are also allowed in TypeScript test templates',
filename: fixture('src/test.spec.ts'),
code: `
@Component({
template: '<ds-base-test-themeable></ds-base-test-themeable>'
})
class Test {
}
`,
},
],
invalid: [
{
name: 'themed override selectors are not allowed in HTML templates',
code: `
<ds-themed-test-themeable/>
<ds-themed-test-themeable></ds-themed-test-themeable>
<ds-themed-test-themeable [test]="something"></ds-themed-test-themeable>
`,
errors: [
{
messageId: Message.WRONG_SELECTOR,
},
{
messageId: Message.WRONG_SELECTOR,
},
{
messageId: Message.WRONG_SELECTOR,
},
],
output: `
<ds-test-themeable/>
<ds-test-themeable></ds-test-themeable>
<ds-test-themeable [test]="something"></ds-test-themeable>
`,
},
{
name: 'base selectors are not allowed in HTML templates',
code: `
<ds-base-test-themeable/>
<ds-base-test-themeable></ds-base-test-themeable>
<ds-base-test-themeable [test]="something"></ds-base-test-themeable>
`,
errors: [
{
messageId: Message.WRONG_SELECTOR,
},
{
messageId: Message.WRONG_SELECTOR,
},
{
messageId: Message.WRONG_SELECTOR,
},
],
output: `
<ds-test-themeable/>
<ds-test-themeable></ds-test-themeable>
<ds-test-themeable [test]="something"></ds-test-themeable>
`,
},
],
} as NamedTests;
export default rule;

View File

@@ -1,304 +0,0 @@
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));
}

View File

@@ -1,35 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import {
bundle,
RuleExports,
} from '../../util/structure';
/* eslint-disable import/no-namespace */
import * as 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 = {
...bundle('dspace-angular-ts', 'TypeScript', index),
};

View File

@@ -1,306 +0,0 @@
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 {}`,
},
],
};

View File

@@ -1,383 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import {
ESLintUtils,
TSESTree,
} from '@typescript-eslint/utils';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { fixture } from '../../../test/fixture';
import {
getComponentImportNode,
getComponentInitializer,
getComponentStandaloneNode,
} from '../../util/angular';
import { appendObjectProperties } from '../../util/fix';
import { DSpaceESLintRuleInfo } from '../../util/structure';
import {
getBaseComponentClassName,
inThemedComponentOverrideFile,
isThemeableComponent,
isThemedComponentWrapper,
} from '../../util/theme-support';
import { getFilename } from '../../util/typescript';
export enum Message {
NOT_STANDALONE = 'mustBeStandalone',
NOT_STANDALONE_IMPORTS_BASE = 'mustBeStandaloneAndImportBase',
WRAPPER_IMPORTS_BASE = 'wrapperShouldImportBase',
}
export const info = {
name: 'themed-component-classes',
meta: {
docs: {
description: `Formatting rules for themeable component classes
- All themeable components must be standalone.
- The base component must always be imported in the \`ThemedComponent\` wrapper. This ensures that it is always sufficient to import just the wrapper whenever we use the component.
`,
},
type: 'problem',
fixable: 'code',
schema: [],
messages: {
[Message.NOT_STANDALONE]: 'Themeable components must be standalone',
[Message.NOT_STANDALONE_IMPORTS_BASE]: 'Themeable component wrapper classes must be standalone and import the base class',
[Message.WRAPPER_IMPORTS_BASE]: 'Themed component wrapper classes must only import the base class',
},
},
optionDocs: [],
defaultOptions: [],
} as DSpaceESLintRuleInfo;
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: RuleContext<Message, unknown[]>) {
const filename = getFilename(context);
if (filename.endsWith('.spec.ts')) {
return {};
}
function enforceStandalone(decoratorNode: TSESTree.Decorator, withBaseImport = false) {
const standaloneNode = getComponentStandaloneNode(decoratorNode);
if (standaloneNode === undefined) {
// We may need to add these properties in one go
if (!withBaseImport) {
context.report({
messageId: Message.NOT_STANDALONE,
node: decoratorNode,
fix(fixer) {
const initializer = getComponentInitializer(decoratorNode);
return appendObjectProperties(context, fixer, initializer, ['standalone: true']);
},
});
}
} else if (!standaloneNode.value) {
context.report({
messageId: Message.NOT_STANDALONE,
node: standaloneNode,
fix(fixer) {
return fixer.replaceText(standaloneNode, 'true');
},
});
}
if (withBaseImport) {
const baseClass = getBaseComponentClassName(decoratorNode);
if (baseClass === undefined) {
return;
}
const importsNode = getComponentImportNode(decoratorNode);
if (importsNode === undefined) {
if (standaloneNode === undefined) {
context.report({
messageId: Message.NOT_STANDALONE_IMPORTS_BASE,
node: decoratorNode,
fix(fixer) {
const initializer = getComponentInitializer(decoratorNode);
return appendObjectProperties(context, fixer, initializer, ['standalone: true', `imports: [${baseClass}]`]);
},
});
} else {
context.report({
messageId: Message.WRAPPER_IMPORTS_BASE,
node: decoratorNode,
fix(fixer) {
const initializer = getComponentInitializer(decoratorNode);
return appendObjectProperties(context, fixer, initializer, [`imports: [${baseClass}]`]);
},
});
}
} else {
// If we have an imports node, standalone: true will be enforced by another rule
const imports = importsNode.elements.map(e => (e as TSESTree.Identifier).name);
if (!imports.includes(baseClass) || imports.length > 1) {
// The wrapper should _only_ import the base component
context.report({
messageId: Message.WRAPPER_IMPORTS_BASE,
node: importsNode,
fix(fixer) {
// todo: this may leave unused imports, but that's better than mangling things
return fixer.replaceText(importsNode, `[${baseClass}]`);
},
});
}
}
}
}
return {
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) {
const classNode = node.parent as TSESTree.ClassDeclaration;
const className = classNode.id?.name;
if (className === undefined) {
return;
}
if (isThemedComponentWrapper(node)) {
enforceStandalone(node, true);
} else if (inThemedComponentOverrideFile(filename)) {
enforceStandalone(node);
} else if (isThemeableComponent(className)) {
enforceStandalone(node);
}
},
};
},
});
export const tests = {
plugin: info.name,
valid: [
{
name: 'Regular non-themeable component',
code: `
@Component({
selector: 'ds-something',
standalone: true,
})
class Something {
}
`,
},
{
name: 'Base component',
code: `
@Component({
selector: 'ds-base-test-themable',
standalone: true,
})
class TestThemeableComponent {
}
`,
},
{
name: 'Wrapper component',
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [
TestThemeableComponent,
],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
},
{
name: 'Override component',
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-themed-test-themable',
standalone: true,
})
class Override extends BaseComponent {
}
`,
},
],
invalid: [
{
name: 'Base component must be standalone',
code: `
@Component({
selector: 'ds-base-test-themable',
})
class TestThemeableComponent {
}
`,
errors:[
{
messageId: Message.NOT_STANDALONE,
},
],
output: `
@Component({
selector: 'ds-base-test-themable',
standalone: true,
})
class TestThemeableComponent {
}
`,
},
{
name: 'Wrapper component must be standalone and import base component',
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-test-themable',
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
errors:[
{
messageId: Message.NOT_STANDALONE_IMPORTS_BASE,
},
],
output: `
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [TestThemeableComponent],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
},
{
name: 'Wrapper component must import base component (array present but empty)',
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
errors:[
{
messageId: Message.WRAPPER_IMPORTS_BASE,
},
],
output: `
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [TestThemeableComponent],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
},
{
name: 'Wrapper component must import base component (array is wrong)',
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
code: `
import { SomethingElse } from './somewhere-else';
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [
SomethingElse,
],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
errors:[
{
messageId: Message.WRAPPER_IMPORTS_BASE,
},
],
output: `
import { SomethingElse } from './somewhere-else';
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [TestThemeableComponent],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
}, {
name: 'Wrapper component must import base component (array is wrong)',
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
code: `
import { Something, SomethingElse } from './somewhere-else';
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [
SomethingElse,
],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
errors:[
{
messageId: Message.WRAPPER_IMPORTS_BASE,
},
],
output: `
import { Something, SomethingElse } from './somewhere-else';
@Component({
selector: 'ds-test-themable',
standalone: true,
imports: [TestThemeableComponent],
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
},
{
name: 'Override component must be standalone',
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-themed-test-themable',
})
class Override extends BaseComponent {
}
`,
errors:[
{
messageId: Message.NOT_STANDALONE,
},
],
output: `
@Component({
selector: 'ds-themed-test-themable',
standalone: true,
})
class Override extends BaseComponent {
}
`,
},
],
};

View File

@@ -1,258 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import {
ESLintUtils,
TSESTree,
} from '@typescript-eslint/utils';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { fixture } from '../../../test/fixture';
import { getComponentSelectorNode } from '../../util/angular';
import { stringLiteral } from '../../util/misc';
import { DSpaceESLintRuleInfo } from '../../util/structure';
import {
inThemedComponentOverrideFile,
isThemeableComponent,
isThemedComponentWrapper,
} from '../../util/theme-support';
import { getFilename } from '../../util/typescript';
export enum Message {
BASE = 'wrongSelectorUnthemedComponent',
WRAPPER = 'wrongSelectorThemedComponentWrapper',
THEMED = 'wrongSelectorThemedComponentOverride',
}
export const info = {
name: 'themed-component-selectors',
meta: {
docs: {
description: `Themeable component selectors should follow the DSpace convention
Each themeable component is comprised of a base component, a wrapper component and any number of themed components
- Base components should have a selector starting with \`ds-base-\`
- Themed components should have a selector starting with \`ds-themed-\`
- Wrapper components should have a selector starting with \`ds-\`, but not \`ds-base-\` or \`ds-themed-\`
- This is the regular DSpace selector prefix
- **When making a regular component themeable, its selector prefix should be changed to \`ds-base-\`, and the new wrapper's component should reuse the previous selector**
Unit tests are exempt from this rule, because they may redefine components using the same class name as other themeable components elsewhere in the source.
`,
},
type: 'problem',
schema: [],
fixable: 'code',
messages: {
[Message.BASE]: 'Unthemed version of themeable component should have a selector starting with \'ds-base-\'',
[Message.WRAPPER]: 'Themed component wrapper of themeable component shouldn\'t have a selector starting with \'ds-themed-\'',
[Message.THEMED]: 'Theme override of themeable component should have a selector starting with \'ds-themed-\'',
},
},
optionDocs: [],
defaultOptions: [],
} as DSpaceESLintRuleInfo;
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: RuleContext<Message, unknown[]>) {
const filename = getFilename(context);
if (filename.endsWith('.spec.ts')) {
return {};
}
function enforceWrapperSelector(selectorNode: TSESTree.StringLiteral) {
if (selectorNode?.value.startsWith('ds-themed-')) {
context.report({
messageId: Message.WRAPPER,
node: selectorNode,
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-themed-', 'ds-')));
},
});
}
}
function enforceBaseSelector(selectorNode: TSESTree.StringLiteral) {
if (!selectorNode?.value.startsWith('ds-base-')) {
context.report({
messageId: Message.BASE,
node: selectorNode,
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-base-')));
},
});
}
}
function enforceThemedSelector(selectorNode: TSESTree.StringLiteral) {
if (!selectorNode?.value.startsWith('ds-themed-')) {
context.report({
messageId: Message.THEMED,
node: selectorNode,
fix(fixer) {
return fixer.replaceText(selectorNode, stringLiteral(selectorNode.value.replace('ds-', 'ds-themed-')));
},
});
}
}
return {
'ClassDeclaration > Decorator[expression.callee.name = "Component"]'(node: TSESTree.Decorator) {
const selectorNode = getComponentSelectorNode(node);
if (selectorNode === undefined) {
return;
}
const selector = selectorNode?.value;
const classNode = node.parent as TSESTree.ClassDeclaration;
const className = classNode.id?.name;
if (selector === undefined || className === undefined) {
return;
}
if (isThemedComponentWrapper(node)) {
enforceWrapperSelector(selectorNode);
} else if (inThemedComponentOverrideFile(filename)) {
enforceThemedSelector(selectorNode);
} else if (isThemeableComponent(className)) {
enforceBaseSelector(selectorNode);
}
},
};
},
});
export const tests = {
plugin: info.name,
valid: [
{
name: 'Regular non-themeable component selector',
code: `
@Component({
selector: 'ds-something',
})
class Something {
}
`,
},
{
name: 'Themeable component selector should replace the original version, unthemed version should be changed to ds-base-',
code: `
@Component({
selector: 'ds-base-something',
})
class Something {
}
@Component({
selector: 'ds-something',
})
class ThemedSomething extends ThemedComponent<Something> {
}
@Component({
selector: 'ds-themed-something',
})
class OverrideSomething extends Something {
}
`,
},
{
name: 'Other themed component wrappers should not interfere',
code: `
@Component({
selector: 'ds-something',
})
class Something {
}
@Component({
selector: 'ds-something-else',
})
class ThemedSomethingElse extends ThemedComponent<SomethingElse> {
}
`,
},
],
invalid: [
{
name: 'Wrong selector for base component',
filename: fixture('src/app/test/test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-something',
})
class TestThemeableComponent {
}
`,
errors: [
{
messageId: Message.BASE,
},
],
output: `
@Component({
selector: 'ds-base-something',
})
class TestThemeableComponent {
}
`,
},
{
name: 'Wrong selector for wrapper component',
filename: fixture('src/app/test/themed-test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-themed-something',
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
errors: [
{
messageId: Message.WRAPPER,
},
],
output: `
@Component({
selector: 'ds-something',
})
class ThemedTestThemeableComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
},
{
name: 'Wrong selector for theme override',
filename: fixture('src/themes/test/app/test/test-themeable.component.ts'),
code: `
@Component({
selector: 'ds-something',
})
class TestThememeableComponent extends BaseComponent {
}
`,
errors: [
{
messageId: Message.THEMED,
},
],
output: `
@Component({
selector: 'ds-themed-something',
})
class TestThememeableComponent extends BaseComponent {
}
`,
},
],
};
export default rule;

View File

@@ -1,503 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import {
ESLintUtils,
TSESTree,
} from '@typescript-eslint/utils';
import { RuleContext } from '@typescript-eslint/utils/ts-eslint';
import { fixture } from '../../../test/fixture';
import {
removeWithCommas,
replaceOrRemoveArrayIdentifier,
} from '../../util/fix';
import { DSpaceESLintRuleInfo } from '../../util/structure';
import {
allThemeableComponents,
DISALLOWED_THEME_SELECTORS,
fixSelectors,
getThemeableComponentByBaseClass,
isAllowedUnthemedUsage,
} from '../../util/theme-support';
import {
findImportSpecifier,
findUsages,
findUsagesByName,
getFilename,
relativePath,
} from '../../util/typescript';
export enum Message {
WRONG_CLASS = 'mustUseThemedWrapperClass',
WRONG_IMPORT = 'mustImportThemedWrapper',
WRONG_SELECTOR = 'mustUseThemedWrapperSelector',
BASE_IN_MODULE = 'baseComponentNotNeededInModule',
}
export const info = {
name: 'themed-component-usages',
meta: {
docs: {
description: `Themeable components should be used via their \`ThemedComponent\` wrapper class
This ensures that custom themes can correctly override _all_ instances of this component.
There are a few exceptions where the base class can still be used:
- Class declaration expressions (otherwise we can't declare, extend or override the class in the first place)
- Angular modules (except for routing modules)
- Angular \`@ViewChild\` decorators
- Type annotations
`,
},
type: 'problem',
schema: [],
fixable: 'code',
messages: {
[Message.WRONG_CLASS]: 'Themeable components should be used via their ThemedComponent wrapper',
[Message.WRONG_IMPORT]: 'Themeable components should be used via their ThemedComponent wrapper',
[Message.WRONG_SELECTOR]: 'Themeable components should be used via their ThemedComponent wrapper',
[Message.BASE_IN_MODULE]: 'Base themeable components shouldn\'t be declared in modules',
},
},
optionDocs: [],
defaultOptions: [],
} as DSpaceESLintRuleInfo;
export const rule = ESLintUtils.RuleCreator.withoutDocs({
...info,
create(context: RuleContext<Message, unknown[]>) {
const filename = getFilename(context);
function handleUnthemedUsagesInTypescript(node: TSESTree.Identifier) {
if (isAllowedUnthemedUsage(node)) {
return;
}
const entry = getThemeableComponentByBaseClass(node.name);
if (entry === undefined) {
// this should never happen
throw new Error(`No such themeable component in registry: '${node.name}'`);
}
context.report({
messageId: Message.WRONG_CLASS,
node: node,
fix(fixer) {
if (node.parent.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
return replaceOrRemoveArrayIdentifier(context, fixer, node, entry.wrapperClass);
} else {
return fixer.replaceText(node, entry.wrapperClass);
}
},
});
}
function handleThemedSelectorQueriesInTests(node: TSESTree.Literal) {
context.report({
node,
messageId: Message.WRONG_SELECTOR,
fix(fixer){
const newSelector = fixSelectors(node.raw);
return fixer.replaceText(node, newSelector);
},
});
}
function handleUnthemedImportsInTypescript(specifierNode: TSESTree.ImportSpecifier) {
const allUsages = findUsages(context, specifierNode.local);
const badUsages = allUsages.filter(usage => !isAllowedUnthemedUsage(usage));
if (badUsages.length === 0) {
return;
}
const importedNode = specifierNode.imported;
const declarationNode = specifierNode.parent as TSESTree.ImportDeclaration;
const entry = getThemeableComponentByBaseClass(importedNode.name);
if (entry === undefined) {
// this should never happen
throw new Error(`No such themeable component in registry: '${importedNode.name}'`);
}
context.report({
messageId: Message.WRONG_IMPORT,
node: importedNode,
fix(fixer) {
const ops = [];
const wrapperImport = findImportSpecifier(context, entry.wrapperClass);
if (findUsagesByName(context, entry.wrapperClass).length === 0) {
// Wrapper is not present in this file, safe to add import
const newImportLine = `import { ${entry.wrapperClass} } from '${relativePath(filename, entry.wrapperPath)}';`;
if (declarationNode.specifiers.length === 1) {
if (allUsages.length === badUsages.length) {
ops.push(fixer.replaceText(declarationNode, newImportLine));
} else if (wrapperImport === undefined) {
ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine));
}
} else {
ops.push(...removeWithCommas(context, fixer, specifierNode));
if (wrapperImport === undefined) {
ops.push(fixer.insertTextAfter(declarationNode, '\n' + newImportLine));
}
}
} else {
// Wrapper already present in the file, remove import instead
if (allUsages.length === badUsages.length) {
if (declarationNode.specifiers.length === 1) {
// Make sure we remove the newline as well
ops.push(fixer.removeRange([declarationNode.range[0], declarationNode.range[1] + 1]));
} else {
ops.push(...removeWithCommas(context, fixer, specifierNode));
}
}
}
return ops;
},
});
}
// ignore tests and non-routing modules
if (filename.endsWith('.spec.ts')) {
return {
[`CallExpression[callee.object.name = "By"][callee.property.name = "css"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests,
};
} else if (filename.endsWith('.cy.ts')) {
return {
[`CallExpression[callee.object.name = "cy"][callee.property.name = "get"] > Literal:first-child[value = /.*${DISALLOWED_THEME_SELECTORS}.*/]`]: handleThemedSelectorQueriesInTests,
};
} else if (
filename.match(/(?!src\/themes\/).*(?!routing).module.ts$/)
|| filename.match(/themed-.+\.component\.ts$/)
) {
// do nothing
return {};
} else {
return allThemeableComponents().reduce(
(rules, entry) => {
return {
...rules,
[`:not(:matches(ClassDeclaration, ImportSpecifier)) > Identifier[name = "${entry.baseClass}"]`]: handleUnthemedUsagesInTypescript,
[`ImportSpecifier[imported.name = "${entry.baseClass}"]`]: handleUnthemedImportsInTypescript,
};
}, {},
);
}
},
});
export const tests = {
plugin: info.name,
valid: [
{
name: 'allow wrapper class usages',
code: `
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
const config = {
a: ThemedTestThemeableComponent,
b: ChipsComponent,
}
`,
},
{
name: 'allow base class in class declaration',
code: `
export class TestThemeableComponent {
}
`,
},
{
name: 'allow inheriting from base class',
code: `
import { TestThemeableComponent } from './app/test/test-themeable.component';
export class ThemedAdminSidebarComponent extends ThemedComponent<TestThemeableComponent> {
}
`,
},
{
name: 'allow base class in ViewChild',
code: `
import { TestThemeableComponent } from './app/test/test-themeable.component';
export class Something {
@ViewChild(TestThemeableComponent) test: TestThemeableComponent;
}
`,
},
{
name: 'allow wrapper selectors in test queries',
filename: fixture('src/app/test/test.component.spec.ts'),
code: `
By.css('ds-themeable');
By.css('#test > ds-themeable > #nest');
`,
},
{
name: 'allow wrapper selectors in cypress queries',
filename: fixture('src/app/test/test.component.cy.ts'),
code: `
By.css('ds-themeable');
By.css('#test > ds-themeable > #nest');
`,
},
],
invalid: [
{
name: 'disallow direct usages of base class',
code: `
import { TestThemeableComponent } from './app/test/test-themeable.component';
import { TestComponent } from './app/test/test.component';
const config = {
a: TestThemeableComponent,
b: TestComponent,
}
`,
errors: [
{
messageId: Message.WRONG_IMPORT,
},
{
messageId: Message.WRONG_CLASS,
},
],
output: `
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
import { TestComponent } from './app/test/test.component';
const config = {
a: ThemedTestThemeableComponent,
b: TestComponent,
}
`,
},
{
name: 'disallow direct usages of base class, keep other imports',
code: `
import { Something, TestThemeableComponent } from './app/test/test-themeable.component';
import { TestComponent } from './app/test/test.component';
const config = {
a: TestThemeableComponent,
b: TestComponent,
c: Something,
}
`,
errors: [
{
messageId: Message.WRONG_IMPORT,
},
{
messageId: Message.WRONG_CLASS,
},
],
output: `
import { Something } from './app/test/test-themeable.component';
import { ThemedTestThemeableComponent } from './app/test/themed-test-themeable.component';
import { TestComponent } from './app/test/test.component';
const config = {
a: ThemedTestThemeableComponent,
b: TestComponent,
c: Something,
}
`,
},
{
name: 'handle array replacements correctly',
code: `
const DECLARATIONS = [
Something,
TestThemeableComponent,
Something,
ThemedTestThemeableComponent,
];
`,
errors: [
{
messageId: Message.WRONG_CLASS,
},
],
output: `
const DECLARATIONS = [
Something,
Something,
ThemedTestThemeableComponent,
];
`,
},
{
name: 'disallow override selector in test queries',
filename: fixture('src/app/test/test.component.spec.ts'),
code: `
By.css('ds-themed-themeable');
By.css('#test > ds-themed-themeable > #nest');
`,
errors: [
{
messageId: Message.WRONG_SELECTOR,
},
{
messageId: Message.WRONG_SELECTOR,
},
],
output: `
By.css('ds-themeable');
By.css('#test > ds-themeable > #nest');
`,
},
{
name: 'disallow base selector in test queries',
filename: fixture('src/app/test/test.component.spec.ts'),
code: `
By.css('ds-base-themeable');
By.css('#test > ds-base-themeable > #nest');
`,
errors: [
{
messageId: Message.WRONG_SELECTOR,
},
{
messageId: Message.WRONG_SELECTOR,
},
],
output: `
By.css('ds-themeable');
By.css('#test > ds-themeable > #nest');
`,
},
{
name: 'disallow override selector in cypress queries',
filename: fixture('src/app/test/test.component.cy.ts'),
code: `
cy.get('ds-themed-themeable');
cy.get('#test > ds-themed-themeable > #nest');
`,
errors: [
{
messageId: Message.WRONG_SELECTOR,
},
{
messageId: Message.WRONG_SELECTOR,
},
],
output: `
cy.get('ds-themeable');
cy.get('#test > ds-themeable > #nest');
`,
},
{
name: 'disallow base selector in cypress queries',
filename: fixture('src/app/test/test.component.cy.ts'),
code: `
cy.get('ds-base-themeable');
cy.get('#test > ds-base-themeable > #nest');
`,
errors: [
{
messageId: Message.WRONG_SELECTOR,
},
{
messageId: Message.WRONG_SELECTOR,
},
],
output: `
cy.get('ds-themeable');
cy.get('#test > ds-themeable > #nest');
`,
},
{
name: 'edge case: unable to find usage node through usage token, but import is still flagged and fixed',
filename: fixture('src/themes/test/app/test/other-themeable.component.ts'),
code: `
import { Component } from '@angular/core';
import { Context } from './app/core/shared/context.model';
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
@Component({
standalone: true,
imports: [TestThemeableComponent],
})
export class UsageComponent {
}
`,
errors: [
{
messageId: Message.WRONG_IMPORT,
},
{
messageId: Message.WRONG_CLASS,
},
],
output: `
import { Component } from '@angular/core';
import { Context } from './app/core/shared/context.model';
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
@Component({
standalone: true,
imports: [ThemedTestThemeableComponent],
})
export class UsageComponent {
}
`,
},
{
name: 'edge case edge case: both are imported, only wrapper is retained',
filename: fixture('src/themes/test/app/test/other-themeable.component.ts'),
code: `
import { Component } from '@angular/core';
import { Context } from './app/core/shared/context.model';
import { TestThemeableComponent } from '../../../../app/test/test-themeable.component';
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
@Component({
standalone: true,
imports: [TestThemeableComponent, ThemedTestThemeableComponent],
})
export class UsageComponent {
}
`,
errors: [
{
messageId: Message.WRONG_IMPORT,
},
{
messageId: Message.WRONG_CLASS,
},
],
output: `
import { Component } from '@angular/core';
import { Context } from './app/core/shared/context.model';
import { ThemedTestThemeableComponent } from '../../../../app/test/themed-test-themeable.component';
@Component({
standalone: true,
imports: [ThemedTestThemeableComponent],
})
export class UsageComponent {
}
`,
},
],
};
export default rule;

View File

@@ -1,280 +0,0 @@
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;
}

View File

@@ -1,158 +0,0 @@
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;
// }
// `,
},
],
};

View File

@@ -1,226 +0,0 @@
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;
}

View File

@@ -1,83 +0,0 @@
/**
* The contents of this file are subject to the license and copyright
* detailed in the LICENSE and NOTICE files at the root of the source
* tree and available online at
*
* http://www.dspace.org/license/
*/
import { TSESTree } from '@typescript-eslint/utils';
import { getObjectPropertyNodeByName } from './typescript';
export function getComponentSelectorNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.StringLiteral | undefined {
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'selector');
if (property !== undefined) {
// todo: support template literals as well
if (property.type === TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'string') {
return property as TSESTree.StringLiteral;
}
}
return undefined;
}
export function getComponentStandaloneNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.BooleanLiteral | undefined {
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'standalone');
if (property !== undefined) {
if (property.type === TSESTree.AST_NODE_TYPES.Literal && typeof property.value === 'boolean') {
return property as TSESTree.BooleanLiteral;
}
}
return undefined;
}
export function getComponentImportNode(componentDecoratorNode: TSESTree.Decorator): TSESTree.ArrayExpression | undefined {
const property = getComponentInitializerNodeByName(componentDecoratorNode, 'imports');
if (property !== undefined) {
if (property.type === TSESTree.AST_NODE_TYPES.ArrayExpression) {
return property as TSESTree.ArrayExpression;
}
}
return undefined;
}
export function getComponentClassName(decoratorNode: TSESTree.Decorator): string | undefined {
if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
return undefined;
}
if (decoratorNode.parent.id?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
return undefined;
}
return decoratorNode.parent.id.name;
}
export function getComponentSuperClassName(decoratorNode: TSESTree.Decorator): string | undefined {
if (decoratorNode.parent.type !== TSESTree.AST_NODE_TYPES.ClassDeclaration) {
return undefined;
}
if (decoratorNode.parent.superClass?.type !== TSESTree.AST_NODE_TYPES.Identifier) {
return undefined;
}
return decoratorNode.parent.superClass.name;
}
export function getComponentInitializer(componentDecoratorNode: TSESTree.Decorator): TSESTree.ObjectExpression {
return (componentDecoratorNode.expression as TSESTree.CallExpression).arguments[0] as TSESTree.ObjectExpression;
}
export function getComponentInitializerNodeByName(componentDecoratorNode: TSESTree.Decorator, name: string): TSESTree.Node | undefined {
const initializer = getComponentInitializer(componentDecoratorNode);
return getObjectPropertyNodeByName(initializer, name);
}
export function isPartOfViewChild(node: TSESTree.Identifier): boolean {
return (node.parent as any)?.callee?.name === 'ViewChild';
}

View File

@@ -1,10 +0,0 @@
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') ;
}

Some files were not shown because too many files have changed in this diff Show More